From e50b9c1913136fb31f6536601ca168caec539ff0 Mon Sep 17 00:00:00 2001 From: Luther Monson Date: Wed, 13 May 2026 21:20:39 -0700 Subject: [PATCH] fat12/16/32: expose attribute flags and timestamps via FileInfo.Sys() FAT FileInfo.Sys() previously returned nil. Introduce fat12.StatT carrying the FAT directory-entry metadata that has no analogue in os.FileMode: read-only/hidden/system/archive/volume-label attribute bits, creation and access timestamps, and the starting cluster of the file's chain. Wire fat12.FileInfo.Sys() to populate and return a *StatT from data already parsed in parseDirEntries. Because fat12 is the shared base for FAT12, FAT16, and FAT32, this single change is reflected through type aliases in the fat16 and fat32 packages: fat32.StatT = fat12.StatT and fat16.StatT = fat12.StatT. Refs #301. --- filesystem/fat12/directoryentry.go | 14 ++++++++++++++ filesystem/fat12/fat12_test.go | 26 ++++++++++++++++++++++++++ filesystem/fat12/file.go | 1 + filesystem/fat12/fileinfo.go | 19 +++++++++++++++++-- filesystem/fat16/fat16.go | 3 +++ filesystem/fat32/fat32.go | 3 +++ 6 files changed, 64 insertions(+), 2 deletions(-) diff --git a/filesystem/fat12/directoryentry.go b/filesystem/fat12/directoryentry.go index 78a71cea..8d3735d3 100644 --- a/filesystem/fat12/directoryentry.go +++ b/filesystem/fat12/directoryentry.go @@ -55,9 +55,23 @@ func (de *directoryEntry) Info() (iofs.FileInfo, error) { shortName: de.fullShortName(), size: int64(de.fileSize), isDir: de.isSubdirectory, + sys: de.stat(), }, nil } +func (de *directoryEntry) stat() *StatT { + return &StatT{ + ReadOnly: de.isReadOnly, + Hidden: de.isHidden, + System: de.isSystem, + Archive: de.isArchiveDirty, + VolumeLabel: de.isVolumeLabel, + CreateTime: de.createTime, + AccessTime: de.accessTime, + Cluster: de.clusterLocation, + } +} + func (de *directoryEntry) nameMatches(name string) bool { return strings.EqualFold(de.filenameLong, name) || strings.EqualFold(de.fullShortName(), name) } diff --git a/filesystem/fat12/fat12_test.go b/filesystem/fat12/fat12_test.go index b2dab0c7..b7abd7d6 100644 --- a/filesystem/fat12/fat12_test.go +++ b/filesystem/fat12/fat12_test.go @@ -408,6 +408,32 @@ func TestFat12Stat(t *testing.T) { } } +func TestFat12StatSys(t *testing.T) { + _, fs := createFAT12(t, "SYSTEST") + writeFile(t, fs, "/sys.txt", []byte("sys")) + + info, err := fs.Stat("sys.txt") + if err != nil { + t.Fatalf("Stat: %v", err) + } + stat, ok := info.Sys().(*fat12.StatT) + if !ok { + t.Fatalf("Sys() did not return *fat12.StatT, got %T", info.Sys()) + } + if stat == nil { + t.Fatal("StatT is nil") + } + if stat.Cluster < 2 { + t.Errorf("Cluster = %d, expected >= 2 (data clusters start at 2)", stat.Cluster) + } + if stat.VolumeLabel { + t.Error("regular file should not have VolumeLabel set") + } + if stat.Hidden || stat.System || stat.ReadOnly { + t.Errorf("unexpected attribute bits set: Hidden=%v System=%v ReadOnly=%v", stat.Hidden, stat.System, stat.ReadOnly) + } +} + // ── ReadFile (fs.ReadFileFS) ────────────────────────────────────────────────── func TestFat12ReadFile(t *testing.T) { diff --git a/filesystem/fat12/file.go b/filesystem/fat12/file.go index fea4749e..6678c9ea 100644 --- a/filesystem/fat12/file.go +++ b/filesystem/fat12/file.go @@ -30,6 +30,7 @@ func (fl *File) Stat() (iofs.FileInfo, error) { shortName: fl.fullShortName(), size: int64(fl.fileSize), isDir: fl.isSubdirectory, + sys: fl.stat(), }, nil } diff --git a/filesystem/fat12/fileinfo.go b/filesystem/fat12/fileinfo.go index 8b64a0ae..150121dc 100644 --- a/filesystem/fat12/fileinfo.go +++ b/filesystem/fat12/fileinfo.go @@ -14,6 +14,21 @@ type FileInfo struct { shortName string size int64 isDir bool + sys *StatT +} + +// StatT carries FAT-specific metadata returned by FileInfo.Sys(). FAT12/16/32 +// have no inodes, uid/gid, or POSIX permissions, but they do carry attribute +// flags and additional timestamps that os.FileMode doesn't represent. +type StatT struct { + ReadOnly bool + Hidden bool + System bool + Archive bool + VolumeLabel bool + CreateTime time.Time + AccessTime time.Time + Cluster uint32 } // IsDir abbreviation for Mode().IsDir() @@ -63,9 +78,9 @@ func (fi FileInfo) Size() int64 { return fi.size } -// Sys underlying data source - not supported yet and so will return nil +// Sys returns *StatT with FAT-specific metadata. // //nolint:gocritic // we need this to comply with fs.FileInfo func (fi FileInfo) Sys() interface{} { - return nil + return fi.sys } diff --git a/filesystem/fat16/fat16.go b/filesystem/fat16/fat16.go index 4a93ec95..57854b70 100644 --- a/filesystem/fat16/fat16.go +++ b/filesystem/fat16/fat16.go @@ -20,6 +20,9 @@ type FileSystem struct { // interface guard var _ filesystem.FileSystem = (*FileSystem)(nil) +// StatT is an alias for fat12.StatT, the metadata returned by FileInfo.Sys(). +type StatT = fat12.StatT + // Type returns filesystem.TypeFat16, overriding fat12.FileSystem.Type(). func (fs *FileSystem) Type() filesystem.Type { return filesystem.TypeFat16 } diff --git a/filesystem/fat32/fat32.go b/filesystem/fat32/fat32.go index 05df6d72..32d235d3 100644 --- a/filesystem/fat32/fat32.go +++ b/filesystem/fat32/fat32.go @@ -28,6 +28,9 @@ const ( // is directly usable in fat12.Dos20BPB struct literals. type SectorSize = fat12.SectorSize +// StatT is an alias for fat12.StatT, the metadata returned by FileInfo.Sys(). +type StatT = fat12.StatT + const ( SectorSize512 SectorSize = 512 SectorSize4096 SectorSize = 4096