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