diff --git a/resources/storage_capacity.go b/resources/storage_capacity.go index a29bddf1..38b4b3ef 100644 --- a/resources/storage_capacity.go +++ b/resources/storage_capacity.go @@ -10,8 +10,6 @@ package resources import ( "context" "crypto/rand" - "crypto/sha256" - "encoding/hex" "errors" "fmt" "math" @@ -19,7 +17,6 @@ import ( "path/filepath" "strings" "sync" - "syscall" ) // StorageFilesystem is a path-free point-in-time capacity observation. The @@ -54,44 +51,6 @@ var storageAuthorityScope struct { err error } -// InspectStorageFilesystem observes the filesystem containing root. root may -// name a not-yet-created application-state directory; in that case the nearest -// existing ancestor determines the target filesystem. -func InspectStorageFilesystem(root string) (StorageFilesystem, error) { - ancestor, err := existingStorageAncestor(root) - if err != nil { - return StorageFilesystem{}, err - } - var state syscall.Statfs_t - if err := syscall.Statfs(ancestor, &state); err != nil { - return StorageFilesystem{}, fmt.Errorf("inspect storage filesystem: %w", err) - } - if state.Bsize <= 0 { - return StorageFilesystem{}, fmt.Errorf("storage filesystem reported invalid block size %d", state.Bsize) - } - blockSize := uint64(state.Bsize) - totalBytes, ok := checkedStorageBytes(uint64(state.Blocks), blockSize) - if !ok { - return StorageFilesystem{}, errors.New("storage filesystem total byte count overflows uint64") - } - availableBytes, ok := checkedStorageBytes(uint64(state.Bavail), blockSize) - if !ok { - return StorageFilesystem{}, errors.New("storage filesystem available byte count overflows uint64") - } - scope, err := storageAuthorityScopeBytes() - if err != nil { - return StorageFilesystem{}, err - } - identity := fmt.Sprintf("%x:%v:%d", scope, state.Fsid, totalBytes) - digest := sha256.Sum256([]byte(identity)) - return StorageFilesystem{ - AuthorityID: "storage/sha256:" + hex.EncodeToString(digest[:16]), - TotalBytes: totalBytes, - AvailableBytes: availableBytes, - AllocationUnitBytes: blockSize, - }, nil -} - // InspectStorageTree measures an existing tree through filesystem metadata. // It never reads file contents and never follows symlinks. Callers must invoke // it only at the execution/storage boundary that owns the inspected tree. diff --git a/resources/storage_capacity_other.go b/resources/storage_capacity_other.go new file mode 100644 index 00000000..4c649c80 --- /dev/null +++ b/resources/storage_capacity_other.go @@ -0,0 +1,16 @@ +//go:build !unix + +package resources + +import ( + "fmt" + "runtime" +) + +// InspectStorageFilesystem is a Unix-only observer: it depends on the statfs +// syscall, which platforms outside the `unix` build constraint (e.g. Windows) +// do not provide. Cross-compiled builds must still link, so the capability +// reports itself unavailable at run time instead of failing to compile. +func InspectStorageFilesystem(root string) (StorageFilesystem, error) { + return StorageFilesystem{}, fmt.Errorf("inspect storage filesystem: unsupported on %s", runtime.GOOS) +} diff --git a/resources/storage_capacity_other_test.go b/resources/storage_capacity_other_test.go new file mode 100644 index 00000000..44691422 --- /dev/null +++ b/resources/storage_capacity_other_test.go @@ -0,0 +1,19 @@ +//go:build !unix + +package resources + +import ( + "context" + "testing" +) + +// On platforms without statfs the observer must still link and fail cleanly at +// run time rather than break the cross-compiled release build. +func TestInspectStorageFilesystemUnsupportedOffUnix(t *testing.T) { + if _, err := InspectStorageFilesystem(t.TempDir()); err == nil { + t.Fatal("expected an unsupported-platform error off unix") + } + if _, err := InspectStorageTree(context.Background(), t.TempDir()); err == nil { + t.Fatal("InspectStorageTree must surface the unsupported-platform error") + } +} diff --git a/resources/storage_capacity_test.go b/resources/storage_capacity_test.go index 95155785..b011436a 100644 --- a/resources/storage_capacity_test.go +++ b/resources/storage_capacity_test.go @@ -1,3 +1,5 @@ +//go:build unix + package resources import ( diff --git a/resources/storage_capacity_unix.go b/resources/storage_capacity_unix.go new file mode 100644 index 00000000..0c50f596 --- /dev/null +++ b/resources/storage_capacity_unix.go @@ -0,0 +1,49 @@ +//go:build unix + +package resources + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "syscall" +) + +// InspectStorageFilesystem observes the filesystem containing root. root may +// name a not-yet-created application-state directory; in that case the nearest +// existing ancestor determines the target filesystem. +func InspectStorageFilesystem(root string) (StorageFilesystem, error) { + ancestor, err := existingStorageAncestor(root) + if err != nil { + return StorageFilesystem{}, err + } + var state syscall.Statfs_t + if err = syscall.Statfs(ancestor, &state); err != nil { + return StorageFilesystem{}, fmt.Errorf("inspect storage filesystem: %w", err) + } + if state.Bsize <= 0 { + return StorageFilesystem{}, fmt.Errorf("storage filesystem reported invalid block size %d", state.Bsize) + } + blockSize := uint64(state.Bsize) + totalBytes, ok := checkedStorageBytes(uint64(state.Blocks), blockSize) + if !ok { + return StorageFilesystem{}, errors.New("storage filesystem total byte count overflows uint64") + } + availableBytes, ok := checkedStorageBytes(uint64(state.Bavail), blockSize) + if !ok { + return StorageFilesystem{}, errors.New("storage filesystem available byte count overflows uint64") + } + scope, err := storageAuthorityScopeBytes() + if err != nil { + return StorageFilesystem{}, err + } + identity := fmt.Sprintf("%x:%v:%d", scope, state.Fsid, totalBytes) + digest := sha256.Sum256([]byte(identity)) + return StorageFilesystem{ + AuthorityID: "storage/sha256:" + hex.EncodeToString(digest[:16]), + TotalBytes: totalBytes, + AvailableBytes: availableBytes, + AllocationUnitBytes: blockSize, + }, nil +}