provenant/parsers/nuget/
packages_lock.rs1use std::path::Path;
7
8use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
9use crate::parser_warn as warn;
10use packageurl::PackageUrl;
11
12use super::super::PackageParser;
13use super::super::utils::{MAX_ITERATION_COUNT, read_file_to_string};
14use super::default_package_data;
15
16pub struct PackagesLockParser;
17
18impl PackageParser for PackagesLockParser {
19 const PACKAGE_TYPE: PackageType = PackageType::Nuget;
20
21 fn is_match(path: &Path) -> bool {
22 path.file_name()
23 .and_then(|name| name.to_str())
24 .is_some_and(|name| name.ends_with("packages.lock.json"))
25 }
26
27 fn extract_packages(path: &Path) -> Vec<PackageData> {
28 let content = match read_file_to_string(path, None) {
29 Ok(c) => c,
30 Err(e) => {
31 warn!("Failed to read packages.lock.json at {:?}: {}", path, e);
32 return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
33 }
34 };
35
36 let parsed: serde_json::Value = match serde_json::from_str(&content) {
37 Ok(v) => v,
38 Err(e) => {
39 warn!("Failed to parse packages.lock.json at {:?}: {}", path, e);
40 return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
41 }
42 };
43
44 let mut dependencies = Vec::new();
45 let mut iteration_count: usize = 0;
46
47 if let Some(deps_obj) = parsed.get("dependencies").and_then(|v| v.as_object()) {
48 for (target_framework, packages) in deps_obj.iter().take(MAX_ITERATION_COUNT) {
49 if let Some(packages_obj) = packages.as_object() {
50 for (package_name, package_info) in
51 packages_obj.iter().take(MAX_ITERATION_COUNT)
52 {
53 iteration_count += 1;
54 if iteration_count > MAX_ITERATION_COUNT {
55 warn!(
56 "Iteration limit exceeded in packages.lock.json at {:?}; stopping at {} dependencies",
57 path, MAX_ITERATION_COUNT
58 );
59 break;
60 }
61 if let Some(info_obj) = package_info.as_object() {
62 let version = info_obj
63 .get("resolved")
64 .and_then(|v| v.as_str())
65 .map(|s| s.to_string());
66
67 let requested = info_obj
68 .get("requested")
69 .and_then(|v| v.as_str())
70 .map(|s| s.to_string());
71
72 let package_type = info_obj.get("type").and_then(|v| v.as_str());
73
74 let is_direct = match package_type {
75 Some("Direct") => Some(true),
76 Some("Transitive") => Some(false),
77 _ => None,
78 };
79
80 let purl = version.as_ref().and_then(|v| {
81 PackageUrl::new("nuget", package_name).ok().map(|mut p| {
82 let _ = p.with_version(v);
83 p.to_string()
84 })
85 });
86
87 let mut extra_data = serde_json::Map::new();
88 extra_data.insert(
89 "target_framework".to_string(),
90 serde_json::Value::String(target_framework.clone()),
91 );
92
93 if let Some(content_hash) =
94 info_obj.get("contentHash").and_then(|v| v.as_str())
95 {
96 extra_data.insert(
97 "content_hash".to_string(),
98 serde_json::Value::String(content_hash.to_string()),
99 );
100 }
101
102 dependencies.push(Dependency {
103 purl,
104 extracted_requirement: requested.or(version),
105 scope: Some(target_framework.clone()),
106 is_runtime: Some(true),
107 is_optional: Some(false),
108 is_pinned: Some(true),
109 is_direct,
110 resolved_package: None,
111 extra_data: if extra_data.is_empty() {
112 None
113 } else {
114 Some(extra_data.into_iter().collect())
115 },
116 });
117 }
118 }
119 }
120 }
121 }
122
123 vec![PackageData {
124 datasource_id: Some(DatasourceId::NugetPackagesLock),
125 package_type: Some(Self::PACKAGE_TYPE),
126 dependencies,
127 ..default_package_data(Some(DatasourceId::NugetPackagesLock))
128 }]
129 }
130
131 fn metadata() -> Vec<super::super::metadata::ParserMetadata> {
132 vec![super::super::metadata::ParserMetadata {
133 description: ".NET packages.lock.json lockfile",
134 file_patterns: &["**/packages.lock.json"],
135 package_type: "nuget",
136 primary_language: "C#",
137 documentation_url: Some(
138 "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies",
139 ),
140 }]
141 }
142}