Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib/fs: Handle DST changes on FAT on Android (fixes #9227) #9231

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 29 additions & 16 deletions lib/fs/mtimefs.go
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,42 @@ 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.Equal(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.Equal(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
l.Debugln("mtimefs: detected and mitigated DST change for", name)
info = mtimeFileInfo{
FileInfo: info,
mtime: mtimeMapping.Virtual,
Expand Down Expand Up @@ -155,9 +167,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
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