syncthing/lib/fs/filesystem_test.go
Simon Frei ac8b3342ac
chore(fs): only cache the cache for case FS, not the entire FS (#9701)
This would have addressed a recent issue that arose when re-ordering our
"filesystem layers". Specifically moving the caseFilesystem to the
outermost layer. The previous cache included the filesystem, and as such
all the layers below. This isn't desirable (to put it mildly), as you
can create different variants of filesystems with different layers for
the same path and options. Concretely this did happen with the mtime
layer, which isn't always present. A test for the mtime related breakage
was added in #9687, and I intend to redo the caseFilesystem reordering
after this.

Ref: #9677
Followup to: #9687
2024-09-12 20:35:21 +02:00

237 lines
6.7 KiB
Go

// Copyright (C) 2017 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package fs
import (
"fmt"
"path/filepath"
"testing"
"time"
"github.com/syncthing/syncthing/lib/build"
)
func TestIsInternal(t *testing.T) {
cases := []struct {
file string
internal bool
}{
{".stfolder", true},
{".stignore", true},
{".stversions", true},
{".stfolder/foo", true},
{".stignore/foo", true},
{".stversions/foo", true},
{".stfolderfoo", false},
{".stignorefoo", false},
{".stversionsfoo", false},
{"foo.stfolder", false},
{"foo.stignore", false},
{"foo.stversions", false},
{"foo/.stfolder", false},
{"foo/.stignore", false},
{"foo/.stversions", false},
}
for _, tc := range cases {
res := IsInternal(filepath.FromSlash(tc.file))
if res != tc.internal {
t.Errorf("Unexpected result: IsInternal(%q): %v should be %v", tc.file, res, tc.internal)
}
}
}
func TestCanonicalize(t *testing.T) {
type testcase struct {
path string
expected string
ok bool
}
cases := []testcase{
// Valid cases
{"/bar", "bar", true},
{"/bar/baz", "bar/baz", true},
{"bar", "bar", true},
{"bar/baz", "bar/baz", true},
// Not escape attempts, but oddly formatted relative paths
{"", ".", true},
{"/", ".", true},
{"/..", ".", true},
{"./bar", "bar", true},
{"./bar/baz", "bar/baz", true},
{"bar/../baz", "baz", true},
{"/bar/../baz", "baz", true},
{"./bar/../baz", "baz", true},
// Results in an allowed path, but does it by probing. Disallowed.
{"../foo", "", false},
{"../foo/bar", "", false},
{"../foo/bar", "", false},
{"../../baz/foo/bar", "", false},
{"bar/../../foo/bar", "", false},
{"bar/../../../baz/foo/bar", "", false},
// Escape attempts.
{"..", "", false},
{"../", "", false},
{"../bar", "", false},
{"../foobar", "", false},
{"bar/../../quux/baz", "", false},
}
for _, tc := range cases {
res, err := Canonicalize(tc.path)
if tc.ok {
if err != nil {
t.Errorf("Unexpected error for Canonicalize(%q): %v", tc.path, err)
continue
}
exp := filepath.FromSlash(tc.expected)
if res != exp {
t.Errorf("Unexpected result for Canonicalize(%q): %q != expected %q", tc.path, res, exp)
}
} else if err == nil {
t.Errorf("Unexpected pass for Canonicalize(%q) => %q", tc.path, res)
continue
}
}
}
func TestFileModeString(t *testing.T) {
var fm FileMode = 0777
exp := "-rwxrwxrwx"
if fm.String() != exp {
t.Fatalf("Got %v, expected %v", fm.String(), exp)
}
}
func TestIsParent(t *testing.T) {
test := func(path, parent string, expected bool) {
t.Helper()
path = filepath.FromSlash(path)
parent = filepath.FromSlash(parent)
if res := IsParent(path, parent); res != expected {
t.Errorf(`Unexpected result: IsParent("%v", "%v"): %v should be %v`, path, parent, res, expected)
}
}
testBoth := func(path, parent string, expected bool) {
t.Helper()
test(path, parent, expected)
if build.IsWindows {
test("C:/"+path, "C:/"+parent, expected)
} else {
test("/"+path, "/"+parent, expected)
}
}
// rel - abs
for _, parent := range []string{"/", "/foo", "/foo/bar"} {
for _, path := range []string{"", ".", "foo", "foo/bar", "bas", "bas/baz"} {
if build.IsWindows {
parent = "C:/" + parent
}
test(parent, path, false)
test(path, parent, false)
}
}
// equal
for i, path := range []string{"/", "/foo", "/foo/bar", "", ".", "foo", "foo/bar"} {
if i < 3 && build.IsWindows {
path = "C:" + path
}
test(path, path, false)
}
test("", ".", false)
test(".", "", false)
for _, parent := range []string{"", "."} {
for _, path := range []string{"foo", "foo/bar"} {
test(path, parent, true)
test(parent, path, false)
}
}
for _, parent := range []string{"foo", "foo/bar"} {
for _, path := range []string{"bar", "bar/foo"} {
testBoth(path, parent, false)
testBoth(parent, path, false)
}
}
for _, parent := range []string{"foo", "foo/bar"} {
for _, path := range []string{"foo/bar/baz", "foo/bar/baz/bas"} {
testBoth(path, parent, true)
testBoth(parent, path, false)
if build.IsWindows {
test("C:/"+path, "D:/"+parent, false)
}
}
}
}
// Reproduces issue 9677:
// The combination of caching the entire case FS and moving the case FS to be
// the outmost layer of the FS lead to the mtime FS disappearing. This is
// because in many places we intentionally create the filesystem without access
// to the DB and thus without the mtime FS layer. With the case FS layer
// outside, all the inner layers are also cached - notable without an mtime FS
// layer. Later when we do try to create an FS with DB/mtime FS, we still get
// the cached FS without mtime FS.
func TestRepro9677MissingMtimeFS(t *testing.T) {
mtimeDB := make(mapStore)
name := "Testfile"
nameLower := UnicodeLowercaseNormalized(name)
testTime := time.Unix(1723491493, 123456789)
// Create a file with an mtime FS entry
firstFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB))
// Create a file, set its mtime and check that we get the expected mtime when stat-ing.
file, err := firstFS.Create(name)
if err != nil {
t.Fatal(err)
}
file.Close()
err = firstFS.Chtimes(name, testTime, testTime)
if err != nil {
t.Fatal(err)
}
checkMtime := func(fs Filesystem) {
t.Helper()
info, err := fs.Lstat(name)
if err != nil {
t.Fatal(err)
}
if !info.ModTime().Equal(testTime) {
t.Errorf("Expected mtime %v for %v, got %v", testTime, name, info.ModTime())
}
info, err = fs.Lstat(nameLower)
if !IsErrCaseConflict(err) {
t.Errorf("Expected case-conflict error, got %v", err)
}
}
checkMtime(firstFS)
// Now syncthing gets upgraded (or even just restarted), which resets the
// case FS registry as it lives in memory.
globalCaseFilesystemRegistry = caseFilesystemRegistry{caseCaches: make(map[fskey]*caseCache)}
// This time we first create some filesystem without a database and thus no
// mtime-FS, which is used in various places outside of the folder code. We
// aren't actually going to do anything, this just adds an entry to the
// caseFS cache. And that's the crucial bit: In the broken case this test is
// reproducing, it will add the FS without mtime-FS, so all future FSes will
// be without mtime, even if requested:
NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{})
newFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB))
checkMtime(newFS)
}