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 env_truthy("SOCKET_EXPERIMENTAL_MAVEN")
33}
34
35#[cfg(feature = "maven")]
36fn warn_maven_disabled(skipped: usize) {
37 eprintln!(
38 "Warning: {} Maven patch(es) skipped — Maven support is experimental.",
39 skipped
40 );
41 eprintln!(" Maven patches corrupt jar sidecar checksums (sha1/md5).");
42 eprintln!(" Set SOCKET_EXPERIMENTAL_MAVEN=1 to enable at your own risk.");
43}
44
45#[cfg(feature = "nuget")]
53fn nuget_runtime_enabled() -> bool {
54 env_truthy("SOCKET_EXPERIMENTAL_NUGET")
55}
56
57#[cfg(feature = "nuget")]
58fn warn_nuget_disabled(skipped: usize) {
59 eprintln!(
60 "Warning: {} NuGet patch(es) skipped — NuGet support is experimental.",
61 skipped
62 );
63 eprintln!(" NuGet patches corrupt the .nupkg.sha512 signature sidecar that");
64 eprintln!(" `dotnet restore` reads as tamper-evidence.");
65 eprintln!(" Set SOCKET_EXPERIMENTAL_NUGET=1 to enable at your own risk.");
66}
67
68#[cfg(any(feature = "maven", feature = "nuget"))]
69fn env_truthy(name: &str) -> bool {
70 std::env::var(name)
71 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
72 .unwrap_or(false)
73}
74
75pub fn partition_purls(
77 purls: &[String],
78 allowed_ecosystems: Option<&[String]>,
79) -> HashMap<Ecosystem, Vec<String>> {
80 let mut map: HashMap<Ecosystem, Vec<String>> = HashMap::new();
81 for purl in purls {
82 if let Some(eco) = Ecosystem::from_purl(purl) {
83 if let Some(allowed) = allowed_ecosystems {
84 if !allowed.iter().any(|a| a == eco.cli_name()) {
85 continue;
86 }
87 }
88 map.entry(eco).or_default().push(purl.clone());
89 }
90 }
91 map
92}
93
94macro_rules! scan_ecosystem {
102 (
103 out = $out:ident,
104 partitioned = $partitioned:expr,
105 eco = $eco:expr,
106 options = $options:expr,
107 silent = $silent:expr,
108 crawler = $crawler:expr,
109 get_paths = $get_paths:ident,
110 using_label = $using_label:expr,
111 err_label = $err_label:expr,
112 purls_override = $purls_override:expr,
113 on_match = $on_match:expr $(,)?
114 ) => {{
115 if let Some(purls) = $partitioned.get(&$eco) {
116 if !purls.is_empty() {
117 let crawler = $crawler;
118 let purls_to_use: Vec<String> = $purls_override(purls);
119 match crawler.$get_paths($options).await {
120 Ok(paths) => {
121 let using: &str = $using_label;
122 if !using.is_empty()
123 && ($options.global || $options.global_prefix.is_some())
124 && !$silent
125 {
126 if let Some(first) = paths.first() {
127 println!("Using {} at: {}", using, first.display());
128 }
129 }
130 for path in &paths {
131 match crawler.find_by_purls(path, &purls_to_use).await {
132 Ok(packages) => {
133 $on_match(&mut $out, purls, packages);
134 }
135 Err(e) => {
136 if !$silent {
137 eprintln!(
138 "Warning: Failed to scan {}: {}",
139 path.display(),
140 e
141 );
142 }
143 }
144 }
145 }
146 }
147 Err(e) => {
148 if !$silent {
149 eprintln!("Failed to find {}: {}", $err_label, e);
150 }
151 }
152 }
153 }
154 }
155 }};
156}
157
158type MergeFn =
162 fn(&mut HashMap<String, PathBuf>, &[String], HashMap<String, CrawledPackage>);
163
164fn merge_first_wins(
166 out: &mut HashMap<String, PathBuf>,
167 _purls: &[String],
168 packages: HashMap<String, socket_patch_core::crawlers::CrawledPackage>,
169) {
170 for (purl, pkg) in packages {
171 out.entry(purl).or_insert(pkg.path);
172 }
173}
174
175fn merge_qualified(
182 out: &mut HashMap<String, PathBuf>,
183 purls: &[String],
184 packages: HashMap<String, socket_patch_core::crawlers::CrawledPackage>,
185) {
186 for (base_purl, pkg) in packages {
187 for qualified in purls {
188 if strip_purl_qualifiers(qualified) == base_purl
189 && !out.contains_key(qualified)
190 {
191 out.insert(qualified.clone(), pkg.path.clone());
192 }
193 }
194 }
195}
196
197fn dedup_qualified_purls(purls: &[String]) -> Vec<String> {
201 purls
202 .iter()
203 .map(|p| strip_purl_qualifiers(p).to_string())
204 .collect::<HashSet<_>>()
205 .into_iter()
206 .collect()
207}
208
209fn passthrough_purls(purls: &[String]) -> Vec<String> {
210 purls.to_vec()
211}
212
213async fn dispatch_find(
222 partitioned: &HashMap<Ecosystem, Vec<String>>,
223 options: &CrawlerOptions,
224 silent: bool,
225 variant_merge: MergeFn,
226) -> HashMap<String, PathBuf> {
227 let mut out: HashMap<String, PathBuf> = HashMap::new();
228
229 scan_ecosystem!(
230 out = out,
231 partitioned = partitioned,
232 eco = Ecosystem::Npm,
233 options = options,
234 silent = silent,
235 crawler = NpmCrawler,
236 get_paths = get_node_modules_paths,
237 using_label = "global npm packages",
238 err_label = "npm packages",
239 purls_override = passthrough_purls,
240 on_match = merge_first_wins,
241 );
242
243 scan_ecosystem!(
244 out = out,
245 partitioned = partitioned,
246 eco = Ecosystem::Pypi,
247 options = options,
248 silent = silent,
249 crawler = PythonCrawler,
250 get_paths = get_site_packages_paths,
251 using_label = "",
252 err_label = "Python packages",
253 purls_override = dedup_qualified_purls,
254 on_match = variant_merge,
255 );
256
257 #[cfg(feature = "cargo")]
258 scan_ecosystem!(
259 out = out,
260 partitioned = partitioned,
261 eco = Ecosystem::Cargo,
262 options = options,
263 silent = silent,
264 crawler = CargoCrawler,
265 get_paths = get_crate_source_paths,
266 using_label = "cargo crate sources",
267 err_label = "Cargo crates",
268 purls_override = passthrough_purls,
269 on_match = merge_first_wins,
270 );
271
272 scan_ecosystem!(
273 out = out,
274 partitioned = partitioned,
275 eco = Ecosystem::Gem,
276 options = options,
277 silent = silent,
278 crawler = RubyCrawler,
279 get_paths = get_gem_paths,
280 using_label = "ruby gem paths",
281 err_label = "Ruby gems",
282 purls_override = dedup_qualified_purls,
286 on_match = variant_merge,
287 );
288
289 #[cfg(feature = "golang")]
290 scan_ecosystem!(
291 out = out,
292 partitioned = partitioned,
293 eco = Ecosystem::Golang,
294 options = options,
295 silent = silent,
296 crawler = GoCrawler,
297 get_paths = get_module_cache_paths,
298 using_label = "Go module cache",
299 err_label = "Go modules",
300 purls_override = passthrough_purls,
301 on_match = merge_first_wins,
302 );
303
304 #[cfg(feature = "maven")]
305 if let Some(maven_purls) = partitioned.get(&Ecosystem::Maven) {
306 if !maven_purls.is_empty() && !maven_runtime_enabled() {
307 if !silent {
308 warn_maven_disabled(maven_purls.len());
309 }
310 } else {
311 scan_ecosystem!(
312 out = out,
313 partitioned = partitioned,
314 eco = Ecosystem::Maven,
315 options = options,
316 silent = silent,
317 crawler = MavenCrawler,
318 get_paths = get_maven_repo_paths,
319 using_label = "Maven repository",
320 err_label = "Maven packages",
321 purls_override = dedup_qualified_purls,
326 on_match = variant_merge,
327 );
328 }
329 }
330
331 #[cfg(feature = "composer")]
332 scan_ecosystem!(
333 out = out,
334 partitioned = partitioned,
335 eco = Ecosystem::Composer,
336 options = options,
337 silent = silent,
338 crawler = ComposerCrawler,
339 get_paths = get_vendor_paths,
340 using_label = "PHP vendor packages",
341 err_label = "PHP packages",
342 purls_override = passthrough_purls,
343 on_match = merge_first_wins,
344 );
345
346 #[cfg(feature = "nuget")]
347 if let Some(nuget_purls) = partitioned.get(&Ecosystem::Nuget) {
348 if !nuget_purls.is_empty() && !nuget_runtime_enabled() {
349 if !silent {
350 warn_nuget_disabled(nuget_purls.len());
351 }
352 } else {
353 scan_ecosystem!(
354 out = out,
355 partitioned = partitioned,
356 eco = Ecosystem::Nuget,
357 options = options,
358 silent = silent,
359 crawler = NuGetCrawler,
360 get_paths = get_nuget_package_paths,
361 using_label = "NuGet packages",
362 err_label = "NuGet packages",
363 purls_override = passthrough_purls,
364 on_match = merge_first_wins,
365 );
366 }
367 }
368
369 #[cfg(feature = "deno")]
370 scan_ecosystem!(
371 out = out,
372 partitioned = partitioned,
373 eco = Ecosystem::Deno,
374 options = options,
375 silent = silent,
376 crawler = DenoCrawler,
377 get_paths = get_jsr_cache_paths,
378 using_label = "Deno JSR cache",
379 err_label = "Deno JSR packages",
380 purls_override = passthrough_purls,
381 on_match = merge_first_wins,
382 );
383
384 out
385}
386
387pub async fn find_packages_for_purls(
391 partitioned: &HashMap<Ecosystem, Vec<String>>,
392 options: &CrawlerOptions,
393 silent: bool,
394) -> HashMap<String, PathBuf> {
395 dispatch_find(partitioned, options, silent, merge_first_wins).await
396}
397
398pub async fn find_packages_for_rollback(
403 partitioned: &HashMap<Ecosystem, Vec<String>>,
404 options: &CrawlerOptions,
405 silent: bool,
406) -> HashMap<String, PathBuf> {
407 dispatch_find(partitioned, options, silent, merge_qualified).await
408}
409
410pub async fn crawl_all_ecosystems(
412 options: &CrawlerOptions,
413) -> (Vec<CrawledPackage>, HashMap<Ecosystem, usize>) {
414 let mut all_packages = Vec::new();
415 let mut counts: HashMap<Ecosystem, usize> = HashMap::new();
416
417 macro_rules! crawl {
418 ($eco:expr, $crawler:expr) => {{
419 let pkgs = $crawler.crawl_all(options).await;
420 counts.insert($eco, pkgs.len());
421 all_packages.extend(pkgs);
422 }};
423 }
424
425 crawl!(Ecosystem::Npm, NpmCrawler);
426 crawl!(Ecosystem::Pypi, PythonCrawler);
427 #[cfg(feature = "cargo")]
428 crawl!(Ecosystem::Cargo, CargoCrawler);
429 crawl!(Ecosystem::Gem, RubyCrawler);
430 #[cfg(feature = "golang")]
431 crawl!(Ecosystem::Golang, GoCrawler);
432 #[cfg(feature = "maven")]
433 if maven_runtime_enabled() {
434 crawl!(Ecosystem::Maven, MavenCrawler);
438 }
439 #[cfg(feature = "composer")]
440 crawl!(Ecosystem::Composer, ComposerCrawler);
441 #[cfg(feature = "nuget")]
442 if nuget_runtime_enabled() {
443 crawl!(Ecosystem::Nuget, NuGetCrawler);
444 }
445 #[cfg(feature = "deno")]
446 crawl!(Ecosystem::Deno, DenoCrawler);
447
448 (all_packages, counts)
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 fn pkg(purl: &str, path: &str) -> CrawledPackage {
458 CrawledPackage {
459 name: "n".to_string(),
460 version: "v".to_string(),
461 namespace: None,
462 purl: purl.to_string(),
463 path: PathBuf::from(path),
464 }
465 }
466
467 fn packages(entries: &[(&str, &str)]) -> HashMap<String, CrawledPackage> {
468 entries
469 .iter()
470 .map(|(purl, path)| (purl.to_string(), pkg(purl, path)))
471 .collect()
472 }
473
474 #[test]
477 fn merge_first_wins_inserts_crawler_keyed_purls() {
478 let mut out: HashMap<String, PathBuf> = HashMap::new();
479 merge_first_wins(
480 &mut out,
481 &[],
482 packages(&[("pkg:npm/foo@1.0", "/a"), ("pkg:npm/bar@2.0", "/b")]),
483 );
484 assert_eq!(out.len(), 2);
485 assert_eq!(out.get("pkg:npm/foo@1.0"), Some(&PathBuf::from("/a")));
486 assert_eq!(out.get("pkg:npm/bar@2.0"), Some(&PathBuf::from("/b")));
487 }
488
489 #[test]
490 fn merge_first_wins_keeps_first_path_across_calls() {
491 let mut out: HashMap<String, PathBuf> = HashMap::new();
494 merge_first_wins(&mut out, &[], packages(&[("pkg:npm/foo@1.0", "/first")]));
495 merge_first_wins(&mut out, &[], packages(&[("pkg:npm/foo@1.0", "/second")]));
496 assert_eq!(out.get("pkg:npm/foo@1.0"), Some(&PathBuf::from("/first")));
497 }
498
499 #[test]
500 fn merge_first_wins_ignores_purls_arg() {
501 let mut out: HashMap<String, PathBuf> = HashMap::new();
504 let unrelated = vec!["pkg:npm/unrelated@9.9".to_string()];
505 merge_first_wins(&mut out, &unrelated, packages(&[("pkg:npm/foo@1.0", "/a")]));
506 assert_eq!(out.len(), 1);
507 assert!(out.contains_key("pkg:npm/foo@1.0"));
508 }
509
510 #[test]
513 fn merge_qualified_fans_base_out_to_every_variant() {
514 let mut out: HashMap<String, PathBuf> = HashMap::new();
518 let qualified = vec![
519 "pkg:pypi/requests@2.28.0?artifact_id=wheel".to_string(),
520 "pkg:pypi/requests@2.28.0?artifact_id=sdist".to_string(),
521 ];
522 merge_qualified(
523 &mut out,
524 &qualified,
525 packages(&[("pkg:pypi/requests@2.28.0", "/site-packages")]),
526 );
527 assert_eq!(out.len(), 2);
528 assert_eq!(
529 out.get("pkg:pypi/requests@2.28.0?artifact_id=wheel"),
530 Some(&PathBuf::from("/site-packages"))
531 );
532 assert_eq!(
533 out.get("pkg:pypi/requests@2.28.0?artifact_id=sdist"),
534 Some(&PathBuf::from("/site-packages"))
535 );
536 }
537
538 #[test]
539 fn merge_qualified_matches_bare_base_identifier() {
540 let mut out: HashMap<String, PathBuf> = HashMap::new();
543 let purls = vec!["pkg:pypi/requests@2.28.0".to_string()];
544 merge_qualified(
545 &mut out,
546 &purls,
547 packages(&[("pkg:pypi/requests@2.28.0", "/sp")]),
548 );
549 assert_eq!(out.get("pkg:pypi/requests@2.28.0"), Some(&PathBuf::from("/sp")));
550 }
551
552 #[test]
553 fn merge_qualified_does_not_cross_versions() {
554 let mut out: HashMap<String, PathBuf> = HashMap::new();
557 let purls = vec!["pkg:pypi/requests@2.29.0?artifact_id=wheel".to_string()];
558 merge_qualified(
559 &mut out,
560 &purls,
561 packages(&[("pkg:pypi/requests@2.28.0", "/sp")]),
562 );
563 assert!(out.is_empty());
564 }
565
566 #[test]
567 fn merge_qualified_keeps_first_path_per_qualified_key() {
568 let mut out: HashMap<String, PathBuf> = HashMap::new();
571 let purls = vec!["pkg:gem/nokogiri@1.16.5?platform=arm64-darwin".to_string()];
572 merge_qualified(&mut out, &purls, packages(&[("pkg:gem/nokogiri@1.16.5", "/first")]));
573 merge_qualified(&mut out, &purls, packages(&[("pkg:gem/nokogiri@1.16.5", "/second")]));
574 assert_eq!(
575 out.get("pkg:gem/nokogiri@1.16.5?platform=arm64-darwin"),
576 Some(&PathBuf::from("/first"))
577 );
578 }
579
580 #[test]
583 fn dedup_qualified_purls_strips_and_dedupes() {
584 let purls = vec![
585 "pkg:pypi/requests@2.28.0?artifact_id=wheel".to_string(),
586 "pkg:pypi/requests@2.28.0?artifact_id=sdist".to_string(),
587 "pkg:pypi/requests@2.28.0".to_string(),
588 ];
589 let mut out = dedup_qualified_purls(&purls);
590 out.sort();
591 assert_eq!(out, vec!["pkg:pypi/requests@2.28.0".to_string()]);
592 }
593
594 #[test]
595 fn dedup_qualified_purls_keeps_distinct_bases() {
596 let purls = vec![
597 "pkg:pypi/requests@2.28.0?artifact_id=wheel".to_string(),
598 "pkg:pypi/flask@3.0.0?artifact_id=wheel".to_string(),
599 ];
600 let mut out = dedup_qualified_purls(&purls);
601 out.sort();
602 assert_eq!(
603 out,
604 vec![
605 "pkg:pypi/flask@3.0.0".to_string(),
606 "pkg:pypi/requests@2.28.0".to_string(),
607 ]
608 );
609 }
610
611 #[test]
612 fn passthrough_purls_is_identity() {
613 let purls = vec![
614 "pkg:npm/foo@1.0".to_string(),
615 "pkg:npm/bar@2.0".to_string(),
616 ];
617 assert_eq!(passthrough_purls(&purls), purls);
618 }
619
620 #[test]
625 fn release_variant_predicate_matches_dispatch_expectations() {
626 assert!(Ecosystem::Pypi.supports_release_variants());
627 assert!(Ecosystem::Gem.supports_release_variants());
628 #[cfg(feature = "maven")]
629 assert!(Ecosystem::Maven.supports_release_variants());
630 assert!(!Ecosystem::Npm.supports_release_variants());
631 #[cfg(feature = "cargo")]
632 assert!(!Ecosystem::Cargo.supports_release_variants());
633 #[cfg(feature = "golang")]
634 assert!(!Ecosystem::Golang.supports_release_variants());
635 #[cfg(feature = "composer")]
636 assert!(!Ecosystem::Composer.supports_release_variants());
637 #[cfg(feature = "nuget")]
638 assert!(!Ecosystem::Nuget.supports_release_variants());
639 #[cfg(feature = "deno")]
640 assert!(!Ecosystem::Deno.supports_release_variants());
641 }
642
643 #[cfg(any(feature = "maven", feature = "nuget"))]
644 #[test]
645 fn env_truthy_accepts_one_and_true_case_insensitive() {
646 let key = "SOCKET_TEST_ENV_TRUTHY";
647 std::env::set_var(key, "1");
648 assert!(env_truthy(key));
649 std::env::set_var(key, "TrUe");
650 assert!(env_truthy(key));
651 std::env::set_var(key, "0");
652 assert!(!env_truthy(key));
653 std::env::set_var(key, "yes");
654 assert!(!env_truthy(key));
655 std::env::remove_var(key);
656 assert!(!env_truthy(key));
657 }
658
659 #[test]
660 fn partition_purls_no_filter_single_npm() {
661 let purls = vec!["pkg:npm/foo@1.0".to_string()];
662 let map = partition_purls(&purls, None);
663 assert_eq!(map.len(), 1);
664 assert_eq!(
665 map.get(&Ecosystem::Npm),
666 Some(&vec!["pkg:npm/foo@1.0".to_string()])
667 );
668 }
669
670 #[test]
671 fn partition_purls_no_filter_mixed_ecosystems() {
672 let purls = vec![
673 "pkg:npm/foo@1.0".to_string(),
674 "pkg:pypi/bar@2.0".to_string(),
675 "pkg:cargo/baz@3.0".to_string(),
676 ];
677 let map = partition_purls(&purls, None);
678 #[cfg(feature = "cargo")]
683 let expected_len = 3;
684 #[cfg(not(feature = "cargo"))]
685 let expected_len = 2;
686 assert_eq!(map.len(), expected_len);
687 assert_eq!(
688 map.get(&Ecosystem::Npm),
689 Some(&vec!["pkg:npm/foo@1.0".to_string()])
690 );
691 assert_eq!(
692 map.get(&Ecosystem::Pypi),
693 Some(&vec!["pkg:pypi/bar@2.0".to_string()])
694 );
695 #[cfg(feature = "cargo")]
696 assert_eq!(
697 map.get(&Ecosystem::Cargo),
698 Some(&vec!["pkg:cargo/baz@3.0".to_string()])
699 );
700 }
701
702 #[test]
703 fn partition_purls_no_filter_empty_input() {
704 let purls: Vec<String> = Vec::new();
705 let map = partition_purls(&purls, None);
706 assert!(map.is_empty());
707 }
708
709 #[test]
710 fn partition_purls_no_filter_duplicate_purls_preserved() {
711 let purls = vec![
712 "pkg:npm/foo@1.0".to_string(),
713 "pkg:npm/foo@1.0".to_string(),
714 ];
715 let map = partition_purls(&purls, None);
716 assert_eq!(map.len(), 1);
717 assert_eq!(
718 map.get(&Ecosystem::Npm),
719 Some(&vec![
720 "pkg:npm/foo@1.0".to_string(),
721 "pkg:npm/foo@1.0".to_string(),
722 ])
723 );
724 }
725
726 #[test]
727 fn partition_purls_no_filter_unknown_ecosystem_dropped() {
728 let purls = vec!["pkg:weirdo/x@1".to_string()];
729 let map = partition_purls(&purls, None);
730 assert!(map.is_empty());
731 }
732
733 #[test]
734 fn partition_purls_allow_list_excludes_one() {
735 let purls = vec![
736 "pkg:npm/foo@1.0".to_string(),
737 "pkg:pypi/bar@2.0".to_string(),
738 ];
739 let allowed = vec!["npm".to_string()];
740 let map = partition_purls(&purls, Some(allowed.as_slice()));
741 assert_eq!(map.len(), 1);
742 assert_eq!(
743 map.get(&Ecosystem::Npm),
744 Some(&vec!["pkg:npm/foo@1.0".to_string()])
745 );
746 assert!(!map.contains_key(&Ecosystem::Pypi));
747 }
748
749 #[test]
750 fn partition_purls_allow_list_matches_none() {
751 let purls = vec!["pkg:npm/foo@1.0".to_string()];
752 let allowed = vec!["pypi".to_string()];
753 let map = partition_purls(&purls, Some(allowed.as_slice()));
754 assert!(map.is_empty());
755 }
756
757 #[test]
758 fn partition_purls_allow_list_matches_all() {
759 let purls = vec![
760 "pkg:npm/foo@1.0".to_string(),
761 "pkg:pypi/bar@2.0".to_string(),
762 ];
763 let allowed = vec!["npm".to_string(), "pypi".to_string()];
764 let map = partition_purls(&purls, Some(allowed.as_slice()));
765 assert_eq!(map.len(), 2);
766 assert_eq!(
767 map.get(&Ecosystem::Npm),
768 Some(&vec!["pkg:npm/foo@1.0".to_string()])
769 );
770 assert_eq!(
771 map.get(&Ecosystem::Pypi),
772 Some(&vec!["pkg:pypi/bar@2.0".to_string()])
773 );
774 }
775
776 #[test]
777 fn partition_purls_empty_allow_list_matches_nothing() {
778 let purls = vec![
779 "pkg:npm/foo@1.0".to_string(),
780 "pkg:pypi/bar@2.0".to_string(),
781 ];
782 let allowed: Vec<String> = Vec::new();
783 let map = partition_purls(&purls, Some(allowed.as_slice()));
784 assert!(map.is_empty());
785 }
786}