github.com/anchore/syft@v1.38.2/syft/internal/fileresolver/unindexed_directory_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package fileresolver 5 6 import ( 7 "context" 8 "io" 9 "os" 10 "path" 11 "path/filepath" 12 "sort" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/google/go-cmp/cmp" 18 "github.com/scylladb/go-set/strset" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 "go.uber.org/goleak" 22 23 stereoscopeFile "github.com/anchore/stereoscope/pkg/file" 24 "github.com/anchore/syft/syft/file" 25 ) 26 27 func Test_UnindexDirectoryResolver_RequestRelativePathWithinSymlink(t *testing.T) { 28 pwd, err := os.Getwd() 29 30 // we need to mimic a shell, otherwise we won't get a path within a symlink 31 targetPath := filepath.Join(pwd, "./test-fixtures/symlinked-root/nested/link-root/nested") 32 t.Setenv("PWD", targetPath) 33 34 require.NoError(t, err) 35 require.NoError(t, os.Chdir(targetPath)) 36 t.Cleanup(func() { 37 require.NoError(t, os.Chdir(pwd)) 38 }) 39 40 resolver := NewFromUnindexedDirectory("./") 41 require.NoError(t, err) 42 43 locations, err := resolver.FilesByPath("file2.txt") 44 require.NoError(t, err) 45 require.Len(t, locations, 1) 46 47 // TODO: this is technically not correct behavior since this is reporting the symlink path (virtual path) and 48 // not the real path. 49 require.False(t, filepath.IsAbs(locations[0].RealPath), "should be relative path") 50 } 51 52 func Test_UnindexDirectoryResolver_FilesByPath_request_response(t *testing.T) { 53 // / 54 // somewhere/ 55 // outside.txt 56 // root-link -> ./ 57 // path/ 58 // to/ 59 // abs-inside.txt -> /path/to/the/file.txt # absolute link to somewhere inside of the root 60 // rel-inside.txt -> ./the/file.txt # relative link to somewhere inside of the root 61 // the/ 62 // file.txt 63 // abs-outside.txt -> /somewhere/outside.txt # absolute link to outside of the root 64 // rel-outside -> ../../../somewhere/outside.txt # relative link to outside of the root 65 // 66 67 testDir, err := os.Getwd() 68 require.NoError(t, err) 69 relative := filepath.Join("test-fixtures", "req-resp") 70 absolute := filepath.Join(testDir, relative) 71 72 absInsidePath := filepath.Join(absolute, "path", "to", "abs-inside.txt") 73 absOutsidePath := filepath.Join(absolute, "path", "to", "the", "abs-outside.txt") 74 75 relativeViaLink := filepath.Join(relative, "root-link") 76 absoluteViaLink := filepath.Join(absolute, "root-link") 77 78 relativeViaDoubleLink := filepath.Join(relative, "root-link", "root-link") 79 absoluteViaDoubleLink := filepath.Join(absolute, "root-link", "root-link") 80 81 cleanup := func() { 82 _ = os.Remove(absInsidePath) 83 _ = os.Remove(absOutsidePath) 84 } 85 86 // ensure the absolute symlinks are cleaned up from any previous runs 87 cleanup() 88 89 require.NoError(t, os.Symlink(filepath.Join(absolute, "path", "to", "the", "file.txt"), absInsidePath)) 90 require.NoError(t, os.Symlink(filepath.Join(absolute, "somewhere", "outside.txt"), absOutsidePath)) 91 92 t.Cleanup(cleanup) 93 94 cases := []struct { 95 name string 96 cwd string 97 root string 98 base string 99 input string 100 expectedRealPath string 101 expectedAccessPath string // if empty, the virtual path should be the same as the real path 102 }{ 103 { 104 name: "relative root, relative request, direct", 105 root: relative, 106 input: "path/to/the/file.txt", 107 expectedRealPath: "path/to/the/file.txt", 108 }, 109 { 110 name: "abs root, relative request, direct", 111 root: absolute, 112 input: "path/to/the/file.txt", 113 expectedRealPath: "path/to/the/file.txt", 114 }, 115 { 116 name: "relative root, abs request, direct", 117 root: relative, 118 input: "/path/to/the/file.txt", 119 expectedRealPath: "path/to/the/file.txt", 120 }, 121 { 122 name: "abs root, abs request, direct", 123 root: absolute, 124 input: "/path/to/the/file.txt", 125 expectedRealPath: "path/to/the/file.txt", 126 }, 127 // cwd within root... 128 { 129 name: "relative root, relative request, direct, cwd within root", 130 cwd: filepath.Join(relative, "path/to"), 131 root: "../../", 132 input: "path/to/the/file.txt", 133 expectedRealPath: "path/to/the/file.txt", 134 }, 135 { 136 name: "abs root, relative request, direct, cwd within root", 137 cwd: filepath.Join(relative, "path/to"), 138 root: absolute, 139 input: "path/to/the/file.txt", 140 expectedRealPath: "path/to/the/file.txt", 141 }, 142 { 143 name: "relative root, abs request, direct, cwd within root", 144 cwd: filepath.Join(relative, "path/to"), 145 root: "../../", 146 input: "/path/to/the/file.txt", 147 expectedRealPath: "path/to/the/file.txt", 148 }, 149 { 150 name: "abs root, abs request, direct, cwd within root", 151 cwd: filepath.Join(relative, "path/to"), 152 153 root: absolute, 154 input: "/path/to/the/file.txt", 155 expectedRealPath: "path/to/the/file.txt", 156 }, 157 // cwd within symlink root... 158 { 159 name: "relative root, relative request, direct, cwd within symlink root", 160 cwd: relativeViaLink, 161 root: "./", 162 input: "path/to/the/file.txt", 163 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 164 // in this case for the unindexed resolver, which is not correct. 165 expectedRealPath: "path/to/the/file.txt", 166 }, 167 { 168 name: "abs root, relative request, direct, cwd within symlink root", 169 cwd: relativeViaLink, 170 root: absoluteViaLink, 171 input: "path/to/the/file.txt", 172 expectedRealPath: "path/to/the/file.txt", 173 }, 174 { 175 name: "relative root, abs request, direct, cwd within symlink root", 176 cwd: relativeViaLink, 177 root: "./", 178 input: "/path/to/the/file.txt", 179 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 180 // in this case for the unindexed resolver, which is not correct. 181 expectedRealPath: "path/to/the/file.txt", 182 }, 183 { 184 name: "abs root, abs request, direct, cwd within symlink root", 185 cwd: relativeViaLink, 186 root: absoluteViaLink, 187 input: "/path/to/the/file.txt", 188 expectedRealPath: "path/to/the/file.txt", 189 }, 190 // cwd within symlink root, request nested within... 191 { 192 name: "relative root, relative nested request, direct, cwd within symlink root", 193 cwd: relativeViaLink, 194 root: "./path", 195 input: "to/the/file.txt", 196 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 197 // in this case for the unindexed resolver, which is not correct. 198 expectedRealPath: "to/the/file.txt", 199 }, 200 { 201 name: "abs root, relative nested request, direct, cwd within symlink root", 202 cwd: relativeViaLink, 203 root: filepath.Join(absoluteViaLink, "path"), 204 input: "to/the/file.txt", 205 expectedRealPath: "to/the/file.txt", 206 }, 207 { 208 name: "relative root, abs nested request, direct, cwd within symlink root", 209 cwd: relativeViaLink, 210 root: "./path", 211 input: "/to/the/file.txt", 212 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 213 // in this case for the unindexed resolver, which is not correct. 214 expectedRealPath: "to/the/file.txt", 215 }, 216 { 217 name: "abs root, abs nested request, direct, cwd within symlink root", 218 cwd: relativeViaLink, 219 root: filepath.Join(absoluteViaLink, "path"), 220 input: "/to/the/file.txt", 221 expectedRealPath: "to/the/file.txt", 222 }, 223 // cwd within DOUBLE symlink root... 224 { 225 name: "relative root, relative request, direct, cwd within (double) symlink root", 226 cwd: relativeViaDoubleLink, 227 root: "./", 228 input: "path/to/the/file.txt", 229 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 230 // in this case for the unindexed resolver, which is not correct. 231 expectedRealPath: "path/to/the/file.txt", 232 }, 233 { 234 name: "abs root, relative request, direct, cwd within (double) symlink root", 235 cwd: relativeViaDoubleLink, 236 root: absoluteViaDoubleLink, 237 input: "path/to/the/file.txt", 238 expectedRealPath: "path/to/the/file.txt", 239 }, 240 { 241 name: "relative root, abs request, direct, cwd within (double) symlink root", 242 cwd: relativeViaDoubleLink, 243 root: "./", 244 input: "/path/to/the/file.txt", 245 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 246 // in this case for the unindexed resolver, which is not correct. 247 expectedRealPath: "path/to/the/file.txt", 248 }, 249 { 250 name: "abs root, abs request, direct, cwd within (double) symlink root", 251 cwd: relativeViaDoubleLink, 252 root: absoluteViaDoubleLink, 253 input: "/path/to/the/file.txt", 254 expectedRealPath: "path/to/the/file.txt", 255 }, 256 // cwd within DOUBLE symlink root, request nested within... 257 { 258 name: "relative root, relative nested request, direct, cwd within (double) symlink root", 259 cwd: relativeViaDoubleLink, 260 root: "./path", 261 input: "to/the/file.txt", 262 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 263 // in this case for the unindexed resolver, which is not correct. 264 expectedRealPath: "to/the/file.txt", 265 }, 266 { 267 name: "abs root, relative nested request, direct, cwd within (double) symlink root", 268 cwd: relativeViaDoubleLink, 269 root: filepath.Join(absoluteViaDoubleLink, "path"), 270 input: "to/the/file.txt", 271 expectedRealPath: "to/the/file.txt", 272 }, 273 { 274 name: "relative root, abs nested request, direct, cwd within (double) symlink root", 275 cwd: relativeViaDoubleLink, 276 root: "./path", 277 input: "/to/the/file.txt", 278 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 279 // in this case for the unindexed resolver, which is not correct. 280 expectedRealPath: "to/the/file.txt", 281 }, 282 { 283 name: "abs root, abs nested request, direct, cwd within (double) symlink root", 284 cwd: relativeViaDoubleLink, 285 root: filepath.Join(absoluteViaDoubleLink, "path"), 286 input: "/to/the/file.txt", 287 expectedRealPath: "to/the/file.txt", 288 }, 289 // cwd within DOUBLE symlink root, request nested DEEP within... 290 { 291 name: "relative root, relative nested request, direct, cwd deep within (double) symlink root", 292 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 293 root: "../", 294 input: "to/the/file.txt", 295 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 296 // in this case for the unindexed resolver, which is not correct. 297 expectedRealPath: "to/the/file.txt", 298 }, 299 { 300 name: "abs root, relative nested request, direct, cwd deep within (double) symlink root", 301 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 302 root: filepath.Join(absoluteViaDoubleLink, "path"), 303 input: "to/the/file.txt", 304 expectedRealPath: "to/the/file.txt", 305 }, 306 { 307 name: "relative root, abs nested request, direct, cwd deep within (double) symlink root", 308 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 309 root: "../", 310 input: "/to/the/file.txt", 311 // note: this is inconsistent with the directory resolver. The real path is essentially the virtual path 312 // in this case for the unindexed resolver, which is not correct. 313 expectedRealPath: "to/the/file.txt", 314 }, 315 { 316 name: "abs root, abs nested request, direct, cwd deep within (double) symlink root", 317 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 318 root: filepath.Join(absoluteViaDoubleLink, "path"), 319 input: "/to/the/file.txt", 320 expectedRealPath: "to/the/file.txt", 321 }, 322 // link to outside of root cases... 323 { 324 name: "relative root, relative request, abs indirect (outside of root)", 325 root: filepath.Join(relative, "path"), 326 input: "to/the/abs-outside.txt", 327 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 328 expectedAccessPath: "to/the/abs-outside.txt", 329 }, 330 { 331 name: "abs root, relative request, abs indirect (outside of root)", 332 root: filepath.Join(absolute, "path"), 333 input: "to/the/abs-outside.txt", 334 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 335 expectedAccessPath: "to/the/abs-outside.txt", 336 }, 337 { 338 name: "relative root, abs request, abs indirect (outside of root)", 339 root: filepath.Join(relative, "path"), 340 input: "/to/the/abs-outside.txt", 341 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 342 expectedAccessPath: "to/the/abs-outside.txt", 343 }, 344 { 345 name: "abs root, abs request, abs indirect (outside of root)", 346 root: filepath.Join(absolute, "path"), 347 input: "/to/the/abs-outside.txt", 348 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 349 expectedAccessPath: "to/the/abs-outside.txt", 350 }, 351 { 352 name: "relative root, relative request, relative indirect (outside of root)", 353 root: filepath.Join(relative, "path"), 354 input: "to/the/rel-outside.txt", 355 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 356 // TODO: the real path is not correct 357 expectedRealPath: "../somewhere/outside.txt", 358 expectedAccessPath: "to/the/rel-outside.txt", 359 }, 360 { 361 name: "abs root, relative request, relative indirect (outside of root)", 362 root: filepath.Join(absolute, "path"), 363 input: "to/the/rel-outside.txt", 364 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 365 // TODO: the real path is not correct 366 expectedRealPath: "../somewhere/outside.txt", 367 expectedAccessPath: "to/the/rel-outside.txt", 368 }, 369 { 370 name: "relative root, abs request, relative indirect (outside of root)", 371 root: filepath.Join(relative, "path"), 372 input: "/to/the/rel-outside.txt", 373 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 374 // TODO: the real path is not correct 375 expectedRealPath: "../somewhere/outside.txt", 376 expectedAccessPath: "to/the/rel-outside.txt", 377 }, 378 { 379 name: "abs root, abs request, relative indirect (outside of root)", 380 root: filepath.Join(absolute, "path"), 381 input: "/to/the/rel-outside.txt", 382 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 383 // TODO: the real path is not correct 384 expectedRealPath: "../somewhere/outside.txt", 385 expectedAccessPath: "to/the/rel-outside.txt", 386 }, 387 // link to outside of root cases... cwd within symlink root 388 { 389 name: "relative root, relative request, abs indirect (outside of root), cwd within symlink root", 390 cwd: relativeViaLink, 391 root: "path", 392 input: "to/the/abs-outside.txt", 393 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 394 expectedAccessPath: "to/the/abs-outside.txt", 395 }, 396 { 397 name: "abs root, relative request, abs indirect (outside of root), cwd within symlink root", 398 cwd: relativeViaLink, 399 root: filepath.Join(absolute, "path"), 400 input: "to/the/abs-outside.txt", 401 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 402 expectedAccessPath: "to/the/abs-outside.txt", 403 }, 404 { 405 name: "relative root, abs request, abs indirect (outside of root), cwd within symlink root", 406 cwd: relativeViaLink, 407 root: "path", 408 input: "/to/the/abs-outside.txt", 409 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 410 expectedAccessPath: "to/the/abs-outside.txt", 411 }, 412 { 413 name: "abs root, abs request, abs indirect (outside of root), cwd within symlink root", 414 cwd: relativeViaLink, 415 root: filepath.Join(absolute, "path"), 416 input: "/to/the/abs-outside.txt", 417 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 418 expectedAccessPath: "to/the/abs-outside.txt", 419 }, 420 { 421 name: "relative root, relative request, relative indirect (outside of root), cwd within symlink root", 422 cwd: relativeViaLink, 423 root: "path", 424 input: "to/the/rel-outside.txt", 425 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 426 // TODO: the real path is not correct 427 expectedRealPath: "../somewhere/outside.txt", 428 expectedAccessPath: "to/the/rel-outside.txt", 429 }, 430 { 431 name: "abs root, relative request, relative indirect (outside of root), cwd within symlink root", 432 cwd: relativeViaLink, 433 root: filepath.Join(absolute, "path"), 434 input: "to/the/rel-outside.txt", 435 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 436 // TODO: the real path is not correct 437 expectedRealPath: "../somewhere/outside.txt", 438 expectedAccessPath: "to/the/rel-outside.txt", 439 }, 440 { 441 name: "relative root, abs request, relative indirect (outside of root), cwd within symlink root", 442 cwd: relativeViaLink, 443 root: "path", 444 input: "/to/the/rel-outside.txt", 445 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 446 // TODO: the real path is not correct 447 expectedRealPath: "../somewhere/outside.txt", 448 expectedAccessPath: "to/the/rel-outside.txt", 449 }, 450 { 451 name: "abs root, abs request, relative indirect (outside of root), cwd within symlink root", 452 cwd: relativeViaLink, 453 root: filepath.Join(absolute, "path"), 454 input: "/to/the/rel-outside.txt", 455 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 456 // TODO: the real path is not correct 457 expectedRealPath: "../somewhere/outside.txt", 458 expectedAccessPath: "to/the/rel-outside.txt", 459 }, 460 { 461 name: "relative root, relative request, relative indirect (outside of root), cwd within DOUBLE symlink root", 462 cwd: relativeViaDoubleLink, 463 root: "path", 464 input: "to/the/rel-outside.txt", 465 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 466 // TODO: the real path is not correct 467 expectedRealPath: "../somewhere/outside.txt", 468 expectedAccessPath: "to/the/rel-outside.txt", 469 }, 470 { 471 name: "abs root, relative request, relative indirect (outside of root), cwd within DOUBLE symlink root", 472 cwd: relativeViaDoubleLink, 473 root: filepath.Join(absolute, "path"), 474 input: "to/the/rel-outside.txt", 475 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 476 // TODO: the real path is not correct 477 expectedRealPath: "../somewhere/outside.txt", 478 expectedAccessPath: "to/the/rel-outside.txt", 479 }, 480 { 481 name: "relative root, abs request, relative indirect (outside of root), cwd within DOUBLE symlink root", 482 cwd: relativeViaDoubleLink, 483 root: "path", 484 input: "/to/the/rel-outside.txt", 485 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 486 // TODO: the real path is not correct 487 expectedRealPath: "../somewhere/outside.txt", 488 expectedAccessPath: "to/the/rel-outside.txt", 489 }, 490 { 491 name: "abs root, abs request, relative indirect (outside of root), cwd within DOUBLE symlink root", 492 cwd: relativeViaDoubleLink, 493 root: filepath.Join(absolute, "path"), 494 input: "/to/the/rel-outside.txt", 495 //expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 496 // TODO: the real path is not correct 497 expectedRealPath: "../somewhere/outside.txt", 498 expectedAccessPath: "to/the/rel-outside.txt", 499 }, 500 } 501 for _, c := range cases { 502 t.Run(c.name, func(t *testing.T) { 503 if c.expectedAccessPath == "" { 504 c.expectedAccessPath = c.expectedRealPath 505 } 506 507 // we need to mimic a shell, otherwise we won't get a path within a symlink 508 targetPath := filepath.Join(testDir, c.cwd) 509 t.Setenv("PWD", filepath.Clean(targetPath)) 510 511 require.NoError(t, err) 512 require.NoError(t, os.Chdir(targetPath)) 513 t.Cleanup(func() { 514 require.NoError(t, os.Chdir(testDir)) 515 }) 516 517 resolver := NewFromUnindexedDirectory(c.root) 518 require.NotNil(t, resolver) 519 520 refs, err := resolver.FilesByPath(c.input) 521 require.NoError(t, err) 522 if c.expectedRealPath == "" { 523 require.Empty(t, refs) 524 return 525 } 526 require.Len(t, refs, 1) 527 assert.Equal(t, c.expectedRealPath, refs[0].RealPath, "real path different") 528 assert.Equal(t, c.expectedAccessPath, refs[0].AccessPath, "virtual path different") 529 }) 530 } 531 } 532 533 func Test_UnindexedDirectoryResolver_Basic(t *testing.T) { 534 wd, err := os.Getwd() 535 require.NoError(t, err) 536 537 r := NewFromUnindexedDirectory(path.Join(wd, "test-fixtures")) 538 locations, err := r.FilesByGlob("image-symlinks/*") 539 require.NoError(t, err) 540 require.Len(t, locations, 5) 541 } 542 543 func Test_UnindexedDirectoryResolver_NoGoroutineLeak(t *testing.T) { 544 defer goleak.VerifyNone(t) 545 wd, err := os.Getwd() 546 require.NoError(t, err) 547 548 r := NewFromUnindexedDirectory(path.Join(wd, "test-fixtures")) 549 ctx, cancel := context.WithCancel(context.Background()) 550 for range r.AllLocations(ctx) { 551 break 552 } 553 cancel() 554 } 555 556 func Test_UnindexedDirectoryResolver_FilesByPath_relativeRoot(t *testing.T) { 557 cases := []struct { 558 name string 559 relativeRoot string 560 input string 561 expected []string 562 }{ 563 { 564 name: "should find a file from an absolute input", 565 relativeRoot: "./test-fixtures/", 566 input: "/image-symlinks/file-1.txt", 567 expected: []string{ 568 "image-symlinks/file-1.txt", 569 }, 570 }, 571 { 572 name: "should find a file from a relative path", 573 relativeRoot: "./test-fixtures/", 574 input: "image-symlinks/file-1.txt", 575 expected: []string{ 576 "image-symlinks/file-1.txt", 577 }, 578 }, 579 { 580 name: "should find a file from a relative path (root above cwd)", 581 // TODO: refactor me! this test depends on the structure of the source dir not changing, which isn't great 582 relativeRoot: "../", 583 input: "fileresolver/deferred.go", 584 expected: []string{ 585 "fileresolver/deferred.go", 586 }, 587 }, 588 } 589 for _, c := range cases { 590 t.Run(c.name, func(t *testing.T) { 591 resolver := NewFromUnindexedDirectory(c.relativeRoot) 592 593 refs, err := resolver.FilesByPath(c.input) 594 require.NoError(t, err) 595 assert.Len(t, refs, len(c.expected)) 596 s := strset.New() 597 for _, actual := range refs { 598 s.Add(actual.RealPath) 599 } 600 assert.ElementsMatch(t, c.expected, s.List()) 601 }) 602 } 603 } 604 605 func Test_UnindexedDirectoryResolver_FilesByPath_absoluteRoot(t *testing.T) { 606 cases := []struct { 607 name string 608 relativeRoot string 609 input string 610 expected []string 611 }{ 612 { 613 name: "should find a file from an absolute input", 614 relativeRoot: "./test-fixtures/", 615 input: "/image-symlinks/file-1.txt", 616 expected: []string{ 617 "image-symlinks/file-1.txt", 618 }, 619 }, 620 { 621 name: "should find a file from a relative path", 622 relativeRoot: "./test-fixtures/", 623 input: "image-symlinks/file-1.txt", 624 expected: []string{ 625 "image-symlinks/file-1.txt", 626 }, 627 }, 628 { 629 name: "should find a file from a relative path (root above cwd)", 630 // TODO: refactor me! this test depends on the structure of the source dir not changing, which isn't great 631 relativeRoot: "../", 632 input: "fileresolver/directory.go", 633 expected: []string{ 634 "fileresolver/directory.go", 635 }, 636 }, 637 } 638 for _, c := range cases { 639 t.Run(c.name, func(t *testing.T) { 640 // note: this test is all about asserting correct functionality when the given analysis path 641 // is an absolute path 642 absRoot, err := filepath.Abs(c.relativeRoot) 643 require.NoError(t, err) 644 645 resolver := NewFromUnindexedDirectory(absRoot) 646 assert.NoError(t, err) 647 648 refs, err := resolver.FilesByPath(c.input) 649 require.NoError(t, err) 650 assert.Len(t, refs, len(c.expected)) 651 s := strset.New() 652 for _, actual := range refs { 653 s.Add(actual.RealPath) 654 } 655 assert.ElementsMatch(t, c.expected, s.List()) 656 }) 657 } 658 } 659 660 func Test_UnindexedDirectoryResolver_FilesByPath(t *testing.T) { 661 cases := []struct { 662 name string 663 root string 664 input string 665 expected string 666 refCount int 667 forcePositiveHasPath bool 668 }{ 669 { 670 name: "finds a file (relative)", 671 root: "./test-fixtures/", 672 input: "image-symlinks/file-1.txt", 673 expected: "image-symlinks/file-1.txt", 674 refCount: 1, 675 }, 676 { 677 name: "finds a file with relative indirection", 678 root: "./test-fixtures/../test-fixtures", 679 input: "image-symlinks/file-1.txt", 680 expected: "image-symlinks/file-1.txt", 681 refCount: 1, 682 }, 683 { 684 name: "managed non-existing files (relative)", 685 root: "./test-fixtures/", 686 input: "test-fixtures/image-symlinks/bogus.txt", 687 refCount: 0, 688 }, 689 { 690 name: "finds a file (absolute)", 691 root: "./test-fixtures/", 692 input: "/image-symlinks/file-1.txt", 693 expected: "image-symlinks/file-1.txt", 694 refCount: 1, 695 }, 696 { 697 name: "directories ignored", 698 root: "./test-fixtures/", 699 input: "/image-symlinks", 700 refCount: 0, 701 forcePositiveHasPath: true, 702 }, 703 } 704 for _, c := range cases { 705 t.Run(c.name, func(t *testing.T) { 706 resolver := NewFromUnindexedDirectory(c.root) 707 708 hasPath := resolver.HasPath(c.input) 709 if !c.forcePositiveHasPath { 710 if c.refCount != 0 && !hasPath { 711 t.Errorf("expected HasPath() to indicate existence, but did not") 712 } else if c.refCount == 0 && hasPath { 713 t.Errorf("expected HasPath() to NOT indicate existence, but does") 714 } 715 } else if !hasPath { 716 t.Errorf("expected HasPath() to indicate existence, but did not (force path)") 717 } 718 719 refs, err := resolver.FilesByPath(c.input) 720 require.NoError(t, err) 721 assert.Len(t, refs, c.refCount) 722 for _, actual := range refs { 723 assert.Equal(t, c.expected, actual.RealPath) 724 } 725 }) 726 } 727 } 728 729 func Test_UnindexedDirectoryResolver_MultipleFilesByPath(t *testing.T) { 730 cases := []struct { 731 name string 732 input []string 733 refCount int 734 }{ 735 { 736 name: "finds multiple files", 737 input: []string{"image-symlinks/file-1.txt", "image-symlinks/file-2.txt"}, 738 refCount: 2, 739 }, 740 { 741 name: "skips non-existing files", 742 input: []string{"image-symlinks/bogus.txt", "image-symlinks/file-1.txt"}, 743 refCount: 1, 744 }, 745 { 746 name: "does not return anything for non-existing directories", 747 input: []string{"non-existing/bogus.txt", "non-existing/file-1.txt"}, 748 refCount: 0, 749 }, 750 } 751 for _, c := range cases { 752 t.Run(c.name, func(t *testing.T) { 753 resolver := NewFromUnindexedDirectory("./test-fixtures") 754 refs, err := resolver.FilesByPath(c.input...) 755 assert.NoError(t, err) 756 757 if len(refs) != c.refCount { 758 t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount) 759 } 760 }) 761 } 762 } 763 764 func Test_UnindexedDirectoryResolver_FilesByGlobMultiple(t *testing.T) { 765 resolver := NewFromUnindexedDirectory("./test-fixtures") 766 refs, err := resolver.FilesByGlob("**/image-symlinks/file*") 767 assert.NoError(t, err) 768 769 assert.Len(t, refs, 2) 770 } 771 772 func Test_UnindexedDirectoryResolver_FilesByGlobRecursive(t *testing.T) { 773 resolver := NewFromUnindexedDirectory("./test-fixtures/image-symlinks") 774 refs, err := resolver.FilesByGlob("**/*.txt") 775 assert.NoError(t, err) 776 assert.Len(t, refs, 6) 777 } 778 779 func Test_UnindexedDirectoryResolver_FilesByGlobSingle(t *testing.T) { 780 resolver := NewFromUnindexedDirectory("./test-fixtures") 781 refs, err := resolver.FilesByGlob("**/image-symlinks/*1.txt") 782 assert.NoError(t, err) 783 784 assert.Len(t, refs, 1) 785 assert.Equal(t, "image-symlinks/file-1.txt", refs[0].RealPath) 786 } 787 788 func Test_UnindexedDirectoryResolver_FilesByPath_ResolvesSymlinks(t *testing.T) { 789 790 tests := []struct { 791 name string 792 fixture string 793 }{ 794 { 795 name: "one degree", 796 fixture: "link_to_new_readme", 797 }, 798 { 799 name: "two degrees", 800 fixture: "link_to_link_to_new_readme", 801 }, 802 } 803 804 for _, test := range tests { 805 t.Run(test.name, func(t *testing.T) { 806 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-simple") 807 808 refs, err := resolver.FilesByPath(test.fixture) 809 require.NoError(t, err) 810 require.Len(t, refs, 1) 811 812 reader, err := resolver.FileContentsByLocation(refs[0]) 813 require.NoError(t, err) 814 815 actual, err := io.ReadAll(reader) 816 require.NoError(t, err) 817 818 expected, err := os.ReadFile("test-fixtures/symlinks-simple/readme") 819 require.NoError(t, err) 820 821 require.Equal(t, string(expected), string(actual)) 822 }) 823 } 824 } 825 826 func Test_UnindexedDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) { 827 // let's make certain that "dev/place" is not ignored, since it is not "/dev/place" 828 resolver := NewFromUnindexedDirectory("test-fixtures/system_paths/target") 829 830 // all paths should be found (non filtering matches a path) 831 locations, err := resolver.FilesByGlob("**/place") 832 assert.NoError(t, err) 833 // 4: within target/ 834 // 1: target/link --> relative path to "place" // NOTE: this is filtered out since it not unique relative to outside_root/link_target/place 835 // 1: outside_root/link_target/place 836 assert.Len(t, locations, 6) 837 838 // ensure that symlink indexing outside of root worked 839 testLocation := "../outside_root/link_target/place" 840 ok := false 841 for _, location := range locations { 842 if strings.HasSuffix(location.RealPath, testLocation) { 843 ok = true 844 } 845 } 846 847 if !ok { 848 t.Fatalf("could not find test location=%q", testLocation) 849 } 850 } 851 852 func Test_UnindexedDirectoryResover_IndexingNestedSymLinks(t *testing.T) { 853 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-simple") 854 855 // check that we can get the real path 856 locations, err := resolver.FilesByPath("./readme") 857 require.NoError(t, err) 858 assert.Len(t, locations, 1) 859 860 // check that we can access the same file via 1 symlink 861 locations, err = resolver.FilesByPath("./link_to_new_readme") 862 require.NoError(t, err) 863 require.Len(t, locations, 1) 864 assert.Equal(t, "readme", locations[0].RealPath) 865 assert.Equal(t, "link_to_new_readme", locations[0].AccessPath) 866 867 // check that we can access the same file via 2 symlinks 868 locations, err = resolver.FilesByPath("./link_to_link_to_new_readme") 869 require.NoError(t, err) 870 require.Len(t, locations, 1) 871 assert.Equal(t, "readme", locations[0].RealPath) 872 assert.Equal(t, "link_to_link_to_new_readme", locations[0].AccessPath) 873 874 // check that we can access the same file via 2 symlinks 875 locations, err = resolver.FilesByGlob("**/link_*") 876 require.NoError(t, err) 877 require.Len(t, locations, 1) // you would think this is 2, however, they point to the same file, and glob only returns unique files 878 879 // returned locations can be in any order 880 expectedAccessPaths := []string{ 881 "link_to_link_to_new_readme", 882 //"link_to_new_readme", // we filter out this one because the first symlink resolves to the same file 883 } 884 885 expectedRealPaths := []string{ 886 "readme", 887 } 888 889 actualRealPaths := strset.New() 890 actualAccessPaths := strset.New() 891 for _, a := range locations { 892 actualAccessPaths.Add(a.AccessPath) 893 actualRealPaths.Add(a.RealPath) 894 } 895 896 assert.ElementsMatch(t, expectedAccessPaths, actualAccessPaths.List()) 897 assert.ElementsMatch(t, expectedRealPaths, actualRealPaths.List()) 898 } 899 900 func Test_UnindexedDirectoryResover_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) { 901 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-multiple-roots/root") 902 903 // check that we can get the real path 904 locations, err := resolver.FilesByPath("./readme") 905 require.NoError(t, err) 906 assert.Len(t, locations, 1) 907 908 // check that we can access the same file via 2 symlinks (link_to_link_to_readme -> link_to_readme -> readme) 909 locations, err = resolver.FilesByPath("./link_to_link_to_readme") 910 require.NoError(t, err) 911 assert.Len(t, locations, 1) 912 913 // something looks wrong here 914 t.Failed() 915 } 916 917 func Test_UnindexedDirectoryResover_RootViaSymlink(t *testing.T) { 918 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinked-root/nested/link-root") 919 920 locations, err := resolver.FilesByPath("./file1.txt") 921 require.NoError(t, err) 922 assert.Len(t, locations, 1) 923 924 locations, err = resolver.FilesByPath("./nested/file2.txt") 925 require.NoError(t, err) 926 assert.Len(t, locations, 1) 927 928 locations, err = resolver.FilesByPath("./nested/linked-file1.txt") 929 require.NoError(t, err) 930 assert.Len(t, locations, 1) 931 } 932 933 func Test_UnindexedDirectoryResolver_FileContentsByLocation(t *testing.T) { 934 cwd, err := os.Getwd() 935 require.NoError(t, err) 936 937 r := NewFromUnindexedDirectory(path.Join(cwd, "test-fixtures/image-simple")) 938 require.NoError(t, err) 939 940 tests := []struct { 941 name string 942 location file.Location 943 expects string 944 err bool 945 }{ 946 { 947 name: "use file reference for content requests", 948 location: file.NewLocation("file-1.txt"), 949 expects: "this file has contents", 950 }, 951 { 952 name: "error on empty file reference", 953 location: file.NewLocationFromDirectory("doesn't matter", stereoscopeFile.Reference{}), 954 err: true, 955 }, 956 } 957 for _, test := range tests { 958 t.Run(test.name, func(t *testing.T) { 959 960 actual, err := r.FileContentsByLocation(test.location) 961 if test.err { 962 require.Error(t, err) 963 return 964 } 965 966 require.NoError(t, err) 967 if test.expects != "" { 968 b, err := io.ReadAll(actual) 969 require.NoError(t, err) 970 assert.Equal(t, test.expects, string(b)) 971 } 972 }) 973 } 974 } 975 976 func Test_UnindexedDirectoryResover_SymlinkLoopWithGlobsShouldResolve(t *testing.T) { 977 test := func(t *testing.T) { 978 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-loop") 979 980 locations, err := resolver.FilesByGlob("**/file.target") 981 require.NoError(t, err) 982 983 require.Len(t, locations, 1) 984 assert.Equal(t, "devices/loop0/file.target", locations[0].RealPath) 985 } 986 987 testWithTimeout(t, 5*time.Second, test) 988 } 989 990 func Test_UnindexedDirectoryResolver_FilesByPath_baseRoot(t *testing.T) { 991 cases := []struct { 992 name string 993 root string 994 input string 995 expected []string 996 }{ 997 { 998 name: "should find the base file", 999 root: "./test-fixtures/symlinks-base/", 1000 input: "./base", 1001 expected: []string{ 1002 "base", 1003 }, 1004 }, 1005 { 1006 name: "should follow a link with a pivoted root", 1007 root: "./test-fixtures/symlinks-base/", 1008 input: "./foo", 1009 expected: []string{ 1010 "base", 1011 }, 1012 }, 1013 { 1014 name: "should follow a relative link with extra parents", 1015 root: "./test-fixtures/symlinks-base/", 1016 input: "./bar", 1017 expected: []string{ 1018 "base", 1019 }, 1020 }, 1021 { 1022 name: "should follow an absolute link with extra parents", 1023 root: "./test-fixtures/symlinks-base/", 1024 input: "./baz", 1025 expected: []string{ 1026 "base", 1027 }, 1028 }, 1029 { 1030 name: "should follow an absolute link with extra parents", 1031 root: "./test-fixtures/symlinks-base/", 1032 input: "./sub/link", 1033 expected: []string{ 1034 "sub/item", 1035 }, 1036 }, 1037 { 1038 name: "should follow chained pivoted link", 1039 root: "./test-fixtures/symlinks-base/", 1040 input: "./chain", 1041 expected: []string{ 1042 "base", 1043 }, 1044 }, 1045 } 1046 for _, c := range cases { 1047 t.Run(c.name, func(t *testing.T) { 1048 resolver := NewFromRootedUnindexedDirectory(c.root, c.root) 1049 1050 refs, err := resolver.FilesByPath(c.input) 1051 require.NoError(t, err) 1052 assert.Len(t, refs, len(c.expected)) 1053 s := strset.New() 1054 for _, actual := range refs { 1055 s.Add(actual.RealPath) 1056 } 1057 assert.ElementsMatch(t, c.expected, s.List()) 1058 }) 1059 } 1060 1061 } 1062 1063 func Test_UnindexedDirectoryResolver_resolvesLinks(t *testing.T) { 1064 tests := []struct { 1065 name string 1066 runner func(file.Resolver) []file.Location 1067 expected []file.Location 1068 }{ 1069 { 1070 name: "by glob to links", 1071 runner: func(resolver file.Resolver) []file.Location { 1072 // links are searched, but resolve to the real files 1073 // for that reason we need to place **/ in front (which is not the same for other resolvers) 1074 actualLocations, err := resolver.FilesByGlob("**/*ink-*") 1075 assert.NoError(t, err) 1076 return actualLocations 1077 }, 1078 expected: []file.Location{ 1079 file.NewVirtualLocation("file-1.txt", "link-1"), 1080 file.NewVirtualLocation("file-2.txt", "link-2"), 1081 // we already have this real file path via another link, so only one is returned 1082 // file.NewVirtualLocation("file-2.txt", "link-indirect"), 1083 file.NewVirtualLocation("file-3.txt", "link-within"), 1084 }, 1085 }, 1086 { 1087 name: "by basename", 1088 runner: func(resolver file.Resolver) []file.Location { 1089 // links are searched, but resolve to the real files 1090 actualLocations, err := resolver.FilesByGlob("**/file-2.txt") 1091 assert.NoError(t, err) 1092 return actualLocations 1093 }, 1094 expected: []file.Location{ 1095 // this has two copies in the base image, which overwrites the same location 1096 file.NewLocation("file-2.txt"), 1097 }, 1098 }, 1099 { 1100 name: "by basename glob", 1101 runner: func(resolver file.Resolver) []file.Location { 1102 // links are searched, but resolve to the real files 1103 actualLocations, err := resolver.FilesByGlob("**/file-?.txt") 1104 assert.NoError(t, err) 1105 return actualLocations 1106 }, 1107 expected: []file.Location{ 1108 file.NewLocation("file-1.txt"), 1109 file.NewLocation("file-2.txt"), 1110 file.NewLocation("file-3.txt"), 1111 file.NewVirtualLocation("parent/file-4.txt", "parent-link/file-4.txt"), 1112 }, 1113 }, 1114 { 1115 name: "by basename glob to links", 1116 runner: func(resolver file.Resolver) []file.Location { 1117 actualLocations, err := resolver.FilesByGlob("**/link-*") 1118 assert.NoError(t, err) 1119 return actualLocations 1120 }, 1121 expected: []file.Location{ 1122 file.NewVirtualLocationFromDirectory("file-1.txt", "link-1", stereoscopeFile.Reference{RealPath: "file-1.txt"}), 1123 file.NewVirtualLocationFromDirectory("file-2.txt", "link-2", stereoscopeFile.Reference{RealPath: "file-2.txt"}), 1124 // we already have this real file path via another link, so only one is returned 1125 //file.NewVirtualLocationFromDirectory("file-2.txt", "link-indirect", stereoscopeFile.Reference{RealPath: "file-2.txt"}), 1126 file.NewVirtualLocationFromDirectory("file-3.txt", "link-within", stereoscopeFile.Reference{RealPath: "file-3.txt"}), 1127 }, 1128 }, 1129 { 1130 name: "by extension", 1131 runner: func(resolver file.Resolver) []file.Location { 1132 // links are searched, but resolve to the real files 1133 actualLocations, err := resolver.FilesByGlob("**/*.txt") 1134 assert.NoError(t, err) 1135 return actualLocations 1136 }, 1137 expected: []file.Location{ 1138 file.NewLocation("file-1.txt"), 1139 file.NewLocation("file-2.txt"), 1140 file.NewLocation("file-3.txt"), 1141 file.NewVirtualLocation("parent/file-4.txt", "parent-link/file-4.txt"), 1142 }, 1143 }, 1144 { 1145 name: "by path to degree 1 link", 1146 runner: func(resolver file.Resolver) []file.Location { 1147 // links resolve to the final file 1148 actualLocations, err := resolver.FilesByPath("/link-2") 1149 assert.NoError(t, err) 1150 return actualLocations 1151 }, 1152 expected: []file.Location{ 1153 // we have multiple copies across layers 1154 file.NewVirtualLocation("file-2.txt", "link-2"), 1155 }, 1156 }, 1157 { 1158 name: "by path to degree 2 link", 1159 runner: func(resolver file.Resolver) []file.Location { 1160 // multiple links resolves to the final file 1161 actualLocations, err := resolver.FilesByPath("/link-indirect") 1162 assert.NoError(t, err) 1163 return actualLocations 1164 }, 1165 expected: []file.Location{ 1166 // we have multiple copies across layers 1167 file.NewVirtualLocation("file-2.txt", "link-indirect"), 1168 }, 1169 }, 1170 } 1171 1172 for _, test := range tests { 1173 t.Run(test.name, func(t *testing.T) { 1174 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-from-image-symlinks-fixture") 1175 1176 actual := test.runner(resolver) 1177 1178 compareLocations(t, test.expected, actual) 1179 }) 1180 } 1181 } 1182 1183 func Test_UnindexedDirectoryResolver_DoNotAddVirtualPathsToTree(t *testing.T) { 1184 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-prune-indexing") 1185 1186 ctx, cancel := context.WithCancel(context.Background()) 1187 defer cancel() 1188 allLocations := resolver.AllLocations(ctx) 1189 var allRealPaths []stereoscopeFile.Path 1190 for l := range allLocations { 1191 allRealPaths = append(allRealPaths, stereoscopeFile.Path(l.RealPath)) 1192 } 1193 pathSet := stereoscopeFile.NewPathSet(allRealPaths...) 1194 1195 assert.False(t, 1196 pathSet.Contains("before-path/file.txt"), 1197 "symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path", 1198 ) 1199 1200 assert.False(t, 1201 pathSet.Contains("a-path/file.txt"), 1202 "symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path", 1203 ) 1204 } 1205 1206 func Test_UnindexedDirectoryResolver_FilesContents_errorOnDirRequest(t *testing.T) { 1207 resolver := NewFromUnindexedDirectory("./test-fixtures/system_paths") 1208 1209 dirLoc := file.NewLocation("arg/foo") 1210 1211 reader, err := resolver.FileContentsByLocation(dirLoc) 1212 require.Error(t, err) 1213 require.Nil(t, reader) 1214 } 1215 1216 func Test_UnindexedDirectoryResolver_AllLocations(t *testing.T) { 1217 resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-from-image-symlinks-fixture") 1218 1219 paths := strset.New() 1220 ctx, cancel := context.WithCancel(context.Background()) 1221 defer cancel() 1222 for loc := range resolver.AllLocations(ctx) { 1223 if strings.HasPrefix(loc.RealPath, "/") { 1224 // ignore outside of the fixture root for now 1225 continue 1226 } 1227 paths.Add(loc.RealPath) 1228 } 1229 expected := []string{ 1230 "file-1.txt", 1231 "file-2.txt", 1232 "file-3.txt", 1233 "link-1", 1234 "link-2", 1235 "link-dead", 1236 "link-indirect", 1237 "link-within", 1238 "parent", 1239 "parent-link", 1240 "parent/file-4.txt", 1241 } 1242 1243 pathsList := paths.List() 1244 sort.Strings(pathsList) 1245 1246 assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List())) 1247 } 1248 1249 func Test_WritableUnindexedDirectoryResolver(t *testing.T) { 1250 tmpdir := t.TempDir() 1251 1252 p := "some/path/file" 1253 c := "some contents" 1254 1255 dr := NewFromUnindexedDirectory(tmpdir) 1256 1257 locations, err := dr.FilesByPath(p) 1258 require.NoError(t, err) 1259 require.Len(t, locations, 0) 1260 1261 err = dr.Write(file.NewLocation(p), strings.NewReader(c)) 1262 require.NoError(t, err) 1263 1264 locations, err = dr.FilesByPath(p) 1265 require.NoError(t, err) 1266 require.Len(t, locations, 1) 1267 1268 reader, err := dr.FileContentsByLocation(locations[0]) 1269 require.NoError(t, err) 1270 bytes, err := io.ReadAll(reader) 1271 require.Equal(t, c, string(bytes)) 1272 } 1273 1274 func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) { 1275 done := make(chan bool) 1276 go func() { 1277 test(t) 1278 done <- true 1279 }() 1280 1281 select { 1282 case <-time.After(timeout): 1283 t.Fatal("test timed out") 1284 case <-done: 1285 } 1286 }