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