diff --git a/pkg/filetree/filetree.go b/pkg/filetree/filetree.go index ecce35d5..33cfdb74 100644 --- a/pkg/filetree/filetree.go +++ b/pkg/filetree/filetree.go @@ -45,7 +45,14 @@ func New() *FileTree { } } -// Copy returns a Copy of the current FileTree. +// Clone returns a shallow Copy of the current FileTree. +func (t *FileTree) Clone() (ReadWriter, error) { + ct := New() + ct.tree = t.tree.Clone() + return ct, nil +} + +// Copy returns a deep Copy of the current FileTree. func (t *FileTree) Copy() (ReadWriter, error) { ct := New() ct.tree = t.tree.Copy() @@ -781,7 +788,8 @@ func (t *FileTree) Walk(fn func(path file.Path, f filenode.FileNode) error, cond // Merge takes the given Tree and combines it with the current Tree, preferring files in the other Tree if there // are path conflicts. This is the basis function for squashing (where the current Tree is the bottom Tree and the -// given Tree is the top Tree). +// given Tree is the top Tree). Note: existing nodes are not mutated during this operation, they are replaced with +// a node from the other tree. // //nolint:gocognit,funlen func (t *FileTree) Merge(upper Reader) error { diff --git a/pkg/filetree/interfaces.go b/pkg/filetree/interfaces.go index 9d813fc4..f7199305 100644 --- a/pkg/filetree/interfaces.go +++ b/pkg/filetree/interfaces.go @@ -29,6 +29,7 @@ type PathReader interface { type Copier interface { Copy() (ReadWriter, error) + Clone() (ReadWriter, error) } type Walker interface { diff --git a/pkg/filetree/union_filetree.go b/pkg/filetree/union_filetree.go index c1606a42..c2e69748 100644 --- a/pkg/filetree/union_filetree.go +++ b/pkg/filetree/union_filetree.go @@ -21,14 +21,18 @@ func (u *UnionFileTree) Squash() (ReadWriter, error) { case 0: return New(), nil case 1: - return u.trees[0].Copy() + // important: use clone over copy to reduce memory footprint. If callers need a distinct tree they can call + // copy after the fact. + return u.trees[0].Clone() } var squashedTree ReadWriter var err error for layerIdx, refTree := range u.trees { if layerIdx == 0 { - squashedTree, err = refTree.Copy() + // important: use clone over copy to reduce memory footprint. If callers need a distinct tree they can call + // copy after the fact. + squashedTree, err = refTree.Clone() if err != nil { return nil, err } diff --git a/pkg/tree/tree.go b/pkg/tree/tree.go index 0dc04557..5e875f31 100644 --- a/pkg/tree/tree.go +++ b/pkg/tree/tree.go @@ -2,6 +2,7 @@ package tree import ( "fmt" + "maps" "github.com/anchore/stereoscope/pkg/tree/node" ) @@ -22,6 +23,16 @@ func NewTree() *Tree { } } +// Clone returns a shallow copy of the Tree. +func (t *Tree) Clone() *Tree { + ct := NewTree() + ct.nodes = maps.Clone(t.nodes) + ct.parent = maps.Clone(t.parent) + ct.children = maps.Clone(t.children) + return ct +} + +// Copy returns a deep copy of the Tree. func (t *Tree) Copy() *Tree { ct := NewTree() for k, v := range t.nodes { diff --git a/test/integration/path_type_change_test.go b/test/integration/path_type_change_test.go new file mode 100644 index 00000000..998acefb --- /dev/null +++ b/test/integration/path_type_change_test.go @@ -0,0 +1,62 @@ +package integration + +import ( + "testing" + + "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/stereoscope/pkg/filetree/filenode" + "github.com/anchore/stereoscope/pkg/imagetest" +) + +func TestPathTypeChangeImage(t *testing.T) { + image := imagetest.GetFixtureImage(t, "docker-archive", "image-path-type-change") + + layerAssertions := []map[string]file.Type{ + make(map[string]file.Type), + { + "/chimera/a.txt": file.TypeRegular, + "/chimera/b.txt": file.TypeRegular, + "/chimera": file.TypeDirectory, + }, + { + "/chimera": file.TypeDirectory, + }, + make(map[string]file.Type), + { + "/chimera": file.TypeRegular, + }, + make(map[string]file.Type), + { + "/chimera": file.TypeSymLink, + }, + make(map[string]file.Type), + { + "/chimera": file.TypeDirectory, + }, + } + + for idx, layer := range image.Layers { + assertions := layerAssertions[idx] + err := layer.SquashedTree.Walk(func(path file.Path, f filenode.FileNode) error { + expect, ok := assertions[string(path)] + if !ok { + return nil + } + if f.FileType != expect { + t.Errorf("at %v got %v want %v", path, f.FileType, expect) + } + delete(assertions, string(path)) + return nil + }, nil) + if err != nil { + t.Error(err) + } + } + for i, a := range layerAssertions { + if len(a) > 0 { + for k, v := range a { + t.Errorf("for layer %v, never saw %v of type %v", i, k, v) + } + } + } +} diff --git a/test/integration/test-fixtures/image-path-type-change/Dockerfile b/test/integration/test-fixtures/image-path-type-change/Dockerfile new file mode 100644 index 00000000..16a58798 --- /dev/null +++ b/test/integration/test-fixtures/image-path-type-change/Dockerfile @@ -0,0 +1,20 @@ +FROM busybox:1.37.0@sha256:db142d433cdde11f10ae479dbf92f3b13d693fd1c91053da9979728cceb1dc68 + +# makes /chimera/a.txt /chimera/b.txt +COPY . / + +# make a layer where the dir is empty +RUN rm /chimera/a.txt /chimera/b.txt + +RUN rmdir /chimera + +RUN touch /chimera + +RUN rm /chimera + +RUN ln -s /etc/hostname /chimera + +RUN unlink /chimera + +RUN mkdir /chimera + diff --git a/test/integration/test-fixtures/image-path-type-change/chimera/a.txt b/test/integration/test-fixtures/image-path-type-change/chimera/a.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/integration/test-fixtures/image-path-type-change/chimera/b.txt b/test/integration/test-fixtures/image-path-type-change/chimera/b.txt new file mode 100644 index 00000000..e69de29b