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