diff --git a/internal/fusefs/fuse_unix.go b/internal/fusefs/fuse_unix.go index 3a1108e..f652583 100644 --- a/internal/fusefs/fuse_unix.go +++ b/internal/fusefs/fuse_unix.go @@ -195,7 +195,7 @@ func (fs *ArtifactFuse) LookUpInode(_ context.Context, op *fuseops.LookUpInodeOp fs.mu.Unlock() op.Entry.Child = ref.ID - op.Entry.Attributes = inodeAttrs(mode, uint64(size), typ, mtime) + op.Entry.Attributes = inodeAttrs(mode, size, typ, mtime) setChildEntryExpiry(&op.Entry, time.Second) return nil } @@ -216,7 +216,7 @@ func (fs *ArtifactFuse) GetInodeAttributes(_ context.Context, op *fuseops.GetIno if err != nil { return syscall.ENOENT } - op.Attributes = inodeAttrs(mode, uint64(size), typ, mtime) + op.Attributes = inodeAttrs(mode, size, typ, mtime) op.AttributesExpiration = attrExpiry(time.Second) return nil } @@ -243,7 +243,7 @@ func (fs *ArtifactFuse) SetInodeAttributes(ctx context.Context, op *fuseops.SetI if op.Mtime != nil { mtime = *op.Mtime } - op.Attributes = inodeAttrs(mode, uint64(size), typ, mtime) + op.Attributes = inodeAttrs(mode, size, typ, mtime) op.AttributesExpiration = attrExpiry(time.Second) return nil } @@ -567,7 +567,15 @@ func TryUnmount(mountPoint string) error { return err } -func inodeAttrs(mode uint32, size uint64, typ string, mtime time.Time) fuseops.InodeAttributes { +// inodeAttrs builds the kernel-visible attributes for an inode. The size is +// taken as a signed int64 because that's how snapshot and overlay state store +// it; a negative value there means the on-disk row is corrupt. Publishing it +// verbatim would wrap into a huge uint64 (e.g. -1 becomes ~18 exabytes), so +// clamp to zero instead. +func inodeAttrs(mode uint32, size int64, typ string, mtime time.Time) fuseops.InodeAttributes { + if size < 0 { + size = 0 + } m := os.FileMode(mode & 0o777) if m == 0 { if typ == "dir" { @@ -586,7 +594,7 @@ func inodeAttrs(mode uint32, size uint64, typ string, mtime time.Time) fuseops.I m |= os.ModeSymlink } return fuseops.InodeAttributes{ - Size: size, + Size: uint64(size), Nlink: 1, Mode: m, Uid: uint32(os.Getuid()), diff --git a/internal/fusefs/inode_attrs_unix_test.go b/internal/fusefs/inode_attrs_unix_test.go new file mode 100644 index 0000000..1f976a4 --- /dev/null +++ b/internal/fusefs/inode_attrs_unix_test.go @@ -0,0 +1,85 @@ +//go:build !windows + +package fusefs + +import ( + "math" + "os" + "testing" + "time" +) + +func TestInodeAttrs_ClampsNegativeSizeToZero(t *testing.T) { + cases := []struct { + name string + size int64 + }{ + {"minus one", -1}, + {"min int64", math.MinInt64}, + {"arbitrary negative", -4096}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := inodeAttrs(0o644, tc.size, "file", time.Unix(0, 0)) + if got.Size != 0 { + t.Fatalf("file size = %d, want 0 for input %d", got.Size, tc.size) + } + }) + } +} + +func TestInodeAttrs_PreservesPositiveFileSize(t *testing.T) { + got := inodeAttrs(0o644, 42, "file", time.Unix(0, 0)) + if got.Size != 42 { + t.Fatalf("file size = %d, want 42", got.Size) + } +} + +func TestInodeAttrs_PreservesMaxInt64(t *testing.T) { + got := inodeAttrs(0o644, math.MaxInt64, "file", time.Unix(0, 0)) + if got.Size != math.MaxInt64 { + t.Fatalf("file size = %d, want MaxInt64", got.Size) + } +} + +func TestInodeAttrs_DirZeroSizeBecomes4096(t *testing.T) { + got := inodeAttrs(0o755, 0, "dir", time.Unix(0, 0)) + if got.Size != 4096 { + t.Fatalf("dir size = %d, want 4096", got.Size) + } +} + +func TestInodeAttrs_DirNegativeSizeAlsoBecomes4096(t *testing.T) { + // Negative clamps to 0, which the dir branch then upgrades to 4096. + got := inodeAttrs(0o755, -1, "dir", time.Unix(0, 0)) + if got.Size != 4096 { + t.Fatalf("dir size = %d, want 4096", got.Size) + } +} + +func TestInodeAttrs_SymlinkModeBitSet(t *testing.T) { + got := inodeAttrs(0o777, 16, "symlink", time.Unix(0, 0)) + if got.Mode&os.ModeSymlink == 0 { + t.Fatalf("symlink mode bit not set in %v", got.Mode) + } + if got.Size != 16 { + t.Fatalf("symlink size = %d, want 16", got.Size) + } +} + +func TestInodeAttrs_DefaultFileModeWhenZero(t *testing.T) { + got := inodeAttrs(0, 1, "file", time.Unix(0, 0)) + if got.Mode.Perm() != 0o644 { + t.Fatalf("file default perm = %v, want 0644", got.Mode.Perm()) + } +} + +func TestInodeAttrs_DefaultDirModeWhenZero(t *testing.T) { + got := inodeAttrs(0, 1, "dir", time.Unix(0, 0)) + if got.Mode.Perm() != 0o755 { + t.Fatalf("dir default perm = %v, want 0755", got.Mode.Perm()) + } + if got.Mode&os.ModeDir == 0 { + t.Fatalf("dir mode bit not set in %v", got.Mode) + } +}