mirror of
https://github.com/syncthing/syncthing.git
synced 2026-05-14 10:06:52 -04:00
Only Require either matching UID & GID OR matching Names. If the 2 devices have a different Name => UID mapping, they can never be totaly equal. Therefore when syncing we try matching the Name and fall back to the UID. However when scanning for changes we currently require both the Name & UID to match. This leads to forever having out of sync files back and forth, or local additions when receive only. This patch does not change the sending behavoir. It only change what we decide is equal for exisiting files with mismapped Name => UID, The added testcases show the change: Test 1,5,6 are the same as current. Test 2,3 Are what change with this patch (from false to true). Test 4 is a subset of test 2 they is currently special cased as true, which does not chnage. Co-authored-by: Jakob Borg <jakob@kastelo.net>
330 lines
9.8 KiB
Go
330 lines
9.8 KiB
Go
// Copyright (C) 2014 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 protocol
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"testing"
|
|
|
|
"github.com/syncthing/syncthing/lib/build"
|
|
)
|
|
|
|
func TestLocalFlagBits(t *testing.T) {
|
|
var f FileInfo
|
|
if f.IsIgnored() || f.MustRescan() || f.IsInvalid() {
|
|
t.Error("file should have no weird bits set by default")
|
|
}
|
|
|
|
f.SetIgnored()
|
|
if !f.IsIgnored() || f.MustRescan() || !f.IsInvalid() {
|
|
t.Error("file should be ignored and invalid")
|
|
}
|
|
|
|
f.SetMustRescan()
|
|
if f.IsIgnored() || !f.MustRescan() || !f.IsInvalid() {
|
|
t.Error("file should be must-rescan and invalid")
|
|
}
|
|
|
|
f.SetUnsupported()
|
|
if f.IsIgnored() || f.MustRescan() || !f.IsInvalid() {
|
|
t.Error("file should be invalid")
|
|
}
|
|
}
|
|
|
|
func TestIsEquivalent(t *testing.T) {
|
|
b := func(v bool) *bool {
|
|
return &v
|
|
}
|
|
|
|
type testCase struct {
|
|
a FileInfo
|
|
b FileInfo
|
|
ignPerms *bool // nil means should not matter, we'll test both variants
|
|
ignBlocks *bool
|
|
ignFlags FlagLocal
|
|
eq bool
|
|
}
|
|
cases := []testCase{
|
|
// Empty FileInfos are equivalent
|
|
{eq: true},
|
|
|
|
// Various basic attributes, all of which cause inequality when
|
|
// they differ
|
|
{
|
|
a: FileInfo{Name: "foo"},
|
|
b: FileInfo{Name: "bar"},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{Type: FileInfoTypeFile},
|
|
b: FileInfo{Type: FileInfoTypeDirectory},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{Size: 1234},
|
|
b: FileInfo{Size: 2345},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{Deleted: false},
|
|
b: FileInfo{Deleted: true},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: 0},
|
|
b: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{ModifiedS: 1234},
|
|
b: FileInfo{ModifiedS: 2345},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{ModifiedNs: 1234},
|
|
b: FileInfo{ModifiedNs: 2345},
|
|
eq: false,
|
|
},
|
|
|
|
// Special handling of local flags and invalidity. "MustRescan"
|
|
// files are never equivalent to each other. Otherwise, equivalence
|
|
// is based just on whether the file becomes IsInvalid() or not, not
|
|
// the specific reason or flag bits.
|
|
{
|
|
a: FileInfo{LocalFlags: FlagLocalMustRescan},
|
|
b: FileInfo{LocalFlags: FlagLocalMustRescan},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
|
|
b: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: FlagLocalUnsupported},
|
|
b: FileInfo{LocalFlags: FlagLocalUnsupported},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
|
|
b: FileInfo{LocalFlags: FlagLocalUnsupported},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: 0},
|
|
b: FileInfo{LocalFlags: FlagLocalReceiveOnly},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: 0},
|
|
b: FileInfo{LocalFlags: FlagLocalReceiveOnly},
|
|
ignFlags: FlagLocalReceiveOnly,
|
|
eq: true,
|
|
},
|
|
|
|
// Difference in blocks is not OK
|
|
{
|
|
a: FileInfo{Blocks: []BlockInfo{{Hash: []byte{1, 2, 3, 4}}}},
|
|
b: FileInfo{Blocks: []BlockInfo{{Hash: []byte{2, 3, 4, 5}}}},
|
|
ignBlocks: b(false),
|
|
eq: false,
|
|
},
|
|
|
|
// ... unless we say it is
|
|
{
|
|
a: FileInfo{Blocks: []BlockInfo{{Hash: []byte{1, 2, 3, 4}}}},
|
|
b: FileInfo{Blocks: []BlockInfo{{Hash: []byte{2, 3, 4, 5}}}},
|
|
ignBlocks: b(true),
|
|
eq: true,
|
|
},
|
|
|
|
// Difference in permissions is not OK.
|
|
{
|
|
a: FileInfo{Permissions: 0o444},
|
|
b: FileInfo{Permissions: 0o666},
|
|
ignPerms: b(false),
|
|
eq: false,
|
|
},
|
|
|
|
// ... unless we say it is
|
|
{
|
|
a: FileInfo{Permissions: 0o666},
|
|
b: FileInfo{Permissions: 0o444},
|
|
ignPerms: b(true),
|
|
eq: true,
|
|
},
|
|
|
|
// These attributes are not checked at all
|
|
{
|
|
a: FileInfo{NoPermissions: false},
|
|
b: FileInfo{NoPermissions: true},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{Version: Vector{Counters: []Counter{{ID: 1, Value: 42}}}},
|
|
b: FileInfo{Version: Vector{Counters: []Counter{{ID: 42, Value: 1}}}},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{Sequence: 1},
|
|
b: FileInfo{Sequence: 2},
|
|
eq: true,
|
|
},
|
|
|
|
// The block size is not checked (but this would fail the blocks
|
|
// check in real world)
|
|
{
|
|
a: FileInfo{RawBlockSize: 1},
|
|
b: FileInfo{RawBlockSize: 2},
|
|
eq: true,
|
|
},
|
|
|
|
// The symlink target is checked for symlinks
|
|
{
|
|
a: FileInfo{Type: FileInfoTypeSymlink, SymlinkTarget: []byte("a")},
|
|
b: FileInfo{Type: FileInfoTypeSymlink, SymlinkTarget: []byte("b")},
|
|
eq: false,
|
|
},
|
|
|
|
// ... but not for non-symlinks
|
|
{
|
|
a: FileInfo{Type: FileInfoTypeFile, SymlinkTarget: []byte("a")},
|
|
b: FileInfo{Type: FileInfoTypeFile, SymlinkTarget: []byte("b")},
|
|
eq: true,
|
|
},
|
|
// Unix Ownership should be the same
|
|
{
|
|
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
|
|
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
|
|
eq: true,
|
|
},
|
|
// ... but matching ID is enough
|
|
{
|
|
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
|
|
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "B", GroupName: "B", UID: 1000, GID: 1000}}},
|
|
eq: true,
|
|
},
|
|
// ... or matching name
|
|
{
|
|
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
|
|
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1001, GID: 1001}}},
|
|
eq: true,
|
|
},
|
|
// ... or empty name
|
|
{
|
|
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
|
|
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "", GroupName: "", UID: 1000, GID: 1000}}},
|
|
eq: true,
|
|
},
|
|
// ... but not different ownership
|
|
{
|
|
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
|
|
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "B", GroupName: "B", UID: 1001, GID: 1001}}},
|
|
eq: false,
|
|
},
|
|
// or missing ownership
|
|
{
|
|
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
|
|
b: FileInfo{Platform: PlatformData{}},
|
|
eq: false,
|
|
},
|
|
}
|
|
|
|
if build.IsWindows {
|
|
// On windows we only check the user writable bit of the permission
|
|
// set, so these are equivalent.
|
|
cases = append(cases, testCase{
|
|
a: FileInfo{Permissions: 0o777},
|
|
b: FileInfo{Permissions: 0o600},
|
|
ignPerms: b(false),
|
|
eq: true,
|
|
})
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
// Check the standard attributes with all permutations of the
|
|
// special ignore flags, unless the value of those flags are given
|
|
// in the tests.
|
|
for _, ignPerms := range []bool{true, false} {
|
|
for _, ignBlocks := range []bool{true, false} {
|
|
if tc.ignPerms != nil && *tc.ignPerms != ignPerms {
|
|
continue
|
|
}
|
|
if tc.ignBlocks != nil && *tc.ignBlocks != ignBlocks {
|
|
continue
|
|
}
|
|
|
|
if res := tc.a.isEquivalent(tc.b, FileInfoComparison{IgnorePerms: ignPerms, IgnoreBlocks: ignBlocks, IgnoreFlags: tc.ignFlags}); res != tc.eq {
|
|
t.Errorf("Case %d:\na: %v\nb: %v\na.IsEquivalent(b, %v, %v) => %v, expected %v", i, tc.a, tc.b, ignPerms, ignBlocks, res, tc.eq)
|
|
}
|
|
if res := tc.b.isEquivalent(tc.a, FileInfoComparison{IgnorePerms: ignPerms, IgnoreBlocks: ignBlocks, IgnoreFlags: tc.ignFlags}); res != tc.eq {
|
|
t.Errorf("Case %d:\na: %v\nb: %v\nb.IsEquivalent(a, %v, %v) => %v, expected %v", i, tc.a, tc.b, ignPerms, ignBlocks, res, tc.eq)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSha256OfEmptyBlock(t *testing.T) {
|
|
// every block size should have a correct entry in sha256OfEmptyBlock
|
|
for blockSize := MinBlockSize; blockSize <= MaxBlockSize; blockSize *= 2 {
|
|
expected := sha256.Sum256(make([]byte, blockSize))
|
|
if sha256OfEmptyBlock[blockSize] != expected {
|
|
t.Error("missing or wrong hash for block of size", blockSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBlocksEqual(t *testing.T) {
|
|
blocksOne := []BlockInfo{{Hash: []byte{1, 2, 3, 4}}}
|
|
blocksTwo := []BlockInfo{{Hash: []byte{5, 6, 7, 8}}}
|
|
hashOne := []byte{42, 42, 42, 42}
|
|
hashTwo := []byte{29, 29, 29, 29}
|
|
|
|
cases := []struct {
|
|
b1 []BlockInfo
|
|
h1 []byte
|
|
b2 []BlockInfo
|
|
h2 []byte
|
|
eq bool
|
|
}{
|
|
{blocksOne, hashOne, blocksOne, hashOne, true}, // everything equal
|
|
{blocksOne, hashOne, blocksTwo, hashTwo, false}, // nothing equal
|
|
{blocksOne, hashOne, blocksOne, nil, true}, // blocks compared
|
|
{blocksOne, nil, blocksOne, nil, true}, // blocks compared
|
|
{blocksOne, nil, blocksTwo, nil, false}, // blocks compared
|
|
{blocksOne, hashOne, blocksTwo, hashOne, true}, // hashes equal, blocks not looked at
|
|
{blocksOne, hashOne, blocksOne, hashTwo, true}, // hashes different, blocks compared
|
|
{blocksOne, hashOne, blocksTwo, hashTwo, false}, // hashes different, blocks compared
|
|
{blocksOne, hashOne, nil, nil, false}, // blocks is different from no blocks
|
|
{blocksOne, nil, nil, nil, false}, // blocks is different from no blocks
|
|
{nil, hashOne, nil, nil, true}, // nil blocks are equal, even of one side has a hash
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
f1 := FileInfo{Blocks: tc.b1, BlocksHash: tc.h1}
|
|
f2 := FileInfo{Blocks: tc.b2, BlocksHash: tc.h2}
|
|
|
|
if !f1.BlocksEqual(f1) {
|
|
t.Error("f1 is always equal to itself", f1)
|
|
}
|
|
if !f2.BlocksEqual(f2) {
|
|
t.Error("f2 is always equal to itself", f2)
|
|
}
|
|
if res := f1.BlocksEqual(f2); res != tc.eq {
|
|
t.Log("f1", f1.BlocksHash, f1.Blocks)
|
|
t.Log("f2", f2.BlocksHash, f2.Blocks)
|
|
t.Errorf("f1.BlocksEqual(f2) == %v but should be %v", res, tc.eq)
|
|
}
|
|
if res := f2.BlocksEqual(f1); res != tc.eq {
|
|
t.Log("f1", f1.BlocksHash, f1.Blocks)
|
|
t.Log("f2", f2.BlocksHash, f2.Blocks)
|
|
t.Errorf("f2.BlocksEqual(f1) == %v but should be %v", res, tc.eq)
|
|
}
|
|
}
|
|
}
|