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