Skip to main content

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