Skip to content

Commit

Permalink
lib/fs: Handle DST changes on FAT on Android (fixes syncthing#9227)
Browse files Browse the repository at this point in the history
  • Loading branch information
calmh committed Nov 17, 2023
1 parent b184d46 commit d139f9e
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 16 deletions.
44 changes: 28 additions & 16 deletions lib/fs/mtimefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package fs
import (
"errors"
"time"

"github.com/syncthing/syncthing/lib/build"
)

// The database is where we store the virtual mtimes
Expand All @@ -23,6 +25,7 @@ type mtimeFS struct {
chtimes func(string, time.Time, time.Time) error
db database
caseInsensitive bool
hackFATDST bool
}

type MtimeFSOption func(*mtimeFS)
Expand Down Expand Up @@ -52,6 +55,7 @@ func (o *optionMtime) apply(fs Filesystem) Filesystem {
Filesystem: fs,
chtimes: fs.Chtimes, // for mocking it out in the tests
db: o.db,
hackFATDST: build.IsAndroid, // or can be set in tests
}
for _, opt := range o.options {
opt(f)
Expand Down Expand Up @@ -81,34 +85,41 @@ func (f *mtimeFS) Chtimes(name string, atime, mtime time.Time) error {
func (f *mtimeFS) Stat(name string) (FileInfo, error) {
info, err := f.Filesystem.Stat(name)
if err != nil {
f.db.Delete(name) // forget any mtime we might have had
return nil, err
}

mtimeMapping, err := f.load(name)
if err != nil {
return nil, err
}
if mtimeMapping.Real == info.ModTime() {
info = mtimeFileInfo{
FileInfo: info,
mtime: mtimeMapping.Virtual,
}
}

return info, nil
return f.mapped(name, info)
}

func (f *mtimeFS) Lstat(name string) (FileInfo, error) {
info, err := f.Filesystem.Lstat(name)
if err != nil {
f.db.Delete(name) // forget any mtime we might have had
return nil, err
}
return f.mapped(name, info)
}

func (f *mtimeFS) mapped(name string, info FileInfo) (FileInfo, error) {
mtimeMapping, err := f.load(name)
if err != nil {
return nil, err
}
if mtimeMapping.Real == info.ModTime() {

if mtimeMapping.Real.IsZero() {
// No entry for this file.
if f.hackFATDST {
// Save one for the future, so we can detect DST changes below.
f.save(name, info.ModTime(), info.ModTime())
}
return info, nil
}

if mtime := info.ModTime(); mtime.Equal(mtimeMapping.Real) ||
f.hackFATDST &&
mtime.Nanosecond() == 0 && // modtime has second precision or worse
mtime.Second()%2 == 0 && // modtime is even
mtime.Sub(mtimeMapping.Real).Abs() == time.Hour { // time is off by precisely one hour
info = mtimeFileInfo{
FileInfo: info,
mtime: mtimeMapping.Virtual,
Expand Down Expand Up @@ -155,9 +166,10 @@ func (f *mtimeFS) save(name string, real, virtual time.Time) {
name = UnicodeLowercaseNormalized(name)
}

if real.Equal(virtual) {
if !f.hackFATDST && real.Equal(virtual) {
// If the virtual time and the real on disk time are equal we don't
// need to store anything.
// need to store anything. Except on Android, where we keep it
// around to catch DST changes.
f.db.Delete(name)
return
}
Expand Down
50 changes: 50 additions & 0 deletions lib/fs/mtimefs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,56 @@ func TestMtimeFS(t *testing.T) {
}
}

func TestHackFATDST(t *testing.T) {
td := t.TempDir()

testFile := filepath.Join(td, "file")
os.WriteFile(testFile, []byte("hello"), 0o644)

// A timestamp that looks like it belongs on a FAT filesystem;
// two-second precision only.
ts := time.Now().Truncate(2 * time.Second)
if err := os.Chtimes(testFile, ts, ts); err != nil {
t.Fatal(err)
}

mtimefs := newMtimeFS(td, make(mapStore))
mtimefs.hackFATDST = true

// Check the file; it should have its original timestamp.
if info, err := mtimefs.Lstat("file"); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if !info.ModTime().Equal(ts) {
t.Errorf("Unexpected time mismatch; %v != %v", info.ModTime(), ts)
}

// Change the timestamp by precisely one hour, simulating a DST change.
dst := ts.Add(time.Hour)
if err := os.Chtimes(testFile, dst, dst); err != nil {
t.Fatal(err)
}

// Check the file; it should still have its original timestamp.
if info, err := mtimefs.Lstat("file"); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if !info.ModTime().Equal(ts) {
t.Errorf("Unexpected time mismatch; %v != %v", info.ModTime(), ts)
}

// Instead, change the timestamp by one hour plus a second.
other := ts.Add(time.Hour).Add(time.Second)
if err := os.Chtimes(testFile, other, other); err != nil {
t.Fatal(err)
}

// Check the file; the new timestamp should shine through.
if info, err := mtimefs.Lstat("file"); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if !info.ModTime().Equal(other) {
t.Errorf("Unexpected time mismatch; %v != %v", info.ModTime(), other)
}
}

func TestMtimeFSWalk(t *testing.T) {
dir := t.TempDir()

Expand Down

0 comments on commit d139f9e

Please sign in to comment.