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