1use crate::models::{
29 DatasourceId, Dependency, Md5Digest, PackageData, PackageType, ResolvedPackage, Sha1Digest,
30 Sha256Digest, Sha512Digest,
31};
32use crate::parsers::utils::{
33 MAX_ITERATION_COUNT, capped_iteration_limit, npm_purl, parse_sri, read_file_to_string,
34 truncate_field,
35};
36use std::path::Path;
37use yaml_serde::Value;
38
39use super::PackageParser;
40use super::metadata::ParserMetadata;
41use super::yarn_lock::extract_namespace_and_name;
42
43pub struct PnpmLockParser;
47
48impl PackageParser for PnpmLockParser {
49 const PACKAGE_TYPE: PackageType = PackageType::PnpmLock;
50
51 fn metadata() -> Vec<ParserMetadata> {
52 vec![ParserMetadata {
53 description: "pnpm lockfile",
54 file_patterns: &["**/pnpm-lock.yaml", "**/shrinkwrap.yaml"],
55 package_type: "npm",
56 primary_language: "JavaScript",
57 documentation_url: Some("https://pnpm.io/next/git#lockfile-compatibility"),
58 }]
59 }
60
61 fn is_match(path: &Path) -> bool {
62 path.file_name()
63 .and_then(|name| name.to_str())
64 .map(|name| name == "pnpm-lock.yaml" || name == "shrinkwrap.yaml")
65 .unwrap_or(false)
66 }
67
68 fn extract_packages(path: &Path) -> Vec<PackageData> {
69 let content = match read_file_to_string(path, None) {
70 Ok(content) => content,
71 Err(e) => {
72 crate::parser_warn!("Failed to read pnpm lockfile at {:?}: {}", path, e);
73 return vec![default_package_data()];
74 }
75 };
76
77 let lock_data: Value = match yaml_serde::from_str(&content) {
78 Ok(data) => data,
79 Err(e) => {
80 crate::parser_warn!("Failed to parse pnpm lockfile at {:?}: {}", path, e);
81 return vec![default_package_data()];
82 }
83 };
84
85 vec![parse_pnpm_lockfile(&lock_data)]
86 }
87}
88
89fn default_package_data() -> PackageData {
91 PackageData {
92 package_type: Some(PnpmLockParser::PACKAGE_TYPE),
93 extra_data: Some(std::collections::HashMap::new()),
94 datasource_id: Some(DatasourceId::PnpmLockYaml),
95 ..Default::default()
96 }
97}
98
99fn compute_dev_only_packages_v9(lock_data: &Value) -> std::collections::HashSet<String> {
107 use std::collections::{HashMap, HashSet, VecDeque};
108
109 let mut prod_roots = HashSet::new();
110 let mut dev_roots = HashSet::new();
111
112 if let Some(importers) = lock_data.get("importers").and_then(|v| v.as_mapping()) {
114 let importer_limit = capped_iteration_limit(importers.len(), "pnpm lockfile importers");
115 for (_importer_path, importer_data) in importers.iter().take(importer_limit) {
116 if let Some(deps) = importer_data
117 .get("dependencies")
118 .and_then(|v| v.as_mapping())
119 {
120 let limit = capped_iteration_limit(deps.len(), "pnpm importer dependencies");
121 for (name, version_data) in deps.iter().take(limit) {
122 if let Some(version) = version_data.get("version").and_then(|v| v.as_str()) {
123 let pkg_key = format_package_key_v9(name.as_str().unwrap_or(""), version);
124 prod_roots.insert(pkg_key);
125 }
126 }
127 }
128
129 if let Some(dev_deps) = importer_data
130 .get("devDependencies")
131 .and_then(|v| v.as_mapping())
132 {
133 let limit = capped_iteration_limit(dev_deps.len(), "pnpm importer devDependencies");
134 for (name, version_data) in dev_deps.iter().take(limit) {
135 if let Some(version) = version_data.get("version").and_then(|v| v.as_str()) {
136 let pkg_key = format_package_key_v9(name.as_str().unwrap_or(""), version);
137 dev_roots.insert(pkg_key);
138 }
139 }
140 }
141 }
142 }
143
144 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
146
147 if let Some(snapshots) = lock_data.get("snapshots").and_then(|v| v.as_mapping()) {
148 let snapshot_limit = capped_iteration_limit(snapshots.len(), "pnpm lockfile snapshots");
149 for (pkg_key, pkg_data) in snapshots.iter().take(snapshot_limit) {
150 let pkg_key_str = pkg_key.as_str().unwrap_or("").to_string();
151 let mut children = Vec::new();
152
153 if let Some(deps) = pkg_data.get("dependencies").and_then(|v| v.as_mapping()) {
154 let limit = capped_iteration_limit(deps.len(), "pnpm snapshot dependencies");
155 for (dep_name, dep_version) in deps.iter().take(limit) {
156 let dep_name_str = dep_name.as_str().unwrap_or("");
157 let dep_version_str = dep_version.as_str().unwrap_or("");
158 let child_key = format!("{}@{}", dep_name_str, dep_version_str);
159 children.push(child_key);
160 }
161 }
162
163 if let Some(opt_deps) = pkg_data
164 .get("optionalDependencies")
165 .and_then(|v| v.as_mapping())
166 {
167 let limit =
168 capped_iteration_limit(opt_deps.len(), "pnpm snapshot optionalDependencies");
169 for (dep_name, dep_version) in opt_deps.iter().take(limit) {
170 let dep_name_str = dep_name.as_str().unwrap_or("");
171 let dep_version_str = dep_version.as_str().unwrap_or("");
172 let child_key = format!("{}@{}", dep_name_str, dep_version_str);
173 children.push(child_key);
174 }
175 }
176
177 graph.insert(pkg_key_str, children);
178 }
179 }
180
181 let mut prod_reachable = HashSet::new();
183 let mut queue = VecDeque::new();
184
185 for root in &prod_roots {
186 queue.push_back(root.clone());
187 prod_reachable.insert(root.clone());
188 }
189
190 let mut bfs_iterations: usize = 0;
191 while let Some(current) = queue.pop_front() {
192 bfs_iterations += 1;
193 if bfs_iterations > MAX_ITERATION_COUNT {
194 crate::parser_warn!(
195 "Truncated pnpm dev/prod reachability traversal at {} iterations (MAX_ITERATION_COUNT); dev classification may be incomplete",
196 MAX_ITERATION_COUNT
197 );
198 break;
199 }
200 if let Some(children) = graph.get(¤t) {
201 for child in children {
202 if prod_reachable.insert(child.clone()) {
203 queue.push_back(child.clone());
204 }
205 }
206 }
207 }
208
209 let mut dev_only = HashSet::new();
211 for pkg_key in graph.keys() {
212 if !prod_reachable.contains(pkg_key) {
213 dev_only.insert(pkg_key.clone());
214 }
215 }
216
217 dev_only
218}
219
220fn format_package_key_v9(name: &str, version: &str) -> String {
222 let clean_version = version.split('(').next().unwrap_or(version);
223 truncate_field(format!("{}@{}", name, clean_version))
224}
225
226fn is_supported_lockfile_version(lockfile_version: &str) -> bool {
230 lockfile_version.starts_with('5')
231 || lockfile_version.starts_with('6')
232 || lockfile_version.starts_with('9')
233}
234
235fn parse_pnpm_lockfile(lock_data: &Value) -> PackageData {
237 let lockfile_version = detect_pnpm_version(lock_data);
238
239 let mut result = default_package_data();
240 result.package_type = Some(PackageType::PnpmLock);
241
242 let dev_only_packages = if lockfile_version.starts_with('9') {
245 compute_dev_only_packages_v9(lock_data)
246 } else {
247 std::collections::HashSet::new()
248 };
249
250 if let Some(packages_map) = lock_data.get("packages").and_then(|v| v.as_mapping()) {
252 if !is_supported_lockfile_version(&lockfile_version) && !packages_map.is_empty() {
256 crate::parser_warn!(
257 "Skipping {} pnpm lockfile package entries: unsupported lockfileVersion {:?}",
258 packages_map.len(),
259 lockfile_version
260 );
261 return result;
262 }
263
264 let limit = capped_iteration_limit(packages_map.len(), "pnpm lockfile packages");
265 for (purl_fields, data) in packages_map.iter().take(limit) {
266 let purl_fields_str = match purl_fields.as_str() {
267 Some(s) => s,
268 None => continue,
269 };
270
271 let clean_purl_fields = clean_purl_fields(purl_fields_str, &lockfile_version);
273
274 let is_dev_only_v9 = lockfile_version.starts_with('9')
276 && dev_only_packages.contains(&clean_purl_fields.to_string());
277
278 if let Some(dependency) =
280 extract_dependency(&clean_purl_fields, data, &lockfile_version, is_dev_only_v9)
281 {
282 result.dependencies.push(dependency);
283 }
284 }
285 }
286
287 result
288}
289
290pub fn detect_pnpm_version(lock_data: &Value) -> String {
292 if let Some(version) = lock_data.get("lockfileVersion") {
293 if let Some(version_str) = version.as_str() {
294 return version_str.to_string();
295 }
296 if let Some(version_num) = version.as_i64() {
297 return version_num.to_string();
298 }
299 if let Some(version_float) = version.as_f64() {
300 return version_float.to_string();
301 }
302 }
303
304 if let Some(version) = lock_data.get("shrinkwrapVersion") {
305 if let Some(version_str) = version.as_str() {
306 if let Some(minor_str) = lock_data
307 .get("shrinkwrapMinorVersion")
308 .and_then(|v| v.as_str())
309 {
310 return format!("{}.{}", version_str, minor_str);
311 }
312 return version_str.to_string();
313 }
314 if let Some(version_num) = version.as_i64() {
315 if let Some(minor_num) = lock_data
316 .get("shrinkwrapMinorVersion")
317 .and_then(|v| v.as_i64())
318 {
319 return format!("{}.{}", version_num, minor_num);
320 }
321 return version_num.to_string();
322 }
323 }
324
325 "5.0".to_string()
326}
327
328pub fn clean_purl_fields(purl_fields: &str, lockfile_version: &str) -> String {
330 let cleaned = if lockfile_version.starts_with('6') {
331 purl_fields
332 .split('(')
333 .next()
334 .unwrap_or(purl_fields)
335 .to_string()
336 } else if lockfile_version.starts_with('5') {
337 let components: Vec<&str> = purl_fields.split('/').collect();
340
341 if let Some(last_component) = components.last() {
342 if last_component.contains('_') {
343 let parts: Vec<&str> = last_component.split('_').collect();
350 for i in 1..=parts.len() {
351 let potential_version = parts[..i].join("_");
352
353 if is_likely_version(&potential_version) {
354 let mut result_components = components[..components.len() - 1].to_vec();
356 result_components.push(&potential_version);
357 return result_components
358 .join("/")
359 .strip_prefix('/')
360 .unwrap_or(&result_components.join("/"))
361 .to_string();
362 }
363 }
364
365 purl_fields.to_string()
367 } else {
368 purl_fields.to_string()
369 }
370 } else {
371 purl_fields.to_string()
372 }
373 } else {
374 purl_fields.to_string()
375 };
376
377 cleaned.strip_prefix('/').unwrap_or(&cleaned).to_string()
378}
379
380fn is_likely_version(s: &str) -> bool {
388 if s.is_empty() {
389 return false;
390 }
391
392 if !s
394 .chars()
395 .next()
396 .map(|c| c.is_ascii_digit())
397 .unwrap_or(false)
398 {
399 return false;
400 }
401
402 if !s.contains('.') {
404 return false;
405 }
406
407 let core_version = s.split(&['-', '+'][..]).next().unwrap_or(s);
410
411 let parts: Vec<&str> = core_version.split('.').collect();
413 if parts.is_empty() {
414 return false;
415 }
416
417 for part in parts {
419 if part.is_empty() || !part.chars().all(|c| c.is_ascii_digit()) {
420 return false;
421 }
422 }
423
424 true
425}
426
427fn parse_nested_dependencies(data: &Value) -> Vec<Dependency> {
428 let mut all_dependencies = Vec::new();
429
430 if let Some(deps) = data.get("dependencies").and_then(|v| v.as_mapping()) {
431 let limit = capped_iteration_limit(deps.len(), "pnpm nested dependencies");
432 for (name, version) in deps.iter().take(limit) {
433 if let Some(dep) = create_simple_dependency(name.as_str(), version.as_str(), None) {
434 all_dependencies.push(dep);
435 }
436 }
437 }
438
439 if let Some(dev_deps) = data.get("devDependencies").and_then(|v| v.as_mapping()) {
440 let limit = capped_iteration_limit(dev_deps.len(), "pnpm nested devDependencies");
441 for (name, version) in dev_deps.iter().take(limit) {
442 if let Some(dep) =
443 create_simple_dependency(name.as_str(), version.as_str(), Some("dev".to_string()))
444 {
445 all_dependencies.push(dep);
446 }
447 }
448 }
449
450 if let Some(peer_deps) = data.get("peerDependencies").and_then(|v| v.as_mapping()) {
451 let limit = capped_iteration_limit(peer_deps.len(), "pnpm nested peerDependencies");
452 for (name, version) in peer_deps.iter().take(limit) {
453 if let Some(dep) =
454 create_simple_dependency(name.as_str(), version.as_str(), Some("peer".to_string()))
455 {
456 all_dependencies.push(dep);
457 }
458 }
459 }
460
461 if let Some(opt_deps) = data
462 .get("optionalDependencies")
463 .and_then(|v| v.as_mapping())
464 {
465 let limit = capped_iteration_limit(opt_deps.len(), "pnpm nested optionalDependencies");
466 for (name, version) in opt_deps.iter().take(limit) {
467 if let Some(dep) = create_simple_dependency(
468 name.as_str(),
469 version.as_str(),
470 Some("optional".to_string()),
471 ) {
472 all_dependencies.push(dep);
473 }
474 }
475 }
476
477 all_dependencies
478}
479
480fn create_simple_dependency(
481 name: Option<&str>,
482 version: Option<&str>,
483 scope: Option<String>,
484) -> Option<Dependency> {
485 let name = name?;
486 let version = version?;
487
488 let (namespace_str, pkg_name) = extract_namespace_and_name(name);
489 let namespace = if !namespace_str.is_empty() {
490 Some(truncate_field(namespace_str))
491 } else {
492 None
493 };
494 let pkg_name = truncate_field(pkg_name.to_string());
495 let version = truncate_field(version.to_string());
496 let purl = create_purl(&namespace, &pkg_name, &version);
497
498 let is_runtime = scope.as_deref() != Some("dev");
499 let is_optional = scope.as_deref() == Some("optional");
500
501 Some(Dependency {
502 purl: Some(purl),
503 extracted_requirement: Some(version),
504 scope,
505 is_runtime: Some(is_runtime),
506 is_optional: Some(is_optional),
507 is_pinned: Some(true),
508 is_direct: Some(false),
509 resolved_package: None,
510 extra_data: None,
511 })
512}
513
514pub fn extract_dependency(
516 clean_purl_fields: &str,
517 data: &Value,
518 lockfile_version: &str,
519 is_dev_only_v9: bool,
520) -> Option<Dependency> {
521 let (namespace, name, version) = match parse_purl_fields(clean_purl_fields, lockfile_version) {
522 Some(parsed) => parsed,
523 None => {
524 if is_supported_lockfile_version(lockfile_version) {
528 crate::parser_warn!(
529 "Skipping pnpm lockfile entry with unparseable key {:?} (lockfileVersion {})",
530 clean_purl_fields,
531 lockfile_version
532 );
533 }
534 return None;
535 }
536 };
537 let namespace = namespace.map(truncate_field);
538 let name = truncate_field(name);
539 let version = truncate_field(version);
540
541 let purl = create_purl(&namespace, &name, &version);
543
544 let (sha1, sha256, sha512, md5) = if let Some(resolution) = data.get("resolution") {
546 if let Some(integrity) = resolution.get("integrity") {
547 if let Some(integrity_str) = integrity.as_str() {
548 parse_integrity(integrity_str)
549 } else {
550 (None, None, None, None)
551 }
552 } else {
553 (None, None, None, None)
554 }
555 } else {
556 (None, None, None, None)
557 };
558
559 let mut extra_data = std::collections::HashMap::new();
561
562 if let (Some(_has_bin), Some(true)) = (
563 data.get("hasBin"),
564 data.get("hasBin").and_then(|v| v.as_bool()),
565 ) {
566 extra_data.insert("hasBin".to_string(), serde_json::Value::Bool(true));
567 }
568
569 if data.get("requiresBuild").and_then(|v| v.as_bool()) == Some(true) {
570 extra_data.insert("requiresBuild".to_string(), serde_json::Value::Bool(true));
571 }
572
573 let is_optional = data
575 .get("optional")
576 .and_then(|v| v.as_bool())
577 .unwrap_or(false);
578 if is_optional {
579 extra_data.insert("optional".to_string(), serde_json::Value::Bool(true));
580 }
581
582 let is_dev = if lockfile_version.starts_with('9') {
586 is_dev_only_v9
587 } else {
588 data.get("dev").and_then(|v| v.as_bool()).unwrap_or(false)
589 };
590
591 if is_dev {
592 extra_data.insert("dev".to_string(), serde_json::Value::Bool(true));
593 }
594
595 let scope = if is_dev {
597 Some("dev".to_string())
598 } else if is_optional {
599 Some("optional".to_string())
600 } else {
601 None
602 };
603
604 let is_runtime = !is_dev;
606
607 let all_dependencies = parse_nested_dependencies(data);
608
609 let resolved_package = ResolvedPackage {
610 primary_language: Some("JavaScript".to_string()),
611 download_url: None,
612 sha1: sha1.and_then(|h| Sha1Digest::from_hex(&h).ok()),
613 sha256: sha256.and_then(|h| Sha256Digest::from_hex(&h).ok()),
614 sha512: sha512.and_then(|h| Sha512Digest::from_hex(&h).ok()),
615 md5: md5.and_then(|h| Md5Digest::from_hex(&h).ok()),
616 is_virtual: true,
617 extra_data: None,
618 dependencies: all_dependencies,
619 repository_homepage_url: None,
620 repository_download_url: None,
621 api_data_url: None,
622 datasource_id: Some(DatasourceId::PnpmLockYaml),
623 purl: None,
624 ..ResolvedPackage::new(
625 PackageType::Npm,
626 namespace.clone().unwrap_or_default(),
627 name.clone(),
628 version.clone(),
629 )
630 };
631
632 let dependency = Dependency {
633 purl: Some(purl),
634 extracted_requirement: Some(version),
635 scope,
636 is_runtime: Some(is_runtime),
637 is_optional: Some(is_optional),
638 is_pinned: Some(true),
639 is_direct: Some(false),
640 resolved_package: Some(Box::new(resolved_package)),
641 extra_data: if extra_data.is_empty() {
642 None
643 } else {
644 Some(extra_data)
645 },
646 };
647
648 Some(dependency)
649}
650
651pub fn parse_purl_fields(
653 clean_purl_fields: &str,
654 lockfile_version: &str,
655) -> Option<(Option<String>, String, String)> {
656 let sections: Vec<&str> = clean_purl_fields.split('/').collect();
657
658 if lockfile_version.starts_with('6') {
659 let last_at_pos = clean_purl_fields.rfind('@')?;
660 let version = clean_purl_fields[last_at_pos + 1..].to_string();
661 let name_part = &clean_purl_fields[..last_at_pos];
662
663 if let Some(stripped) = name_part.strip_prefix('@') {
664 let parts: Vec<&str> = stripped.split('/').collect();
665 if parts.len() == 2 {
666 Some((
667 Some(format!("@{}", parts[0])),
668 parts[1].to_string(),
669 version,
670 ))
671 } else {
672 None
673 }
674 } else if name_part.contains('/') {
675 let parts: Vec<&str> = name_part.split('/').collect();
676 if parts.len() == 2 && parts[0].starts_with('@') {
677 Some((Some(parts[0].to_string()), parts[1].to_string(), version))
678 } else if parts.len() == 2 {
679 Some((None, format!("{}/{}", parts[0], parts[1]), version))
680 } else {
681 Some((None, name_part.to_string(), version))
682 }
683 } else {
684 Some((None, name_part.to_string(), version))
685 }
686 } else if lockfile_version.starts_with('9') {
687 let last_at_pos = clean_purl_fields.rfind('@')?;
688 let name_part = &clean_purl_fields[..last_at_pos];
689 let version = clean_purl_fields[last_at_pos + 1..].to_string();
690
691 if let Some(stripped) = name_part.strip_prefix('@') {
692 let parts: Vec<&str> = stripped.split('/').collect();
693 if parts.len() == 2 {
694 Some((Some(parts[0].to_string()), parts[1].to_string(), version))
695 } else {
696 None
697 }
698 } else {
699 Some((None, name_part.to_string(), version))
700 }
701 } else if lockfile_version.starts_with('5') {
702 if sections.len() == 4 && sections[0].is_empty() && sections[1].starts_with('@') {
703 let scope = sections[1];
704 let name = sections[2];
705 let version = sections[3].to_string();
706 Some((Some(scope.to_string()), name.to_string(), version))
707 } else if sections.len() == 4 && sections[0].is_empty() && !sections[1].starts_with('@') {
708 let name = sections[1];
709 let version = sections[2].to_string();
710 Some((None, name.to_string(), version))
711 } else if sections.len() == 3 && sections[0].starts_with('@') {
712 let scope = sections[0];
713 let name = sections[1];
714 let version = sections[2].to_string();
715 Some((Some(scope.to_string()), name.to_string(), version))
716 } else if sections.len() == 2 {
717 let name = sections[0];
718 let version = sections[1].to_string();
719 Some((None, name.to_string(), version))
720 } else {
721 None
722 }
723 } else {
724 None
725 }
726}
727
728pub fn create_purl(namespace: &Option<String>, name: &str, version: &str) -> String {
729 let full_name = match namespace {
730 Some(ns) if !ns.is_empty() => {
731 let ns_with_at = if ns.starts_with('@') {
732 ns.clone()
733 } else {
734 format!("@{}", ns)
735 };
736 format!("{}/{}", ns_with_at, name)
737 }
738 _ => name.to_string(),
739 };
740 npm_purl(&full_name, Some(version)).unwrap_or_else(|| format!("pkg:npm/{}", name))
741}
742
743fn parse_integrity(
744 integrity: &str,
745) -> (
746 Option<String>,
747 Option<String>,
748 Option<String>,
749 Option<String>,
750) {
751 let (algo, hex_digest) = match parse_sri(integrity) {
752 Some(pair) => pair,
753 None => return (None, None, None, None),
754 };
755
756 let algo_lower = algo.to_lowercase();
757 if algo_lower.contains("sha1") {
758 (Some(hex_digest), None, None, None)
759 } else if algo_lower.contains("sha256") {
760 (None, Some(hex_digest), None, None)
761 } else if algo_lower.contains("sha512") {
762 (None, None, Some(hex_digest), None)
763 } else if algo_lower.contains("md5") {
764 (None, None, None, Some(hex_digest))
765 } else {
766 (None, None, None, None)
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773
774 #[test]
775 fn test_detect_pnpm_version_v5() {
776 let yaml = "lockfileVersion: 5.4\n";
777 let data: Value = yaml_serde::from_str(yaml).unwrap();
778 assert_eq!(detect_pnpm_version(&data), "5.4");
779 }
780
781 #[test]
782 fn test_detect_pnpm_version_v6() {
783 let yaml = "lockfileVersion: '6.0'\n";
784 let data: Value = yaml_serde::from_str(yaml).unwrap();
785 assert_eq!(detect_pnpm_version(&data), "6.0");
786 }
787
788 #[test]
789 fn test_detect_pnpm_version_v9() {
790 let yaml = "lockfileVersion: '9.0'\n";
791 let data: Value = yaml_serde::from_str(yaml).unwrap();
792 assert_eq!(detect_pnpm_version(&data), "9.0");
793 }
794
795 #[test]
796 fn test_clean_purl_fields_v6() {
797 let purl_fields = "@babel/runtime@7.18.9(react@18.0.0)";
798 assert_eq!(
799 clean_purl_fields(purl_fields, "6.0"),
800 "@babel/runtime@7.18.9"
801 );
802
803 let purl_fields = "@babel/runtime@7.18.9(";
804 assert_eq!(
805 clean_purl_fields(purl_fields, "6.0"),
806 "@babel/runtime@7.18.9"
807 );
808 }
809
810 #[test]
811 fn test_clean_purl_fields_v5() {
812 let purl_fields = "/_/@headlessui/react/1.6.6_biqbaboplfbrettd7655fr4n2y";
813 assert_eq!(
814 clean_purl_fields(purl_fields, "5.0"),
815 "_/@headlessui/react/1.6.6"
816 );
817 }
818
819 #[test]
820 fn test_clean_purl_fields_v9() {
821 let purl_fields = "@babel/helper-string-parser@7.24.8";
822 assert_eq!(
823 clean_purl_fields(purl_fields, "9.0"),
824 "@babel/helper-string-parser@7.24.8"
825 );
826 }
827
828 #[test]
829 fn test_parse_purl_fields_v6_scoped() {
830 let (namespace, name, version) = parse_purl_fields("@babel/runtime@7.18.9", "6.0").unwrap();
831 assert_eq!(namespace, Some("@babel".to_string()));
832 assert_eq!(name, "runtime".to_string());
833 assert_eq!(version, "7.18.9".to_string());
834 }
835
836 #[test]
837 fn test_parse_purl_fields_v9_scoped() {
838 let (namespace, name, version) =
839 parse_purl_fields("@babel/helper-string-parser@7.24.8", "9.0").unwrap();
840 assert_eq!(namespace, Some("babel".to_string()));
841 assert_eq!(name, "helper-string-parser".to_string());
842 assert_eq!(version, "7.24.8".to_string());
843 }
844
845 #[test]
846 fn test_parse_purl_fields_v9_non_scoped() {
847 let (namespace, name, version) =
848 parse_purl_fields("anve-upload-upyun@1.0.8", "9.0").unwrap();
849 assert_eq!(namespace, None);
850 assert_eq!(name, "anve-upload-upyun".to_string());
851 assert_eq!(version, "1.0.8".to_string());
852 }
853
854 #[test]
855 fn test_parse_purl_fields_v5_scoped() {
856 let (namespace, name, version) = parse_purl_fields("@babel/runtime/7.18.9", "5.0").unwrap();
857 assert_eq!(namespace, Some("@babel".to_string()));
858 assert_eq!(name, "runtime".to_string());
859 assert_eq!(version, "7.18.9".to_string());
860 }
861
862 #[test]
863 fn test_parse_integrity() {
864 let (sha1, sha256, sha512, md5) = parse_integrity(
865 "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
866 );
867 assert!(sha1.is_none());
868 assert!(sha256.is_none());
869 assert!(sha512.is_some());
870 assert!(md5.is_none());
871
872 let (sha1, sha256, sha512, md5) = parse_integrity("sha1-w7M6te42DYbg5ijwRorn7yfWVN8=");
873 assert!(sha1.is_some());
874 assert!(sha256.is_none());
875 assert!(sha512.is_none());
876 assert!(md5.is_none());
877 }
878}