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