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