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