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