Skip to main content

provenant/parsers/nuget/
deps_json.rs

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);