mirror of
https://github.com/restic/restic.git
synced 2024-08-31 16:47:25 +02:00
In principle, the JSON format of Tree objects is extensible without requiring a format change. In order to not loose information just play it safe and reject rewriting trees for which we could loose data.
223 lines
5.0 KiB
Go
223 lines
5.0 KiB
Go
package walker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/pkg/errors"
|
|
"github.com/restic/restic/internal/restic"
|
|
)
|
|
|
|
// WritableTreeMap also support saving
|
|
type WritableTreeMap struct {
|
|
TreeMap
|
|
}
|
|
|
|
func (t WritableTreeMap) SaveBlob(ctx context.Context, tpe restic.BlobType, buf []byte, id restic.ID, storeDuplicate bool) (newID restic.ID, known bool, size int, err error) {
|
|
if tpe != restic.TreeBlob {
|
|
return restic.ID{}, false, 0, errors.New("can only save trees")
|
|
}
|
|
|
|
if id.IsNull() {
|
|
id = restic.Hash(buf)
|
|
}
|
|
_, ok := t.TreeMap[id]
|
|
if ok {
|
|
return id, false, 0, nil
|
|
}
|
|
|
|
t.TreeMap[id] = append([]byte{}, buf...)
|
|
return id, true, len(buf), nil
|
|
}
|
|
|
|
func (t WritableTreeMap) Dump() {
|
|
for k, v := range t.TreeMap {
|
|
fmt.Printf("%v: %v", k, string(v))
|
|
}
|
|
}
|
|
|
|
type checkRewriteFunc func(t testing.TB) (visitor TreeFilterVisitor, final func(testing.TB))
|
|
|
|
// checkRewriteItemOrder ensures that the order of the 'path' arguments is the one passed in as 'want'.
|
|
func checkRewriteItemOrder(want []string) checkRewriteFunc {
|
|
pos := 0
|
|
return func(t testing.TB) (visitor TreeFilterVisitor, final func(testing.TB)) {
|
|
vis := TreeFilterVisitor{
|
|
SelectByName: func(path string) bool {
|
|
if pos >= len(want) {
|
|
t.Errorf("additional unexpected path found: %v", path)
|
|
return false
|
|
}
|
|
|
|
if path != want[pos] {
|
|
t.Errorf("wrong path found, want %q, got %q", want[pos], path)
|
|
}
|
|
pos++
|
|
return true
|
|
},
|
|
}
|
|
|
|
final = func(t testing.TB) {
|
|
if pos != len(want) {
|
|
t.Errorf("not enough items returned, want %d, got %d", len(want), pos)
|
|
}
|
|
}
|
|
|
|
return vis, final
|
|
}
|
|
}
|
|
|
|
// checkRewriteSkips excludes nodes if path is in skipFor, it checks that all excluded entries are printed.
|
|
func checkRewriteSkips(skipFor map[string]struct{}, want []string) checkRewriteFunc {
|
|
var pos int
|
|
printed := make(map[string]struct{})
|
|
|
|
return func(t testing.TB) (visitor TreeFilterVisitor, final func(testing.TB)) {
|
|
vis := TreeFilterVisitor{
|
|
SelectByName: func(path string) bool {
|
|
if pos >= len(want) {
|
|
t.Errorf("additional unexpected path found: %v", path)
|
|
return false
|
|
}
|
|
|
|
if path != want[pos] {
|
|
t.Errorf("wrong path found, want %q, got %q", want[pos], path)
|
|
}
|
|
pos++
|
|
|
|
_, ok := skipFor[path]
|
|
return !ok
|
|
},
|
|
PrintExclude: func(s string) {
|
|
if _, ok := printed[s]; ok {
|
|
t.Errorf("path was already printed %v", s)
|
|
}
|
|
printed[s] = struct{}{}
|
|
},
|
|
}
|
|
|
|
final = func(t testing.TB) {
|
|
if !cmp.Equal(skipFor, printed) {
|
|
t.Errorf("unexpected paths skipped: %s", cmp.Diff(skipFor, printed))
|
|
}
|
|
if pos != len(want) {
|
|
t.Errorf("not enough items returned, want %d, got %d", len(want), pos)
|
|
}
|
|
}
|
|
|
|
return vis, final
|
|
}
|
|
}
|
|
|
|
func TestRewriter(t *testing.T) {
|
|
var tests = []struct {
|
|
tree TestTree
|
|
newTree TestTree
|
|
check checkRewriteFunc
|
|
}{
|
|
{ // don't change
|
|
tree: TestTree{
|
|
"foo": TestFile{},
|
|
"subdir": TestTree{
|
|
"subfile": TestFile{},
|
|
},
|
|
},
|
|
check: checkRewriteItemOrder([]string{
|
|
"/foo",
|
|
"/subdir",
|
|
"/subdir/subfile",
|
|
}),
|
|
},
|
|
{ // exclude file
|
|
tree: TestTree{
|
|
"foo": TestFile{},
|
|
"subdir": TestTree{
|
|
"subfile": TestFile{},
|
|
},
|
|
},
|
|
newTree: TestTree{
|
|
"foo": TestFile{},
|
|
"subdir": TestTree{},
|
|
},
|
|
check: checkRewriteSkips(
|
|
map[string]struct{}{
|
|
"/subdir/subfile": {},
|
|
},
|
|
[]string{
|
|
"/foo",
|
|
"/subdir",
|
|
"/subdir/subfile",
|
|
},
|
|
),
|
|
},
|
|
{ // exclude dir
|
|
tree: TestTree{
|
|
"foo": TestFile{},
|
|
"subdir": TestTree{
|
|
"subfile": TestFile{},
|
|
},
|
|
},
|
|
newTree: TestTree{
|
|
"foo": TestFile{},
|
|
},
|
|
check: checkRewriteSkips(
|
|
map[string]struct{}{
|
|
"/subdir": {},
|
|
},
|
|
[]string{
|
|
"/foo",
|
|
"/subdir",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
repo, root := BuildTreeMap(test.tree)
|
|
if test.newTree == nil {
|
|
test.newTree = test.tree
|
|
}
|
|
expRepo, expRoot := BuildTreeMap(test.newTree)
|
|
modrepo := WritableTreeMap{repo}
|
|
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
defer cancel()
|
|
|
|
vis, last := test.check(t)
|
|
newRoot, err := FilterTree(ctx, modrepo, "/", root, &vis)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
last(t)
|
|
|
|
// verifying against the expected tree root also implicitly checks the structural integrity
|
|
if newRoot != expRoot {
|
|
t.Error("hash mismatch")
|
|
fmt.Println("Got")
|
|
modrepo.Dump()
|
|
fmt.Println("Expected")
|
|
WritableTreeMap{expRepo}.Dump()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRewriterFailOnUnknownFields(t *testing.T) {
|
|
tm := WritableTreeMap{TreeMap{}}
|
|
node := []byte(`{"nodes":[{"name":"subfile","type":"file","mtime":"0001-01-01T00:00:00Z","atime":"0001-01-01T00:00:00Z","ctime":"0001-01-01T00:00:00Z","uid":0,"gid":0,"content":null,"unknown_field":42}]}`)
|
|
id := restic.Hash(node)
|
|
tm.TreeMap[id] = node
|
|
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
defer cancel()
|
|
// use nil visitor to crash if the tree loading works unexpectedly
|
|
_, err := FilterTree(ctx, tm, "/", id, nil)
|
|
|
|
if err == nil {
|
|
t.Error("missing error on unknown field")
|
|
}
|
|
}
|