Skip to main content

provenant/parsers/nuget/
deps_json.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use 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}