github.com/anchore/syft@v1.38.2/syft/internal/fileresolver/filetree_resolver_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package fileresolver 5 6 import ( 7 "context" 8 "io" 9 "io/fs" 10 "os" 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 // Tests for filetree resolver when directory is used for index 28 func TestDirectoryResolver_FilesByPath_request_response(t *testing.T) { 29 // / 30 // somewhere/ 31 // outside.txt 32 // root-link -> ./ 33 // path/ 34 // to/ 35 // abs-inside.txt -> /path/to/the/file.txt # absolute link to somewhere inside of the root 36 // rel-inside.txt -> ./the/file.txt # relative link to somewhere inside of the root 37 // the/ 38 // file.txt 39 // abs-outside.txt -> /somewhere/outside.txt # absolute link to outside of the root 40 // rel-outside -> ../../../somewhere/outside.txt # relative link to outside of the root 41 // 42 43 testDir, err := os.Getwd() 44 require.NoError(t, err) 45 relative := filepath.Join("test-fixtures", "req-resp") 46 absolute := filepath.Join(testDir, relative) 47 48 absInsidePath := filepath.Join(absolute, "path", "to", "abs-inside.txt") 49 absOutsidePath := filepath.Join(absolute, "path", "to", "the", "abs-outside.txt") 50 51 relativeViaLink := filepath.Join(relative, "root-link") 52 absoluteViaLink := filepath.Join(absolute, "root-link") 53 54 relativeViaDoubleLink := filepath.Join(relative, "root-link", "root-link") 55 absoluteViaDoubleLink := filepath.Join(absolute, "root-link", "root-link") 56 57 cleanup := func() { 58 _ = os.Remove(absInsidePath) 59 _ = os.Remove(absOutsidePath) 60 } 61 62 // ensure the absolute symlinks are cleaned up from any previous runs 63 cleanup() 64 65 require.NoError(t, os.Symlink(filepath.Join(absolute, "path", "to", "the", "file.txt"), absInsidePath)) 66 require.NoError(t, os.Symlink(filepath.Join(absolute, "somewhere", "outside.txt"), absOutsidePath)) 67 68 t.Cleanup(cleanup) 69 70 cases := []struct { 71 name string 72 cwd string 73 root string 74 base string 75 input string 76 expectedRealPath string 77 expectedAccessPath string // note: if empty it will be assumed to match the expectedRealPath 78 }{ 79 { 80 name: "relative root, relative request, direct", 81 root: relative, 82 input: "path/to/the/file.txt", 83 expectedRealPath: "path/to/the/file.txt", 84 }, 85 { 86 name: "abs root, relative request, direct", 87 root: absolute, 88 input: "path/to/the/file.txt", 89 expectedRealPath: "path/to/the/file.txt", 90 }, 91 { 92 name: "relative root, abs request, direct", 93 root: relative, 94 input: "/path/to/the/file.txt", 95 expectedRealPath: "path/to/the/file.txt", 96 }, 97 { 98 name: "abs root, abs request, direct", 99 root: absolute, 100 input: "/path/to/the/file.txt", 101 expectedRealPath: "path/to/the/file.txt", 102 }, 103 // cwd within root... 104 { 105 name: "relative root, relative request, direct, cwd within root", 106 cwd: filepath.Join(relative, "path/to"), 107 root: "../../", 108 input: "path/to/the/file.txt", 109 expectedRealPath: "path/to/the/file.txt", 110 }, 111 { 112 name: "abs root, relative request, direct, cwd within root", 113 cwd: filepath.Join(relative, "path/to"), 114 root: absolute, 115 input: "path/to/the/file.txt", 116 expectedRealPath: "path/to/the/file.txt", 117 }, 118 { 119 name: "relative root, abs request, direct, cwd within root", 120 cwd: filepath.Join(relative, "path/to"), 121 root: "../../", 122 input: "/path/to/the/file.txt", 123 expectedRealPath: "path/to/the/file.txt", 124 }, 125 { 126 name: "abs root, abs request, direct, cwd within root", 127 cwd: filepath.Join(relative, "path/to"), 128 129 root: absolute, 130 input: "/path/to/the/file.txt", 131 expectedRealPath: "path/to/the/file.txt", 132 }, 133 // cwd within symlink root... 134 { 135 name: "relative root, relative request, direct, cwd within symlink root", 136 cwd: relativeViaLink, 137 root: "./", 138 input: "path/to/the/file.txt", 139 // note: why not expect "path/to/the/file.txt" here? 140 // this is because we don't know that the path used to access this path (which is a link within 141 // the root) resides within the root. Without this information it appears as if this file resides 142 // outside the root. 143 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 144 //expectedRealPath: "path/to/the/file.txt", 145 expectedAccessPath: "path/to/the/file.txt", 146 }, 147 { 148 name: "abs root, relative request, direct, cwd within symlink root", 149 cwd: relativeViaLink, 150 root: absoluteViaLink, 151 input: "path/to/the/file.txt", 152 expectedRealPath: "path/to/the/file.txt", 153 }, 154 { 155 name: "relative root, abs request, direct, cwd within symlink root", 156 cwd: relativeViaLink, 157 root: "./", 158 input: "/path/to/the/file.txt", 159 // note: why not expect "path/to/the/file.txt" here? 160 // this is because we don't know that the path used to access this path (which is a link within 161 // the root) resides within the root. Without this information it appears as if this file resides 162 // outside the root. 163 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 164 //expectedRealPath: "path/to/the/file.txt", 165 expectedAccessPath: "path/to/the/file.txt", 166 }, 167 { 168 name: "abs root, abs 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 // cwd within symlink root, request nested within... 175 { 176 name: "relative root, relative nested request, direct, cwd within symlink root", 177 cwd: relativeViaLink, 178 root: "./path", 179 input: "to/the/file.txt", 180 // note: why not expect "to/the/file.txt" here? 181 // this is because we don't know that the path used to access this path (which is a link within 182 // the root) resides within the root. Without this information it appears as if this file resides 183 // outside the root. 184 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 185 //expectedRealPath: "to/the/file.txt", 186 expectedAccessPath: "to/the/file.txt", 187 }, 188 { 189 name: "abs root, relative nested request, direct, cwd within symlink root", 190 cwd: relativeViaLink, 191 root: filepath.Join(absoluteViaLink, "path"), 192 input: "to/the/file.txt", 193 expectedRealPath: "to/the/file.txt", 194 }, 195 { 196 name: "relative root, abs nested request, direct, cwd within symlink root", 197 cwd: relativeViaLink, 198 root: "./path", 199 input: "/to/the/file.txt", 200 // note: why not expect "to/the/file.txt" here? 201 // this is because we don't know that the path used to access this path (which is a link within 202 // the root) resides within the root. Without this information it appears as if this file resides 203 // outside the root. 204 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 205 //expectedRealPath: "to/the/file.txt", 206 expectedAccessPath: "to/the/file.txt", 207 }, 208 { 209 name: "abs root, abs nested request, direct, cwd within symlink root", 210 cwd: relativeViaLink, 211 root: filepath.Join(absoluteViaLink, "path"), 212 input: "/to/the/file.txt", 213 expectedRealPath: "to/the/file.txt", 214 }, 215 // cwd within DOUBLE symlink root... 216 { 217 name: "relative root, relative request, direct, cwd within (double) symlink root", 218 cwd: relativeViaDoubleLink, 219 root: "./", 220 input: "path/to/the/file.txt", 221 // note: why not expect "path/to/the/file.txt" here? 222 // this is because we don't know that the path used to access this path (which is a link within 223 // the root) resides within the root. Without this information it appears as if this file resides 224 // outside the root. 225 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 226 //expectedRealPath: "path/to/the/file.txt", 227 expectedAccessPath: "path/to/the/file.txt", 228 }, 229 { 230 name: "abs root, relative request, direct, cwd within (double) symlink root", 231 cwd: relativeViaDoubleLink, 232 root: absoluteViaDoubleLink, 233 input: "path/to/the/file.txt", 234 expectedRealPath: "path/to/the/file.txt", 235 }, 236 { 237 name: "relative root, abs request, direct, cwd within (double) symlink root", 238 cwd: relativeViaDoubleLink, 239 root: "./", 240 input: "/path/to/the/file.txt", 241 // note: why not expect "path/to/the/file.txt" here? 242 // this is because we don't know that the path used to access this path (which is a link within 243 // the root) resides within the root. Without this information it appears as if this file resides 244 // outside the root. 245 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 246 //expectedRealPath: "path/to/the/file.txt", 247 expectedAccessPath: "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: why not expect "path/to/the/file.txt" here? 263 // this is because we don't know that the path used to access this path (which is a link within 264 // the root) resides within the root. Without this information it appears as if this file resides 265 // outside the root. 266 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 267 //expectedRealPath: "to/the/file.txt", 268 expectedAccessPath: "to/the/file.txt", 269 }, 270 { 271 name: "abs root, relative nested request, direct, cwd within (double) symlink root", 272 cwd: relativeViaDoubleLink, 273 root: filepath.Join(absoluteViaDoubleLink, "path"), 274 input: "to/the/file.txt", 275 expectedRealPath: "to/the/file.txt", 276 }, 277 { 278 name: "relative root, abs nested request, direct, cwd within (double) symlink root", 279 cwd: relativeViaDoubleLink, 280 root: "./path", 281 input: "/to/the/file.txt", 282 // note: why not expect "path/to/the/file.txt" here? 283 // this is because we don't know that the path used to access this path (which is a link within 284 // the root) resides within the root. Without this information it appears as if this file resides 285 // outside the root. 286 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 287 //expectedRealPath: "to/the/file.txt", 288 expectedAccessPath: "to/the/file.txt", 289 }, 290 { 291 name: "abs root, abs nested request, direct, cwd within (double) symlink root", 292 cwd: relativeViaDoubleLink, 293 root: filepath.Join(absoluteViaDoubleLink, "path"), 294 input: "/to/the/file.txt", 295 expectedRealPath: "to/the/file.txt", 296 }, 297 // cwd within DOUBLE symlink root, request nested DEEP within... 298 { 299 name: "relative root, relative nested request, direct, cwd deep within (double) symlink root", 300 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 301 root: "../", 302 input: "to/the/file.txt", 303 // note: why not expect "path/to/the/file.txt" here? 304 // this is because we don't know that the path used to access this path (which is a link within 305 // the root) resides within the root. Without this information it appears as if this file resides 306 // outside the root. 307 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 308 //expectedRealPath: "to/the/file.txt", 309 expectedAccessPath: "to/the/file.txt", 310 }, 311 { 312 name: "abs root, relative nested request, direct, cwd deep within (double) symlink root", 313 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 314 root: filepath.Join(absoluteViaDoubleLink, "path"), 315 input: "to/the/file.txt", 316 expectedRealPath: "to/the/file.txt", 317 }, 318 { 319 name: "relative root, abs nested request, direct, cwd deep within (double) symlink root", 320 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 321 root: "../", 322 input: "/to/the/file.txt", 323 // note: why not expect "path/to/the/file.txt" here? 324 // this is because we don't know that the path used to access this path (which is a link within 325 // the root) resides within the root. Without this information it appears as if this file resides 326 // outside the root. 327 expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"), 328 //expectedRealPath: "to/the/file.txt", 329 expectedAccessPath: "to/the/file.txt", 330 }, 331 { 332 name: "abs root, abs nested request, direct, cwd deep within (double) symlink root", 333 cwd: filepath.Join(relativeViaDoubleLink, "path", "to"), 334 root: filepath.Join(absoluteViaDoubleLink, "path"), 335 input: "/to/the/file.txt", 336 expectedRealPath: "to/the/file.txt", 337 }, 338 // link to outside of root cases... 339 { 340 name: "relative root, relative request, abs indirect (outside of root)", 341 root: filepath.Join(relative, "path"), 342 input: "to/the/abs-outside.txt", 343 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 344 expectedAccessPath: "to/the/abs-outside.txt", 345 }, 346 { 347 name: "abs root, relative request, abs indirect (outside of root)", 348 root: filepath.Join(absolute, "path"), 349 input: "to/the/abs-outside.txt", 350 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 351 expectedAccessPath: "to/the/abs-outside.txt", 352 }, 353 { 354 name: "relative root, abs request, abs indirect (outside of root)", 355 root: filepath.Join(relative, "path"), 356 input: "/to/the/abs-outside.txt", 357 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 358 expectedAccessPath: "to/the/abs-outside.txt", 359 }, 360 { 361 name: "abs root, abs request, abs indirect (outside of root)", 362 root: filepath.Join(absolute, "path"), 363 input: "/to/the/abs-outside.txt", 364 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 365 expectedAccessPath: "to/the/abs-outside.txt", 366 }, 367 { 368 name: "relative root, relative 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 expectedAccessPath: "to/the/rel-outside.txt", 373 }, 374 { 375 name: "abs root, relative request, relative indirect (outside of root)", 376 root: filepath.Join(absolute, "path"), 377 input: "to/the/rel-outside.txt", 378 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 379 expectedAccessPath: "to/the/rel-outside.txt", 380 }, 381 { 382 name: "relative root, abs request, relative indirect (outside of root)", 383 root: filepath.Join(relative, "path"), 384 input: "/to/the/rel-outside.txt", 385 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 386 expectedAccessPath: "to/the/rel-outside.txt", 387 }, 388 { 389 name: "abs root, abs request, relative indirect (outside of root)", 390 root: filepath.Join(absolute, "path"), 391 input: "/to/the/rel-outside.txt", 392 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 393 expectedAccessPath: "to/the/rel-outside.txt", 394 }, 395 // link to outside of root cases... cwd within symlink root 396 { 397 name: "relative root, relative request, abs indirect (outside of root), cwd within symlink root", 398 cwd: relativeViaLink, 399 root: "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: "abs root, relative request, abs indirect (outside of root), cwd within symlink root", 406 cwd: relativeViaLink, 407 root: filepath.Join(absolute, "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: "relative root, abs request, abs indirect (outside of root), cwd within symlink root", 414 cwd: relativeViaLink, 415 root: "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: "abs root, abs request, abs indirect (outside of root), cwd within symlink root", 422 cwd: relativeViaLink, 423 root: filepath.Join(absolute, "path"), 424 input: "/to/the/abs-outside.txt", 425 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 426 expectedAccessPath: "to/the/abs-outside.txt", 427 }, 428 { 429 name: "relative root, relative request, relative indirect (outside of root), cwd within symlink root", 430 cwd: relativeViaLink, 431 root: "path", 432 input: "to/the/rel-outside.txt", 433 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 434 expectedAccessPath: "to/the/rel-outside.txt", 435 }, 436 { 437 name: "abs root, relative request, relative indirect (outside of root), cwd within symlink root", 438 cwd: relativeViaLink, 439 root: filepath.Join(absolute, "path"), 440 input: "to/the/rel-outside.txt", 441 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 442 expectedAccessPath: "to/the/rel-outside.txt", 443 }, 444 { 445 name: "relative root, abs request, relative indirect (outside of root), cwd within symlink root", 446 cwd: relativeViaLink, 447 root: "path", 448 input: "/to/the/rel-outside.txt", 449 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 450 expectedAccessPath: "to/the/rel-outside.txt", 451 }, 452 { 453 name: "abs root, abs request, relative indirect (outside of root), cwd within symlink root", 454 cwd: relativeViaLink, 455 root: filepath.Join(absolute, "path"), 456 input: "/to/the/rel-outside.txt", 457 expectedRealPath: filepath.Join(absolute, "/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 expectedAccessPath: "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 expectedAccessPath: "to/the/rel-outside.txt", 475 }, 476 { 477 name: "relative root, abs request, relative indirect (outside of root), cwd within DOUBLE symlink root", 478 cwd: relativeViaDoubleLink, 479 root: "path", 480 input: "/to/the/rel-outside.txt", 481 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 482 expectedAccessPath: "to/the/rel-outside.txt", 483 }, 484 { 485 name: "abs root, abs request, relative indirect (outside of root), cwd within DOUBLE symlink root", 486 cwd: relativeViaDoubleLink, 487 root: filepath.Join(absolute, "path"), 488 input: "/to/the/rel-outside.txt", 489 expectedRealPath: filepath.Join(absolute, "/somewhere/outside.txt"), 490 expectedAccessPath: "to/the/rel-outside.txt", 491 }, 492 } 493 for _, c := range cases { 494 t.Run(c.name, func(t *testing.T) { 495 if c.expectedAccessPath == "" { 496 c.expectedAccessPath = c.expectedRealPath 497 } 498 499 // we need to mimic a shell, otherwise we won't get a path within a symlink 500 targetPath := filepath.Join(testDir, c.cwd) 501 t.Setenv("PWD", filepath.Clean(targetPath)) 502 503 require.NoError(t, err) 504 require.NoError(t, os.Chdir(targetPath)) 505 t.Cleanup(func() { 506 require.NoError(t, os.Chdir(testDir)) 507 }) 508 509 resolver, err := NewFromDirectory(c.root, c.base) 510 require.NoError(t, err) 511 require.NotNil(t, resolver) 512 513 refs, err := resolver.FilesByPath(c.input) 514 require.NoError(t, err) 515 if c.expectedRealPath == "" { 516 require.Empty(t, refs) 517 return 518 } 519 require.Len(t, refs, 1) 520 assert.Equal(t, c.expectedRealPath, refs[0].RealPath, "real path different") 521 assert.Equal(t, c.expectedAccessPath, refs[0].AccessPath, "virtual path different") 522 }) 523 } 524 } 525 526 func TestDirectoryResolver_FilesByPath_relativeRoot(t *testing.T) { 527 cases := []struct { 528 name string 529 relativeRoot string 530 input string 531 expected []string 532 }{ 533 { 534 name: "should find a file from an absolute input", 535 relativeRoot: "./test-fixtures/", 536 input: "/image-symlinks/file-1.txt", 537 expected: []string{ 538 "image-symlinks/file-1.txt", 539 }, 540 }, 541 { 542 name: "should find a file from a relative path", 543 relativeRoot: "./test-fixtures/", 544 input: "image-symlinks/file-1.txt", 545 expected: []string{ 546 "image-symlinks/file-1.txt", 547 }, 548 }, 549 { 550 name: "should find a file from a relative path (root above cwd)", 551 // TODO: refactor me! this test depends on the structure of the source dir not changing, which isn't great 552 relativeRoot: "../", 553 input: "fileresolver/directory.go", 554 expected: []string{ 555 "fileresolver/directory.go", 556 }, 557 }, 558 } 559 for _, c := range cases { 560 t.Run(c.name, func(t *testing.T) { 561 resolver, err := NewFromDirectory(c.relativeRoot, "") 562 assert.NoError(t, err) 563 564 refs, err := resolver.FilesByPath(c.input) 565 require.NoError(t, err) 566 assert.Len(t, refs, len(c.expected)) 567 s := strset.New() 568 for _, actual := range refs { 569 s.Add(actual.RealPath) 570 } 571 assert.ElementsMatch(t, c.expected, s.List()) 572 }) 573 } 574 } 575 576 func TestDirectoryResolver_FilesByPath_absoluteRoot(t *testing.T) { 577 cases := []struct { 578 name string 579 relativeRoot string 580 input string 581 expected []string 582 }{ 583 { 584 name: "should find a file from an absolute input", 585 relativeRoot: "./test-fixtures/", 586 input: "/image-symlinks/file-1.txt", 587 expected: []string{ 588 "image-symlinks/file-1.txt", 589 }, 590 }, 591 { 592 name: "should find a file from a relative path", 593 relativeRoot: "./test-fixtures/", 594 input: "image-symlinks/file-1.txt", 595 expected: []string{ 596 "image-symlinks/file-1.txt", 597 }, 598 }, 599 { 600 name: "should find a file from a relative path (root above cwd)", 601 // TODO: refactor me! this test depends on the structure of the source dir not changing, which isn't great 602 relativeRoot: "../", 603 input: "fileresolver/directory.go", 604 expected: []string{ 605 "fileresolver/directory.go", 606 }, 607 }, 608 } 609 for _, c := range cases { 610 t.Run(c.name, func(t *testing.T) { 611 // note: this test is all about asserting correct functionality when the given analysis path 612 // is an absolute path 613 absRoot, err := filepath.Abs(c.relativeRoot) 614 require.NoError(t, err) 615 616 resolver, err := NewFromDirectory(absRoot, "") 617 assert.NoError(t, err) 618 619 refs, err := resolver.FilesByPath(c.input) 620 require.NoError(t, err) 621 assert.Len(t, refs, len(c.expected)) 622 s := strset.New() 623 for _, actual := range refs { 624 s.Add(actual.RealPath) 625 } 626 assert.ElementsMatch(t, c.expected, s.List()) 627 }) 628 } 629 } 630 631 func TestDirectoryResolver_FilesByPath(t *testing.T) { 632 cases := []struct { 633 name string 634 root string 635 input string 636 expected string 637 refCount int 638 forcePositiveHasPath bool 639 }{ 640 { 641 name: "finds a file (relative)", 642 root: "./test-fixtures/", 643 input: "image-symlinks/file-1.txt", 644 expected: "image-symlinks/file-1.txt", 645 refCount: 1, 646 }, 647 { 648 name: "finds a file with relative indirection", 649 root: "./test-fixtures/../test-fixtures", 650 input: "image-symlinks/file-1.txt", 651 expected: "image-symlinks/file-1.txt", 652 refCount: 1, 653 }, 654 { 655 name: "managed non-existing files (relative)", 656 root: "./test-fixtures/", 657 input: "test-fixtures/image-symlinks/bogus.txt", 658 refCount: 0, 659 }, 660 { 661 name: "finds a file (absolute)", 662 root: "./test-fixtures/", 663 input: "/image-symlinks/file-1.txt", 664 expected: "image-symlinks/file-1.txt", 665 refCount: 1, 666 }, 667 { 668 name: "directories ignored", 669 root: "./test-fixtures/", 670 input: "/image-symlinks", 671 refCount: 0, 672 forcePositiveHasPath: true, 673 }, 674 } 675 for _, c := range cases { 676 t.Run(c.name, func(t *testing.T) { 677 resolver, err := NewFromDirectory(c.root, "") 678 assert.NoError(t, err) 679 680 hasPath := resolver.HasPath(c.input) 681 if !c.forcePositiveHasPath { 682 if c.refCount != 0 && !hasPath { 683 t.Errorf("expected HasPath() to indicate existence, but did not") 684 } else if c.refCount == 0 && hasPath { 685 t.Errorf("expected HasPath() to NOT indicate existence, but does") 686 } 687 } else if !hasPath { 688 t.Errorf("expected HasPath() to indicate existence, but did not (force path)") 689 } 690 691 refs, err := resolver.FilesByPath(c.input) 692 require.NoError(t, err) 693 assert.Len(t, refs, c.refCount) 694 for _, actual := range refs { 695 assert.Equal(t, c.expected, actual.RealPath) 696 } 697 }) 698 } 699 } 700 701 func TestDirectoryResolver_MultipleFilesByPath(t *testing.T) { 702 cases := []struct { 703 name string 704 input []string 705 refCount int 706 }{ 707 { 708 name: "finds multiple files", 709 input: []string{"image-symlinks/file-1.txt", "image-symlinks/file-2.txt"}, 710 refCount: 2, 711 }, 712 { 713 name: "skips non-existing files", 714 input: []string{"image-symlinks/bogus.txt", "image-symlinks/file-1.txt"}, 715 refCount: 1, 716 }, 717 { 718 name: "does not return anything for non-existing directories", 719 input: []string{"non-existing/bogus.txt", "non-existing/file-1.txt"}, 720 refCount: 0, 721 }, 722 } 723 for _, c := range cases { 724 t.Run(c.name, func(t *testing.T) { 725 resolver, err := NewFromDirectory("./test-fixtures", "") 726 assert.NoError(t, err) 727 refs, err := resolver.FilesByPath(c.input...) 728 assert.NoError(t, err) 729 730 if len(refs) != c.refCount { 731 t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount) 732 } 733 }) 734 } 735 } 736 737 func TestDirectoryResolver_FilesByGlobMultiple(t *testing.T) { 738 resolver, err := NewFromDirectory("./test-fixtures", "") 739 assert.NoError(t, err) 740 refs, err := resolver.FilesByGlob("**/image-symlinks/file*") 741 assert.NoError(t, err) 742 743 assert.Len(t, refs, 2) 744 } 745 746 func TestDirectoryResolver_FilesByGlobRecursive(t *testing.T) { 747 resolver, err := NewFromDirectory("./test-fixtures/image-symlinks", "") 748 assert.NoError(t, err) 749 refs, err := resolver.FilesByGlob("**/*.txt") 750 assert.NoError(t, err) 751 assert.Len(t, refs, 6) 752 } 753 754 func TestDirectoryResolver_FilesByGlobSingle(t *testing.T) { 755 resolver, err := NewFromDirectory("./test-fixtures", "") 756 assert.NoError(t, err) 757 refs, err := resolver.FilesByGlob("**/image-symlinks/*1.txt") 758 assert.NoError(t, err) 759 760 assert.Len(t, refs, 1) 761 assert.Equal(t, "image-symlinks/file-1.txt", refs[0].RealPath) 762 } 763 764 func TestDirectoryResolver_FilesByPath_ResolvesSymlinks(t *testing.T) { 765 766 tests := []struct { 767 name string 768 fixture string 769 }{ 770 { 771 name: "one degree", 772 fixture: "link_to_new_readme", 773 }, 774 { 775 name: "two degrees", 776 fixture: "link_to_link_to_new_readme", 777 }, 778 } 779 780 for _, test := range tests { 781 t.Run(test.name, func(t *testing.T) { 782 resolver, err := NewFromDirectory("./test-fixtures/symlinks-simple", "") 783 assert.NoError(t, err) 784 785 refs, err := resolver.FilesByPath(test.fixture) 786 require.NoError(t, err) 787 assert.Len(t, refs, 1) 788 789 reader, err := resolver.FileContentsByLocation(refs[0]) 790 require.NoError(t, err) 791 792 actual, err := io.ReadAll(reader) 793 require.NoError(t, err) 794 795 expected, err := os.ReadFile("test-fixtures/symlinks-simple/readme") 796 require.NoError(t, err) 797 798 assert.Equal(t, string(expected), string(actual)) 799 }) 800 } 801 } 802 803 func TestDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) { 804 // let's make certain that "dev/place" is not ignored, since it is not "/dev/place" 805 resolver, err := NewFromDirectory("test-fixtures/system_paths/target", "") 806 assert.NoError(t, err) 807 808 // all paths should be found (non filtering matches a path) 809 locations, err := resolver.FilesByGlob("**/place") 810 assert.NoError(t, err) 811 // 4: within target/ 812 // 1: target/link --> relative path to "place" // NOTE: this is filtered out since it not unique relative to outside_root/link_target/place 813 // 1: outside_root/link_target/place 814 assert.Len(t, locations, 6) 815 816 // ensure that symlink indexing outside of root worked 817 testLocation := "test-fixtures/system_paths/outside_root/link_target/place" 818 ok := false 819 for _, location := range locations { 820 if strings.HasSuffix(location.RealPath, testLocation) { 821 ok = true 822 } 823 } 824 825 if !ok { 826 t.Fatalf("could not find test location=%q", testLocation) 827 } 828 } 829 830 func Test_directoryResolver_FilesByMIMEType(t *testing.T) { 831 tests := []struct { 832 fixturePath string 833 mimeType string 834 expectedPaths *strset.Set 835 }{ 836 { 837 fixturePath: "./test-fixtures/image-simple", 838 mimeType: "text/plain", 839 expectedPaths: strset.New("file-1.txt", "file-2.txt", "target/really/nested/file-3.txt", "Dockerfile"), 840 }, 841 } 842 for _, test := range tests { 843 t.Run(test.fixturePath, func(t *testing.T) { 844 resolver, err := NewFromDirectory(test.fixturePath, "") 845 assert.NoError(t, err) 846 locations, err := resolver.FilesByMIMEType(test.mimeType) 847 assert.NoError(t, err) 848 assert.Equal(t, test.expectedPaths.Size(), len(locations)) 849 for _, l := range locations { 850 assert.True(t, test.expectedPaths.Has(l.RealPath), "does not have path %q", l.RealPath) 851 } 852 }) 853 } 854 } 855 856 func Test_IndexingNestedSymLinks(t *testing.T) { 857 resolver, err := NewFromDirectory("./test-fixtures/symlinks-simple", "") 858 require.NoError(t, err) 859 860 // check that we can get the real path 861 locations, err := resolver.FilesByPath("./readme") 862 require.NoError(t, err) 863 assert.Len(t, locations, 1) 864 865 // check that we can access the same file via 1 symlink 866 locations, err = resolver.FilesByPath("./link_to_new_readme") 867 require.NoError(t, err) 868 require.Len(t, locations, 1) 869 assert.Equal(t, "readme", locations[0].RealPath) 870 assert.Equal(t, "link_to_new_readme", locations[0].AccessPath) 871 872 // check that we can access the same file via 2 symlinks 873 locations, err = resolver.FilesByPath("./link_to_link_to_new_readme") 874 require.NoError(t, err) 875 require.Len(t, locations, 1) 876 assert.Equal(t, "readme", locations[0].RealPath) 877 assert.Equal(t, "link_to_link_to_new_readme", locations[0].AccessPath) 878 879 // check that we can access the same file via 2 symlinks 880 locations, err = resolver.FilesByGlob("**/link_*") 881 require.NoError(t, err) 882 require.Len(t, locations, 1) // you would think this is 2, however, they point to the same file, and glob only returns unique files 883 884 // returned locations can be in any order 885 expectedAccessPaths := []string{ 886 "link_to_link_to_new_readme", 887 //"link_to_new_readme", // we filter out this one because the first symlink resolves to the same file 888 } 889 890 expectedRealPaths := []string{ 891 "readme", 892 } 893 894 actualRealPaths := strset.New() 895 actualAccessPaths := strset.New() 896 for _, a := range locations { 897 actualAccessPaths.Add(a.AccessPath) 898 actualRealPaths.Add(a.RealPath) 899 } 900 901 assert.ElementsMatch(t, expectedAccessPaths, actualAccessPaths.List()) 902 assert.ElementsMatch(t, expectedRealPaths, actualRealPaths.List()) 903 } 904 905 func Test_IndexingNestedSymLinks_ignoredIndexes(t *testing.T) { 906 filterFn := func(_, path string, _ os.FileInfo, _ error) error { 907 if strings.HasSuffix(path, string(filepath.Separator)+"readme") { 908 return ErrSkipPath 909 } 910 return nil 911 } 912 913 resolver, err := NewFromDirectory("./test-fixtures/symlinks-simple", "", filterFn) 914 require.NoError(t, err) 915 916 // the path to the real file is PRUNED from the index, so we should NOT expect a location returned 917 locations, err := resolver.FilesByPath("./readme") 918 require.NoError(t, err) 919 assert.Empty(t, locations) 920 921 // check that we cannot access the file even via symlink 922 locations, err = resolver.FilesByPath("./link_to_new_readme") 923 require.NoError(t, err) 924 assert.Empty(t, locations) 925 926 // check that we still cannot access the same file via 2 symlinks 927 locations, err = resolver.FilesByPath("./link_to_link_to_new_readme") 928 require.NoError(t, err) 929 assert.Empty(t, locations) 930 } 931 932 func Test_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) { 933 resolver, err := NewFromDirectory("./test-fixtures/symlinks-multiple-roots/root", "") 934 require.NoError(t, err) 935 936 // check that we can get the real path 937 locations, err := resolver.FilesByPath("./readme") 938 require.NoError(t, err) 939 assert.Len(t, locations, 1) 940 941 // check that we can access the same file via 2 symlinks (link_to_link_to_readme -> link_to_readme -> readme) 942 locations, err = resolver.FilesByPath("./link_to_link_to_readme") 943 require.NoError(t, err) 944 assert.Len(t, locations, 1) 945 946 // something looks wrong here 947 t.Failed() 948 } 949 950 func Test_RootViaSymlink(t *testing.T) { 951 resolver, err := NewFromDirectory("./test-fixtures/symlinked-root/nested/link-root", "") 952 require.NoError(t, err) 953 954 locations, err := resolver.FilesByPath("./file1.txt") 955 require.NoError(t, err) 956 assert.Len(t, locations, 1) 957 958 locations, err = resolver.FilesByPath("./nested/file2.txt") 959 require.NoError(t, err) 960 assert.Len(t, locations, 1) 961 962 locations, err = resolver.FilesByPath("./nested/linked-file1.txt") 963 require.NoError(t, err) 964 assert.Len(t, locations, 1) 965 } 966 967 func Test_directoryResolver_FileContentsByLocation(t *testing.T) { 968 cwd, err := os.Getwd() 969 require.NoError(t, err) 970 971 r, err := NewFromDirectory(".", "") 972 require.NoError(t, err) 973 974 exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt"))) 975 require.True(t, exists) 976 require.NoError(t, err) 977 require.True(t, existingPath.HasReference()) 978 979 tests := []struct { 980 name string 981 location file.Location 982 expects string 983 err bool 984 }{ 985 { 986 name: "use file reference for content requests", 987 location: file.NewLocationFromDirectory("some/place", *existingPath.Reference), 988 expects: "this file has contents", 989 }, 990 { 991 name: "error on empty file reference", 992 location: file.NewLocationFromDirectory("doesn't matter", stereoscopeFile.Reference{}), 993 err: true, 994 }, 995 } 996 for _, test := range tests { 997 t.Run(test.name, func(t *testing.T) { 998 999 actual, err := r.FileContentsByLocation(test.location) 1000 if test.err { 1001 require.Error(t, err) 1002 return 1003 } 1004 1005 require.NoError(t, err) 1006 if test.expects != "" { 1007 b, err := io.ReadAll(actual) 1008 require.NoError(t, err) 1009 assert.Equal(t, test.expects, string(b)) 1010 } 1011 }) 1012 } 1013 } 1014 1015 func Test_SymlinkLoopWithGlobsShouldResolve(t *testing.T) { 1016 test := func(t *testing.T) { 1017 resolver, err := NewFromDirectory("./test-fixtures/symlinks-loop", "") 1018 require.NoError(t, err) 1019 1020 locations, err := resolver.FilesByGlob("**/file.target") 1021 require.NoError(t, err) 1022 1023 require.Len(t, locations, 1) 1024 assert.Equal(t, "devices/loop0/file.target", locations[0].RealPath) 1025 } 1026 1027 testWithTimeout(t, 5*time.Second, test) 1028 } 1029 1030 func TestDirectoryResolver_FilesByPath_baseRoot(t *testing.T) { 1031 cases := []struct { 1032 name string 1033 root string 1034 input string 1035 expected []string 1036 }{ 1037 { 1038 name: "should find the base file", 1039 root: "./test-fixtures/symlinks-base/", 1040 input: "./base", 1041 expected: []string{ 1042 "/base", 1043 }, 1044 }, 1045 { 1046 name: "should follow a link with a pivoted root", 1047 root: "./test-fixtures/symlinks-base/", 1048 input: "./foo", 1049 expected: []string{ 1050 "/base", 1051 }, 1052 }, 1053 { 1054 name: "should follow a relative link with extra parents", 1055 root: "./test-fixtures/symlinks-base/", 1056 input: "./bar", 1057 expected: []string{ 1058 "/base", 1059 }, 1060 }, 1061 { 1062 name: "should follow an absolute link with extra parents", 1063 root: "./test-fixtures/symlinks-base/", 1064 input: "./baz", 1065 expected: []string{ 1066 "/base", 1067 }, 1068 }, 1069 { 1070 name: "should follow an absolute link with extra parents", 1071 root: "./test-fixtures/symlinks-base/", 1072 input: "./sub/link", 1073 expected: []string{ 1074 "/sub/item", 1075 }, 1076 }, 1077 { 1078 name: "should follow chained pivoted link", 1079 root: "./test-fixtures/symlinks-base/", 1080 input: "./chain", 1081 expected: []string{ 1082 "/base", 1083 }, 1084 }, 1085 } 1086 for _, c := range cases { 1087 t.Run(c.name, func(t *testing.T) { 1088 resolver, err := NewFromDirectory(c.root, c.root) 1089 assert.NoError(t, err) 1090 1091 refs, err := resolver.FilesByPath(c.input) 1092 require.NoError(t, err) 1093 assert.Len(t, refs, len(c.expected)) 1094 s := strset.New() 1095 for _, actual := range refs { 1096 s.Add(actual.RealPath) 1097 } 1098 assert.ElementsMatch(t, c.expected, s.List()) 1099 }) 1100 } 1101 1102 } 1103 1104 func Test_directoryResolver_resolvesLinks(t *testing.T) { 1105 tests := []struct { 1106 name string 1107 runner func(file.Resolver) []file.Location 1108 expected []file.Location 1109 }{ 1110 { 1111 name: "by mimetype", 1112 runner: func(resolver file.Resolver) []file.Location { 1113 // links should not show up when searching mimetype 1114 actualLocations, err := resolver.FilesByMIMEType("text/plain") 1115 assert.NoError(t, err) 1116 return actualLocations 1117 }, 1118 expected: []file.Location{ 1119 file.NewLocation("file-1.txt"), // note: missing virtual path "file-1.txt" 1120 file.NewLocation("file-3.txt"), // note: missing virtual path "file-3.txt" 1121 file.NewLocation("file-2.txt"), // note: missing virtual path "file-2.txt" 1122 file.NewLocation("parent/file-4.txt"), // note: missing virtual path "file-4.txt" 1123 }, 1124 }, 1125 { 1126 name: "by glob to links", 1127 runner: func(resolver file.Resolver) []file.Location { 1128 // links are searched, but resolve to the real files 1129 // for that reason we need to place **/ in front (which is not the same for other resolvers) 1130 actualLocations, err := resolver.FilesByGlob("**/*ink-*") 1131 assert.NoError(t, err) 1132 return actualLocations 1133 }, 1134 expected: []file.Location{ 1135 file.NewVirtualLocation("file-1.txt", "link-1"), 1136 file.NewVirtualLocation("file-2.txt", "link-2"), 1137 // we already have this real file path via another link, so only one is returned 1138 //file.NewVirtualLocation("file-2.txt", "link-indirect"), 1139 file.NewVirtualLocation("file-3.txt", "link-within"), 1140 }, 1141 }, 1142 { 1143 name: "by basename", 1144 runner: func(resolver file.Resolver) []file.Location { 1145 // links are searched, but resolve to the real files 1146 actualLocations, err := resolver.FilesByGlob("**/file-2.txt") 1147 assert.NoError(t, err) 1148 return actualLocations 1149 }, 1150 expected: []file.Location{ 1151 // this has two copies in the base image, which overwrites the same location 1152 file.NewLocation("file-2.txt"), // note: missing virtual path "file-2.txt", 1153 }, 1154 }, 1155 { 1156 name: "by basename glob", 1157 runner: func(resolver file.Resolver) []file.Location { 1158 // links are searched, but resolve to the real files 1159 actualLocations, err := resolver.FilesByGlob("**/file-?.txt") 1160 assert.NoError(t, err) 1161 return actualLocations 1162 }, 1163 expected: []file.Location{ 1164 file.NewLocation("file-1.txt"), // note: missing virtual path "file-1.txt" 1165 file.NewLocation("file-2.txt"), // note: missing virtual path "file-2.txt" 1166 file.NewLocation("file-3.txt"), // note: missing virtual path "file-3.txt" 1167 file.NewLocation("parent/file-4.txt"), // note: missing virtual path "parent/file-4.txt" 1168 }, 1169 }, 1170 { 1171 name: "by basename glob to links", 1172 runner: func(resolver file.Resolver) []file.Location { 1173 actualLocations, err := resolver.FilesByGlob("**/link-*") 1174 assert.NoError(t, err) 1175 return actualLocations 1176 }, 1177 expected: []file.Location{ 1178 file.NewVirtualLocation("file-1.txt", "link-1"), 1179 file.NewVirtualLocation("file-2.txt", "link-2"), 1180 1181 // we already have this real file path via another link, so only one is returned 1182 //file.NewVirtualLocation("file-2.txt", "link-indirect"), 1183 1184 file.NewVirtualLocation("file-3.txt", "link-within"), 1185 }, 1186 }, 1187 { 1188 name: "by extension", 1189 runner: func(resolver file.Resolver) []file.Location { 1190 // links are searched, but resolve to the real files 1191 actualLocations, err := resolver.FilesByGlob("**/*.txt") 1192 assert.NoError(t, err) 1193 return actualLocations 1194 }, 1195 expected: []file.Location{ 1196 file.NewLocation("file-1.txt"), // note: missing virtual path "file-1.txt" 1197 file.NewLocation("file-2.txt"), // note: missing virtual path "file-2.txt" 1198 file.NewLocation("file-3.txt"), // note: missing virtual path "file-3.txt" 1199 file.NewLocation("parent/file-4.txt"), // note: missing virtual path "parent/file-4.txt" 1200 }, 1201 }, 1202 { 1203 name: "by path to degree 1 link", 1204 runner: func(resolver file.Resolver) []file.Location { 1205 // links resolve to the final file 1206 actualLocations, err := resolver.FilesByPath("/link-2") 1207 assert.NoError(t, err) 1208 return actualLocations 1209 }, 1210 expected: []file.Location{ 1211 // we have multiple copies across layers 1212 file.NewVirtualLocation("file-2.txt", "link-2"), 1213 }, 1214 }, 1215 { 1216 name: "by path to degree 2 link", 1217 runner: func(resolver file.Resolver) []file.Location { 1218 // multiple links resolves to the final file 1219 actualLocations, err := resolver.FilesByPath("/link-indirect") 1220 assert.NoError(t, err) 1221 return actualLocations 1222 }, 1223 expected: []file.Location{ 1224 // we have multiple copies across layers 1225 file.NewVirtualLocation("file-2.txt", "link-indirect"), 1226 }, 1227 }, 1228 } 1229 1230 for _, test := range tests { 1231 t.Run(test.name, func(t *testing.T) { 1232 resolver, err := NewFromDirectory("./test-fixtures/symlinks-from-image-symlinks-fixture", "") 1233 require.NoError(t, err) 1234 assert.NoError(t, err) 1235 1236 actual := test.runner(resolver) 1237 1238 compareLocations(t, test.expected, actual) 1239 }) 1240 } 1241 } 1242 1243 func TestDirectoryResolver_DoNotAddVirtualPathsToTree(t *testing.T) { 1244 resolver, err := NewFromDirectory("./test-fixtures/symlinks-prune-indexing", "") 1245 require.NoError(t, err) 1246 1247 var allRealPaths []stereoscopeFile.Path 1248 for l := range resolver.AllLocations(context.Background()) { 1249 allRealPaths = append(allRealPaths, stereoscopeFile.Path(l.RealPath)) 1250 } 1251 pathSet := stereoscopeFile.NewPathSet(allRealPaths...) 1252 1253 assert.False(t, 1254 pathSet.Contains("before-path/file.txt"), 1255 "symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path", 1256 ) 1257 1258 assert.False(t, 1259 pathSet.Contains("a-path/file.txt"), 1260 "symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path", 1261 ) 1262 1263 } 1264 1265 func TestDirectoryResolver_FilesContents_errorOnDirRequest(t *testing.T) { 1266 defer goleak.VerifyNone(t) 1267 resolver, err := NewFromDirectory("./test-fixtures/system_paths", "") 1268 assert.NoError(t, err) 1269 1270 var dirLoc *file.Location 1271 ctx, cancel := context.WithCancel(context.Background()) 1272 defer cancel() 1273 for loc := range resolver.AllLocations(ctx) { 1274 entry, err := resolver.Index.Get(loc.Reference()) 1275 require.NoError(t, err) 1276 if entry.Metadata.IsDir() { 1277 dirLoc = &loc 1278 break 1279 } 1280 } 1281 1282 require.NotNil(t, dirLoc) 1283 1284 reader, err := resolver.FileContentsByLocation(*dirLoc) 1285 require.Error(t, err) 1286 require.Nil(t, reader) 1287 } 1288 1289 func TestDirectoryResolver_AllLocations(t *testing.T) { 1290 resolver, err := NewFromDirectory("./test-fixtures/symlinks-from-image-symlinks-fixture", "") 1291 assert.NoError(t, err) 1292 1293 paths := strset.New() 1294 for loc := range resolver.AllLocations(context.Background()) { 1295 if strings.HasPrefix(loc.RealPath, "/") { 1296 // ignore outside the fixture root for now 1297 continue 1298 } 1299 paths.Add(loc.RealPath) 1300 } 1301 expected := []string{ 1302 "file-1.txt", 1303 "file-2.txt", 1304 "file-3.txt", 1305 "link-1", 1306 "link-2", 1307 "link-dead", 1308 "link-indirect", 1309 "link-within", 1310 "parent", 1311 "parent-link", 1312 "parent/file-4.txt", 1313 } 1314 1315 pathsList := paths.List() 1316 sort.Strings(pathsList) 1317 1318 assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List())) 1319 } 1320 1321 func TestAllLocationsDoesNotLeakGoRoutine(t *testing.T) { 1322 defer goleak.VerifyNone(t) 1323 resolver, err := NewFromDirectory("./test-fixtures/symlinks-from-image-symlinks-fixture", "") 1324 require.NoError(t, err) 1325 ctx, cancel := context.WithCancel(context.Background()) 1326 for range resolver.AllLocations(ctx) { 1327 break 1328 } 1329 cancel() 1330 } 1331 1332 var _ fs.FileInfo = (*testFileInfo)(nil) 1333 1334 type testFileInfo struct { 1335 mode os.FileMode 1336 } 1337 1338 func (t testFileInfo) Name() string { 1339 panic("implement me") 1340 } 1341 1342 func (t testFileInfo) Size() int64 { 1343 panic("implement me") 1344 } 1345 1346 func (t testFileInfo) Mode() fs.FileMode { 1347 return t.mode 1348 } 1349 1350 func (t testFileInfo) ModTime() time.Time { 1351 panic("implement me") 1352 } 1353 1354 func (t testFileInfo) IsDir() bool { 1355 panic("implement me") 1356 } 1357 1358 func (t testFileInfo) Sys() interface{} { 1359 panic("implement me") 1360 } 1361 1362 // Tests for filetree resolver when single file is used for index 1363 func TestFileResolver_FilesByPath(t *testing.T) { 1364 tests := []struct { 1365 description string 1366 filePath string // relative to cwd 1367 fileByPathInput string 1368 expectedRealPath string 1369 expectedAccessPath string 1370 cwd string 1371 }{ 1372 { 1373 description: "Finds file if searched by filepath", 1374 filePath: "./test-fixtures/req-resp/path/to/the/file.txt", 1375 fileByPathInput: "file.txt", 1376 expectedRealPath: "/file.txt", 1377 expectedAccessPath: "/file.txt", 1378 }, 1379 } 1380 1381 for _, tt := range tests { 1382 t.Run(tt.description, func(t *testing.T) { 1383 parentPath, err := absoluteSymlinkFreePathToParent(tt.filePath) 1384 require.NoError(t, err) 1385 require.NotNil(t, parentPath) 1386 1387 resolver, err := NewFromFile(tt.filePath) 1388 require.NoError(t, err) 1389 require.NotNil(t, resolver) 1390 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1391 1392 refs, err := resolver.FilesByPath(tt.fileByPathInput) 1393 require.NoError(t, err) 1394 if tt.expectedRealPath == "" { 1395 require.Empty(t, refs) 1396 return 1397 } 1398 require.Len(t, refs, 1) 1399 assert.Equal(t, tt.expectedRealPath, refs[0].RealPath, "real path different") 1400 assert.Equal(t, tt.expectedAccessPath, refs[0].AccessPath, "virtual path different") 1401 }) 1402 } 1403 } 1404 1405 func TestFileResolver_MultipleFilesByPath(t *testing.T) { 1406 tests := []struct { 1407 description string 1408 input []string 1409 refCount int 1410 }{ 1411 { 1412 description: "finds file ", 1413 input: []string{"file.txt"}, 1414 refCount: 1, 1415 }, 1416 { 1417 description: "skip non-existing files", 1418 input: []string{"file.txt", "bogus.txt"}, 1419 refCount: 1, 1420 }, 1421 { 1422 description: "does not return anything for non-existing files", 1423 input: []string{"non-existing/bogus.txt", "another-bogus.txt"}, 1424 refCount: 0, 1425 }, 1426 } 1427 1428 for _, tt := range tests { 1429 t.Run(tt.description, func(t *testing.T) { 1430 filePath := "./test-fixtures/req-resp/path/to/the/file.txt" 1431 parentPath, err := absoluteSymlinkFreePathToParent(filePath) 1432 require.NoError(t, err) 1433 require.NotNil(t, parentPath) 1434 1435 resolver, err := NewFromFile(filePath) 1436 assert.NoError(t, err) 1437 require.NotNil(t, resolver) 1438 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1439 1440 refs, err := resolver.FilesByPath(tt.input...) 1441 assert.NoError(t, err) 1442 1443 if len(refs) != tt.refCount { 1444 t.Errorf("unexpected number of refs: %d != %d", len(refs), tt.refCount) 1445 } 1446 }) 1447 } 1448 } 1449 1450 func TestFileResolver_FilesByGlob(t *testing.T) { 1451 filePath := "./test-fixtures/req-resp/path/to/the/file.txt" 1452 parentPath, err := absoluteSymlinkFreePathToParent(filePath) 1453 require.NoError(t, err) 1454 require.NotNil(t, parentPath) 1455 1456 resolver, err := NewFromFile(filePath) 1457 assert.NoError(t, err) 1458 require.NotNil(t, resolver) 1459 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1460 1461 refs, err := resolver.FilesByGlob("**/*.txt") 1462 assert.NoError(t, err) 1463 1464 assert.Len(t, refs, 1) 1465 } 1466 1467 func Test_fileResolver_FilesByMIMEType(t *testing.T) { 1468 tests := []struct { 1469 fixturePath string 1470 mimeType string 1471 expectedPaths *strset.Set 1472 }{ 1473 { 1474 fixturePath: "./test-fixtures/image-simple/file-1.txt", 1475 mimeType: "text/plain", 1476 expectedPaths: strset.New("/file-1.txt"), 1477 }, 1478 } 1479 for _, test := range tests { 1480 t.Run(test.fixturePath, func(t *testing.T) { 1481 filePath := "./test-fixtures/image-simple/file-1.txt" 1482 parentPath, err := absoluteSymlinkFreePathToParent(filePath) 1483 require.NoError(t, err) 1484 require.NotNil(t, parentPath) 1485 1486 resolver, err := NewFromFile(filePath) 1487 assert.NoError(t, err) 1488 require.NotNil(t, resolver) 1489 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1490 1491 locations, err := resolver.FilesByMIMEType(test.mimeType) 1492 assert.NoError(t, err) 1493 assert.Equal(t, test.expectedPaths.Size(), len(locations)) 1494 for _, l := range locations { 1495 assert.True(t, test.expectedPaths.Has(l.RealPath), "does not have path %q", l.RealPath) 1496 } 1497 }) 1498 } 1499 } 1500 1501 func Test_fileResolver_FileContentsByLocation(t *testing.T) { 1502 cwd, err := os.Getwd() 1503 require.NoError(t, err) 1504 1505 filePath := "./test-fixtures/image-simple/file-1.txt" 1506 parentPath, err := absoluteSymlinkFreePathToParent(filePath) 1507 require.NoError(t, err) 1508 require.NotNil(t, parentPath) 1509 1510 resolver, err := NewFromFile(filePath) 1511 require.NoError(t, err) 1512 require.NotNil(t, resolver) 1513 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1514 1515 exists, existingPath, err := resolver.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt"))) 1516 require.True(t, exists) 1517 require.NoError(t, err) 1518 require.True(t, existingPath.HasReference()) 1519 1520 tests := []struct { 1521 name string 1522 location file.Location 1523 expects string 1524 err bool 1525 }{ 1526 { 1527 name: "use file reference for content requests", 1528 location: file.NewLocationFromDirectory("some/place", *existingPath.Reference), 1529 expects: "this file has contents", 1530 }, 1531 { 1532 name: "error on empty file reference", 1533 location: file.NewLocationFromDirectory("doesn't matter", stereoscopeFile.Reference{}), 1534 err: true, 1535 }, 1536 } 1537 for _, test := range tests { 1538 t.Run(test.name, func(t *testing.T) { 1539 1540 actual, err := resolver.FileContentsByLocation(test.location) 1541 if test.err { 1542 require.Error(t, err) 1543 return 1544 } 1545 1546 require.NoError(t, err) 1547 if test.expects != "" { 1548 b, err := io.ReadAll(actual) 1549 require.NoError(t, err) 1550 assert.Equal(t, test.expects, string(b)) 1551 } 1552 }) 1553 } 1554 } 1555 1556 func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) { 1557 filePath := "./test-fixtures/system_paths/target/home/place" 1558 parentPath, err := absoluteSymlinkFreePathToParent(filePath) 1559 require.NoError(t, err) 1560 require.NotNil(t, parentPath) 1561 1562 resolver, err := NewFromFile(filePath) 1563 require.NoError(t, err) 1564 require.NotNil(t, resolver) 1565 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1566 1567 var dirLoc *file.Location 1568 ctx, cancel := context.WithCancel(context.Background()) 1569 defer cancel() 1570 for loc := range resolver.AllLocations(ctx) { 1571 entry, err := resolver.Index.Get(loc.Reference()) 1572 require.NoError(t, err) 1573 if dirLoc == nil && entry.Metadata.IsDir() { 1574 dirLoc = &loc 1575 } 1576 } 1577 1578 require.NotNil(t, dirLoc) 1579 1580 reader, err := resolver.FileContentsByLocation(*dirLoc) 1581 require.Error(t, err) 1582 require.Nil(t, reader) 1583 1584 goleak.VerifyNone(t) 1585 } 1586 1587 func TestFileResolver_AllLocations(t *testing.T) { 1588 // Verify both the parent and the file itself are indexed 1589 filePath := "./test-fixtures/system_paths/target/home/place" 1590 parentPath, err := absoluteSymlinkFreePathToParent(filePath) 1591 require.NoError(t, err) 1592 require.NotNil(t, parentPath) 1593 1594 resolver, err := NewFromFile(filePath) 1595 require.NoError(t, err) 1596 require.NotNil(t, resolver) 1597 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1598 1599 paths := strset.New() 1600 for loc := range resolver.AllLocations(context.Background()) { 1601 paths.Add(loc.RealPath) 1602 } 1603 expected := []string{ 1604 "/place", 1605 "", // This is how we see the parent dir, since we're resolving wrt the parent directory. 1606 } 1607 1608 pathsList := paths.List() 1609 sort.Strings(pathsList) 1610 1611 assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List())) 1612 1613 goleak.VerifyNone(t) 1614 } 1615 1616 func Test_FileResolver_AllLocationsDoesNotLeakGoRoutine(t *testing.T) { 1617 filePath := "./test-fixtures/system_paths/target/home/place" 1618 parentPath, err := absoluteSymlinkFreePathToParent(filePath) 1619 require.NoError(t, err) 1620 require.NotNil(t, parentPath) 1621 1622 resolver, err := NewFromFile(filePath) 1623 require.NoError(t, err) 1624 require.NotNil(t, resolver) 1625 assert.Equal(t, resolver.Chroot.Base(), parentPath) 1626 1627 require.NoError(t, err) 1628 ctx, cancel := context.WithCancel(context.Background()) 1629 for range resolver.AllLocations(ctx) { 1630 break 1631 } 1632 cancel() 1633 1634 goleak.VerifyNone(t) 1635 }