// 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 uint32 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{RawInvalid: false}, b: FileInfo{RawInvalid: true}, 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{RawInvalid: true}, b: FileInfo{RawInvalid: true}, eq: true, }, { a: FileInfo{LocalFlags: FlagLocalUnsupported}, b: FileInfo{LocalFlags: FlagLocalUnsupported}, eq: true, }, { a: FileInfo{RawInvalid: true}, 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: "a"}, b: FileInfo{Type: FileInfoTypeSymlink, SymlinkTarget: "b"}, eq: false, }, // ... but not for non-symlinks { a: FileInfo{Type: FileInfoTypeFile, SymlinkTarget: "a"}, b: FileInfo{Type: FileInfoTypeFile, SymlinkTarget: "b"}, eq: true, }, } 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) } } }