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