1 | | - | //go:build !windows |
2 | | - | // +build !windows |
3 | | - | |
4 | | - | package source |
5 | | - | |
6 | | - | import ( |
7 | | - | "io" |
8 | | - | "io/fs" |
9 | | - | "os" |
10 | | - | "os/exec" |
11 | | - | "path" |
12 | | - | "path/filepath" |
13 | | - | "sort" |
14 | | - | "strings" |
15 | | - | "syscall" |
16 | | - | "testing" |
17 | | - | "time" |
18 | | - | |
19 | | - | "github.com/google/go-cmp/cmp" |
20 | | - | "github.com/stretchr/testify/assert" |
21 | | - | "github.com/stretchr/testify/require" |
22 | | - | |
23 | | - | "github.com/anchore/stereoscope/pkg/image" |
24 | | - | "github.com/anchore/stereoscope/pkg/imagetest" |
25 | | - | "github.com/anchore/syft/syft/artifact" |
26 | | - | "github.com/anchore/syft/syft/internal/fileresolver" |
27 | | - | ) |
28 | | - | |
29 | | - | func TestParseInput(t *testing.T) { |
30 | | - | tests := []struct { |
31 | | - | name string |
32 | | - | input string |
33 | | - | platform string |
34 | | - | expected Scheme |
35 | | - | errFn require.ErrorAssertionFunc |
36 | | - | }{ |
37 | | - | { |
38 | | - | name: "ParseInput parses a file input", |
39 | | - | input: "test-fixtures/image-simple/file-1.txt", |
40 | | - | expected: FileScheme, |
41 | | - | }, |
42 | | - | { |
43 | | - | name: "errors out when using platform for non-image scheme", |
44 | | - | input: "test-fixtures/image-simple/file-1.txt", |
45 | | - | platform: "arm64", |
46 | | - | errFn: require.Error, |
47 | | - | }, |
48 | | - | } |
49 | | - | |
50 | | - | for _, test := range tests { |
51 | | - | t.Run(test.name, func(t *testing.T) { |
52 | | - | if test.errFn == nil { |
53 | | - | test.errFn = require.NoError |
54 | | - | } |
55 | | - | sourceInput, err := ParseInput(test.input, test.platform) |
56 | | - | test.errFn(t, err) |
57 | | - | if test.expected != "" { |
58 | | - | require.NotNil(t, sourceInput) |
59 | | - | assert.Equal(t, sourceInput.Scheme, test.expected) |
60 | | - | } |
61 | | - | }) |
62 | | - | } |
63 | | - | } |
64 | | - | |
65 | | - | func TestNewFromImageFails(t *testing.T) { |
66 | | - | t.Run("no image given", func(t *testing.T) { |
67 | | - | _, err := NewFromImage(nil, "") |
68 | | - | if err == nil { |
69 | | - | t.Errorf("expected an error condition but none was given") |
70 | | - | } |
71 | | - | }) |
72 | | - | } |
73 | | - | |
74 | | - | func TestSetID(t *testing.T) { |
75 | | - | layer := image.NewLayer(nil) |
76 | | - | layer.Metadata = image.LayerMetadata{ |
77 | | - | Digest: "sha256:6f4fb385d4e698647bf2a450749dfbb7bc2831ec9a730ef4046c78c08d468e89", |
78 | | - | } |
79 | | - | img := image.Image{ |
80 | | - | Layers: []*image.Layer{layer}, |
81 | | - | } |
82 | | - | |
83 | | - | tests := []struct { |
84 | | - | name string |
85 | | - | input *Source |
86 | | - | expected artifact.ID |
87 | | - | }{ |
88 | | - | { |
89 | | - | name: "source.SetID sets the ID for FileScheme", |
90 | | - | input: &Source{ |
91 | | - | Metadata: Metadata{ |
92 | | - | Scheme: FileScheme, |
93 | | - | Path: "test-fixtures/image-simple/file-1.txt", |
94 | | - | }, |
95 | | - | }, |
96 | | - | expected: artifact.ID("55096713247489add592ce977637be868497132b36d1e294a3831925ec64319a"), |
97 | | - | }, |
98 | | - | { |
99 | | - | name: "source.SetID sets the ID for ImageScheme", |
100 | | - | input: &Source{ |
101 | | - | Image: &img, |
102 | | - | Metadata: Metadata{ |
103 | | - | Scheme: ImageScheme, |
104 | | - | }, |
105 | | - | }, |
106 | | - | expected: artifact.ID("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), |
107 | | - | }, |
108 | | - | { |
109 | | - | name: "source.SetID sets the ID for DirectoryScheme", |
110 | | - | input: &Source{ |
111 | | - | Image: &img, |
112 | | - | Metadata: Metadata{ |
113 | | - | Scheme: DirectoryScheme, |
114 | | - | Path: "test-fixtures/image-simple", |
115 | | - | }, |
116 | | - | }, |
117 | | - | expected: artifact.ID("91db61e5e0ae097ef764796ce85e442a93f2a03e5313d4c7307e9b413f62e8c4"), |
118 | | - | }, |
119 | | - | { |
120 | | - | name: "source.SetID sets the ID for UnknownScheme", |
121 | | - | input: &Source{ |
122 | | - | Image: &img, |
123 | | - | Metadata: Metadata{ |
124 | | - | Scheme: UnknownScheme, |
125 | | - | Path: "test-fixtures/image-simple", |
126 | | - | }, |
127 | | - | }, |
128 | | - | expected: artifact.ID("1b0dc351e6577b01"), |
129 | | - | }, |
130 | | - | } |
131 | | - | |
132 | | - | for _, test := range tests { |
133 | | - | t.Run(test.name, func(t *testing.T) { |
134 | | - | test.input.SetID() |
135 | | - | assert.Equal(t, test.expected, test.input.ID()) |
136 | | - | }) |
137 | | - | } |
138 | | - | } |
139 | | - | |
140 | | - | func TestNewFromImage(t *testing.T) { |
141 | | - | layer := image.NewLayer(nil) |
142 | | - | img := image.Image{ |
143 | | - | Layers: []*image.Layer{layer}, |
144 | | - | } |
145 | | - | |
146 | | - | t.Run("create a new source object from image", func(t *testing.T) { |
147 | | - | _, err := NewFromImage(&img, "") |
148 | | - | if err != nil { |
149 | | - | t.Errorf("unexpected error when creating a new Locations from img: %+v", err) |
150 | | - | } |
151 | | - | }) |
152 | | - | } |
153 | | - | |
154 | | - | func TestNewFromDirectory(t *testing.T) { |
155 | | - | testCases := []struct { |
156 | | - | desc string |
157 | | - | input string |
158 | | - | expString string |
159 | | - | inputPaths []string |
160 | | - | expectedRefs int |
161 | | - | expectedErr bool |
162 | | - | }{ |
163 | | - | { |
164 | | - | desc: "no paths exist", |
165 | | - | input: "foobar/", |
166 | | - | inputPaths: []string{"/opt/", "/other"}, |
167 | | - | expectedErr: true, |
168 | | - | }, |
169 | | - | { |
170 | | - | desc: "path detected", |
171 | | - | input: "test-fixtures", |
172 | | - | inputPaths: []string{"path-detected/.vimrc"}, |
173 | | - | expectedRefs: 1, |
174 | | - | }, |
175 | | - | { |
176 | | - | desc: "directory ignored", |
177 | | - | input: "test-fixtures", |
178 | | - | inputPaths: []string{"path-detected"}, |
179 | | - | expectedRefs: 0, |
180 | | - | }, |
181 | | - | { |
182 | | - | desc: "no files-by-path detected", |
183 | | - | input: "test-fixtures", |
184 | | - | inputPaths: []string{"no-path-detected"}, |
185 | | - | expectedRefs: 0, |
186 | | - | }, |
187 | | - | } |
188 | | - | for _, test := range testCases { |
189 | | - | t.Run(test.desc, func(t *testing.T) { |
190 | | - | src, err := NewFromDirectory(test.input) |
191 | | - | require.NoError(t, err) |
192 | | - | assert.Equal(t, test.input, src.Metadata.Path) |
193 | | - | |
194 | | - | res, err := src.FileResolver(SquashedScope) |
195 | | - | if test.expectedErr { |
196 | | - | if err == nil { |
197 | | - | t.Fatal("expected an error when making the resolver but got none") |
198 | | - | } |
199 | | - | return |
200 | | - | } else { |
201 | | - | require.NoError(t, err) |
202 | | - | } |
203 | | - | |
204 | | - | refs, err := res.FilesByPath(test.inputPaths...) |
205 | | - | if err != nil { |
206 | | - | t.Errorf("FilesByPath call produced an error: %+v", err) |
207 | | - | } |
208 | | - | if len(refs) != test.expectedRefs { |
209 | | - | t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expectedRefs) |
210 | | - | |
211 | | - | } |
212 | | - | |
213 | | - | }) |
214 | | - | } |
215 | | - | } |
216 | | - | |
217 | | - | func TestNewFromFile(t *testing.T) { |
218 | | - | testCases := []struct { |
219 | | - | desc string |
220 | | - | input string |
221 | | - | expString string |
222 | | - | inputPaths []string |
223 | | - | expRefs int |
224 | | - | }{ |
225 | | - | { |
226 | | - | desc: "path detected", |
227 | | - | input: "test-fixtures/path-detected", |
228 | | - | inputPaths: []string{"/.vimrc"}, |
229 | | - | expRefs: 1, |
230 | | - | }, |
231 | | - | } |
232 | | - | for _, test := range testCases { |
233 | | - | t.Run(test.desc, func(t *testing.T) { |
234 | | - | src, cleanup := NewFromFile(test.input) |
235 | | - | if cleanup != nil { |
236 | | - | t.Cleanup(cleanup) |
237 | | - | } |
238 | | - | |
239 | | - | assert.Equal(t, test.input, src.Metadata.Path) |
240 | | - | assert.Equal(t, src.Metadata.Path, src.path) |
241 | | - | |
242 | | - | res, err := src.FileResolver(SquashedScope) |
243 | | - | require.NoError(t, err) |
244 | | - | |
245 | | - | refs, err := res.FilesByPath(test.inputPaths...) |
246 | | - | require.NoError(t, err) |
247 | | - | assert.Len(t, refs, test.expRefs) |
248 | | - | |
249 | | - | }) |
250 | | - | } |
251 | | - | } |
252 | | - | |
253 | | - | func TestNewFromFile_WithArchive(t *testing.T) { |
254 | | - | testCases := []struct { |
255 | | - | desc string |
256 | | - | input string |
257 | | - | expString string |
258 | | - | inputPaths []string |
259 | | - | expRefs int |
260 | | - | layer2 bool |
261 | | - | contents string |
262 | | - | }{ |
263 | | - | { |
264 | | - | desc: "path detected", |
265 | | - | input: "test-fixtures/path-detected", |
266 | | - | inputPaths: []string{"/.vimrc"}, |
267 | | - | expRefs: 1, |
268 | | - | }, |
269 | | - | { |
270 | | - | desc: "lest entry for duplicate paths", |
271 | | - | input: "test-fixtures/path-detected", |
272 | | - | inputPaths: []string{"/.vimrc"}, |
273 | | - | expRefs: 1, |
274 | | - | layer2: true, |
275 | | - | contents: "Another .vimrc file", |
276 | | - | }, |
277 | | - | } |
278 | | - | for _, test := range testCases { |
279 | | - | t.Run(test.desc, func(t *testing.T) { |
280 | | - | archivePath := setupArchiveTest(t, test.input, test.layer2) |
281 | | - | |
282 | | - | src, cleanup := NewFromFile(archivePath) |
283 | | - | if cleanup != nil { |
284 | | - | t.Cleanup(cleanup) |
285 | | - | } |
286 | | - | |
287 | | - | assert.Equal(t, archivePath, src.Metadata.Path) |
288 | | - | assert.NotEqual(t, src.Metadata.Path, src.path) |
289 | | - | |
290 | | - | res, err := src.FileResolver(SquashedScope) |
291 | | - | require.NoError(t, err) |
292 | | - | |
293 | | - | refs, err := res.FilesByPath(test.inputPaths...) |
294 | | - | require.NoError(t, err) |
295 | | - | assert.Len(t, refs, test.expRefs) |
296 | | - | |
297 | | - | if test.contents != "" { |
298 | | - | reader, err := res.FileContentsByLocation(refs[0]) |
299 | | - | require.NoError(t, err) |
300 | | - | |
301 | | - | data, err := io.ReadAll(reader) |
302 | | - | require.NoError(t, err) |
303 | | - | |
304 | | - | assert.Equal(t, test.contents, string(data)) |
305 | | - | } |
306 | | - | |
307 | | - | }) |
308 | | - | } |
309 | | - | } |
310 | | - | |
311 | | - | func TestNewFromDirectoryShared(t *testing.T) { |
312 | | - | testCases := []struct { |
313 | | - | desc string |
314 | | - | input string |
315 | | - | expString string |
316 | | - | notExist string |
317 | | - | inputPaths []string |
318 | | - | expRefs int |
319 | | - | }{ |
320 | | - | { |
321 | | - | desc: "path detected", |
322 | | - | input: "test-fixtures", |
323 | | - | notExist: "foobar/", |
324 | | - | inputPaths: []string{"path-detected/.vimrc"}, |
325 | | - | expRefs: 1, |
326 | | - | }, |
327 | | - | { |
328 | | - | desc: "directory ignored", |
329 | | - | input: "test-fixtures", |
330 | | - | notExist: "foobar/", |
331 | | - | inputPaths: []string{"path-detected"}, |
332 | | - | expRefs: 0, |
333 | | - | }, |
334 | | - | { |
335 | | - | desc: "no files-by-path detected", |
336 | | - | input: "test-fixtures", |
337 | | - | notExist: "foobar/", |
338 | | - | inputPaths: []string{"no-path-detected"}, |
339 | | - | expRefs: 0, |
340 | | - | }, |
341 | | - | } |
342 | | - | for _, test := range testCases { |
343 | | - | t.Run(test.desc, func(t *testing.T) { |
344 | | - | src, err := NewFromDirectory(test.input) |
345 | | - | |
346 | | - | if err != nil { |
347 | | - | t.Errorf("could not create NewDirScope: %+v", err) |
348 | | - | } |
349 | | - | if src.Metadata.Path != test.input { |
350 | | - | t.Errorf("mismatched stringer: '%s' != '%s'", src.Metadata.Path, test.input) |
351 | | - | } |
352 | | - | |
353 | | - | _, err = src.FileResolver(SquashedScope) |
354 | | - | assert.NoError(t, err) |
355 | | - | |
356 | | - | src.Metadata.Path = test.notExist |
357 | | - | resolver, err := src.FileResolver(SquashedScope) |
358 | | - | assert.NoError(t, err) |
359 | | - | |
360 | | - | refs, err := resolver.FilesByPath(test.inputPaths...) |
361 | | - | if err != nil { |
362 | | - | t.Errorf("FilesByPath call produced an error: %+v", err) |
363 | | - | } |
364 | | - | if len(refs) != test.expRefs { |
365 | | - | t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expRefs) |
366 | | - | |
367 | | - | } |
368 | | - | |
369 | | - | }) |
370 | | - | } |
371 | | - | } |
372 | | - | |
373 | | - | func TestFilesByPathDoesNotExist(t *testing.T) { |
374 | | - | testCases := []struct { |
375 | | - | desc string |
376 | | - | input string |
377 | | - | path string |
378 | | - | expected string |
379 | | - | }{ |
380 | | - | { |
381 | | - | input: "test-fixtures/path-detected", |
382 | | - | desc: "path does not exist", |
383 | | - | path: "foo", |
384 | | - | }, |
385 | | - | } |
386 | | - | for _, test := range testCases { |
387 | | - | t.Run(test.desc, func(t *testing.T) { |
388 | | - | src, err := NewFromDirectory(test.input) |
389 | | - | if err != nil { |
390 | | - | t.Errorf("could not create NewDirScope: %+v", err) |
391 | | - | } |
392 | | - | res, err := src.FileResolver(SquashedScope) |
393 | | - | if err != nil { |
394 | | - | t.Errorf("could not get resolver error: %+v", err) |
395 | | - | } |
396 | | - | refs, err := res.FilesByPath(test.path) |
397 | | - | if err != nil { |
398 | | - | t.Errorf("could not get file references from path: %s, %v", test.path, err) |
399 | | - | } |
400 | | - | |
401 | | - | if len(refs) != 0 { |
402 | | - | t.Errorf("didnt' expect a ref, but got: %d", len(refs)) |
403 | | - | } |
404 | | - | |
405 | | - | }) |
406 | | - | } |
407 | | - | } |
408 | | - | |
409 | | - | func TestFilesByGlob(t *testing.T) { |
410 | | - | testCases := []struct { |
411 | | - | desc string |
412 | | - | input string |
413 | | - | glob string |
414 | | - | expected int |
415 | | - | }{ |
416 | | - | { |
417 | | - | input: "test-fixtures", |
418 | | - | desc: "no matches", |
419 | | - | glob: "bar/foo", |
420 | | - | expected: 0, |
421 | | - | }, |
422 | | - | { |
423 | | - | input: "test-fixtures/path-detected", |
424 | | - | desc: "a single match", |
425 | | - | glob: "**/*vimrc", |
426 | | - | expected: 1, |
427 | | - | }, |
428 | | - | { |
429 | | - | input: "test-fixtures/path-detected", |
430 | | - | desc: "multiple matches", |
431 | | - | glob: "**", |
432 | | - | expected: 2, |
433 | | - | }, |
434 | | - | } |
435 | | - | for _, test := range testCases { |
436 | | - | t.Run(test.desc, func(t *testing.T) { |
437 | | - | src, err := NewFromDirectory(test.input) |
438 | | - | if err != nil { |
439 | | - | t.Errorf("could not create NewDirScope: %+v", err) |
440 | | - | } |
441 | | - | res, err := src.FileResolver(SquashedScope) |
442 | | - | if err != nil { |
443 | | - | t.Errorf("could not get resolver error: %+v", err) |
444 | | - | } |
445 | | - | contents, err := res.FilesByGlob(test.glob) |
446 | | - | if err != nil { |
447 | | - | t.Errorf("could not get files by glob: %s+v", err) |
448 | | - | } |
449 | | - | if len(contents) != test.expected { |
450 | | - | t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected) |
451 | | - | } |
452 | | - | |
453 | | - | }) |
454 | | - | } |
455 | | - | } |
456 | | - | |
457 | | - | func TestDirectoryExclusions(t *testing.T) { |
458 | | - | testCases := []struct { |
459 | | - | desc string |
460 | | - | input string |
461 | | - | glob string |
462 | | - | expected []string |
463 | | - | exclusions []string |
464 | | - | err bool |
465 | | - | }{ |
466 | | - | { |
467 | | - | input: "test-fixtures/system_paths", |
468 | | - | desc: "exclude everything", |
469 | | - | glob: "**", |
470 | | - | expected: nil, |
471 | | - | exclusions: []string{"**/*"}, |
472 | | - | }, |
473 | | - | { |
474 | | - | input: "test-fixtures/image-simple", |
475 | | - | desc: "a single path excluded", |
476 | | - | glob: "**", |
477 | | - | expected: []string{ |
478 | | - | "Dockerfile", |
479 | | - | "file-1.txt", |
480 | | - | "file-2.txt", |
481 | | - | }, |
482 | | - | exclusions: []string{"**/target/**"}, |
483 | | - | }, |
484 | | - | { |
485 | | - | input: "test-fixtures/image-simple", |
486 | | - | desc: "exclude explicit directory relative to the root", |
487 | | - | glob: "**", |
488 | | - | expected: []string{ |
489 | | - | "Dockerfile", |
490 | | - | "file-1.txt", |
491 | | - | "file-2.txt", |
492 | | - | //"target/really/nested/file-3.txt", // explicitly skipped |
493 | | - | }, |
494 | | - | exclusions: []string{"./target"}, |
495 | | - | }, |
496 | | - | { |
497 | | - | input: "test-fixtures/image-simple", |
498 | | - | desc: "exclude explicit file relative to the root", |
499 | | - | glob: "**", |
500 | | - | expected: []string{ |
501 | | - | "Dockerfile", |
502 | | - | //"file-1.txt", // explicitly skipped |
503 | | - | "file-2.txt", |
504 | | - | "target/really/nested/file-3.txt", |
505 | | - | }, |
506 | | - | exclusions: []string{"./file-1.txt"}, |
507 | | - | }, |
508 | | - | { |
509 | | - | input: "test-fixtures/image-simple", |
510 | | - | desc: "exclude wildcard relative to the root", |
511 | | - | glob: "**", |
512 | | - | expected: []string{ |
513 | | - | "Dockerfile", |
514 | | - | //"file-1.txt", // explicitly skipped |
515 | | - | //"file-2.txt", // explicitly skipped |
516 | | - | "target/really/nested/file-3.txt", |
517 | | - | }, |
518 | | - | exclusions: []string{"./*.txt"}, |
519 | | - | }, |
520 | | - | { |
521 | | - | input: "test-fixtures/image-simple", |
522 | | - | desc: "exclude files deeper", |
523 | | - | glob: "**", |
524 | | - | expected: []string{ |
525 | | - | "Dockerfile", |
526 | | - | "file-1.txt", |
527 | | - | "file-2.txt", |
528 | | - | //"target/really/nested/file-3.txt", // explicitly skipped |
529 | | - | }, |
530 | | - | exclusions: []string{"**/really/**"}, |
531 | | - | }, |
532 | | - | { |
533 | | - | input: "test-fixtures/image-simple", |
534 | | - | desc: "files excluded with extension", |
535 | | - | glob: "**", |
536 | | - | expected: []string{ |
537 | | - | "Dockerfile", |
538 | | - | //"file-1.txt", // explicitly skipped |
539 | | - | //"file-2.txt", // explicitly skipped |
540 | | - | //"target/really/nested/file-3.txt", // explicitly skipped |
541 | | - | }, |
542 | | - | exclusions: []string{"**/*.txt"}, |
543 | | - | }, |
544 | | - | { |
545 | | - | input: "test-fixtures/image-simple", |
546 | | - | desc: "keep files with different extensions", |
547 | | - | glob: "**", |
548 | | - | expected: []string{ |
549 | | - | "Dockerfile", |
550 | | - | "file-1.txt", |
551 | | - | "file-2.txt", |
552 | | - | "target/really/nested/file-3.txt", |
553 | | - | }, |
554 | | - | exclusions: []string{"**/target/**/*.jar"}, |
555 | | - | }, |
556 | | - | { |
557 | | - | input: "test-fixtures/path-detected", |
558 | | - | desc: "file directly excluded", |
559 | | - | glob: "**", |
560 | | - | expected: []string{ |
561 | | - | ".vimrc", |
562 | | - | }, |
563 | | - | exclusions: []string{"**/empty"}, |
564 | | - | }, |
565 | | - | { |
566 | | - | input: "test-fixtures/path-detected", |
567 | | - | desc: "pattern error containing **/", |
568 | | - | glob: "**", |
569 | | - | expected: []string{ |
570 | | - | ".vimrc", |
571 | | - | }, |
572 | | - | exclusions: []string{"/**/empty"}, |
573 | | - | err: true, |
574 | | - | }, |
575 | | - | { |
576 | | - | input: "test-fixtures/path-detected", |
577 | | - | desc: "pattern error incorrect start", |
578 | | - | glob: "**", |
579 | | - | expected: []string{ |
580 | | - | ".vimrc", |
581 | | - | }, |
582 | | - | exclusions: []string{"empty"}, |
583 | | - | err: true, |
584 | | - | }, |
585 | | - | { |
586 | | - | input: "test-fixtures/path-detected", |
587 | | - | desc: "pattern error starting with /", |
588 | | - | glob: "**", |
589 | | - | expected: []string{ |
590 | | - | ".vimrc", |
591 | | - | }, |
592 | | - | exclusions: []string{"/empty"}, |
593 | | - | err: true, |
594 | | - | }, |
595 | | - | } |
596 | | - | registryOpts := &image.RegistryOptions{} |
597 | | - | for _, test := range testCases { |
598 | | - | t.Run(test.desc, func(t *testing.T) { |
599 | | - | sourceInput, err := ParseInput("dir:"+test.input, "") |
600 | | - | require.NoError(t, err) |
601 | | - | src, fn, err := New(*sourceInput, registryOpts, test.exclusions) |
602 | | - | defer fn() |
603 | | - | |
604 | | - | if test.err { |
605 | | - | _, err = src.FileResolver(SquashedScope) |
606 | | - | if err == nil { |
607 | | - | t.Errorf("expected an error for patterns: %s", strings.Join(test.exclusions, " or ")) |
608 | | - | } |
609 | | - | return |
610 | | - | } |
611 | | - | |
612 | | - | if err != nil { |
613 | | - | t.Errorf("could not create NewDirScope: %+v", err) |
614 | | - | } |
615 | | - | res, err := src.FileResolver(SquashedScope) |
616 | | - | if err != nil { |
617 | | - | t.Errorf("could not get resolver error: %+v", err) |
618 | | - | } |
619 | | - | locations, err := res.FilesByGlob(test.glob) |
620 | | - | if err != nil { |
621 | | - | t.Errorf("could not get files by glob: %s+v", err) |
622 | | - | } |
623 | | - | var actual []string |
624 | | - | for _, l := range locations { |
625 | | - | actual = append(actual, l.RealPath) |
626 | | - | } |
627 | | - | |
628 | | - | sort.Strings(test.expected) |
629 | | - | sort.Strings(actual) |
630 | | - | |
631 | | - | assert.Equal(t, test.expected, actual, "diff \n"+cmp.Diff(test.expected, actual)) |
632 | | - | }) |
633 | | - | } |
634 | | - | } |
635 | | - | |
636 | | - | func TestImageExclusions(t *testing.T) { |
637 | | - | testCases := []struct { |
638 | | - | desc string |
639 | | - | input string |
640 | | - | glob string |
641 | | - | expected int |
642 | | - | exclusions []string |
643 | | - | }{ |
644 | | - | // NOTE: in the Dockerfile, /target is moved to /, which makes /really a top-level dir |
645 | | - | { |
646 | | - | input: "image-simple", |
647 | | - | desc: "a single path excluded", |
648 | | - | glob: "**", |
649 | | - | expected: 2, |
650 | | - | exclusions: []string{"/really/**"}, |
651 | | - | }, |
652 | | - | { |
653 | | - | input: "image-simple", |
654 | | - | desc: "a directly referenced directory is excluded", |
655 | | - | glob: "**", |
656 | | - | expected: 2, |
657 | | - | exclusions: []string{"/really"}, |
658 | | - | }, |
659 | | - | { |
660 | | - | input: "image-simple", |
661 | | - | desc: "a partial directory is not excluded", |
662 | | - | glob: "**", |
663 | | - | expected: 3, |
664 | | - | exclusions: []string{"/reall"}, |
665 | | - | }, |
666 | | - | { |
667 | | - | input: "image-simple", |
668 | | - | desc: "exclude files deeper", |
669 | | - | glob: "**", |
670 | | - | expected: 2, |
671 | | - | exclusions: []string{"**/nested/**"}, |
672 | | - | }, |
673 | | - | { |
674 | | - | input: "image-simple", |
675 | | - | desc: "files excluded with extension", |
676 | | - | glob: "**", |
677 | | - | expected: 2, |
678 | | - | exclusions: []string{"**/*1.txt"}, |
679 | | - | }, |
680 | | - | { |
681 | | - | input: "image-simple", |
682 | | - | desc: "keep files with different extensions", |
683 | | - | glob: "**", |
684 | | - | expected: 3, |
685 | | - | exclusions: []string{"**/target/**/*.jar"}, |
686 | | - | }, |
687 | | - | { |
688 | | - | input: "image-simple", |
689 | | - | desc: "file directly excluded", |
690 | | - | glob: "**", |
691 | | - | expected: 2, |
692 | | - | exclusions: []string{"**/somefile-1.txt"}, // file-1 renamed to somefile-1 in Dockerfile |
693 | | - | }, |
694 | | - | } |
695 | | - | registryOpts := &image.RegistryOptions{} |
696 | | - | for _, test := range testCases { |
697 | | - | t.Run(test.desc, func(t *testing.T) { |
698 | | - | archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input) |
699 | | - | sourceInput, err := ParseInput(archiveLocation, "") |
700 | | - | require.NoError(t, err) |
701 | | - | src, fn, err := New(*sourceInput, registryOpts, test.exclusions) |
702 | | - | defer fn() |
703 | | - | |
704 | | - | if err != nil { |
705 | | - | t.Errorf("could not create NewDirScope: %+v", err) |
706 | | - | } |
707 | | - | res, err := src.FileResolver(SquashedScope) |
708 | | - | if err != nil { |
709 | | - | t.Errorf("could not get resolver error: %+v", err) |
710 | | - | } |
711 | | - | contents, err := res.FilesByGlob(test.glob) |
712 | | - | if err != nil { |
713 | | - | t.Errorf("could not get files by glob: %s+v", err) |
714 | | - | } |
715 | | - | if len(contents) != test.expected { |
716 | | - | t.Errorf("wrong number of files after exclusions (%s): %d != %d", test.glob, len(contents), test.expected) |
717 | | - | } |
718 | | - | }) |
719 | | - | } |
720 | | - | } |
721 | | - | |
722 | | - | type dummyInfo struct { |
723 | | - | isDir bool |
724 | | - | } |
725 | | - | |
726 | | - | func (d dummyInfo) Name() string { |
727 | | - | //TODO implement me |
728 | | - | panic("implement me") |
729 | | - | } |
730 | | - | |
731 | | - | func (d dummyInfo) Size() int64 { |
732 | | - | //TODO implement me |
733 | | - | panic("implement me") |
734 | | - | } |
735 | | - | |
736 | | - | func (d dummyInfo) Mode() fs.FileMode { |
737 | | - | //TODO implement me |
738 | | - | panic("implement me") |
739 | | - | } |
740 | | - | |
741 | | - | func (d dummyInfo) ModTime() time.Time { |
742 | | - | //TODO implement me |
743 | | - | panic("implement me") |
744 | | - | } |
745 | | - | |
746 | | - | func (d dummyInfo) IsDir() bool { |
747 | | - | return d.isDir |
748 | | - | } |
749 | | - | |
750 | | - | func (d dummyInfo) Sys() any { |
751 | | - | //TODO implement me |
752 | | - | panic("implement me") |
753 | | - | } |
754 | | - | |
755 | | - | func Test_crossPlatformExclusions(t *testing.T) { |
756 | | - | testCases := []struct { |
757 | | - | desc string |
758 | | - | root string |
759 | | - | path string |
760 | | - | finfo os.FileInfo |
761 | | - | exclude string |
762 | | - | walkHint error |
763 | | - | }{ |
764 | | - | { |
765 | | - | desc: "directory exclusion", |
766 | | - | root: "/", |
767 | | - | path: "/usr/var/lib", |
768 | | - | exclude: "**/var/lib", |
769 | | - | finfo: dummyInfo{isDir: true}, |
770 | | - | walkHint: fs.SkipDir, |
771 | | - | }, |
772 | | - | { |
773 | | - | desc: "no file info", |
774 | | - | root: "/", |
775 | | - | path: "/usr/var/lib", |
776 | | - | exclude: "**/var/lib", |
777 | | - | walkHint: fileresolver.ErrSkipPath, |
778 | | - | }, |
779 | | - | // linux specific tests... |
780 | | - | { |
781 | | - | desc: "linux doublestar", |
782 | | - | root: "/usr", |
783 | | - | path: "/usr/var/lib/etc.txt", |
784 | | - | exclude: "**/*.txt", |
785 | | - | finfo: dummyInfo{isDir: false}, |
786 | | - | walkHint: fileresolver.ErrSkipPath, |
787 | | - | }, |
788 | | - | { |
789 | | - | desc: "linux relative", |
790 | | - | root: "/usr/var/lib", |
791 | | - | path: "/usr/var/lib/etc.txt", |
792 | | - | exclude: "./*.txt", |
793 | | - | finfo: dummyInfo{isDir: false}, |
794 | | - | |
795 | | - | walkHint: fileresolver.ErrSkipPath, |
796 | | - | }, |
797 | | - | { |
798 | | - | desc: "linux one level", |
799 | | - | root: "/usr", |
800 | | - | path: "/usr/var/lib/etc.txt", |
801 | | - | exclude: "*/*.txt", |
802 | | - | finfo: dummyInfo{isDir: false}, |
803 | | - | walkHint: nil, |
804 | | - | }, |
805 | | - | // NOTE: since these tests will run in linux and macOS, the windows paths will be |
806 | | - | // considered relative if they do not start with a forward slash and paths with backslashes |
807 | | - | // won't be modified by the filepath.ToSlash call, so these are emulating the result of |
808 | | - | // filepath.ToSlash usage |
809 | | - | |
810 | | - | // windows specific tests... |
811 | | - | { |
812 | | - | desc: "windows doublestar", |
813 | | - | root: "/C:/User/stuff", |
814 | | - | path: "/C:/User/stuff/thing.txt", |
815 | | - | exclude: "**/*.txt", |
816 | | - | finfo: dummyInfo{isDir: false}, |
817 | | - | walkHint: fileresolver.ErrSkipPath, |
818 | | - | }, |
819 | | - | { |
820 | | - | desc: "windows relative", |
821 | | - | root: "/C:/User/stuff", |
822 | | - | path: "/C:/User/stuff/thing.txt", |
823 | | - | exclude: "./*.txt", |
824 | | - | finfo: dummyInfo{isDir: false}, |
825 | | - | walkHint: fileresolver.ErrSkipPath, |
826 | | - | }, |
827 | | - | { |
828 | | - | desc: "windows one level", |
829 | | - | root: "/C:/User/stuff", |
830 | | - | path: "/C:/User/stuff/thing.txt", |
831 | | - | exclude: "*/*.txt", |
832 | | - | finfo: dummyInfo{isDir: false}, |
833 | | - | walkHint: nil, |
834 | | - | }, |
835 | | - | } |
836 | | - | |
837 | | - | for _, test := range testCases { |
838 | | - | t.Run(test.desc, func(t *testing.T) { |
839 | | - | fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude}) |
840 | | - | require.NoError(t, err) |
841 | | - | |
842 | | - | for _, f := range fns { |
843 | | - | result := f(test.path, test.finfo, nil) |
844 | | - | require.Equal(t, test.walkHint, result) |
845 | | - | } |
846 | | - | }) |
847 | | - | } |
848 | | - | } |
849 | | - | |
850 | | - | // createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath. |
851 | | - | func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, layer2 bool) { |
852 | | - | t.Helper() |
853 | | - | |
854 | | - | cwd, err := os.Getwd() |
855 | | - | if err != nil { |
856 | | - | t.Fatalf("unable to get cwd: %+v", err) |
857 | | - | } |
858 | | - | |
859 | | - | cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath)) |
860 | | - | cmd.Dir = filepath.Join(cwd, "test-fixtures") |
861 | | - | |
862 | | - | if err := cmd.Start(); err != nil { |
863 | | - | t.Fatalf("unable to start generate zip fixture script: %+v", err) |
864 | | - | } |
865 | | - | |
866 | | - | if err := cmd.Wait(); err != nil { |
867 | | - | if exiterr, ok := err.(*exec.ExitError); ok { |
868 | | - | // The program has exited with an exit code != 0 |
869 | | - | |
870 | | - | // This works on both Unix and Windows. Although package |
871 | | - | // syscall is generally platform dependent, WaitStatus is |
872 | | - | // defined for both Unix and Windows and in both cases has |
873 | | - | // an ExitStatus() method with the same signature. |
874 | | - | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { |
875 | | - | if status.ExitStatus() != 0 { |
876 | | - | t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus()) |
877 | | - | } |
878 | | - | } |
879 | | - | } else { |
880 | | - | t.Fatalf("unable to get generate fixture script result: %+v", err) |
881 | | - | } |
882 | | - | } |
883 | | - | |
884 | | - | if layer2 { |
885 | | - | cmd = exec.Command("tar", "-rvf", destinationArchivePath, ".") |
886 | | - | cmd.Dir = filepath.Join(cwd, "test-fixtures", path.Base(sourceDirPath+"-2")) |
887 | | - | if err := cmd.Start(); err != nil { |
888 | | - | t.Fatalf("unable to start tar appending fixture script: %+v", err) |
889 | | - | } |
890 | | - | _ = cmd.Wait() |
891 | | - | } |
892 | | - | } |
893 | | - | |
894 | | - | // setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function, |
895 | | - | // which should be called (typically deferred) by the caller, the path of the created tar archive, and an error, |
896 | | - | // which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil |
897 | | - | // (even if there's an error), and it should always be called. |
898 | | - | func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string { |
899 | | - | t.Helper() |
900 | | - | |
901 | | - | archivePrefix, err := os.CreateTemp("", "syft-archive-TEST-") |
902 | | - | require.NoError(t, err) |
903 | | - | |
904 | | - | t.Cleanup( |
905 | | - | assertNoError(t, |
906 | | - | func() error { |
907 | | - | return os.Remove(archivePrefix.Name()) |
908 | | - | }, |
909 | | - | ), |
910 | | - | ) |
911 | | - | |
912 | | - | destinationArchiveFilePath := archivePrefix.Name() + ".tar" |
913 | | - | t.Logf("archive path: %s", destinationArchiveFilePath) |
914 | | - | createArchive(t, sourceDirPath, destinationArchiveFilePath, layer2) |
915 | | - | |
916 | | - | t.Cleanup( |
917 | | - | assertNoError(t, |
918 | | - | func() error { |
919 | | - | return os.Remove(destinationArchiveFilePath) |
920 | | - | }, |
921 | | - | ), |
922 | | - | ) |
923 | | - | |
924 | | - | cwd, err := os.Getwd() |
925 | | - | require.NoError(t, err) |
926 | | - | |
927 | | - | t.Logf("running from: %s", cwd) |
928 | | - | |
929 | | - | return destinationArchiveFilePath |
930 | | - | } |
931 | | - | |
932 | | - | func assertNoError(t testing.TB, fn func() error) func() { |
933 | | - | return func() { |
934 | | - | assert.NoError(t, fn()) |
935 | | - | } |
936 | | - | } |
937 | | - | |