1use std::path::Path;
5
6use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
7use crate::parser_warn as warn;
8
9use super::super::PackageParser;
10use super::super::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
11use super::{
12 build_nuget_party, build_nuget_purl, build_nuget_urls, default_package_data,
13 insert_extra_string,
14};
15
16pub struct ProjectJsonParser;
17
18impl PackageParser for ProjectJsonParser {
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 == "project.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 project.json at {:?}: {}", path, e);
32 return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
33 }
34 };
35
36 let parsed: serde_json::Value = match serde_json::from_str(&content) {
37 Ok(value) => value,
38 Err(e) => {
39 warn!("Failed to parse project.json at {:?}: {}", path, e);
40 return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
41 }
42 };
43
44 vec![parse_project_json_manifest(&parsed)]
45 }
46}
47
48pub struct ProjectLockJsonParser;
49
50impl PackageParser for ProjectLockJsonParser {
51 const PACKAGE_TYPE: PackageType = PackageType::Nuget;
52
53 fn is_match(path: &Path) -> bool {
54 path.file_name()
55 .and_then(|name| name.to_str())
56 .is_some_and(|name| name == "project.lock.json")
57 }
58
59 fn extract_packages(path: &Path) -> Vec<PackageData> {
60 let content = match read_file_to_string(path, None) {
61 Ok(c) => c,
62 Err(e) => {
63 warn!("Failed to read project.lock.json at {:?}: {}", path, e);
64 return vec![default_package_data(Some(
65 DatasourceId::NugetProjectLockJson,
66 ))];
67 }
68 };
69
70 let parsed: serde_json::Value = match serde_json::from_str(&content) {
71 Ok(value) => value,
72 Err(e) => {
73 warn!("Failed to parse project.lock.json at {:?}: {}", path, e);
74 return vec![default_package_data(Some(
75 DatasourceId::NugetProjectLockJson,
76 ))];
77 }
78 };
79
80 vec![parse_project_lock_manifest(&parsed)]
81 }
82}
83
84fn parse_project_json_manifest(parsed: &serde_json::Value) -> PackageData {
85 let name = parsed
86 .get("name")
87 .and_then(|value| value.as_str())
88 .map(|value| value.to_string());
89 let version = parsed
90 .get("version")
91 .and_then(|value| value.as_str())
92 .map(|value| value.to_string());
93 let description = parsed
94 .get("description")
95 .and_then(|value| value.as_str())
96 .map(|value| value.to_string());
97 let homepage_url = parsed
98 .get("projectUrl")
99 .and_then(|value| value.as_str())
100 .map(|value| value.to_string());
101 let extracted_license_statement = parsed
102 .get("license")
103 .or_else(|| parsed.get("licenseUrl"))
104 .and_then(|value| value.as_str())
105 .map(|value| value.to_string());
106
107 let mut parties = Vec::new();
108 if let Some(authors) = parsed.get("authors") {
109 let author_name = if let Some(value) = authors.as_str() {
110 Some(value.to_string())
111 } else {
112 authors.as_array().map(|entries| {
113 entries
114 .iter()
115 .filter_map(|entry| entry.as_str())
116 .collect::<Vec<_>>()
117 .join(", ")
118 })
119 };
120
121 if let Some(author_name) = author_name.filter(|value| !value.is_empty()) {
122 parties.push(build_nuget_party("author", author_name));
123 }
124 }
125
126 let mut dependencies = Vec::new();
127
128 if let Some(root_dependencies) = parsed
129 .get("dependencies")
130 .and_then(|value| value.as_object())
131 {
132 for (dependency_name, dependency_spec) in root_dependencies.iter().take(MAX_ITERATION_COUNT)
133 {
134 if let Some(dependency) =
135 parse_project_json_dependency(dependency_name, dependency_spec, None)
136 {
137 dependencies.push(dependency);
138 }
139 }
140 }
141
142 if let Some(frameworks) = parsed.get("frameworks").and_then(|value| value.as_object()) {
143 for (framework, framework_value) in frameworks.iter().take(MAX_ITERATION_COUNT) {
144 let Some(framework_dependencies) = framework_value
145 .get("dependencies")
146 .and_then(|value| value.as_object())
147 else {
148 continue;
149 };
150
151 for (dependency_name, dependency_spec) in
152 framework_dependencies.iter().take(MAX_ITERATION_COUNT)
153 {
154 if let Some(dependency) = parse_project_json_dependency(
155 dependency_name,
156 dependency_spec,
157 Some(framework.clone()),
158 ) {
159 dependencies.push(dependency);
160 }
161 }
162 }
163 }
164
165 let (repository_homepage_url, repository_download_url, api_data_url) =
166 build_nuget_urls(name.as_deref(), version.as_deref());
167
168 PackageData {
169 datasource_id: Some(DatasourceId::NugetProjectJson),
170 package_type: Some(PackageType::Nuget),
171 name: name.clone().map(truncate_field),
172 version: version.clone().map(truncate_field),
173 purl: build_nuget_purl(name.as_deref(), version.as_deref()),
174 description: description.map(truncate_field),
175 homepage_url: homepage_url.map(truncate_field),
176 parties,
177 dependencies,
178 extracted_license_statement: extracted_license_statement.map(truncate_field),
179 repository_homepage_url,
180 repository_download_url,
181 api_data_url,
182 ..default_package_data(Some(DatasourceId::NugetProjectJson))
183 }
184}
185
186fn parse_project_json_dependency(
187 dependency_name: &str,
188 dependency_spec: &serde_json::Value,
189 scope: Option<String>,
190) -> Option<Dependency> {
191 let mut extra_data = serde_json::Map::new();
192
193 let requirement = match dependency_spec {
194 serde_json::Value::String(version) => Some(version.clone()),
195 serde_json::Value::Object(object) => {
196 let requirement = object
197 .get("version")
198 .and_then(|value| value.as_str())
199 .map(|value| value.to_string());
200 insert_extra_string(
201 &mut extra_data,
202 "include",
203 object
204 .get("include")
205 .and_then(|value| value.as_str())
206 .map(|value| value.to_string()),
207 );
208 insert_extra_string(
209 &mut extra_data,
210 "exclude",
211 object
212 .get("exclude")
213 .and_then(|value| value.as_str())
214 .map(|value| value.to_string()),
215 );
216 insert_extra_string(
217 &mut extra_data,
218 "type",
219 object
220 .get("type")
221 .and_then(|value| value.as_str())
222 .map(|value| value.to_string()),
223 );
224 requirement
225 }
226 _ => return None,
227 };
228
229 Some(Dependency {
230 purl: build_nuget_purl(Some(dependency_name), None),
231 extracted_requirement: requirement,
232 scope,
233 is_runtime: Some(true),
234 is_optional: Some(false),
235 is_pinned: Some(false),
236 is_direct: Some(true),
237 resolved_package: None,
238 extra_data: if extra_data.is_empty() {
239 None
240 } else {
241 Some(extra_data.into_iter().collect())
242 },
243 })
244}
245
246fn parse_project_lock_manifest(parsed: &serde_json::Value) -> PackageData {
247 let mut dependencies = Vec::new();
248
249 if let Some(groups) = parsed
250 .get("projectFileDependencyGroups")
251 .and_then(|value| value.as_object())
252 {
253 for (framework, entries) in groups.iter().take(MAX_ITERATION_COUNT) {
254 let Some(entries) = entries.as_array() else {
255 continue;
256 };
257
258 for entry in entries
259 .iter()
260 .take(MAX_ITERATION_COUNT)
261 .filter_map(|value| value.as_str())
262 {
263 if let Some(dependency) = parse_project_lock_dependency(
264 entry,
265 (!framework.is_empty()).then(|| framework.clone()),
266 ) {
267 dependencies.push(dependency);
268 }
269 }
270 }
271 }
272
273 PackageData {
274 datasource_id: Some(DatasourceId::NugetProjectLockJson),
275 package_type: Some(PackageType::Nuget),
276 dependencies,
277 ..default_package_data(Some(DatasourceId::NugetProjectLockJson))
278 }
279}
280
281fn parse_project_lock_dependency(entry: &str, scope: Option<String>) -> Option<Dependency> {
282 let trimmed = entry.trim();
283 if trimmed.is_empty() {
284 return None;
285 }
286
287 let mut parts = trimmed.split_whitespace();
288 let name = parts.next()?;
289 let requirement = parts.collect::<Vec<_>>().join(" ");
290
291 Some(Dependency {
292 purl: build_nuget_purl(Some(name), None),
293 extracted_requirement: (!requirement.is_empty()).then_some(requirement),
294 scope,
295 is_runtime: Some(true),
296 is_optional: Some(false),
297 is_pinned: Some(false),
298 is_direct: Some(true),
299 resolved_package: None,
300 extra_data: None,
301 })
302}
303
304crate::register_parser!(
305 ".NET project.json manifest",
306 &["**/project.json"],
307 "nuget",
308 "C#",
309 Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
310);
311
312crate::register_parser!(
313 ".NET project.lock.json lockfile",
314 &["**/project.lock.json"],
315 "nuget",
316 "C#",
317 Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
318);