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