move tree/ to rootfs/

This commit is contained in:
2021-05-24 00:11:58 +03:00
parent b76a500f2e
commit 8dedaebcf7
5 changed files with 11 additions and 26 deletions

View File

@@ -5,18 +5,19 @@ go_library(
srcs = [
"doc.go",
"rootfs.go",
"tree.go",
],
importpath = "github.com/motiejus/code/undocker/rootfs",
visibility = ["//visibility:public"],
deps = [
"//src/undocker/internal/tree:go_default_library",
"@org_uber_go_multierr//:go_default_library",
],
deps = ["@org_uber_go_multierr//:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["rootfs_test.go"],
srcs = [
"rootfs_test.go",
"tree_test.go",
],
embed = [":go_default_library"],
deps = [
"@com_github_stretchr_testify//assert:go_default_library",

View File

@@ -9,7 +9,6 @@ import (
"path/filepath"
"strings"
"github.com/motiejus/code/undocker/internal/tree"
"go.uber.org/multierr"
)
@@ -194,10 +193,10 @@ func writeFile(tr *tar.Reader, tw *tar.Writer, hdr *tar.Header) error {
return nil
}
func whiteoutDirs(whreaddir map[string]int, nlayers int) []*tree.Tree {
ret := make([]*tree.Tree, nlayers)
func whiteoutDirs(whreaddir map[string]int, nlayers int) []*Tree {
ret := make([]*Tree, nlayers)
for i := range ret {
ret[i] = tree.New()
ret[i] = New()
}
for fname, layer := range whreaddir {
if layer == 0 {

120
rootfs/tree.go Normal file
View File

@@ -0,0 +1,120 @@
package rootfs
import (
"path/filepath"
"sort"
"strings"
)
// Tree is a way to store directory paths for whiteouts.
// It is semi-optimized for reads and non-optimized for writes;
// See Merge() and HasPrefix for trade-offs.
type Tree struct {
name string
children []*Tree
end bool
}
// New creates a new tree from a given path.
func New(paths ...string) *Tree {
t := &Tree{name: ".", children: []*Tree{}}
for _, path := range paths {
t.Add(path)
}
return t
}
// Add adds a sequence to a tree
func (t *Tree) Add(path string) {
t.add(strings.Split(filepath.Clean(path), "/"))
}
// HasPrefix returns if tree contains a prefix matching a given sequence.
// Search algorithm is naive: it does linear search when going through the
// nodes instead of binary-search. Since we expect number of children to be
// really small (usually 1 or 2), it does not really matter. If you find a
// real-world container with 30+ whiteout paths on a single path, please ping
// the author/maintainer of this code.
func (t *Tree) HasPrefix(path string) bool {
return t.hasprefix(strings.Split(filepath.Clean(path), "/"))
}
// Merge merges adds t2 to t. It is not optimized for speed, since it's walking
// full branch for every other branch.
func (t *Tree) Merge(t2 *Tree) {
t.merge(t2, []string{})
}
// String stringifies a tree
func (t *Tree) String() string {
if len(t.children) == 0 {
return "<empty>"
}
res := &stringer{[]string{}}
res.stringify(t, []string{})
sort.Strings(res.res)
return strings.Join(res.res, ":")
}
func (t *Tree) add(nodes []string) {
if len(nodes) == 0 {
t.end = true
return
}
for i := range t.children {
if t.children[i].name == nodes[0] {
t.children[i].add(nodes[1:])
return
}
}
newNode := &Tree{name: nodes[0]}
t.children = append(t.children, newNode)
newNode.add(nodes[1:])
}
func (t *Tree) hasprefix(nodes []string) bool {
if len(nodes) == 0 {
return t.end
}
if t.end {
return true
}
for i := range t.children {
if t.children[i].name == nodes[0] {
return t.children[i].hasprefix(nodes[1:])
}
}
return false
}
type stringer struct {
res []string
}
func (s *stringer) stringify(t *Tree, acc []string) {
if t.name == "" {
return
}
acc = append(acc, t.name)
if t.end {
s.res = append(s.res, strings.Join(acc, "/"))
}
for _, child := range t.children {
s.stringify(child, acc)
}
}
func (t *Tree) merge(t2 *Tree, acc []string) {
if t2.end {
t.add(append(acc[1:], t2.name))
}
acc = append(acc, t2.name)
for _, child := range t2.children {
t.merge(child, acc)
}
}

97
rootfs/tree_test.go Normal file
View File

@@ -0,0 +1,97 @@
package rootfs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTree(t *testing.T) {
tests := []struct {
name string
paths []string
matchTrue []string
matchFalse []string
}{
{
name: "empty sequence matches nothing",
paths: []string{},
matchFalse: []string{"a", "b"},
},
{
name: "directory",
paths: []string{"a/b"},
matchTrue: []string{"a/b/"},
},
{
name: "a few sequences",
paths: []string{"a", "b", "c/b/a"},
matchTrue: []string{"a", "a/b/c", "c/b/a", "c/b/a/d"},
matchFalse: []string{"c/d", "c", "c/b"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tree := New(tt.paths...)
for _, path := range tt.matchTrue {
t.Run(path, func(t *testing.T) {
assert.True(t, tree.HasPrefix(path),
"expected %s to be a prefix of %s", path, tree)
})
}
for _, path := range tt.matchFalse {
t.Run(path, func(t *testing.T) {
assert.False(t, tree.HasPrefix(path),
"expected %s to not be a prefix of %s", path, tree)
})
}
})
}
}
func TestTreeMerge(t *testing.T) {
tree1 := New("bin/ar", "var/cache/apt")
tree2 := New("bin/ar", "bin/busybox", "usr/share/doc")
tree1.Merge(tree2)
assert.Equal(t, "./bin/ar:./bin/busybox:./usr/share/doc:./var/cache/apt", tree1.String())
assert.Equal(t, "./bin/ar:./bin/busybox:./usr/share/doc", tree2.String())
}
func TestString(t *testing.T) {
tests := []struct {
name string
paths []string
wantStr string
}{
{
name: "empty",
paths: []string{},
wantStr: "<empty>",
},
{
name: "simple path",
paths: []string{"a/b/c"},
wantStr: "./a/b/c",
},
{
name: "duplicate paths",
paths: []string{"a/a", "a//a"},
wantStr: "./a/a",
},
{
name: "a few sequences",
paths: []string{"bin/ar", "bin/busybox", "var/cache/apt/archives"},
wantStr: "./bin/ar:./bin/busybox:./var/cache/apt/archives",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tree := New(tt.paths...)
assert.Equal(t, tt.wantStr, tree.String())
})
}
}