Skip to main content

provenant/parsers/nuget/
project_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, truncate_field};
11use super::{
12    build_nuget_party, build_nuget_purl, build_nuget_urls, default_package_data,
13    insert_extra_string,
14};
15
16pub struct ProjectJsonParser;
17
18impl PackageParser for ProjectJsonParser {
19    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
20
21    fn is_match(path: &Path) -> bool {
22        path.file_name()
23            .and_then(|name| name.to_str())
24            .is_some_and(|name| name == "project.json")
25    }
26
27    fn extract_packages(path: &Path) -> Vec<PackageData> {
28        let content = match read_file_to_string(path, None) {
29            Ok(c) => c,
30            Err(e) => {
31                warn!("Failed to read project.json at {:?}: {}", path, e);
32                return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
33            }
34        };
35
36        let parsed: serde_json::Value = match serde_json::from_str(&content) {
37            Ok(value) => value,
38            Err(e) => {
39                warn!("Failed to parse project.json at {:?}: {}", path, e);
40                return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
41            }
42        };
43
44        vec![parse_project_json_manifest(&parsed)]
45    }
46}
47
48pub struct ProjectLockJsonParser;
49
50impl PackageParser for ProjectLockJsonParser {
51    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
52
53    fn is_match(path: &Path) -> bool {
54        path.file_name()
55            .and_then(|name| name.to_str())
56            .is_some_and(|name| name == "project.lock.json")
57    }
58
59    fn extract_packages(path: &Path) -> Vec<PackageData> {
60        let content = match read_file_to_string(path, None) {
61            Ok(c) => c,
62            Err(e) => {
63                warn!("Failed to read project.lock.json at {:?}: {}", path, e);
64                return vec![default_package_data(Some(
65                    DatasourceId::NugetProjectLockJson,
66                ))];
67            }
68        };
69
70        let parsed: serde_json::Value = match serde_json::from_str(&content) {
71            Ok(value) => value,
72            Err(e) => {
73                warn!("Failed to parse project.lock.json at {:?}: {}", path, e);
74                return vec![default_package_data(Some(
75                    DatasourceId::NugetProjectLockJson,
76                ))];
77            }
78        };
79
80        vec![parse_project_lock_manifest(&parsed)]
81    }
82}
83
84fn parse_project_json_manifest(parsed: &serde_json::Value) -> PackageData {
85    let name = parsed
86        .get("name")
87        .and_then(|value| value.as_str())
88        .map(|value| value.to_string());
89    let version = parsed
90        .get("version")
91        .and_then(|value| value.as_str())
92        .map(|value| value.to_string());
93    let description = parsed
94        .get("description")
95        .and_then(|value| value.as_str())
96        .map(|value| value.to_string());
97    let homepage_url = parsed
98        .get("projectUrl")
99        .and_then(|value| value.as_str())
100        .map(|value| value.to_string());
101    let extracted_license_statement = parsed
102        .get("license")
103        .or_else(|| parsed.get("licenseUrl"))
104        .and_then(|value| value.as_str())
105        .map(|value| value.to_string());
106
107    let mut parties = Vec::new();
108    if let Some(authors) = parsed.get("authors") {
109        let author_name = if let Some(value) = authors.as_str() {
110            Some(value.to_string())
111        } else {
112            authors.as_array().map(|entries| {
113                entries
114                    .iter()
115                    .filter_map(|entry| entry.as_str())
116                    .collect::<Vec<_>>()
117                    .join(", ")
118            })
119        };
120
121        if let Some(author_name) = author_name.filter(|value| !value.is_empty()) {
122            parties.push(build_nuget_party("author", author_name));
123        }
124    }
125
126    let mut dependencies = Vec::new();
127
128    if let Some(root_dependencies) = parsed
129        .get("dependencies")
130        .and_then(|value| value.as_object())
131    {
132        for (dependency_name, dependency_spec) in root_dependencies.iter().take(MAX_ITERATION_COUNT)
133        {
134            if let Some(dependency) =
135                parse_project_json_dependency(dependency_name, dependency_spec, None)
136            {
137                dependencies.push(dependency);
138            }
139        }
140    }
141
142    if let Some(frameworks) = parsed.get("frameworks").and_then(|value| value.as_object()) {
143        for (framework, framework_value) in frameworks.iter().take(MAX_ITERATION_COUNT) {
144            let Some(framework_dependencies) = framework_value
145                .get("dependencies")
146                .and_then(|value| value.as_object())
147            else {
148                continue;
149            };
150
151            for (dependency_name, dependency_spec) in
152                framework_dependencies.iter().take(MAX_ITERATION_COUNT)
153            {
154                if let Some(dependency) = parse_project_json_dependency(
155                    dependency_name,
156                    dependency_spec,
157                    Some(framework.clone()),
158                ) {
159                    dependencies.push(dependency);
160                }
161            }
162        }
163    }
164
165    let (repository_homepage_url, repository_download_url, api_data_url) =
166        build_nuget_urls(name.as_deref(), version.as_deref());
167
168    PackageData {
169        datasource_id: Some(DatasourceId::NugetProjectJson),
170        package_type: Some(PackageType::Nuget),
171        name: name.clone().map(truncate_field),
172        version: version.clone().map(truncate_field),
173        purl: build_nuget_purl(name.as_deref(), version.as_deref()),
174        description: description.map(truncate_field),
175        homepage_url: homepage_url.map(truncate_field),
176        parties,
177        dependencies,
178        extracted_license_statement: extracted_license_statement.map(truncate_field),
179        repository_homepage_url,
180        repository_download_url,
181        api_data_url,
182        ..default_package_data(Some(DatasourceId::NugetProjectJson))
183    }
184}
185
186fn parse_project_json_dependency(
187    dependency_name: &str,
188    dependency_spec: &serde_json::Value,
189    scope: Option<String>,
190) -> Option<Dependency> {
191    let mut extra_data = serde_json::Map::new();
192
193    let requirement = match dependency_spec {
194        serde_json::Value::String(version) => Some(version.clone()),
195        serde_json::Value::Object(object) => {
196            let requirement = object
197                .get("version")
198                .and_then(|value| value.as_str())
199                .map(|value| value.to_string());
200            insert_extra_string(
201                &mut extra_data,
202                "include",
203                object
204                    .get("include")
205                    .and_then(|value| value.as_str())
206                    .map(|value| value.to_string()),
207            );
208            insert_extra_string(
209                &mut extra_data,
210                "exclude",
211                object
212                    .get("exclude")
213                    .and_then(|value| value.as_str())
214                    .map(|value| value.to_string()),
215            );
216            insert_extra_string(
217                &mut extra_data,
218                "type",
219                object
220                    .get("type")
221                    .and_then(|value| value.as_str())
222                    .map(|value| value.to_string()),
223            );
224            requirement
225        }
226        _ => return None,
227    };
228
229    Some(Dependency {
230        purl: build_nuget_purl(Some(dependency_name), None),
231        extracted_requirement: requirement,
232        scope,
233        is_runtime: Some(true),
234        is_optional: Some(false),
235        is_pinned: Some(false),
236        is_direct: Some(true),
237        resolved_package: None,
238        extra_data: if extra_data.is_empty() {
239            None
240        } else {
241            Some(extra_data.into_iter().collect())
242        },
243    })
244}
245
246fn parse_project_lock_manifest(parsed: &serde_json::Value) -> PackageData {
247    let mut dependencies = Vec::new();
248
249    if let Some(groups) = parsed
250        .get("projectFileDependencyGroups")
251        .and_then(|value| value.as_object())
252    {
253        for (framework, entries) in groups.iter().take(MAX_ITERATION_COUNT) {
254            let Some(entries) = entries.as_array() else {
255                continue;
256            };
257
258            for entry in entries
259                .iter()
260                .take(MAX_ITERATION_COUNT)
261                .filter_map(|value| value.as_str())
262            {
263                if let Some(dependency) = parse_project_lock_dependency(
264                    entry,
265                    (!framework.is_empty()).then(|| framework.clone()),
266                ) {
267                    dependencies.push(dependency);
268                }
269            }
270        }
271    }
272
273    PackageData {
274        datasource_id: Some(DatasourceId::NugetProjectLockJson),
275        package_type: Some(PackageType::Nuget),
276        dependencies,
277        ..default_package_data(Some(DatasourceId::NugetProjectLockJson))
278    }
279}
280
281fn parse_project_lock_dependency(entry: &str, scope: Option<String>) -> Option<Dependency> {
282    let trimmed = entry.trim();
283    if trimmed.is_empty() {
284        return None;
285    }
286
287    let mut parts = trimmed.split_whitespace();
288    let name = parts.next()?;
289    let requirement = parts.collect::<Vec<_>>().join(" ");
290
291    Some(Dependency {
292        purl: build_nuget_purl(Some(name), None),
293        extracted_requirement: (!requirement.is_empty()).then_some(requirement),
294        scope,
295        is_runtime: Some(true),
296        is_optional: Some(false),
297        is_pinned: Some(false),
298        is_direct: Some(true),
299        resolved_package: None,
300        extra_data: None,
301    })
302}
303
304crate::register_parser!(
305    ".NET project.json manifest",
306    &["**/project.json"],
307    "nuget",
308    "C#",
309    Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
310);
311
312crate::register_parser!(
313    ".NET project.lock.json lockfile",
314    &["**/project.lock.json"],
315    "nuget",
316    "C#",
317    Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
318);