Skip to content

Commit 816c616

Browse files
committed
Remove fstest dependency on root continuity package
Replace the fstest package's use of continuity.NewContext, continuity.BuildManifest, and the continuity Resource interfaces with a local directory walker and concrete resource type. This breaks the second-order dependency chain where external consumers of CheckDirectoryEqual (used in ~37 test files across containerd and buildkit) transitively pulled in the entire root package with its manifest, protobuf, and driver machinery. The new walker uses filepath.Walk, crypto/sha256 for content comparison, and platform-specific stat calls for uid/gid, hardlinks, and device info. Signed-off-by: Derek McGowan <derek@mcg.dev>
1 parent 96e7788 commit 816c616

5 files changed

Lines changed: 285 additions & 111 deletions

File tree

fs/fstest/compare.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,22 @@ package fstest
1919
import (
2020
"fmt"
2121
"os"
22-
23-
"github.com/containerd/continuity"
2422
)
2523

2624
// CheckDirectoryEqual compares two directory paths to make sure that
2725
// the content of the directories is the same.
2826
func CheckDirectoryEqual(d1, d2 string) error {
29-
c1, err := continuity.NewContext(d1)
30-
if err != nil {
31-
return fmt.Errorf("failed to build context: %w", err)
32-
}
33-
34-
c2, err := continuity.NewContext(d2)
35-
if err != nil {
36-
return fmt.Errorf("failed to build context: %w", err)
37-
}
38-
39-
m1, err := continuity.BuildManifest(c1)
27+
r1, err := buildResources(d1)
4028
if err != nil {
41-
return fmt.Errorf("failed to build manifest: %w", err)
29+
return fmt.Errorf("failed to walk %s: %w", d1, err)
4230
}
4331

44-
m2, err := continuity.BuildManifest(c2)
32+
r2, err := buildResources(d2)
4533
if err != nil {
46-
return fmt.Errorf("failed to build manifest: %w", err)
34+
return fmt.Errorf("failed to walk %s: %w", d2, err)
4735
}
4836

49-
diff := diffResourceList(m1.Resources, m2.Resources)
37+
diff := diffResourceList(r1, r2)
5038
if diff.HasDiff() {
5139
return fmt.Errorf("directory diff between %s and %s\n%s", d1, d2, diff.String())
5240
}

fs/fstest/continuity_util.go

Lines changed: 41 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,24 @@ package fstest
1919
import (
2020
"bytes"
2121
"fmt"
22-
23-
"github.com/containerd/continuity"
22+
"os"
2423
)
2524

2625
type resourceUpdate struct {
27-
Original continuity.Resource
28-
Updated continuity.Resource
26+
Original resource
27+
Updated resource
2928
}
3029

3130
func (u resourceUpdate) String() string {
3231
return fmt.Sprintf("%s(mode: %o, uid: %d, gid: %d) -> %s(mode: %o, uid: %d, gid: %d)",
33-
u.Original.Path(), u.Original.Mode(), u.Original.UID(), u.Original.GID(),
34-
u.Updated.Path(), u.Updated.Mode(), u.Updated.UID(), u.Updated.GID(),
32+
u.Original.path, u.Original.mode, u.Original.uid, u.Original.gid,
33+
u.Updated.path, u.Updated.mode, u.Updated.uid, u.Updated.gid,
3534
)
3635
}
3736

3837
type resourceListDifference struct {
39-
Additions []continuity.Resource
40-
Deletions []continuity.Resource
38+
Additions []resource
39+
Deletions []resource
4140
Updates []resourceUpdate
4241
}
4342

@@ -47,7 +46,7 @@ func (l resourceListDifference) HasDiff() bool {
4746
}
4847

4948
for _, add := range l.Additions {
50-
if ok := metadataFiles[add.Path()]; !ok {
49+
if ok := metadataFiles[add.path]; !ok {
5150
return true
5251
}
5352
}
@@ -58,28 +57,28 @@ func (l resourceListDifference) HasDiff() bool {
5857
func (l resourceListDifference) String() string {
5958
buf := bytes.NewBuffer(nil)
6059
for _, add := range l.Additions {
61-
fmt.Fprintf(buf, "+ %s\n", add.Path())
60+
fmt.Fprintf(buf, "+ %s\n", add.path)
6261
}
6362
for _, del := range l.Deletions {
64-
fmt.Fprintf(buf, "- %s\n", del.Path())
63+
fmt.Fprintf(buf, "- %s\n", del.path)
6564
}
6665
for _, upt := range l.Updates {
6766
fmt.Fprintf(buf, "~ %s\n", upt.String())
6867
}
6968
return buf.String()
7069
}
7170

72-
// diffManifest compares two resource lists and returns the list
71+
// diffResourceList compares two resource lists and returns the list
7372
// of adds updates and deletes, resource lists are not reordered
7473
// before doing difference.
75-
func diffResourceList(r1, r2 []continuity.Resource) resourceListDifference {
74+
func diffResourceList(r1, r2 []resource) resourceListDifference {
7675
i1 := 0
7776
i2 := 0
7877
var d resourceListDifference
7978

8079
for i1 < len(r1) && i2 < len(r2) {
81-
p1 := r1[i1].Path()
82-
p2 := r2[i2].Path()
80+
p1 := r1[i1].path
81+
p2 := r2[i2].path
8382
switch {
8483
case p1 < p2:
8584
d.Deletions = append(d.Deletions, r1[i1])
@@ -112,103 +111,51 @@ func diffResourceList(r1, r2 []continuity.Resource) resourceListDifference {
112111
return d
113112
}
114113

115-
func compareResource(r1, r2 continuity.Resource) bool {
116-
if r1.Path() != r2.Path() {
114+
func compareResource(r1, r2 resource) bool {
115+
if r1.path != r2.path {
117116
return false
118117
}
119-
if r1.Mode() != r2.Mode() {
118+
if r1.mode != r2.mode {
120119
return false
121120
}
122-
if r1.UID() != r2.UID() {
121+
if r1.uid != r2.uid {
123122
return false
124123
}
125-
if r1.GID() != r2.GID() {
124+
if r1.gid != r2.gid {
126125
return false
127126
}
128127

129-
// TODO(dmcgowan): Check if is XAttrer
130-
131-
return compareResourceTypes(r1, r2)
128+
return compareResourceType(r1, r2)
132129
}
133130

134-
func compareResourceTypes(r1, r2 continuity.Resource) bool {
135-
switch t1 := r1.(type) {
136-
case continuity.RegularFile:
137-
t2, ok := r2.(continuity.RegularFile)
138-
if !ok {
131+
func compareResourceType(r1, r2 resource) bool {
132+
mode := r1.mode
133+
switch {
134+
case mode.IsRegular():
135+
if r1.size != r2.size {
139136
return false
140137
}
141-
return compareRegularFile(t1, t2)
142-
case continuity.Directory:
143-
t2, ok := r2.(continuity.Directory)
144-
if !ok {
138+
if r1.sha256 != r2.sha256 {
145139
return false
146140
}
147-
return compareDirectory(t1, t2)
148-
case continuity.SymLink:
149-
t2, ok := r2.(continuity.SymLink)
150-
if !ok {
141+
if len(r1.paths) != len(r2.paths) {
151142
return false
152143
}
153-
return compareSymLink(t1, t2)
154-
case continuity.NamedPipe:
155-
t2, ok := r2.(continuity.NamedPipe)
156-
if !ok {
157-
return false
158-
}
159-
return compareNamedPipe(t1, t2)
160-
case continuity.Device:
161-
t2, ok := r2.(continuity.Device)
162-
if !ok {
163-
return false
144+
for i := range r1.paths {
145+
if r1.paths[i] != r2.paths[i] {
146+
return false
147+
}
164148
}
165-
return compareDevice(t1, t2)
149+
return true
150+
case mode.IsDir():
151+
return true
152+
case mode&os.ModeSymlink != 0:
153+
return r1.target == r2.target
154+
case mode&os.ModeNamedPipe != 0:
155+
return true
156+
case mode&os.ModeDevice != 0:
157+
return r1.major == r2.major && r1.minor == r2.minor
166158
default:
167-
// TODO(dmcgowan): Should this panic?
168-
return r1 == r2
169-
}
170-
}
171-
172-
func compareRegularFile(r1, r2 continuity.RegularFile) bool {
173-
if r1.Size() != r2.Size() {
174-
return false
175-
}
176-
p1 := r1.Paths()
177-
p2 := r2.Paths()
178-
if len(p1) != len(p2) {
179-
return false
180-
}
181-
for i := range p1 {
182-
if p1[i] != p2[i] {
183-
return false
184-
}
185-
}
186-
d1 := r1.Digests()
187-
d2 := r2.Digests()
188-
if len(d1) != len(d2) {
189-
return false
190-
}
191-
for i := range d1 {
192-
if d1[i] != d2[i] {
193-
return false
194-
}
159+
return true
195160
}
196-
197-
return true
198-
}
199-
200-
func compareSymLink(r1, r2 continuity.SymLink) bool {
201-
return r1.Target() == r2.Target()
202-
}
203-
204-
func compareDirectory(r1, r2 continuity.Directory) bool {
205-
return true
206-
}
207-
208-
func compareNamedPipe(r1, r2 continuity.NamedPipe) bool {
209-
return true
210-
}
211-
212-
func compareDevice(r1, r2 continuity.Device) bool {
213-
return r1.Major() == r2.Major() && r1.Minor() == r2.Minor()
214161
}

fs/fstest/walker.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package fstest
18+
19+
import (
20+
"crypto/sha256"
21+
"io"
22+
"os"
23+
"path/filepath"
24+
"sort"
25+
)
26+
27+
// resource represents a filesystem entry for directory comparison.
28+
type resource struct {
29+
path string
30+
paths []string // all paths, including hardlinks (sorted)
31+
mode os.FileMode
32+
uid int64
33+
gid int64
34+
size int64
35+
sha256 [sha256.Size]byte // regular files only
36+
target string // symlinks only
37+
major uint64 // devices only
38+
minor uint64 // devices only
39+
}
40+
41+
// buildResources walks root and returns a sorted list of resources.
42+
func buildResources(root string) ([]resource, error) {
43+
root, err := filepath.Abs(root)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
type entry struct {
49+
res resource
50+
fi os.FileInfo
51+
}
52+
53+
// hlKey -> index into entries for the first file with that inode.
54+
hardlinks := map[hardlinkKey]int{}
55+
var entries []entry
56+
57+
err = filepath.Walk(root, func(p string, fi os.FileInfo, err error) error {
58+
if err != nil {
59+
return err
60+
}
61+
62+
rel, err := filepath.Rel(root, p)
63+
if err != nil {
64+
return err
65+
}
66+
// Use absolute-style paths like continuity does (rooted at "/").
67+
rel = "/" + filepath.ToSlash(rel)
68+
if rel == "/." {
69+
// skip root directory itself
70+
return nil
71+
}
72+
73+
r := resource{
74+
path: rel,
75+
mode: fi.Mode(),
76+
}
77+
statResource(fi, &r)
78+
79+
if fi.Mode().IsRegular() {
80+
r.size = fi.Size()
81+
h, err := hashFile(p)
82+
if err != nil {
83+
return err
84+
}
85+
r.sha256 = h
86+
87+
// Check for hardlink.
88+
if key, ok := getHardlinkKey(fi); ok {
89+
if idx, exists := hardlinks[key]; exists {
90+
// Merge into existing entry.
91+
entries[idx].res.paths = append(entries[idx].res.paths, rel)
92+
return nil
93+
}
94+
hardlinks[key] = len(entries)
95+
}
96+
} else if fi.Mode()&os.ModeSymlink != 0 {
97+
target, err := os.Readlink(p)
98+
if err != nil {
99+
return err
100+
}
101+
r.target = target
102+
} else if fi.Mode()&os.ModeDevice != 0 {
103+
r.major, r.minor = getDeviceInfo(fi)
104+
} else if fi.Mode()&os.ModeNamedPipe != 0 {
105+
// Check for hardlink on named pipes.
106+
if key, ok := getHardlinkKey(fi); ok {
107+
if idx, exists := hardlinks[key]; exists {
108+
entries[idx].res.paths = append(entries[idx].res.paths, rel)
109+
return nil
110+
}
111+
hardlinks[key] = len(entries)
112+
}
113+
}
114+
115+
r.paths = []string{rel}
116+
entries = append(entries, entry{res: r, fi: fi})
117+
return nil
118+
})
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
resources := make([]resource, len(entries))
124+
for i, e := range entries {
125+
sort.Strings(e.res.paths)
126+
e.res.path = e.res.paths[0]
127+
resources[i] = e.res
128+
}
129+
sort.Slice(resources, func(i, j int) bool {
130+
return resources[i].path < resources[j].path
131+
})
132+
133+
return resources, nil
134+
}
135+
136+
func hashFile(path string) ([sha256.Size]byte, error) {
137+
f, err := os.Open(path)
138+
if err != nil {
139+
return [sha256.Size]byte{}, err
140+
}
141+
defer f.Close()
142+
h := sha256.New()
143+
if _, err := io.Copy(h, f); err != nil {
144+
return [sha256.Size]byte{}, err
145+
}
146+
var sum [sha256.Size]byte
147+
copy(sum[:], h.Sum(nil))
148+
return sum, nil
149+
}

0 commit comments

Comments
 (0)