github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/filesystem_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package filesystem_test 16 17 import ( 18 "context" 19 "errors" 20 "io" 21 "io/fs" 22 "os" 23 "path" 24 "path/filepath" 25 "regexp" 26 "runtime" 27 "slices" 28 "sort" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/gobwas/glob" 34 "github.com/google/go-cmp/cmp" 35 "github.com/google/go-cmp/cmp/cmpopts" 36 "github.com/google/osv-scalibr/extractor" 37 "github.com/google/osv-scalibr/extractor/filesystem" 38 scalibrfs "github.com/google/osv-scalibr/fs" 39 "github.com/google/osv-scalibr/inventory" 40 "github.com/google/osv-scalibr/plugin" 41 "github.com/google/osv-scalibr/stats" 42 "github.com/google/osv-scalibr/testing/extracttest" 43 fe "github.com/google/osv-scalibr/testing/fakeextractor" 44 "github.com/google/osv-scalibr/testing/fakefs" 45 ) 46 47 // Map of file paths to contents. Empty contents denote directories. 48 type mapFS map[string][]byte 49 50 func TestInitWalkContext(t *testing.T) { 51 dummyFS := scalibrfs.DirFS(".") 52 testCases := []struct { 53 desc string 54 scanRoots map[string][]string 55 pathsToExtract map[string][]string 56 dirsToSkip map[string][]string 57 wantErr error 58 }{ 59 { 60 desc: "valid_config_with_pathsToExtract_raises_no_error", 61 scanRoots: map[string][]string{ 62 "darwin": {"/scanroot/"}, 63 "linux": {"/scanroot/"}, 64 "windows": {"C:\\scanroot\\"}, 65 }, 66 pathsToExtract: map[string][]string{ 67 "darwin": {"/scanroot/file1.txt", "/scanroot/file2.txt"}, 68 "linux": {"/scanroot/file1.txt", "/scanroot/file2.txt"}, 69 "windows": {"C:\\scanroot\\file1.txt", "C:\\scanroot\\file2.txt"}, 70 }, 71 wantErr: nil, 72 }, 73 { 74 desc: "valid_config_with_dirsToSkip_raises_no_error", 75 scanRoots: map[string][]string{ 76 "darwin": {"/scanroot/", "/someotherroot/"}, 77 "linux": {"/scanroot/", "/someotherroot/"}, 78 "windows": {"C:\\scanroot\\", "D:\\someotherroot\\"}, 79 }, 80 dirsToSkip: map[string][]string{ 81 "darwin": {"/scanroot/mydir/", "/someotherroot/mydir/"}, 82 "linux": {"/scanroot/mydir/", "/someotherroot/mydir/"}, 83 "windows": {"C:\\scanroot\\mydir\\", "D:\\someotherroot\\mydir\\"}, 84 }, 85 wantErr: nil, 86 }, 87 { 88 desc: "pathsToExtract_not_relative_to_any_root_raises_error", 89 scanRoots: map[string][]string{ 90 "darwin": {"/scanroot/"}, 91 "linux": {"/scanroot/"}, 92 "windows": {"C:\\scanroot\\"}, 93 }, 94 pathsToExtract: map[string][]string{ 95 "darwin": {"/scanroot/myfile.txt", "/myotherroot/file1.txt"}, 96 "linux": {"/scanroot/myfile.txt", "/myotherroot/file1.txt"}, 97 "windows": {"C:\\scanroot\\myfile.txt", "D:\\myotherroot\\file1.txt"}, 98 }, 99 wantErr: filesystem.ErrNotRelativeToScanRoots, 100 }, 101 { 102 desc: "dirsToSkip_not_relative_to_any_root_raises_error", 103 scanRoots: map[string][]string{ 104 "darwin": {"/scanroot/"}, 105 "linux": {"/scanroot/"}, 106 "windows": {"C:\\scanroot\\"}, 107 }, 108 dirsToSkip: map[string][]string{ 109 "darwin": {"/scanroot/mydir/", "/myotherroot/mydir/"}, 110 "linux": {"/scanroot/mydir/", "/myotherroot/mydir/"}, 111 "windows": {"C:\\scanroot\\mydir\\", "D:\\myotherroot\\mydir\\"}, 112 }, 113 wantErr: filesystem.ErrNotRelativeToScanRoots, 114 }, 115 } 116 117 for _, tc := range testCases { 118 t.Run(tc.desc, func(t *testing.T) { 119 os := runtime.GOOS 120 if _, ok := tc.scanRoots[os]; !ok { 121 t.Fatalf("system %q not defined in test, please extend the tests", os) 122 } 123 config := &filesystem.Config{ 124 PathsToExtract: tc.pathsToExtract[os], 125 DirsToSkip: tc.dirsToSkip[os], 126 } 127 scanRoots := []*scalibrfs.ScanRoot{} 128 for _, p := range tc.scanRoots[os] { 129 scanRoots = append(scanRoots, &scalibrfs.ScanRoot{FS: dummyFS, Path: p}) 130 } 131 _, err := filesystem.InitWalkContext( 132 t.Context(), config, scanRoots, 133 ) 134 if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" { 135 t.Errorf("filesystem.InitializeWalkContext(%v) error got diff (-want +got):\n%s", config, diff) 136 } 137 }) 138 } 139 } 140 141 // fakeExtractorFS is a mock extractor for testing embedded filesystem extraction. 142 // It simulates extracting an embedded filesystem from a VMDK file (e.g., disk.vmdk) 143 // and provides a function to return the embedded filesystem for scanning. 144 type fakeExtractorFS struct { 145 name string // Name of the extractor (e.g., "fake-ex-fs"). 146 getEmbeddedFS func(ctx context.Context) (scalibrfs.FS, error) // Function to return the embedded filesystem for disk.vmdk:1. 147 } 148 149 func (e *fakeExtractorFS) Name() string { return e.name } 150 func (e *fakeExtractorFS) Version() int { return 1 } 151 func (e *fakeExtractorFS) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 152 func (e *fakeExtractorFS) FileRequired(api filesystem.FileAPI) bool { 153 path := api.Path() 154 return path == "disk.vmdk" 155 } 156 func (e *fakeExtractorFS) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 157 path := input.Path 158 if path != "disk.vmdk" { 159 return inventory.Inventory{}, errors.New("unrecognized path") 160 } 161 return inventory.Inventory{ 162 EmbeddedFSs: []*inventory.EmbeddedFS{ 163 { 164 Path: "disk.vmdk:1", 165 GetEmbeddedFS: e.getEmbeddedFS, // Use stored function 166 }, 167 }, 168 }, nil 169 } 170 171 // fakeExtractorSoftware is a mock extractor for testing package detection. 172 // It simulates detecting a software package from a file (e.g., file.txt) within 173 // an embedded filesystem. 174 type fakeExtractorSoftware struct { 175 name string // Name of the extractor (e.g., "fake-ex-software"). 176 } 177 178 func (e *fakeExtractorSoftware) Name() string { return e.name } 179 func (e *fakeExtractorSoftware) Version() int { return 1 } 180 func (e *fakeExtractorSoftware) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 181 func (e *fakeExtractorSoftware) FileRequired(api filesystem.FileAPI) bool { 182 path := filepath.ToSlash(api.Path()) 183 return strings.HasSuffix(path, "file.txt") || strings.HasSuffix(path, "/file.txt") || path == "file.txt" || path == "./file.txt" 184 } 185 func (e *fakeExtractorSoftware) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 186 path := filepath.ToSlash(input.Path) 187 if !strings.HasSuffix(path, "file.txt") { 188 return inventory.Inventory{}, errors.New("not a file.txt") 189 } 190 return inventory.Inventory{ 191 Packages: []*extractor.Package{ 192 { 193 Name: "Software", 194 Locations: []string{path}, 195 Plugins: []string{e.Name()}, 196 }, 197 }, 198 }, nil 199 } 200 201 func TestRun_EmbeddedFS(t *testing.T) { 202 success := &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded} 203 fsys := setupMapFS(t, mapFS{ 204 "disk.vmdk": []byte("VMDK Content"), 205 }) 206 207 // Create temporary directory for embedded filesystem 208 embeddedDir := t.TempDir() 209 err := os.WriteFile(filepath.Join(embeddedDir, "file.txt"), []byte("Content"), fs.ModePerm) 210 if err != nil { 211 t.Fatalf("os.WriteFile(%q): %v", filepath.Join(embeddedDir, "file.txt"), err) 212 } 213 embeddedFS := scalibrfs.DirFS(embeddedDir) 214 215 fakeExFS := &fakeExtractorFS{ 216 name: "fake-ex-fs", 217 getEmbeddedFS: func(ctx context.Context) (scalibrfs.FS, error) { 218 return embeddedFS, nil 219 }, 220 } 221 fakeExSoftware := &fakeExtractorSoftware{name: "fake-ex-software"} 222 extractors := []filesystem.Extractor{fakeExFS, fakeExSoftware} 223 224 // Create config with a single ScanRoot 225 config := &filesystem.Config{ 226 Extractors: extractors, 227 ScanRoots: []*scalibrfs.ScanRoot{{ 228 FS: fsys, 229 Path: ".", 230 }}, 231 Stats: &fakeCollector{}, 232 } 233 234 // Run the test 235 gotInv, gotStatus, err := filesystem.Run(t.Context(), config) 236 if err != nil { 237 t.Fatalf("filesystem.Run(%v): %v", config, err) 238 } 239 240 // Expected inventory 241 wantInv := inventory.Inventory{ 242 Packages: []*extractor.Package{ 243 { 244 Name: "Software", 245 Locations: []string{"disk.vmdk:1:file.txt"}, 246 Plugins: []string{"fake-ex-software", "fake-ex-software"}, // Expect duplicate due to observed behavior 247 }, 248 }, 249 EmbeddedFSs: []*inventory.EmbeddedFS{ 250 { 251 Path: "disk.vmdk:1", 252 GetEmbeddedFS: fakeExFS.getEmbeddedFS, 253 }, 254 }, 255 } 256 257 // Expected status 258 wantStatus := []*plugin.Status{ 259 {Name: "fake-ex-fs", Version: 1, Status: success}, 260 {Name: "fake-ex-software", Version: 1, Status: success}, 261 } 262 263 // Sort package locations for comparison 264 for _, p := range gotInv.Packages { 265 sort.Strings(p.Locations) 266 } 267 268 // Compare inventory 269 if diff := cmp.Diff(wantInv, gotInv, cmpopts.SortSlices(extracttest.PackageCmpLess), fe.AllowUnexported, cmp.AllowUnexported(fakeExtractorFS{}, fakeExtractorSoftware{}), cmpopts.EquateErrors(), cmpopts.IgnoreFields(inventory.EmbeddedFS{}, "GetEmbeddedFS")); diff != "" { 270 t.Errorf("filesystem.Run(%v): unexpected findings (-want +got):\n%s", config, diff) 271 } 272 273 // Deduplicate status entries, keeping the latest for each extractor 274 seen := make(map[string]*plugin.Status) 275 for _, s := range gotStatus { 276 s.Status.FailureReason = "" 277 seen[s.Name] = s 278 } 279 var dedupedStatus []*plugin.Status 280 for _, s := range seen { 281 dedupedStatus = append(dedupedStatus, s) 282 } 283 sort.Slice(dedupedStatus, func(i, j int) bool { 284 return dedupedStatus[i].Name < dedupedStatus[j].Name 285 }) 286 287 // Compare status 288 if diff := cmp.Diff(wantStatus, dedupedStatus, cmpopts.SortSlices(func(s1, s2 *plugin.Status) bool { 289 return s1.Name < s2.Name 290 })); diff != "" { 291 t.Errorf("filesystem.Run(%v): unexpected status (-want +got):\n%s", config, diff) 292 } 293 } 294 295 // A fake extractor that only extracts directories. 296 type fakeExtractorDirs struct { 297 dir string 298 name string 299 } 300 301 func (fakeExtractorDirs) Name() string { return "ex-dirs" } 302 func (fakeExtractorDirs) Version() int { return 1 } 303 func (fakeExtractorDirs) Requirements() *plugin.Capabilities { 304 return &plugin.Capabilities{ExtractFromDirs: true} 305 } 306 func (e fakeExtractorDirs) FileRequired(api filesystem.FileAPI) bool { 307 return api.Path() == e.dir 308 } 309 func (e fakeExtractorDirs) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 310 path := filepath.ToSlash(input.Path) 311 if path == e.dir { 312 return inventory.Inventory{Packages: []*extractor.Package{&extractor.Package{ 313 Name: e.name, 314 Locations: []string{path}, 315 }}}, nil 316 } 317 return inventory.Inventory{}, errors.New("unrecognized path") 318 } 319 320 func TestRunFS(t *testing.T) { 321 success := &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded} 322 dir1 := "dir1" 323 path1 := "dir1/file1.txt" 324 path2 := "dir2/sub/file2.txt" 325 fsys := setupMapFS(t, mapFS{ 326 ".": nil, 327 "dir1": nil, 328 "dir2": nil, 329 "dir1/file1.txt": []byte("Content"), 330 "dir2/sub/file2.txt": []byte("More content"), 331 }) 332 name1 := "software1" 333 name2 := "software2" 334 335 fakeEx1 := fe.New("ex1", 1, []string{path1}, map[string]fe.NamesErr{path1: {Names: []string{name1}, Err: nil}}) 336 fakeEx2 := fe.New("ex2", 2, []string{path2}, map[string]fe.NamesErr{path2: {Names: []string{name2}, Err: nil}}) 337 fakeEx2WithPKG1 := fe.New("ex2", 2, []string{path2}, map[string]fe.NamesErr{path2: {Names: []string{name1}, Err: nil}}) 338 fakeExWithPartialResult := fe.New("ex1", 1, []string{path1}, map[string]fe.NamesErr{path1: {Names: []string{name1}, Err: errors.New("extraction failed")}}) 339 fakeExDirs := &fakeExtractorDirs{dir: dir1, name: name2} 340 fakeExDirsRequiresFile := &fakeExtractorDirs{dir: path1, name: name2} 341 342 cwd, err := os.Getwd() 343 if err != nil { 344 t.Fatalf("os.Getwd(): %v", err) 345 } 346 347 testCases := []struct { 348 desc string 349 ex []filesystem.Extractor 350 pathsToExtract []string 351 ignoreSubDirs bool 352 dirsToSkip []string 353 skipDirGlob string 354 skipDirRegex string 355 storeAbsPath bool 356 maxInodes int 357 maxFileSizeBytes int 358 wantErr error 359 wantPkg inventory.Inventory 360 wantStatus []*plugin.Status 361 wantInodeCount int 362 }{ 363 { 364 desc: "Extractors_successful", 365 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 366 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 367 { 368 Name: name1, 369 Locations: []string{path1}, 370 Plugins: []string{fakeEx1.Name()}, 371 }, 372 { 373 Name: name2, 374 Locations: []string{path2}, 375 Plugins: []string{fakeEx2.Name()}, 376 }, 377 }}, 378 wantStatus: []*plugin.Status{ 379 {Name: "ex1", Version: 1, Status: success}, 380 {Name: "ex2", Version: 2, Status: success}, 381 }, 382 wantInodeCount: 6, 383 }, 384 { 385 desc: "Dir_skipped", 386 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 387 // ScanRoot is CWD 388 dirsToSkip: []string{path.Join(cwd, "dir1")}, 389 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 390 { 391 Name: name2, 392 Locations: []string{path2}, 393 Plugins: []string{fakeEx2.Name()}, 394 }, 395 }}, 396 wantStatus: []*plugin.Status{ 397 {Name: "ex1", Version: 1, Status: success}, 398 {Name: "ex2", Version: 2, Status: success}, 399 }, 400 wantInodeCount: 5, 401 }, 402 { 403 desc: "Dir skipped with absolute path", 404 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 405 dirsToSkip: []string{"dir1"}, 406 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 407 { 408 Name: name2, 409 Locations: []string{path2}, 410 Plugins: []string{fakeEx2.Name()}, 411 }, 412 }}, 413 wantStatus: []*plugin.Status{ 414 {Name: "ex1", Version: 1, Status: success}, 415 {Name: "ex2", Version: 2, Status: success}, 416 }, 417 wantInodeCount: 5, 418 }, 419 { 420 desc: "Dir skipped using regex", 421 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 422 skipDirRegex: ".*1", 423 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 424 { 425 Name: name2, 426 Locations: []string{path2}, 427 Plugins: []string{fakeEx2.Name()}, 428 }, 429 }}, 430 wantStatus: []*plugin.Status{ 431 {Name: "ex1", Version: 1, Status: success}, 432 {Name: "ex2", Version: 2, Status: success}, 433 }, 434 wantInodeCount: 5, 435 }, 436 { 437 desc: "Dir skipped with full match of dirname", 438 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 439 skipDirRegex: "/sub$", 440 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 441 { 442 Name: name1, 443 Locations: []string{path1}, 444 Plugins: []string{fakeEx1.Name()}, 445 }, 446 }}, 447 wantStatus: []*plugin.Status{ 448 {Name: "ex1", Version: 1, Status: success}, 449 {Name: "ex2", Version: 2, Status: success}, 450 }, 451 wantInodeCount: 5, 452 }, 453 { 454 desc: "skip regex set but not match", 455 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 456 skipDirRegex: "asdf", 457 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 458 { 459 Name: name1, 460 Locations: []string{path1}, 461 Plugins: []string{fakeEx1.Name()}, 462 }, 463 { 464 Name: name2, 465 Locations: []string{path2}, 466 Plugins: []string{fakeEx2.Name()}, 467 }, 468 }}, 469 wantStatus: []*plugin.Status{ 470 {Name: "ex1", Version: 1, Status: success}, 471 {Name: "ex2", Version: 2, Status: success}, 472 }, 473 wantInodeCount: 6, 474 }, 475 { 476 desc: "Dirs skipped using glob", 477 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 478 skipDirGlob: "dir*", 479 wantPkg: inventory.Inventory{}, 480 wantStatus: []*plugin.Status{ 481 {Name: "ex1", Version: 1, Status: success}, 482 {Name: "ex2", Version: 2, Status: success}, 483 }, 484 wantInodeCount: 3, 485 }, 486 { 487 desc: "Subdirectory skipped using glob", 488 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 489 skipDirGlob: "**/sub", 490 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 491 { 492 Name: name1, 493 Locations: []string{path1}, 494 Plugins: []string{fakeEx1.Name()}, 495 }, 496 }}, 497 wantStatus: []*plugin.Status{ 498 {Name: "ex1", Version: 1, Status: success}, 499 {Name: "ex2", Version: 2, Status: success}, 500 }, 501 wantInodeCount: 5, 502 }, 503 { 504 desc: "Dirs skipped using glob pattern lists", 505 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 506 skipDirGlob: "{dir1,dir2}", 507 wantPkg: inventory.Inventory{}, 508 wantStatus: []*plugin.Status{ 509 {Name: "ex1", Version: 1, Status: success}, 510 {Name: "ex2", Version: 2, Status: success}, 511 }, 512 wantInodeCount: 3, 513 }, 514 { 515 desc: "No directories matched using glob", 516 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 517 skipDirGlob: "none", 518 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 519 { 520 Name: name1, 521 Locations: []string{path1}, 522 Plugins: []string{fakeEx1.Name()}, 523 }, 524 { 525 Name: name2, 526 Locations: []string{path2}, 527 Plugins: []string{fakeEx2.Name()}, 528 }, 529 }}, 530 wantStatus: []*plugin.Status{ 531 {Name: "ex1", Version: 1, Status: success}, 532 {Name: "ex2", Version: 2, Status: success}, 533 }, 534 wantInodeCount: 6, 535 }, 536 { 537 desc: "Duplicate_inventory_results_kept_separate", 538 ex: []filesystem.Extractor{fakeEx1, fakeEx2WithPKG1}, 539 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 540 { 541 Name: name1, 542 Locations: []string{path1}, 543 Plugins: []string{fakeEx1.Name()}, 544 }, 545 { 546 Name: name1, 547 Locations: []string{path2}, 548 Plugins: []string{fakeEx2WithPKG1.Name()}, 549 }, 550 }}, 551 wantStatus: []*plugin.Status{ 552 {Name: "ex1", Version: 1, Status: success}, 553 {Name: "ex2", Version: 2, Status: success}, 554 }, 555 wantInodeCount: 6, 556 }, 557 { 558 desc: "Extract_specific_file", 559 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 560 // ScanRoot is CWD 561 pathsToExtract: []string{path.Join(cwd, path2)}, 562 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 563 { 564 Name: name2, 565 Locations: []string{path2}, 566 Plugins: []string{fakeEx2.Name()}, 567 }, 568 }}, 569 wantStatus: []*plugin.Status{ 570 {Name: "ex1", Version: 1, Status: success}, 571 {Name: "ex2", Version: 2, Status: success}, 572 }, 573 wantInodeCount: 1, 574 }, 575 { 576 desc: "Extract specific file with absolute path", 577 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 578 pathsToExtract: []string{path2}, 579 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 580 { 581 Name: name2, 582 Locations: []string{path2}, 583 Plugins: []string{fakeEx2.Name()}, 584 }, 585 }}, 586 wantStatus: []*plugin.Status{ 587 {Name: "ex1", Version: 1, Status: success}, 588 {Name: "ex2", Version: 2, Status: success}, 589 }, 590 wantInodeCount: 1, 591 }, 592 { 593 desc: "Extract directory contents", 594 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 595 pathsToExtract: []string{"dir2"}, 596 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 597 { 598 Name: name2, 599 Locations: []string{path2}, 600 Plugins: []string{fakeEx2.Name()}, 601 }, 602 }}, 603 wantStatus: []*plugin.Status{ 604 {Name: "ex1", Version: 1, Status: success}, 605 {Name: "ex2", Version: 2, Status: success}, 606 }, 607 wantInodeCount: 3, 608 }, 609 { 610 desc: "Point to nonexistent file", 611 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 612 pathsToExtract: []string{"nonexistent"}, 613 wantPkg: inventory.Inventory{}, 614 wantStatus: []*plugin.Status{ 615 {Name: "ex1", Version: 1, Status: success}, 616 {Name: "ex2", Version: 2, Status: success}, 617 }, 618 wantInodeCount: 1, 619 }, 620 { 621 desc: "Skip sub-dirs: Inventory found in root dir", 622 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 623 pathsToExtract: []string{"dir1"}, 624 ignoreSubDirs: true, 625 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 626 { 627 Name: name1, 628 Locations: []string{path1}, 629 Plugins: []string{fakeEx1.Name()}, 630 }, 631 }}, 632 wantStatus: []*plugin.Status{ 633 {Name: "ex1", Version: 1, Status: success}, 634 {Name: "ex2", Version: 2, Status: success}, 635 }, 636 wantInodeCount: 2, 637 }, 638 { 639 desc: "Skip sub-dirs: Inventory not found in root dir", 640 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 641 pathsToExtract: []string{"dir2"}, 642 ignoreSubDirs: true, 643 wantPkg: inventory.Inventory{}, 644 wantStatus: []*plugin.Status{ 645 {Name: "ex1", Version: 1, Status: success}, 646 {Name: "ex2", Version: 2, Status: success}, 647 }, 648 wantInodeCount: 2, 649 }, 650 { 651 desc: "nil_result", 652 ex: []filesystem.Extractor{ 653 // An Extractor that returns nil. 654 fe.New("ex1", 1, []string{path1}, map[string]fe.NamesErr{path1: {Names: nil, Err: nil}}), 655 }, 656 wantPkg: inventory.Inventory{}, 657 wantStatus: []*plugin.Status{ 658 {Name: "ex1", Version: 1, Status: success}, 659 }, 660 wantInodeCount: 6, 661 }, 662 { 663 desc: "Extraction_fails_with_partial_results", 664 ex: []filesystem.Extractor{fakeExWithPartialResult}, 665 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 666 { 667 Name: name1, 668 Locations: []string{path1}, 669 Plugins: []string{fakeExWithPartialResult.Name()}, 670 }, 671 }}, 672 wantStatus: []*plugin.Status{ 673 {Name: "ex1", Version: 1, Status: &plugin.ScanStatus{ 674 Status: plugin.ScanStatusPartiallySucceeded, 675 FailureReason: "encountered 1 error(s) while running plugin; check file-specific errors for details", 676 FileErrors: []*plugin.FileError{ 677 {FilePath: path1, ErrorMessage: "extraction failed"}, 678 }, 679 }}, 680 }, 681 wantInodeCount: 6, 682 }, 683 { 684 desc: "Extraction_fails_with_no_results", 685 ex: []filesystem.Extractor{ 686 fe.New("ex1", 1, []string{path1}, map[string]fe.NamesErr{path1: {Names: nil, Err: errors.New("extraction failed")}}), 687 }, 688 wantPkg: inventory.Inventory{}, 689 wantStatus: []*plugin.Status{ 690 {Name: "ex1", Version: 1, Status: &plugin.ScanStatus{ 691 Status: plugin.ScanStatusFailed, 692 FailureReason: "encountered 1 error(s) while running plugin; check file-specific errors for details", 693 FileErrors: []*plugin.FileError{ 694 {FilePath: path1, ErrorMessage: "extraction failed"}, 695 }, 696 }}, 697 }, 698 wantInodeCount: 6, 699 }, 700 { 701 desc: "Extraction_fails_several_times", 702 ex: []filesystem.Extractor{ 703 fe.New("ex1", 1, []string{path1, path2}, map[string]fe.NamesErr{ 704 path1: {Names: nil, Err: errors.New("extraction failed")}, 705 path2: {Names: nil, Err: errors.New("extraction failed")}, 706 }), 707 }, 708 wantPkg: inventory.Inventory{}, 709 wantStatus: []*plugin.Status{ 710 {Name: "ex1", Version: 1, Status: &plugin.ScanStatus{ 711 Status: plugin.ScanStatusFailed, 712 FailureReason: "encountered 2 error(s) while running plugin; check file-specific errors for details", 713 FileErrors: []*plugin.FileError{ 714 {FilePath: path1, ErrorMessage: "extraction failed"}, 715 {FilePath: path2, ErrorMessage: "extraction failed"}, 716 }, 717 }}, 718 }, 719 wantInodeCount: 6, 720 }, 721 { 722 desc: "More inodes visited than limit, Error", 723 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 724 maxInodes: 2, 725 wantPkg: inventory.Inventory{}, 726 wantStatus: []*plugin.Status{ 727 {Name: "ex1", Version: 1, Status: success}, 728 {Name: "ex2", Version: 2, Status: success}, 729 }, 730 wantInodeCount: 2, 731 wantErr: cmpopts.AnyError, 732 }, 733 { 734 desc: "Less inodes visited than limit, no Error", 735 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 736 maxInodes: 6, 737 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 738 { 739 Name: name1, 740 Locations: []string{path1}, 741 Plugins: []string{fakeEx1.Name()}, 742 }, 743 { 744 Name: name2, 745 Locations: []string{path2}, 746 Plugins: []string{fakeEx2.Name()}, 747 }, 748 }}, 749 wantStatus: []*plugin.Status{ 750 {Name: "ex1", Version: 1, Status: success}, 751 {Name: "ex2", Version: 2, Status: success}, 752 }, 753 wantInodeCount: 6, 754 }, 755 { 756 desc: "Large files skipped", 757 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 758 maxInodes: 6, 759 maxFileSizeBytes: 10, 760 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 761 { 762 Name: name1, 763 Locations: []string{path1}, 764 Plugins: []string{fakeEx1.Name()}, 765 }, 766 }}, 767 wantStatus: []*plugin.Status{ 768 {Name: "ex1", Version: 1, Status: success}, 769 {Name: "ex2", Version: 2, Status: success}, 770 }, 771 wantInodeCount: 6, 772 }, 773 { 774 desc: "Extractors_successful_store_absolute_path_when_requested", 775 ex: []filesystem.Extractor{fakeEx1, fakeEx2}, 776 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 777 { 778 Name: name1, 779 Locations: []string{filepath.Join(cwd, path1)}, 780 Plugins: []string{fakeEx1.Name()}, 781 }, 782 { 783 Name: name2, 784 Locations: []string{filepath.Join(cwd, path2)}, 785 Plugins: []string{fakeEx2.Name()}, 786 }, 787 }}, 788 storeAbsPath: true, 789 wantStatus: []*plugin.Status{ 790 {Name: "ex1", Version: 1, Status: success}, 791 {Name: "ex2", Version: 2, Status: success}, 792 }, 793 wantInodeCount: 6, 794 }, 795 { 796 desc: "Extractor_runs_on_directory", 797 ex: []filesystem.Extractor{fakeEx1, fakeExDirs}, 798 wantPkg: inventory.Inventory{Packages: []*extractor.Package{ 799 { 800 Name: name1, 801 Locations: []string{path1}, 802 Plugins: []string{fakeEx1.Name()}, 803 }, 804 { 805 Name: name2, 806 Locations: []string{dir1}, 807 Plugins: []string{fakeExDirs.Name()}, 808 }, 809 }}, 810 wantStatus: []*plugin.Status{ 811 {Name: "ex1", Version: 1, Status: success}, 812 {Name: "ex-dirs", Version: 1, Status: success}, 813 }, 814 wantInodeCount: 6, 815 }, 816 { 817 desc: "Directory Extractor ignores files", 818 ex: []filesystem.Extractor{fakeExDirsRequiresFile}, 819 wantPkg: inventory.Inventory{Packages: nil}, 820 wantStatus: []*plugin.Status{ 821 {Name: "ex-dirs", Version: 1, Status: success}, 822 }, 823 wantInodeCount: 6, 824 }, 825 } 826 827 for _, tc := range testCases { 828 t.Run(tc.desc, func(t *testing.T) { 829 fc := &fakeCollector{} 830 var skipDirRegex *regexp.Regexp 831 var skipDirGlob glob.Glob 832 if tc.skipDirRegex != "" { 833 skipDirRegex = regexp.MustCompile(tc.skipDirRegex) 834 } 835 if tc.skipDirGlob != "" { 836 skipDirGlob = glob.MustCompile(tc.skipDirGlob) 837 } 838 config := &filesystem.Config{ 839 Extractors: tc.ex, 840 PathsToExtract: tc.pathsToExtract, 841 IgnoreSubDirs: tc.ignoreSubDirs, 842 DirsToSkip: tc.dirsToSkip, 843 SkipDirRegex: skipDirRegex, 844 SkipDirGlob: skipDirGlob, 845 MaxInodes: tc.maxInodes, 846 MaxFileSize: tc.maxFileSizeBytes, 847 ScanRoots: []*scalibrfs.ScanRoot{{ 848 FS: fsys, Path: ".", 849 }}, 850 Stats: fc, 851 StoreAbsolutePath: tc.storeAbsPath, 852 } 853 wc, err := filesystem.InitWalkContext( 854 t.Context(), config, []*scalibrfs.ScanRoot{{ 855 FS: fsys, Path: cwd, 856 }}, 857 ) 858 if err != nil { 859 t.Fatalf("filesystem.InitializeWalkContext(..., %v): %v", fsys, err) 860 } 861 if err = wc.PrepareNewScan(cwd, fsys); err != nil { 862 t.Fatalf("wc.UpdateScanRoot(..., %v): %v", fsys, err) 863 } 864 gotInv, gotStatus, err := filesystem.RunFS(t.Context(), config, wc) 865 if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" { 866 t.Errorf("extractor.Run(%v) error got diff (-want +got):\n%s", tc.ex, diff) 867 } 868 869 if fc.AfterInodeVisitedCount != tc.wantInodeCount { 870 t.Errorf("extractor.Run(%v) inodes visited: got %d, want %d", tc.ex, fc.AfterInodeVisitedCount, tc.wantInodeCount) 871 } 872 873 // The order of the locations doesn't matter. 874 for _, p := range gotInv.Packages { 875 sort.Strings(p.Locations) 876 } 877 878 if diff := cmp.Diff(tc.wantPkg, gotInv, cmpopts.SortSlices(extracttest.PackageCmpLess), fe.AllowUnexported, cmp.AllowUnexported(fakeExtractorDirs{}), cmpopts.EquateErrors()); diff != "" { 879 t.Errorf("extractor.Run(%v): unexpected findings (-want +got):\n%s", tc.ex, diff) 880 } 881 882 // The order of the statuses doesn't matter. 883 for _, s := range gotStatus { 884 if s.Status.FileErrors != nil { 885 sort.Slice(s.Status.FileErrors, func(i, j int) bool { 886 return s.Status.FileErrors[i].FilePath < s.Status.FileErrors[j].FilePath 887 }) 888 } 889 } 890 891 sortStatus := func(s1, s2 *plugin.Status) bool { 892 return s1.Name < s2.Name 893 } 894 if diff := cmp.Diff(tc.wantStatus, gotStatus, cmpopts.SortSlices(sortStatus)); diff != "" { 895 t.Errorf("extractor.Run(%v): unexpected status (-want +got):\n%s", tc.ex, diff) 896 } 897 }) 898 } 899 } 900 901 func TestRunFSGitignore(t *testing.T) { 902 cwd, err := os.Getwd() 903 if err != nil { 904 t.Fatalf("os.Getwd(): %v", err) 905 } 906 907 name1 := "software1" 908 name2 := "software2" 909 path1 := "dir1/file1.txt" 910 path2 := "dir2/sub/file2.txt" 911 fakeEx1 := fe.New("ex1", 1, []string{path1}, map[string]fe.NamesErr{path1: {Names: []string{name1}, Err: nil}}) 912 fakeEx2 := fe.New("ex2", 2, []string{path2}, map[string]fe.NamesErr{path2: {Names: []string{name2}, Err: nil}}) 913 ex := []filesystem.Extractor{fakeEx1, fakeEx2} 914 915 testCases := []struct { 916 desc string 917 mapFS mapFS 918 pathToExtract string 919 ignoreSubDirs bool 920 wantPkg1 bool 921 wantPkg2 bool 922 wantInodeCount int 923 }{ 924 { 925 desc: "Skip_file", 926 mapFS: mapFS{ 927 ".": nil, 928 "dir1": nil, 929 "dir1/file1.txt": []byte("Content 1"), 930 "dir1/.gitignore": []byte("file1.txt"), 931 }, 932 pathToExtract: "dir1", 933 wantPkg1: false, 934 wantInodeCount: 3, 935 }, 936 { 937 desc: "Skip_dir", 938 mapFS: mapFS{ 939 ".": nil, 940 "dir2": nil, 941 "dir2/sub": nil, 942 "dir2/sub/file2.txt": []byte("Content 2"), 943 "dir2/.gitignore": []byte("sub"), 944 }, 945 pathToExtract: "", 946 wantPkg2: false, 947 wantInodeCount: 4, 948 }, 949 { 950 desc: "Dont_skip_if_no_match", 951 mapFS: mapFS{ 952 ".": nil, 953 "dir1": nil, 954 "dir1/file1.txt": []byte("Content 1"), 955 "dir1/.gitignore": []byte("no-match.txt"), 956 }, 957 pathToExtract: "", 958 wantPkg1: true, 959 wantPkg2: false, 960 wantInodeCount: 4, 961 }, 962 { 963 desc: "Skip_based_on_parent_gitignore", 964 mapFS: mapFS{ 965 ".": nil, 966 "dir2": nil, 967 "dir2/sub": nil, 968 "dir2/sub/file2.txt": []byte("Content 1"), 969 "dir2/.gitignore": []byte("file2.txt"), 970 }, 971 pathToExtract: "dir2/sub", 972 wantPkg1: false, 973 wantInodeCount: 2, 974 }, 975 { 976 desc: "Skip_based_on_child_gitignore", 977 mapFS: mapFS{ 978 ".": nil, 979 "dir1": nil, 980 "dir2": nil, 981 "dir2/sub": nil, 982 "dir1/file1.txt": []byte("Content 1"), 983 "dir1/.gitignore": []byte("file1.txt\nfile2.txt"), 984 // Not skipped since the skip pattern is in dir1 985 "dir2/sub/file2.txt": []byte("Content 2"), 986 }, 987 pathToExtract: "", 988 wantPkg1: false, 989 wantPkg2: true, 990 wantInodeCount: 7, 991 }, 992 { 993 desc: "ignore_sub_dirs", 994 mapFS: mapFS{ 995 ".": nil, 996 "dir": nil, 997 ".gitignore": []byte("file1.txt"), 998 "file1.txt": []byte("Content 1"), 999 "dir/.gitignore": []byte("file1.txt"), 1000 "dir/file2.txt": []byte("Content 2"), 1001 }, 1002 pathToExtract: "", 1003 ignoreSubDirs: true, 1004 wantPkg1: false, // Skipped because of .gitignore 1005 wantPkg2: false, // Skipped because of IgnoreSubDirs 1006 wantInodeCount: 4, 1007 }, 1008 } 1009 1010 for _, tc := range testCases { 1011 t.Run(tc.desc, func(t *testing.T) { 1012 fc := &fakeCollector{} 1013 fsys := setupMapFS(t, tc.mapFS) 1014 config := &filesystem.Config{ 1015 Extractors: ex, 1016 PathsToExtract: []string{tc.pathToExtract}, 1017 IgnoreSubDirs: tc.ignoreSubDirs, 1018 UseGitignore: true, 1019 ScanRoots: []*scalibrfs.ScanRoot{{ 1020 FS: fsys, Path: ".", 1021 }}, 1022 Stats: fc, 1023 StoreAbsolutePath: false, 1024 } 1025 wc, err := filesystem.InitWalkContext( 1026 t.Context(), config, []*scalibrfs.ScanRoot{{ 1027 FS: fsys, Path: cwd, 1028 }}, 1029 ) 1030 if err != nil { 1031 t.Fatalf("filesystem.InitializeWalkContext(..., %v): %v", fsys, err) 1032 } 1033 if err = wc.PrepareNewScan(cwd, fsys); err != nil { 1034 t.Fatalf("wc.UpdateScanRoot(..., %v): %v", fsys, err) 1035 } 1036 gotInv, _, err := filesystem.RunFS(t.Context(), config, wc) 1037 if err != nil { 1038 t.Errorf("filesystem.RunFS(%v, %v): %v", config, wc, err) 1039 } 1040 1041 if fc.AfterInodeVisitedCount != tc.wantInodeCount { 1042 t.Errorf("filesystem.RunFS(%v, %v) inodes visited: got %d, want %d", config, wc, fc.AfterInodeVisitedCount, tc.wantInodeCount) 1043 } 1044 1045 gotPkg1 := slices.ContainsFunc(gotInv.Packages, func(p *extractor.Package) bool { 1046 return p.Name == name1 1047 }) 1048 gotPkg2 := slices.ContainsFunc(gotInv.Packages, func(p *extractor.Package) bool { 1049 return p.Name == name2 1050 }) 1051 if gotPkg1 != tc.wantPkg1 { 1052 t.Errorf("filesystem.Run(%v, %v): got inv1: %v, want: %v", config, wc, gotPkg1, tc.wantPkg1) 1053 } 1054 if gotPkg2 != tc.wantPkg2 { 1055 t.Errorf("filesystem.Run(%v, %v): got inv2: %v, want: %v", config, wc, gotPkg2, tc.wantPkg2) 1056 } 1057 }) 1058 } 1059 } 1060 1061 func setupMapFS(t *testing.T, mapFS mapFS) scalibrfs.FS { 1062 t.Helper() 1063 1064 root := t.TempDir() 1065 for path, content := range mapFS { 1066 path = filepath.FromSlash(path) 1067 if content == nil { 1068 err := os.MkdirAll(filepath.Join(root, path), fs.ModePerm) 1069 if err != nil { 1070 t.Fatalf("os.MkdirAll(%q): %v", path, err) 1071 } 1072 } else { 1073 dir := filepath.Dir(path) 1074 err := os.MkdirAll(filepath.Join(root, dir), fs.ModePerm) 1075 if err != nil { 1076 t.Fatalf("os.MkdirAll(%q): %v", dir, err) 1077 } 1078 err = os.WriteFile(filepath.Join(root, path), content, fs.ModePerm) 1079 if err != nil { 1080 t.Fatalf("os.WriteFile(%q): %v", path, err) 1081 } 1082 } 1083 } 1084 return scalibrfs.DirFS(root) 1085 } 1086 1087 // To not break the test every time we add a new metric, we inherit from the NoopCollector. 1088 type fakeCollector struct { 1089 stats.NoopCollector 1090 1091 AfterInodeVisitedCount int 1092 } 1093 1094 func (c *fakeCollector) AfterInodeVisited(path string) { c.AfterInodeVisitedCount++ } 1095 1096 // A fake implementation of fs.FS with a single file under root which errors when its opened. 1097 type fakeFS struct{} 1098 1099 func (fakeFS) Open(name string) (fs.File, error) { 1100 if name == "." { 1101 return &fakeDir{dirs: []fs.DirEntry{&fakeDirEntry{}}}, nil 1102 } 1103 return nil, errors.New("failed to open") 1104 } 1105 func (fakeFS) ReadDir(name string) ([]fs.DirEntry, error) { 1106 return nil, errors.New("not implemented") 1107 } 1108 func (fakeFS) Stat(name string) (fs.FileInfo, error) { 1109 return &fakeFileInfo{dir: true}, nil 1110 } 1111 1112 type fakeDir struct { 1113 dirs []fs.DirEntry 1114 } 1115 1116 func (fakeDir) Stat() (fs.FileInfo, error) { return &fakeFileInfo{dir: true}, nil } 1117 func (fakeDir) Read([]byte) (int, error) { return 0, errors.New("failed to read") } 1118 func (fakeDir) Close() error { return nil } 1119 func (f *fakeDir) ReadDir(n int) ([]fs.DirEntry, error) { 1120 if n <= 0 { 1121 t := f.dirs 1122 f.dirs = []fs.DirEntry{} 1123 return t, nil 1124 } 1125 if len(f.dirs) == 0 { 1126 return f.dirs, io.EOF 1127 } 1128 n = min(n, len(f.dirs)) 1129 t := f.dirs[:n] 1130 f.dirs = f.dirs[n:] 1131 return t, nil 1132 } 1133 1134 type fakeFileInfo struct{ dir bool } 1135 1136 func (fakeFileInfo) Name() string { return "/" } 1137 func (fakeFileInfo) Size() int64 { return 1 } 1138 func (i *fakeFileInfo) Mode() fs.FileMode { 1139 if i.dir { 1140 return fs.ModeDir + 0777 1141 } 1142 return 0777 1143 } 1144 func (fakeFileInfo) ModTime() time.Time { return time.Now() } 1145 func (i *fakeFileInfo) IsDir() bool { return i.dir } 1146 func (fakeFileInfo) Sys() any { return nil } 1147 1148 type fakeDirEntry struct{} 1149 1150 func (fakeDirEntry) Name() string { return "file" } 1151 func (fakeDirEntry) IsDir() bool { return false } 1152 func (fakeDirEntry) Type() fs.FileMode { return 0777 } 1153 func (fakeDirEntry) Info() (fs.FileInfo, error) { return &fakeFileInfo{dir: false}, nil } 1154 1155 func TestRunFS_ReadError(t *testing.T) { 1156 ex := []filesystem.Extractor{ 1157 fe.New("ex1", 1, []string{"file"}, 1158 map[string]fe.NamesErr{"file": {Names: []string{"software"}, Err: nil}}), 1159 } 1160 wantStatus := []*plugin.Status{ 1161 {Name: "ex1", Version: 1, Status: &plugin.ScanStatus{ 1162 Status: plugin.ScanStatusFailed, FailureReason: "encountered 1 error(s) while running plugin; check file-specific errors for details", FileErrors: []*plugin.FileError{ 1163 {FilePath: "file", ErrorMessage: "Open(file): failed to open"}, 1164 }, 1165 }}, 1166 } 1167 fsys := &fakeFS{} 1168 config := &filesystem.Config{ 1169 Extractors: ex, 1170 DirsToSkip: []string{}, 1171 ScanRoots: []*scalibrfs.ScanRoot{{ 1172 FS: fsys, Path: ".", 1173 }}, 1174 Stats: stats.NoopCollector{}, 1175 } 1176 wc, err := filesystem.InitWalkContext(t.Context(), config, config.ScanRoots) 1177 if err != nil { 1178 t.Fatalf("filesystem.InitializeWalkContext(%v): %v", config, err) 1179 } 1180 if err := wc.PrepareNewScan(".", fsys); err != nil { 1181 t.Fatalf("wc.UpdateScanRoot(%v): %v", config, err) 1182 } 1183 gotInv, gotStatus, err := filesystem.RunFS(t.Context(), config, wc) 1184 if err != nil { 1185 t.Fatalf("extractor.Run(%v): %v", ex, err) 1186 } 1187 1188 if !gotInv.IsEmpty() { 1189 t.Errorf("extractor.Run(%v): expected empty inventory, got %v", ex, gotInv) 1190 } 1191 1192 if diff := cmp.Diff(wantStatus, gotStatus); diff != "" { 1193 t.Errorf("extractor.Run(%v): unexpected status (-want +got):\n%s", ex, diff) 1194 } 1195 } 1196 1197 type fakeFileAPI struct { 1198 path string 1199 info fakefs.FakeFileInfo 1200 } 1201 1202 func (f fakeFileAPI) Path() string { return f.path } 1203 func (f fakeFileAPI) Stat() (fs.FileInfo, error) { 1204 return f.info, nil 1205 } 1206 1207 func TestIsInterestingExecutable(t *testing.T) { 1208 tests := []struct { 1209 name string 1210 path string 1211 mode fs.FileMode 1212 want bool 1213 wantWindows bool 1214 }{ 1215 { 1216 name: "user_executable", 1217 path: "some/path/a", 1218 mode: 0766, 1219 want: true, 1220 }, 1221 { 1222 name: "group_executable", 1223 path: "some/path/a", 1224 mode: 0676, 1225 want: true, 1226 }, 1227 { 1228 name: "other_executable", 1229 path: "some/path/a", 1230 mode: 0667, 1231 want: true, 1232 }, 1233 { 1234 name: "windows_exe", 1235 path: "some/path/a.exe", 1236 mode: 0666, 1237 want: true, 1238 }, 1239 { 1240 name: "windows_dll", 1241 path: "some/path/a.dll", 1242 mode: 0666, 1243 want: true, 1244 }, 1245 { 1246 name: "not executable bit set", 1247 path: "some/path/a", 1248 mode: 0640, 1249 want: false, 1250 wantWindows: false, 1251 }, 1252 { 1253 name: "executable_required", 1254 path: "some/path/a", 1255 mode: 0766, 1256 want: true, 1257 }, 1258 { 1259 name: "unwanted_extension", 1260 path: "some/path/a.html", 1261 mode: 0766, 1262 want: false, 1263 }, 1264 { 1265 name: "another_unwanted_extension", 1266 path: "some/path/a.txt", 1267 mode: 0766, 1268 want: false, 1269 }, 1270 { 1271 name: "python_script_without_execute_permissions", 1272 path: "some/path/a.py", 1273 mode: 0666, 1274 want: true, 1275 }, 1276 { 1277 name: "shell_script_without_execute_permissions", 1278 path: "some/path/a.sh", 1279 mode: 0666, 1280 want: true, 1281 }, 1282 { 1283 name: "shared_library_without_execute_permissions", 1284 path: "some/path/a.so", 1285 mode: 0666, 1286 want: true, 1287 }, 1288 { 1289 name: "binary_file_without_execute_permissions", 1290 path: "some/path/a.bin", 1291 mode: 0666, 1292 want: true, 1293 }, 1294 { 1295 name: "versioned_shared_library", 1296 path: "some/path/library.so.1", 1297 mode: 0666, 1298 want: true, 1299 }, 1300 { 1301 name: "versioned_shared_library_with_multiple_digits", 1302 path: "some/path/library.so.12", 1303 mode: 0666, 1304 want: true, 1305 }, 1306 { 1307 name: "not_a_versioned_shared_library", 1308 path: "some/path/library.so.foo", 1309 mode: 0666, 1310 want: false, 1311 }, 1312 } 1313 1314 for _, tt := range tests { 1315 t.Run(tt.name, func(t *testing.T) { 1316 got := filesystem.IsInterestingExecutable(fakeFileAPI{tt.path, fakefs.FakeFileInfo{ 1317 FileName: filepath.Base(tt.path), 1318 FileMode: tt.mode, 1319 }}) 1320 1321 want := tt.want 1322 // For Windows we don't check the executable bit on files. 1323 if runtime.GOOS == "windows" && !want { 1324 want = tt.wantWindows 1325 } 1326 1327 if got != want { 1328 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, got, want) 1329 } 1330 }) 1331 } 1332 }