1use socket_patch_core::crawlers::{
2 CrawledPackage, CrawlerOptions, Ecosystem, NpmCrawler, PythonCrawler,
3};
4use socket_patch_core::utils::purl::strip_purl_qualifiers;
5use std::collections::{HashMap, HashSet};
6use std::path::PathBuf;
7
8#[cfg(feature = "cargo")]
9use socket_patch_core::crawlers::CargoCrawler;
10use socket_patch_core::crawlers::RubyCrawler;
11#[cfg(feature = "golang")]
12use socket_patch_core::crawlers::GoCrawler;
13#[cfg(feature = "maven")]
14use socket_patch_core::crawlers::MavenCrawler;
15#[cfg(feature = "composer")]
16use socket_patch_core::crawlers::ComposerCrawler;
17#[cfg(feature = "nuget")]
18use socket_patch_core::crawlers::NuGetCrawler;
19#[cfg(feature = "deno")]
20use socket_patch_core::crawlers::DenoCrawler;
21
22#[cfg(feature = "maven")]
31fn maven_runtime_enabled() -> bool {
32 std::env::var("SOCKET_EXPERIMENTAL_MAVEN")
33 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
34 .unwrap_or(false)
35}
36
37#[cfg(feature = "maven")]
40fn warn_maven_disabled(skipped: usize) {
41 eprintln!(
42 "Warning: {} Maven patch(es) skipped — Maven support is experimental.",
43 skipped
44 );
45 eprintln!(" Maven patches corrupt jar sidecar checksums (sha1/md5).");
46 eprintln!(" Set SOCKET_EXPERIMENTAL_MAVEN=1 to enable at your own risk.");
47}
48
49#[cfg(feature = "nuget")]
59fn nuget_runtime_enabled() -> bool {
60 std::env::var("SOCKET_EXPERIMENTAL_NUGET")
61 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
62 .unwrap_or(false)
63}
64
65#[cfg(feature = "nuget")]
68fn warn_nuget_disabled(skipped: usize) {
69 eprintln!(
70 "Warning: {} NuGet patch(es) skipped — NuGet support is experimental.",
71 skipped
72 );
73 eprintln!(" NuGet patches corrupt the .nupkg.sha512 signature sidecar that");
74 eprintln!(" `dotnet restore` reads as tamper-evidence.");
75 eprintln!(" Set SOCKET_EXPERIMENTAL_NUGET=1 to enable at your own risk.");
76}
77
78pub fn partition_purls(
80 purls: &[String],
81 allowed_ecosystems: Option<&[String]>,
82) -> HashMap<Ecosystem, Vec<String>> {
83 let mut map: HashMap<Ecosystem, Vec<String>> = HashMap::new();
84
85 for purl in purls {
86 if let Some(eco) = Ecosystem::from_purl(purl) {
87 if let Some(allowed) = allowed_ecosystems {
88 if !allowed.iter().any(|a| a == eco.cli_name()) {
89 continue;
90 }
91 }
92 map.entry(eco).or_default().push(purl.clone());
93 }
94 }
95
96 map
97}
98
99pub async fn find_packages_for_purls(
103 partitioned: &HashMap<Ecosystem, Vec<String>>,
104 options: &CrawlerOptions,
105 silent: bool,
106) -> HashMap<String, PathBuf> {
107 let mut all_packages: HashMap<String, PathBuf> = HashMap::new();
108
109 if let Some(npm_purls) = partitioned.get(&Ecosystem::Npm) {
111 if !npm_purls.is_empty() {
112 let npm_crawler = NpmCrawler;
113 match npm_crawler.get_node_modules_paths(options).await {
114 Ok(nm_paths) => {
115 if (options.global || options.global_prefix.is_some()) && !silent {
116 if let Some(first) = nm_paths.first() {
117 println!("Using global npm packages at: {}", first.display());
118 }
119 }
120 for nm_path in &nm_paths {
121 match npm_crawler.find_by_purls(nm_path, npm_purls).await {
122 Ok(packages) => {
123 for (purl, pkg) in packages {
124 all_packages.entry(purl).or_insert(pkg.path);
125 }
126 }
127 Err(e) => {
128 if !silent {
129 eprintln!("Warning: Failed to scan {}: {}", nm_path.display(), e);
130 }
131 }
132 }
133 }
134 }
135 Err(e) => {
136 if !silent {
137 eprintln!("Failed to find npm packages: {e}");
138 }
139 }
140 }
141 }
142 }
143
144 if let Some(pypi_purls) = partitioned.get(&Ecosystem::Pypi) {
146 if !pypi_purls.is_empty() {
147 let python_crawler = PythonCrawler;
148 let base_pypi_purls: Vec<String> = pypi_purls
149 .iter()
150 .map(|p| strip_purl_qualifiers(p).to_string())
151 .collect::<HashSet<_>>()
152 .into_iter()
153 .collect();
154
155 match python_crawler.get_site_packages_paths(options).await {
156 Ok(sp_paths) => {
157 for sp_path in &sp_paths {
158 match python_crawler.find_by_purls(sp_path, &base_pypi_purls).await {
159 Ok(packages) => {
160 for (purl, pkg) in packages {
161 all_packages.entry(purl).or_insert(pkg.path);
162 }
163 }
164 Err(e) => {
165 if !silent {
166 eprintln!("Warning: Failed to scan {}: {}", sp_path.display(), e);
167 }
168 }
169 }
170 }
171 }
172 Err(e) => {
173 if !silent {
174 eprintln!("Failed to find Python packages: {e}");
175 }
176 }
177 }
178 }
179 }
180
181 #[cfg(feature = "cargo")]
183 if let Some(cargo_purls) = partitioned.get(&Ecosystem::Cargo) {
184 if !cargo_purls.is_empty() {
185 let cargo_crawler = CargoCrawler;
186 match cargo_crawler.get_crate_source_paths(options).await {
187 Ok(src_paths) => {
188 if (options.global || options.global_prefix.is_some()) && !silent {
189 if let Some(first) = src_paths.first() {
190 println!("Using cargo crate sources at: {}", first.display());
191 }
192 }
193 for src_path in &src_paths {
194 match cargo_crawler.find_by_purls(src_path, cargo_purls).await {
195 Ok(packages) => {
196 for (purl, pkg) in packages {
197 all_packages.entry(purl).or_insert(pkg.path);
198 }
199 }
200 Err(e) => {
201 if !silent {
202 eprintln!("Warning: Failed to scan {}: {}", src_path.display(), e);
203 }
204 }
205 }
206 }
207 }
208 Err(e) => {
209 if !silent {
210 eprintln!("Failed to find Cargo crates: {e}");
211 }
212 }
213 }
214 }
215 }
216
217 if let Some(gem_purls) = partitioned.get(&Ecosystem::Gem) {
219 if !gem_purls.is_empty() {
220 let ruby_crawler = RubyCrawler;
221 match ruby_crawler.get_gem_paths(options).await {
222 Ok(gem_paths) => {
223 if (options.global || options.global_prefix.is_some()) && !silent {
224 if let Some(first) = gem_paths.first() {
225 println!("Using ruby gem paths at: {}", first.display());
226 }
227 }
228 for gem_path in &gem_paths {
229 match ruby_crawler.find_by_purls(gem_path, gem_purls).await {
230 Ok(packages) => {
231 for (purl, pkg) in packages {
232 all_packages.entry(purl).or_insert(pkg.path);
233 }
234 }
235 Err(e) => {
236 if !silent {
237 eprintln!("Warning: Failed to scan {}: {}", gem_path.display(), e);
238 }
239 }
240 }
241 }
242 }
243 Err(e) => {
244 if !silent {
245 eprintln!("Failed to find Ruby gems: {e}");
246 }
247 }
248 }
249 }
250 }
251
252 #[cfg(feature = "golang")]
254 if let Some(golang_purls) = partitioned.get(&Ecosystem::Golang) {
255 if !golang_purls.is_empty() {
256 let go_crawler = GoCrawler;
257 match go_crawler.get_module_cache_paths(options).await {
258 Ok(cache_paths) => {
259 if (options.global || options.global_prefix.is_some()) && !silent {
260 if let Some(first) = cache_paths.first() {
261 println!("Using Go module cache at: {}", first.display());
262 }
263 }
264 for cache_path in &cache_paths {
265 match go_crawler.find_by_purls(cache_path, golang_purls).await {
266 Ok(packages) => {
267 for (purl, pkg) in packages {
268 all_packages.entry(purl).or_insert(pkg.path);
269 }
270 }
271 Err(e) => {
272 if !silent {
273 eprintln!("Warning: Failed to scan {}: {}", cache_path.display(), e);
274 }
275 }
276 }
277 }
278 }
279 Err(e) => {
280 if !silent {
281 eprintln!("Failed to find Go modules: {e}");
282 }
283 }
284 }
285 }
286 }
287
288 #[cfg(feature = "maven")]
290 if let Some(maven_purls) = partitioned.get(&Ecosystem::Maven) {
291 if !maven_purls.is_empty() && !maven_runtime_enabled() {
292 if !silent {
293 warn_maven_disabled(maven_purls.len());
294 }
295 } else if !maven_purls.is_empty() {
296 let maven_crawler = MavenCrawler;
297 match maven_crawler.get_maven_repo_paths(options).await {
298 Ok(repo_paths) => {
299 if (options.global || options.global_prefix.is_some()) && !silent {
300 if let Some(first) = repo_paths.first() {
301 println!("Using Maven repository at: {}", first.display());
302 }
303 }
304 for repo_path in &repo_paths {
305 match maven_crawler.find_by_purls(repo_path, maven_purls).await {
306 Ok(packages) => {
307 for (purl, pkg) in packages {
308 all_packages.entry(purl).or_insert(pkg.path);
309 }
310 }
311 Err(e) => {
312 if !silent {
313 eprintln!("Warning: Failed to scan {}: {}", repo_path.display(), e);
314 }
315 }
316 }
317 }
318 }
319 Err(e) => {
320 if !silent {
321 eprintln!("Failed to find Maven packages: {e}");
322 }
323 }
324 }
325 }
326 }
327
328 #[cfg(feature = "composer")]
330 if let Some(composer_purls) = partitioned.get(&Ecosystem::Composer) {
331 if !composer_purls.is_empty() {
332 let composer_crawler = ComposerCrawler;
333 match composer_crawler.get_vendor_paths(options).await {
334 Ok(vendor_paths) => {
335 if (options.global || options.global_prefix.is_some()) && !silent {
336 if let Some(first) = vendor_paths.first() {
337 println!("Using PHP vendor packages at: {}", first.display());
338 }
339 }
340 for vendor_path in &vendor_paths {
341 match composer_crawler.find_by_purls(vendor_path, composer_purls).await {
342 Ok(packages) => {
343 for (purl, pkg) in packages {
344 all_packages.entry(purl).or_insert(pkg.path);
345 }
346 }
347 Err(e) => {
348 if !silent {
349 eprintln!("Warning: Failed to scan {}: {}", vendor_path.display(), e);
350 }
351 }
352 }
353 }
354 }
355 Err(e) => {
356 if !silent {
357 eprintln!("Failed to find PHP packages: {e}");
358 }
359 }
360 }
361 }
362 }
363
364 #[cfg(feature = "nuget")]
366 if let Some(nuget_purls) = partitioned.get(&Ecosystem::Nuget) {
367 if !nuget_purls.is_empty() && !nuget_runtime_enabled() {
368 if !silent {
369 warn_nuget_disabled(nuget_purls.len());
370 }
371 } else if !nuget_purls.is_empty() {
372 let nuget_crawler = NuGetCrawler;
373 match nuget_crawler.get_nuget_package_paths(options).await {
374 Ok(pkg_paths) => {
375 if (options.global || options.global_prefix.is_some()) && !silent {
376 if let Some(first) = pkg_paths.first() {
377 println!("Using NuGet packages at: {}", first.display());
378 }
379 }
380 for pkg_path in &pkg_paths {
381 match nuget_crawler.find_by_purls(pkg_path, nuget_purls).await {
382 Ok(packages) => {
383 for (purl, pkg) in packages {
384 all_packages.entry(purl).or_insert(pkg.path);
385 }
386 }
387 Err(e) => {
388 if !silent {
389 eprintln!("Warning: Failed to scan {}: {}", pkg_path.display(), e);
390 }
391 }
392 }
393 }
394 }
395 Err(e) => {
396 if !silent {
397 eprintln!("Failed to find NuGet packages: {e}");
398 }
399 }
400 }
401 }
402 }
403
404 #[cfg(feature = "deno")]
406 if let Some(deno_purls) = partitioned.get(&Ecosystem::Deno) {
407 if !deno_purls.is_empty() {
408 let deno_crawler = DenoCrawler;
409 match deno_crawler.get_jsr_cache_paths(options).await {
410 Ok(cache_paths) => {
411 if (options.global || options.global_prefix.is_some()) && !silent {
412 if let Some(first) = cache_paths.first() {
413 println!("Using Deno JSR cache at: {}", first.display());
414 }
415 }
416 for cache_path in &cache_paths {
417 match deno_crawler.find_by_purls(cache_path, deno_purls).await {
418 Ok(packages) => {
419 for (purl, pkg) in packages {
420 all_packages.entry(purl).or_insert(pkg.path);
421 }
422 }
423 Err(e) => {
424 if !silent {
425 eprintln!("Warning: Failed to scan {}: {}", cache_path.display(), e);
426 }
427 }
428 }
429 }
430 }
431 Err(e) => {
432 if !silent {
433 eprintln!("Failed to find Deno JSR packages: {e}");
434 }
435 }
436 }
437 }
438 }
439
440 all_packages
441}
442
443pub async fn crawl_all_ecosystems(
445 options: &CrawlerOptions,
446) -> (Vec<CrawledPackage>, HashMap<Ecosystem, usize>) {
447 let mut all_packages = Vec::new();
448 let mut counts: HashMap<Ecosystem, usize> = HashMap::new();
449
450 let npm_crawler = NpmCrawler;
451 let npm_packages = npm_crawler.crawl_all(options).await;
452 counts.insert(Ecosystem::Npm, npm_packages.len());
453 all_packages.extend(npm_packages);
454
455 let python_crawler = PythonCrawler;
456 let python_packages = python_crawler.crawl_all(options).await;
457 counts.insert(Ecosystem::Pypi, python_packages.len());
458 all_packages.extend(python_packages);
459
460 #[cfg(feature = "cargo")]
461 {
462 let cargo_crawler = CargoCrawler;
463 let cargo_packages = cargo_crawler.crawl_all(options).await;
464 counts.insert(Ecosystem::Cargo, cargo_packages.len());
465 all_packages.extend(cargo_packages);
466 }
467
468 {
469 let ruby_crawler = RubyCrawler;
470 let gem_packages = ruby_crawler.crawl_all(options).await;
471 counts.insert(Ecosystem::Gem, gem_packages.len());
472 all_packages.extend(gem_packages);
473 }
474
475 #[cfg(feature = "golang")]
476 {
477 let go_crawler = GoCrawler;
478 let go_packages = go_crawler.crawl_all(options).await;
479 counts.insert(Ecosystem::Golang, go_packages.len());
480 all_packages.extend(go_packages);
481 }
482
483 #[cfg(feature = "maven")]
484 if maven_runtime_enabled() {
485 let maven_crawler = MavenCrawler;
489 let maven_packages = maven_crawler.crawl_all(options).await;
490 counts.insert(Ecosystem::Maven, maven_packages.len());
491 all_packages.extend(maven_packages);
492 }
493
494 #[cfg(feature = "composer")]
495 {
496 let composer_crawler = ComposerCrawler;
497 let composer_packages = composer_crawler.crawl_all(options).await;
498 counts.insert(Ecosystem::Composer, composer_packages.len());
499 all_packages.extend(composer_packages);
500 }
501
502 #[cfg(feature = "nuget")]
503 if nuget_runtime_enabled() {
504 let nuget_crawler = NuGetCrawler;
506 let nuget_packages = nuget_crawler.crawl_all(options).await;
507 counts.insert(Ecosystem::Nuget, nuget_packages.len());
508 all_packages.extend(nuget_packages);
509 }
510
511 #[cfg(feature = "deno")]
512 {
513 let deno_crawler = DenoCrawler;
514 let deno_packages = deno_crawler.crawl_all(options).await;
515 counts.insert(Ecosystem::Deno, deno_packages.len());
516 all_packages.extend(deno_packages);
517 }
518
519 (all_packages, counts)
520}
521
522pub async fn find_packages_for_rollback(
526 partitioned: &HashMap<Ecosystem, Vec<String>>,
527 options: &CrawlerOptions,
528 silent: bool,
529) -> HashMap<String, PathBuf> {
530 let mut all_packages: HashMap<String, PathBuf> = HashMap::new();
531
532 if let Some(npm_purls) = partitioned.get(&Ecosystem::Npm) {
534 if !npm_purls.is_empty() {
535 let npm_crawler = NpmCrawler;
536 match npm_crawler.get_node_modules_paths(options).await {
537 Ok(nm_paths) => {
538 if (options.global || options.global_prefix.is_some()) && !silent {
539 if let Some(first) = nm_paths.first() {
540 println!("Using global npm packages at: {}", first.display());
541 }
542 }
543 for nm_path in &nm_paths {
544 match npm_crawler.find_by_purls(nm_path, npm_purls).await {
545 Ok(packages) => {
546 for (purl, pkg) in packages {
547 all_packages.entry(purl).or_insert(pkg.path);
548 }
549 }
550 Err(e) => {
551 if !silent {
552 eprintln!("Warning: Failed to scan {}: {}", nm_path.display(), e);
553 }
554 }
555 }
556 }
557 }
558 Err(e) => {
559 if !silent {
560 eprintln!("Failed to find npm packages: {e}");
561 }
562 }
563 }
564 }
565 }
566
567 if let Some(pypi_purls) = partitioned.get(&Ecosystem::Pypi) {
569 if !pypi_purls.is_empty() {
570 let python_crawler = PythonCrawler;
571 let base_pypi_purls: Vec<String> = pypi_purls
572 .iter()
573 .map(|p| strip_purl_qualifiers(p).to_string())
574 .collect::<HashSet<_>>()
575 .into_iter()
576 .collect();
577
578 if let Ok(sp_paths) = python_crawler.get_site_packages_paths(options).await {
579 for sp_path in &sp_paths {
580 match python_crawler.find_by_purls(sp_path, &base_pypi_purls).await {
581 Ok(packages) => {
582 for (base_purl, pkg) in packages {
583 for qualified_purl in pypi_purls {
584 if strip_purl_qualifiers(qualified_purl) == base_purl
585 && !all_packages.contains_key(qualified_purl)
586 {
587 all_packages
588 .insert(qualified_purl.clone(), pkg.path.clone());
589 }
590 }
591 }
592 }
593 Err(e) => {
594 if !silent {
595 eprintln!("Warning: Failed to scan {}: {}", sp_path.display(), e);
596 }
597 }
598 }
599 }
600 }
601 }
602 }
603
604 #[cfg(feature = "cargo")]
606 if let Some(cargo_purls) = partitioned.get(&Ecosystem::Cargo) {
607 if !cargo_purls.is_empty() {
608 let cargo_crawler = CargoCrawler;
609 match cargo_crawler.get_crate_source_paths(options).await {
610 Ok(src_paths) => {
611 if (options.global || options.global_prefix.is_some()) && !silent {
612 if let Some(first) = src_paths.first() {
613 println!("Using cargo crate sources at: {}", first.display());
614 }
615 }
616 for src_path in &src_paths {
617 match cargo_crawler.find_by_purls(src_path, cargo_purls).await {
618 Ok(packages) => {
619 for (purl, pkg) in packages {
620 all_packages.entry(purl).or_insert(pkg.path);
621 }
622 }
623 Err(e) => {
624 if !silent {
625 eprintln!("Warning: Failed to scan {}: {}", src_path.display(), e);
626 }
627 }
628 }
629 }
630 }
631 Err(e) => {
632 if !silent {
633 eprintln!("Failed to find Cargo crates: {e}");
634 }
635 }
636 }
637 }
638 }
639
640 if let Some(gem_purls) = partitioned.get(&Ecosystem::Gem) {
642 if !gem_purls.is_empty() {
643 let ruby_crawler = RubyCrawler;
644 match ruby_crawler.get_gem_paths(options).await {
645 Ok(gem_paths) => {
646 if (options.global || options.global_prefix.is_some()) && !silent {
647 if let Some(first) = gem_paths.first() {
648 println!("Using ruby gem paths at: {}", first.display());
649 }
650 }
651 for gem_path in &gem_paths {
652 match ruby_crawler.find_by_purls(gem_path, gem_purls).await {
653 Ok(packages) => {
654 for (purl, pkg) in packages {
655 all_packages.entry(purl).or_insert(pkg.path);
656 }
657 }
658 Err(e) => {
659 if !silent {
660 eprintln!("Warning: Failed to scan {}: {}", gem_path.display(), e);
661 }
662 }
663 }
664 }
665 }
666 Err(e) => {
667 if !silent {
668 eprintln!("Failed to find Ruby gems: {e}");
669 }
670 }
671 }
672 }
673 }
674
675 #[cfg(feature = "golang")]
677 if let Some(golang_purls) = partitioned.get(&Ecosystem::Golang) {
678 if !golang_purls.is_empty() {
679 let go_crawler = GoCrawler;
680 match go_crawler.get_module_cache_paths(options).await {
681 Ok(cache_paths) => {
682 if (options.global || options.global_prefix.is_some()) && !silent {
683 if let Some(first) = cache_paths.first() {
684 println!("Using Go module cache at: {}", first.display());
685 }
686 }
687 for cache_path in &cache_paths {
688 match go_crawler.find_by_purls(cache_path, golang_purls).await {
689 Ok(packages) => {
690 for (purl, pkg) in packages {
691 all_packages.entry(purl).or_insert(pkg.path);
692 }
693 }
694 Err(e) => {
695 if !silent {
696 eprintln!("Warning: Failed to scan {}: {}", cache_path.display(), e);
697 }
698 }
699 }
700 }
701 }
702 Err(e) => {
703 if !silent {
704 eprintln!("Failed to find Go modules: {e}");
705 }
706 }
707 }
708 }
709 }
710
711 #[cfg(feature = "maven")]
713 if let Some(maven_purls) = partitioned.get(&Ecosystem::Maven) {
714 if !maven_purls.is_empty() && !maven_runtime_enabled() {
715 if !silent {
716 warn_maven_disabled(maven_purls.len());
717 }
718 } else if !maven_purls.is_empty() {
719 let maven_crawler = MavenCrawler;
720 match maven_crawler.get_maven_repo_paths(options).await {
721 Ok(repo_paths) => {
722 if (options.global || options.global_prefix.is_some()) && !silent {
723 if let Some(first) = repo_paths.first() {
724 println!("Using Maven repository at: {}", first.display());
725 }
726 }
727 for repo_path in &repo_paths {
728 match maven_crawler.find_by_purls(repo_path, maven_purls).await {
729 Ok(packages) => {
730 for (purl, pkg) in packages {
731 all_packages.entry(purl).or_insert(pkg.path);
732 }
733 }
734 Err(e) => {
735 if !silent {
736 eprintln!("Warning: Failed to scan {}: {}", repo_path.display(), e);
737 }
738 }
739 }
740 }
741 }
742 Err(e) => {
743 if !silent {
744 eprintln!("Failed to find Maven packages: {e}");
745 }
746 }
747 }
748 }
749 }
750
751 #[cfg(feature = "composer")]
753 if let Some(composer_purls) = partitioned.get(&Ecosystem::Composer) {
754 if !composer_purls.is_empty() {
755 let composer_crawler = ComposerCrawler;
756 match composer_crawler.get_vendor_paths(options).await {
757 Ok(vendor_paths) => {
758 if (options.global || options.global_prefix.is_some()) && !silent {
759 if let Some(first) = vendor_paths.first() {
760 println!("Using PHP vendor packages at: {}", first.display());
761 }
762 }
763 for vendor_path in &vendor_paths {
764 match composer_crawler.find_by_purls(vendor_path, composer_purls).await {
765 Ok(packages) => {
766 for (purl, pkg) in packages {
767 all_packages.entry(purl).or_insert(pkg.path);
768 }
769 }
770 Err(e) => {
771 if !silent {
772 eprintln!("Warning: Failed to scan {}: {}", vendor_path.display(), e);
773 }
774 }
775 }
776 }
777 }
778 Err(e) => {
779 if !silent {
780 eprintln!("Failed to find PHP packages: {e}");
781 }
782 }
783 }
784 }
785 }
786
787 #[cfg(feature = "nuget")]
789 if let Some(nuget_purls) = partitioned.get(&Ecosystem::Nuget) {
790 if !nuget_purls.is_empty() && !nuget_runtime_enabled() {
791 if !silent {
792 warn_nuget_disabled(nuget_purls.len());
793 }
794 } else if !nuget_purls.is_empty() {
795 let nuget_crawler = NuGetCrawler;
796 match nuget_crawler.get_nuget_package_paths(options).await {
797 Ok(pkg_paths) => {
798 if (options.global || options.global_prefix.is_some()) && !silent {
799 if let Some(first) = pkg_paths.first() {
800 println!("Using NuGet packages at: {}", first.display());
801 }
802 }
803 for pkg_path in &pkg_paths {
804 match nuget_crawler.find_by_purls(pkg_path, nuget_purls).await {
805 Ok(packages) => {
806 for (purl, pkg) in packages {
807 all_packages.entry(purl).or_insert(pkg.path);
808 }
809 }
810 Err(e) => {
811 if !silent {
812 eprintln!("Warning: Failed to scan {}: {}", pkg_path.display(), e);
813 }
814 }
815 }
816 }
817 }
818 Err(e) => {
819 if !silent {
820 eprintln!("Failed to find NuGet packages: {e}");
821 }
822 }
823 }
824 }
825 }
826
827 all_packages
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 #[test]
835 fn partition_purls_no_filter_single_npm() {
836 let purls = vec!["pkg:npm/foo@1.0".to_string()];
837 let map = partition_purls(&purls, None);
838 assert_eq!(map.len(), 1);
839 assert_eq!(
840 map.get(&Ecosystem::Npm),
841 Some(&vec!["pkg:npm/foo@1.0".to_string()])
842 );
843 }
844
845 #[test]
846 fn partition_purls_no_filter_mixed_ecosystems() {
847 let purls = vec![
848 "pkg:npm/foo@1.0".to_string(),
849 "pkg:pypi/bar@2.0".to_string(),
850 "pkg:cargo/baz@3.0".to_string(),
851 ];
852 let map = partition_purls(&purls, None);
853 assert_eq!(map.len(), 3);
854 assert_eq!(
855 map.get(&Ecosystem::Npm),
856 Some(&vec!["pkg:npm/foo@1.0".to_string()])
857 );
858 assert_eq!(
859 map.get(&Ecosystem::Pypi),
860 Some(&vec!["pkg:pypi/bar@2.0".to_string()])
861 );
862 #[cfg(feature = "cargo")]
863 assert_eq!(
864 map.get(&Ecosystem::Cargo),
865 Some(&vec!["pkg:cargo/baz@3.0".to_string()])
866 );
867 }
868
869 #[test]
870 fn partition_purls_no_filter_empty_input() {
871 let purls: Vec<String> = Vec::new();
872 let map = partition_purls(&purls, None);
873 assert!(map.is_empty());
874 }
875
876 #[test]
877 fn partition_purls_no_filter_duplicate_purls_preserved() {
878 let purls = vec![
879 "pkg:npm/foo@1.0".to_string(),
880 "pkg:npm/foo@1.0".to_string(),
881 ];
882 let map = partition_purls(&purls, None);
883 assert_eq!(map.len(), 1);
884 assert_eq!(
885 map.get(&Ecosystem::Npm),
886 Some(&vec![
887 "pkg:npm/foo@1.0".to_string(),
888 "pkg:npm/foo@1.0".to_string(),
889 ])
890 );
891 }
892
893 #[test]
894 fn partition_purls_no_filter_unknown_ecosystem_dropped() {
895 let purls = vec!["pkg:weirdo/x@1".to_string()];
896 let map = partition_purls(&purls, None);
897 assert!(map.is_empty());
898 }
899
900 #[test]
901 fn partition_purls_allow_list_excludes_one() {
902 let purls = vec![
903 "pkg:npm/foo@1.0".to_string(),
904 "pkg:pypi/bar@2.0".to_string(),
905 ];
906 let allowed = vec!["npm".to_string()];
907 let map = partition_purls(&purls, Some(allowed.as_slice()));
908 assert_eq!(map.len(), 1);
909 assert_eq!(
910 map.get(&Ecosystem::Npm),
911 Some(&vec!["pkg:npm/foo@1.0".to_string()])
912 );
913 assert!(!map.contains_key(&Ecosystem::Pypi));
914 }
915
916 #[test]
917 fn partition_purls_allow_list_matches_none() {
918 let purls = vec!["pkg:npm/foo@1.0".to_string()];
919 let allowed = vec!["pypi".to_string()];
920 let map = partition_purls(&purls, Some(allowed.as_slice()));
921 assert!(map.is_empty());
922 }
923
924 #[test]
925 fn partition_purls_allow_list_matches_all() {
926 let purls = vec![
927 "pkg:npm/foo@1.0".to_string(),
928 "pkg:pypi/bar@2.0".to_string(),
929 ];
930 let allowed = vec!["npm".to_string(), "pypi".to_string()];
931 let map = partition_purls(&purls, Some(allowed.as_slice()));
932 assert_eq!(map.len(), 2);
933 assert_eq!(
934 map.get(&Ecosystem::Npm),
935 Some(&vec!["pkg:npm/foo@1.0".to_string()])
936 );
937 assert_eq!(
938 map.get(&Ecosystem::Pypi),
939 Some(&vec!["pkg:pypi/bar@2.0".to_string()])
940 );
941 }
942
943 #[test]
944 fn partition_purls_empty_allow_list_matches_nothing() {
945 let purls = vec![
946 "pkg:npm/foo@1.0".to_string(),
947 "pkg:pypi/bar@2.0".to_string(),
948 ];
949 let allowed: Vec<String> = Vec::new();
950 let map = partition_purls(&purls, Some(allowed.as_slice()));
951 assert!(map.is_empty());
952 }
953}