From 429cdfb6dce2f84a25cc14e057b9b505f3fb3436 Mon Sep 17 00:00:00 2001 From: Hans van den Bogert Date: Fri, 10 Oct 2025 08:46:57 +0200 Subject: [PATCH 1/2] test: add modtime write testcase --- memfs/memory_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/memfs/memory_test.go b/memfs/memory_test.go index 96391c7..ccec142 100644 --- a/memfs/memory_test.go +++ b/memfs/memory_test.go @@ -65,6 +65,11 @@ func TestModTime(t *testing.T) { // new calls to ModTime() retain existing mod time. assert.Equal(t, modtime, fi1a.ModTime()) + + // Writing to to file should change the mod time + util.WriteFile(fs, "/file1", []byte{0}, 0o666) + fi1c, err := fs.Stat("/file1") + assert.NotEqual(t, modtime, fi1c.ModTime()) } func TestNegativeOffsets(t *testing.T) { From 7f3cb2e27b2af8f13e6bd8823c1ae09f6d566ea5 Mon Sep 17 00:00:00 2001 From: Hans van den Bogert Date: Sat, 11 Oct 2025 14:38:20 +0200 Subject: [PATCH 2/2] fix: modtimes should be part of the content, not of the file Otherwise f.Duplicate() copies the modTime. Subsequent modtime mutations are then on this copy instead of the original file's modtime. Effectively any mutation to a modtime can never be seen by other callers of f.Stat() --- memfs/file.go | 11 +++++------ memfs/memory_test.go | 6 ++++-- memfs/storage.go | 12 +++++++----- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/memfs/file.go b/memfs/file.go index 7ebe76b..801ab43 100644 --- a/memfs/file.go +++ b/memfs/file.go @@ -17,7 +17,6 @@ type file struct { position int64 flag int mode os.FileMode - modTime time.Time isClosed bool } @@ -81,7 +80,6 @@ func (f *file) WriteAt(p []byte, off int64) (int, error) { return 0, errors.New("write not supported") } - f.modTime = time.Now() n, err := f.content.WriteAt(p, off) f.position = off + int64(n) @@ -113,7 +111,6 @@ func (f *file) Duplicate(filename string, mode fs.FileMode, flag int) billy.File content: f.content, mode: mode, flag: flag, - modTime: f.modTime, } if isTruncate(flag) { @@ -132,7 +129,7 @@ func (f *file) Stat() (os.FileInfo, error) { name: f.Name(), mode: f.mode, size: f.content.Len(), - modTime: f.modTime, + modTime: f.content.modTime, }, nil } @@ -178,8 +175,9 @@ func (*fileInfo) Sys() interface{} { } type content struct { - name string - bytes []byte + name string + bytes []byte + modTime time.Time m sync.RWMutex } @@ -205,6 +203,7 @@ func (c *content) WriteAt(p []byte, off int64) (int, error) { if len(c.bytes) < prev { c.bytes = c.bytes[:prev] } + c.modTime = time.Now() c.m.Unlock() return len(p), nil diff --git a/memfs/memory_test.go b/memfs/memory_test.go index ccec142..1c7d1e9 100644 --- a/memfs/memory_test.go +++ b/memfs/memory_test.go @@ -66,9 +66,11 @@ func TestModTime(t *testing.T) { // new calls to ModTime() retain existing mod time. assert.Equal(t, modtime, fi1a.ModTime()) - // Writing to to file should change the mod time - util.WriteFile(fs, "/file1", []byte{0}, 0o666) + // Writing to file should change the mod time + err = util.WriteFile(fs, "/file1", []byte{0}, 0o666) + require.NoError(t, err) fi1c, err := fs.Stat("/file1") + require.NoError(t, err) assert.NotEqual(t, modtime, fi1c.ModTime()) } diff --git a/memfs/storage.go b/memfs/storage.go index f939e56..f7c0d1e 100644 --- a/memfs/storage.go +++ b/memfs/storage.go @@ -44,11 +44,13 @@ func (s *storage) New(path string, mode fs.FileMode, flag int) (*file, error) { name := filepath.Base(path) f := &file{ - name: name, - content: &content{name: name}, - mode: mode, - flag: flag, - modTime: time.Now(), + name: name, + content: &content{ + name: name, + modTime: time.Now(), + }, + mode: mode, + flag: flag, } s.mf.Lock()