mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 19:08:58 +00:00
ac8b3342ac
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
237 lines
6.7 KiB
Go
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)
|
|
}
|