From 11fbcd7f459457b23e86201d496b4cf4ba2ecff1 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Wed, 7 Oct 2026 15:46:30 +1100 Subject: [PATCH] Record FAT file timestamps in UTC go_fatfs_get_fattime called Date() on the result of time.Now(), which is in the local time zone. To calculate the local date, Date() must load the local time zone. This caused the time zone loader (time.loadTzinfo, time.tzset and related functions) to be included in the binary. In the examples/console/fatfs/machine example on pico2, this increased the binary size by 4,408 bytes of flash. Write all FAT timestamps in UTC. Most microcontrollers do not have a real-time clock, so these timestamps are seldom correct, and a time zone adds no value. --- README.md | 1 + fatfs/go_fatfs_callbacks.go | 2 +- fatfs/go_fatfs_test.go | 73 ++++++++++++++++++++++--------------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 1b55230..65ec0c3 100644 --- a/README.md +++ b/README.md @@ -104,3 +104,4 @@ Successfully mounted LittleFS filesystem. The FAT file system is not currently working, due to https://github.com/tinygo-org/tinygo/issues/3460. +FAT file timestamps use UTC, with two-second resolution. diff --git a/fatfs/go_fatfs_callbacks.go b/fatfs/go_fatfs_callbacks.go index fa4f258..8d69c7c 100644 --- a/fatfs/go_fatfs_callbacks.go +++ b/fatfs/go_fatfs_callbacks.go @@ -98,7 +98,7 @@ func go_fatfs_get_fattime() (t uint32) { secondsPerUnit = 2 // FAT timestamps have two-second resolution ) - now := time.Now() + now := time.Now().UTC() year, month, day := now.Date() hour, minute, second := now.Hour(), now.Minute(), now.Second() t |= uint32(year-epochYear) << yearShift diff --git a/fatfs/go_fatfs_test.go b/fatfs/go_fatfs_test.go index 4454d69..897d126 100644 --- a/fatfs/go_fatfs_test.go +++ b/fatfs/go_fatfs_test.go @@ -18,38 +18,51 @@ const ( ) func TestStoredModificationTime(t *testing.T) { - fs, dev, unmount := createTestFS(t) - defer unmount() - if typ := Type(fs.fs.fs_type); typ != TypeFAT12 && typ != TypeFAT16 { - t.Fatalf("expected FAT12 or FAT16, got %s", typ) - } - before := time.Now().Truncate(2 * time.Second) - f, err := fs.OpenFile("TIME.TXT", os.O_CREATE|os.O_WRONLY|os.O_TRUNC) - check(t, err) - _, err = f.Write([]byte("timestamp")) - check(t, err) - check(t, f.Close()) - after := time.Now() + for _, loc := range []*time.Location{ + time.UTC, + time.FixedZone("UTC+14", 14*60*60), + time.FixedZone("UTC-12", -12*60*60), + } { + t.Run(loc.String(), func(t *testing.T) { + // Stored timestamps must be UTC regardless of the local time zone. + local := time.Local + time.Local = loc + t.Cleanup(func() { time.Local = local }) - root := make([]byte, int(fs.fs.n_rootdir)*32) - _, err = dev.ReadAt(root, int64(fs.fs.dirbase)*SectorSize) - check(t, err) - for offset := 0; offset < len(root); offset += 32 { - entry := root[offset : offset+32] - if string(entry[:11]) != "TIME TXT" { - continue - } - // FAT directory entries store modification time at bytes 22-25. - clock := binary.LittleEndian.Uint16(entry[22:24]) - date := binary.LittleEndian.Uint16(entry[24:26]) - got := time.Date(1980+int(date>>9), time.Month(date>>5&15), int(date&31), - int(clock>>11), int(clock>>5&63), int(clock&31)*2, 0, time.Local) - if got.Before(before) || got.After(after) { - t.Fatalf("stored modification time = %s, want between %s and %s", got, before, after) - } - return + fs, dev, unmount := createTestFS(t) + defer unmount() + if typ := Type(fs.fs.fs_type); typ != TypeFAT12 && typ != TypeFAT16 { + t.Fatalf("expected FAT12 or FAT16, got %s", typ) + } + before := time.Now().UTC().Truncate(2 * time.Second) + f, err := fs.OpenFile("TIME.TXT", os.O_CREATE|os.O_WRONLY|os.O_TRUNC) + check(t, err) + _, err = f.Write([]byte("timestamp")) + check(t, err) + check(t, f.Close()) + after := time.Now().UTC() + + root := make([]byte, int(fs.fs.n_rootdir)*32) + _, err = dev.ReadAt(root, int64(fs.fs.dirbase)*SectorSize) + check(t, err) + for offset := 0; offset < len(root); offset += 32 { + entry := root[offset : offset+32] + if string(entry[:11]) != "TIME TXT" { + continue + } + // FAT directory entries store modification time at bytes 22-25. + clock := binary.LittleEndian.Uint16(entry[22:24]) + date := binary.LittleEndian.Uint16(entry[24:26]) + got := time.Date(1980+int(date>>9), time.Month(date>>5&15), int(date&31), + int(clock>>11), int(clock>>5&63), int(clock&31)*2, 0, time.UTC) + if got.Before(before) || got.After(after) { + t.Fatalf("stored modification time = %s, want between %s and %s", got, before, after) + } + return + } + t.Fatal("TIME.TXT not found in root directory") + }) } - t.Fatal("TIME.TXT not found in root directory") } func TestType_String(t *testing.T) {