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};
8use super::{build_nuget_purl, build_nuget_urls, default_package_data};
9
10pub struct DotNetDepsJsonParser;
11
12impl PackageParser for DotNetDepsJsonParser {
13 const PACKAGE_TYPE: PackageType = PackageType::Nuget;
14
15 fn is_match(path: &Path) -> bool {
16 path.file_name()
17 .and_then(|name| name.to_str())
18 .is_some_and(|name| name.ends_with(".deps.json"))
19 }
20
21 fn extract_packages(path: &Path) -> Vec<PackageData> {
22 let content = match read_file_to_string(path, None) {
23 Ok(c) => c,
24 Err(e) => {
25 warn!("Failed to read .deps.json at {:?}: {}", path, e);
26 return vec![default_package_data(Some(DatasourceId::NugetDepsJson))];
27 }
28 };
29
30 let parsed: serde_json::Value = match serde_json::from_str(&content) {
31 Ok(value) => value,
32 Err(e) => {
33 warn!("Failed to parse .deps.json at {:?}: {}", path, e);
34 return vec![default_package_data(Some(DatasourceId::NugetDepsJson))];
35 }
36 };
37
38 vec![parse_dotnet_deps_json(&parsed, path)]
39 }
40}
41
42fn parse_dotnet_deps_json(parsed: &serde_json::Value, path: &Path) -> PackageData {
43 let Some(libraries) = parsed.get("libraries").and_then(|value| value.as_object()) else {
44 return default_package_data(Some(DatasourceId::NugetDepsJson));
45 };
46
47 let Some((selected_target_name, selected_target)) = select_deps_target(parsed) else {
48 return default_package_data(Some(DatasourceId::NugetDepsJson));
49 };
50
51 let root_key = select_root_library_key(path, libraries, &selected_target);
52 let root_dependencies = root_key
53 .as_deref()
54 .and_then(|root_key| selected_target.get(root_key))
55 .and_then(|value| value.get("dependencies"))
56 .and_then(|value| value.as_object())
57 .cloned()
58 .unwrap_or_default();
59
60 let mut dependencies = Vec::new();
61 let mut iteration_count: usize = 0;
62 for (library_key, target_entry) in selected_target.iter().take(MAX_ITERATION_COUNT) {
63 iteration_count += 1;
64 if iteration_count > MAX_ITERATION_COUNT {
65 warn!(
66 "Iteration limit exceeded in .deps.json at {:?}; stopping at {} dependencies",
67 path, MAX_ITERATION_COUNT
68 );
69 break;
70 }
71 if root_key.as_deref() == Some(library_key.as_str()) {
72 continue;
73 }
74
75 let Some((name, version)) = split_library_key(library_key) else {
76 continue;
77 };
78 let Some(library_metadata) = libraries
79 .get(library_key)
80 .and_then(|value| value.as_object())
81 else {
82 continue;
83 };
84
85 let mut extra_data = serde_json::Map::new();
86 extra_data.insert(
87 "target_name".to_string(),
88 serde_json::Value::String(selected_target_name.clone()),
89 );
90
91 for field in [
92 "type",
93 "sha512",
94 "path",
95 "hashPath",
96 "runtimeStoreManifestName",
97 ] {
98 if let Some(value) = library_metadata.get(field) {
99 extra_data.insert(field.to_string(), value.clone());
100 }
101 }
102
103 if let Some(value) = library_metadata.get("serviceable") {
104 extra_data.insert("serviceable".to_string(), value.clone());
105 }
106
107 if let Some(object) = target_entry.as_object() {
108 for field in ["runtime", "native", "runtimeTargets", "resources"] {
109 if let Some(value) = object.get(field) {
110 extra_data.insert(field.to_string(), value.clone());
111 }
112 }
113 if let Some(value) = object.get("compileOnly") {
114 extra_data.insert("compileOnly".to_string(), value.clone());
115 }
116 }
117
118 let is_direct = if root_key.is_some() {
119 Some(root_dependencies.contains_key(name))
120 } else {
121 None
122 };
123
124 let compile_only = target_entry
125 .get("compileOnly")
126 .and_then(|value| value.as_bool())
127 .unwrap_or(false);
128
129 dependencies.push(Dependency {
130 purl: build_nuget_purl(Some(name), Some(version)),
131 extracted_requirement: Some(version.to_string()),
132 scope: Some(selected_target_name.clone()),
133 is_runtime: Some(!compile_only),
134 is_optional: Some(compile_only),
135 is_pinned: Some(true),
136 is_direct,
137 resolved_package: None,
138 extra_data: if extra_data.is_empty() {
139 None
140 } else {
141 Some(extra_data.into_iter().collect())
142 },
143 });
144 }
145
146 let mut package_data = if let Some(root_key) = root_key {
147 let (name, version) = split_library_key(&root_key).unwrap_or(("", ""));
148 let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
149 package.name = (!name.is_empty()).then(|| name.to_string());
150 package.version = (!version.is_empty()).then(|| version.to_string());
151 package.purl = build_nuget_purl(package.name.as_deref(), package.version.as_deref());
152 let (repository_homepage_url, repository_download_url, api_data_url) =
153 build_nuget_urls(package.name.as_deref(), package.version.as_deref());
154 package.repository_homepage_url = repository_homepage_url;
155 package.repository_download_url = repository_download_url;
156 package.api_data_url = api_data_url;
157 package
158 } else {
159 let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
160 let file_stem = path
161 .file_name()
162 .and_then(|name| name.to_str())
163 .and_then(|name| name.strip_suffix(".deps.json"))
164 .filter(|name| !name.trim().is_empty())
165 .map(|name| name.to_string());
166 package.name = file_stem.clone();
167 package.purl = build_nuget_purl(file_stem.as_deref(), None);
168 package
169 };
170
171 let mut extra_data = serde_json::Map::new();
172 if let Some(runtime_target) = parsed
173 .get("runtimeTarget")
174 .and_then(|value| value.as_object())
175 {
176 if let Some(name) = runtime_target.get("name").and_then(|value| value.as_str()) {
177 extra_data.insert(
178 "runtime_target_name".to_string(),
179 serde_json::Value::String(name.to_string()),
180 );
181 if let Some((framework, runtime_identifier)) = name.split_once('/') {
182 extra_data.insert(
183 "target_framework".to_string(),
184 serde_json::Value::String(framework.to_string()),
185 );
186 extra_data.insert(
187 "runtime_identifier".to_string(),
188 serde_json::Value::String(runtime_identifier.to_string()),
189 );
190 } else {
191 extra_data.insert(
192 "target_framework".to_string(),
193 serde_json::Value::String(name.to_string()),
194 );
195 }
196 }
197 if let Some(signature) = runtime_target.get("signature") {
198 extra_data.insert("runtime_signature".to_string(), signature.clone());
199 }
200 } else {
201 extra_data.insert(
202 "target_name".to_string(),
203 serde_json::Value::String(selected_target_name.clone()),
204 );
205 if let Some((framework, runtime_identifier)) = selected_target_name.split_once('/') {
206 extra_data.insert(
207 "target_framework".to_string(),
208 serde_json::Value::String(framework.to_string()),
209 );
210 extra_data.insert(
211 "runtime_identifier".to_string(),
212 serde_json::Value::String(runtime_identifier.to_string()),
213 );
214 } else {
215 extra_data.insert(
216 "target_framework".to_string(),
217 serde_json::Value::String(selected_target_name.clone()),
218 );
219 }
220 }
221
222 package_data.dependencies = dependencies;
223 package_data.extra_data = if extra_data.is_empty() {
224 None
225 } else {
226 Some(extra_data.into_iter().collect())
227 };
228 package_data
229}
230
231fn select_deps_target(
232 parsed: &serde_json::Value,
233) -> Option<(String, serde_json::Map<String, serde_json::Value>)> {
234 let targets = parsed.get("targets")?.as_object()?;
235
236 if let Some(runtime_target_name) = parsed
237 .get("runtimeTarget")
238 .and_then(|value| value.get("name"))
239 .and_then(|value| value.as_str())
240 && let Some(target) = targets
241 .get(runtime_target_name)
242 .and_then(|value| value.as_object())
243 {
244 return Some((runtime_target_name.to_string(), target.clone()));
245 }
246
247 if let Some((name, value)) = targets
248 .iter()
249 .find(|(name, value)| name.contains('/') && value.is_object())
250 && let Some(target) = value.as_object()
251 {
252 return Some((name.clone(), target.clone()));
253 }
254
255 targets.iter().find_map(|(name, value)| {
256 value
257 .as_object()
258 .map(|target| (name.clone(), target.clone()))
259 })
260}
261
262fn select_root_library_key(
263 path: &Path,
264 libraries: &serde_json::Map<String, serde_json::Value>,
265 target: &serde_json::Map<String, serde_json::Value>,
266) -> Option<String> {
267 let base_name = path
268 .file_name()
269 .and_then(|name| name.to_str())
270 .and_then(|name| name.strip_suffix(".deps.json"));
271
272 let project_keys: Vec<String> = target
273 .keys()
274 .filter(|key| {
275 libraries
276 .get(*key)
277 .and_then(|value| value.get("type"))
278 .and_then(|value| value.as_str())
279 == Some("project")
280 })
281 .cloned()
282 .collect();
283
284 if let Some(base_name) = base_name
285 && let Some(matched) = project_keys.iter().find(|key| {
286 split_library_key(key)
287 .map(|(name, _)| name.eq_ignore_ascii_case(base_name))
288 .unwrap_or(false)
289 })
290 {
291 return Some(matched.clone());
292 }
293
294 project_keys.into_iter().next()
295}
296
297fn split_library_key(key: &str) -> Option<(&str, &str)> {
298 key.rsplit_once('/')
299}
300
301crate::register_parser!(
302 ".NET .deps.json runtime dependency graph",
303 &["**/*.deps.json"],
304 "nuget",
305 "C#",
306 Some("https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing"),
307);