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