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::{capped_iteration_limit, 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 limit = capped_iteration_limit(selected_target.len(), "nuget .deps.json: target libraries");
77 for (library_key, target_entry) in selected_target.iter().take(limit) {
78 if root_key.as_deref() == Some(library_key.as_str()) {
79 continue;
80 }
81
82 let Some((name, version)) = split_library_key(library_key) else {
83 continue;
84 };
85 let Some(library_metadata) = libraries
86 .get(library_key)
87 .and_then(|value| value.as_object())
88 else {
89 continue;
90 };
91
92 let mut extra_data = serde_json::Map::new();
93 extra_data.insert(
94 "target_name".to_string(),
95 serde_json::Value::String(selected_target_name.clone()),
96 );
97
98 for field in [
99 "type",
100 "sha512",
101 "path",
102 "hashPath",
103 "runtimeStoreManifestName",
104 ] {
105 if let Some(value) = library_metadata.get(field) {
106 extra_data.insert(field.to_string(), value.clone());
107 }
108 }
109
110 if let Some(value) = library_metadata.get("serviceable") {
111 extra_data.insert("serviceable".to_string(), value.clone());
112 }
113
114 if let Some(object) = target_entry.as_object() {
115 for field in ["runtime", "native", "runtimeTargets", "resources"] {
116 if let Some(value) = object.get(field) {
117 extra_data.insert(field.to_string(), value.clone());
118 }
119 }
120 if let Some(value) = object.get("compileOnly") {
121 extra_data.insert("compileOnly".to_string(), value.clone());
122 }
123 }
124
125 let is_direct = if root_key.is_some() {
126 Some(root_dependencies.contains_key(name))
127 } else {
128 None
129 };
130
131 let compile_only = target_entry
132 .get("compileOnly")
133 .and_then(|value| value.as_bool())
134 .unwrap_or(false);
135
136 dependencies.push(Dependency {
137 purl: build_nuget_purl(Some(name), Some(version)),
138 extracted_requirement: Some(version.to_string()),
139 scope: Some(selected_target_name.clone()),
140 is_runtime: Some(!compile_only),
141 is_optional: Some(compile_only),
142 is_pinned: Some(true),
143 is_direct,
144 resolved_package: None,
145 extra_data: if extra_data.is_empty() {
146 None
147 } else {
148 Some(extra_data.into_iter().collect())
149 },
150 });
151 }
152
153 let mut package_data = if let Some(root_key) = root_key {
154 let (name, version) = split_library_key(&root_key).unwrap_or(("", ""));
155 let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
156 package.name = (!name.is_empty()).then(|| name.to_string());
157 package.version = (!version.is_empty()).then(|| version.to_string());
158 package.purl = build_nuget_purl(package.name.as_deref(), package.version.as_deref());
159 let (repository_homepage_url, repository_download_url, api_data_url) =
160 build_nuget_urls(package.name.as_deref(), package.version.as_deref());
161 package.repository_homepage_url = repository_homepage_url;
162 package.repository_download_url = repository_download_url;
163 package.api_data_url = api_data_url;
164 package
165 } else {
166 let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
167 let file_stem = path
168 .file_name()
169 .and_then(|name| name.to_str())
170 .and_then(|name| name.strip_suffix(".deps.json"))
171 .filter(|name| !name.trim().is_empty())
172 .map(|name| name.to_string());
173 package.name = file_stem.clone();
174 package.purl = build_nuget_purl(file_stem.as_deref(), None);
175 package
176 };
177
178 let mut extra_data = serde_json::Map::new();
179 if let Some(runtime_target) = parsed
180 .get("runtimeTarget")
181 .and_then(|value| value.as_object())
182 {
183 if let Some(name) = runtime_target.get("name").and_then(|value| value.as_str()) {
184 extra_data.insert(
185 "runtime_target_name".to_string(),
186 serde_json::Value::String(name.to_string()),
187 );
188 if let Some((framework, runtime_identifier)) = name.split_once('/') {
189 extra_data.insert(
190 "target_framework".to_string(),
191 serde_json::Value::String(framework.to_string()),
192 );
193 extra_data.insert(
194 "runtime_identifier".to_string(),
195 serde_json::Value::String(runtime_identifier.to_string()),
196 );
197 } else {
198 extra_data.insert(
199 "target_framework".to_string(),
200 serde_json::Value::String(name.to_string()),
201 );
202 }
203 }
204 if let Some(signature) = runtime_target.get("signature") {
205 extra_data.insert("runtime_signature".to_string(), signature.clone());
206 }
207 } else {
208 extra_data.insert(
209 "target_name".to_string(),
210 serde_json::Value::String(selected_target_name.clone()),
211 );
212 if let Some((framework, runtime_identifier)) = selected_target_name.split_once('/') {
213 extra_data.insert(
214 "target_framework".to_string(),
215 serde_json::Value::String(framework.to_string()),
216 );
217 extra_data.insert(
218 "runtime_identifier".to_string(),
219 serde_json::Value::String(runtime_identifier.to_string()),
220 );
221 } else {
222 extra_data.insert(
223 "target_framework".to_string(),
224 serde_json::Value::String(selected_target_name.clone()),
225 );
226 }
227 }
228
229 package_data.dependencies = dependencies;
230 package_data.extra_data = if extra_data.is_empty() {
231 None
232 } else {
233 Some(extra_data.into_iter().collect())
234 };
235 package_data
236}
237
238fn select_deps_target(
239 parsed: &serde_json::Value,
240) -> Option<(String, serde_json::Map<String, serde_json::Value>)> {
241 let targets = parsed.get("targets")?.as_object()?;
242
243 if let Some(runtime_target_name) = parsed
244 .get("runtimeTarget")
245 .and_then(|value| value.get("name"))
246 .and_then(|value| value.as_str())
247 && let Some(target) = targets
248 .get(runtime_target_name)
249 .and_then(|value| value.as_object())
250 {
251 return Some((runtime_target_name.to_string(), target.clone()));
252 }
253
254 if let Some((name, value)) = targets
255 .iter()
256 .find(|(name, value)| name.contains('/') && value.is_object())
257 && let Some(target) = value.as_object()
258 {
259 return Some((name.clone(), target.clone()));
260 }
261
262 targets.iter().find_map(|(name, value)| {
263 value
264 .as_object()
265 .map(|target| (name.clone(), target.clone()))
266 })
267}
268
269fn select_root_library_key(
270 path: &Path,
271 libraries: &serde_json::Map<String, serde_json::Value>,
272 target: &serde_json::Map<String, serde_json::Value>,
273) -> Option<String> {
274 let base_name = path
275 .file_name()
276 .and_then(|name| name.to_str())
277 .and_then(|name| name.strip_suffix(".deps.json"));
278
279 let project_keys: Vec<String> = target
280 .keys()
281 .filter(|key| {
282 libraries
283 .get(*key)
284 .and_then(|value| value.get("type"))
285 .and_then(|value| value.as_str())
286 == Some("project")
287 })
288 .cloned()
289 .collect();
290
291 if let Some(base_name) = base_name
292 && let Some(matched) = project_keys.iter().find(|key| {
293 split_library_key(key)
294 .map(|(name, _)| name.eq_ignore_ascii_case(base_name))
295 .unwrap_or(false)
296 })
297 {
298 return Some(matched.clone());
299 }
300
301 if project_keys.len() == 1 {
302 project_keys.into_iter().next()
303 } else {
304 None
305 }
306}
307
308fn split_library_key(key: &str) -> Option<(&str, &str)> {
309 key.rsplit_once('/')
310}