Skip to main content

provenant/parsers/
nuget.rs

1//! Parser for NuGet package manifests and configuration files.
2//!
3//! Extracts package metadata and dependencies from .NET/NuGet ecosystem files:
4//! - packages.config (legacy .NET Framework format)
5//! - .nuspec (NuGet package specification)
6//! - packages.lock.json (NuGet lock file)
7//! - .nupkg (NuGet package archive — metadata extraction)
8//!
9//! # Supported Formats
10//! - packages.config (XML)
11//! - *.nuspec (XML)
12//! - packages.lock.json (JSON)
13//! - *.nupkg (ZIP archive with .nuspec inside)
14//!
15//! # Key Features
16//! - Dependency extraction with targetFramework support
17//! - Dependency groups by framework version
18//! - Package URL (purl) generation
19//!
20//! # Implementation Notes
21//! - Uses quick-xml for XML parsing
22//! - Graceful error handling with warn!()
23//! - No unwrap/expect in library code
24
25use std::collections::{HashMap, HashSet};
26use std::fs::{self, File};
27use std::io::{BufReader, Read};
28use std::path::{Path, PathBuf};
29
30use crate::parser_warn as warn;
31use packageurl::PackageUrl;
32use quick_xml::Reader;
33use quick_xml::events::Event;
34
35use crate::models::{DatasourceId, Dependency, PackageData, PackageType, Party};
36
37use super::PackageParser;
38use super::license_normalization::{empty_declared_license_data, normalize_spdx_declared_license};
39use super::utils::{MAX_ITERATION_COUNT, MAX_MANIFEST_SIZE, read_file_to_string, truncate_field};
40
41const MAX_RECURSION_DEPTH: usize = 50;
42
43fn check_file_size(path: &Path) -> Result<(), String> {
44    match fs::metadata(path) {
45        Ok(metadata) => {
46            if metadata.len() > MAX_MANIFEST_SIZE {
47                return Err(format!(
48                    "File {:?} is {} bytes, exceeding the {} byte limit",
49                    path,
50                    metadata.len(),
51                    MAX_MANIFEST_SIZE
52                ));
53            }
54            Ok(())
55        }
56        Err(e) => Err(format!("Cannot stat file {:?}: {}", path, e)),
57    }
58}
59
60const PROJECT_FILE_EXTENSIONS: [&str; 3] = ["csproj", "vbproj", "fsproj"];
61
62#[derive(Default)]
63struct RepositoryMetadata {
64    vcs_url: Option<String>,
65    branch: Option<String>,
66    commit: Option<String>,
67}
68
69fn build_nuget_party(role: &str, name: String) -> Party {
70    Party {
71        r#type: Some("person".to_string()),
72        role: Some(role.to_string()),
73        name: Some(name),
74        email: None,
75        url: None,
76        organization: None,
77        organization_url: None,
78        timezone: None,
79    }
80}
81
82fn insert_extra_string(
83    extra_data: &mut serde_json::Map<String, serde_json::Value>,
84    key: &str,
85    value: Option<String>,
86) {
87    if let Some(value) = value
88        .map(|v| v.trim().to_string())
89        .filter(|v| !v.is_empty())
90    {
91        extra_data.insert(key.to_string(), serde_json::Value::String(value));
92    }
93}
94
95fn parse_repository_metadata(element: &quick_xml::events::BytesStart) -> RepositoryMetadata {
96    let mut repo_type = None;
97    let mut repo_url = None;
98    let mut branch = None;
99    let mut commit = None;
100
101    for attr in element.attributes().filter_map(|a| a.ok()) {
102        match attr.key.as_ref() {
103            b"type" => repo_type = String::from_utf8(attr.value.to_vec()).ok(),
104            b"url" => repo_url = String::from_utf8(attr.value.to_vec()).ok(),
105            b"branch" => branch = String::from_utf8(attr.value.to_vec()).ok(),
106            b"commit" => commit = String::from_utf8(attr.value.to_vec()).ok(),
107            _ => {}
108        }
109    }
110
111    RepositoryMetadata {
112        vcs_url: repo_url.map(|url| match repo_type {
113            Some(vcs_type) if !vcs_type.trim().is_empty() => format!("{}+{}", vcs_type, url),
114            _ => url,
115        }),
116        branch,
117        commit,
118    }
119}
120
121fn build_nuget_urls(
122    name: Option<&str>,
123    version: Option<&str>,
124) -> (Option<String>, Option<String>, Option<String>) {
125    let repository_homepage_url = name.and_then(|name| {
126        version.map(|version| format!("https://www.nuget.org/packages/{}/{}", name, version))
127    });
128
129    let repository_download_url = name.and_then(|name| {
130        version.map(|version| format!("https://www.nuget.org/api/v2/package/{}/{}", name, version))
131    });
132
133    let api_data_url = name.and_then(|name| {
134        version.map(|version| {
135            format!(
136                "https://api.nuget.org/v3/registration3/{}/{}.json",
137                name.to_lowercase(),
138                version
139            )
140        })
141    });
142
143    (
144        repository_homepage_url,
145        repository_download_url,
146        api_data_url,
147    )
148}
149
150fn build_nuget_purl(name: Option<&str>, version: Option<&str>) -> Option<String> {
151    let name = name?;
152    let mut package_url = PackageUrl::new("nuget", name).ok()?;
153
154    if let Some(version) = version {
155        package_url.with_version(version).ok()?;
156    }
157
158    Some(package_url.to_string())
159}
160
161fn project_file_datasource_id(path: &Path) -> Option<DatasourceId> {
162    match path.extension().and_then(|ext| ext.to_str()) {
163        Some("csproj") => Some(DatasourceId::NugetCsproj),
164        Some("vbproj") => Some(DatasourceId::NugetVbproj),
165        Some("fsproj") => Some(DatasourceId::NugetFsproj),
166        _ => None,
167    }
168}
169
170fn build_nuget_description(
171    summary: Option<&str>,
172    description: Option<&str>,
173    title: Option<&str>,
174    name: Option<&str>,
175) -> Option<String> {
176    let summary = summary.map(|s| s.trim()).filter(|s| !s.is_empty());
177    let description = description.map(|s| s.trim()).filter(|s| !s.is_empty());
178    let title = title.map(|s| s.trim()).filter(|s| !s.is_empty());
179
180    let mut result = match (summary, description) {
181        (None, None) => return None,
182        (Some(s), None) => s.to_string(),
183        (None, Some(d)) => d.to_string(),
184        (Some(s), Some(d)) => {
185            if d.contains(s) {
186                d.to_string()
187            } else {
188                format!("{}\n{}", s, d)
189            }
190        }
191    };
192
193    if let Some(t) = title {
194        if let Some(n) = name {
195            if t != n {
196                result = format!("{}\n{}", t, result);
197            }
198        } else {
199            result = format!("{}\n{}", t, result);
200        }
201    }
202
203    Some(result)
204}
205
206/// Parser for packages.config (legacy .NET Framework format)
207pub struct PackagesConfigParser;
208
209impl PackageParser for PackagesConfigParser {
210    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
211
212    fn is_match(path: &Path) -> bool {
213        path.file_name()
214            .and_then(|name| name.to_str())
215            .is_some_and(|name| name == "packages.config")
216    }
217
218    fn extract_packages(path: &Path) -> Vec<PackageData> {
219        if let Err(e) = check_file_size(path) {
220            warn!("{}", e);
221            return vec![default_package_data(Some(
222                DatasourceId::NugetPackagesConfig,
223            ))];
224        }
225
226        let file = match File::open(path) {
227            Ok(f) => f,
228            Err(e) => {
229                warn!("Failed to open packages.config at {:?}: {}", path, e);
230                return vec![default_package_data(Some(
231                    DatasourceId::NugetPackagesConfig,
232                ))];
233            }
234        };
235
236        let reader = BufReader::new(file);
237        let mut xml_reader = Reader::from_reader(reader);
238        xml_reader.config_mut().trim_text(true);
239
240        let mut dependencies = Vec::new();
241        let mut buf = Vec::new();
242        let mut iteration_count: usize = 0;
243
244        loop {
245            iteration_count += 1;
246            if iteration_count > MAX_ITERATION_COUNT {
247                warn!(
248                    "Iteration limit exceeded in packages.config at {:?}; stopping at {} items",
249                    path, MAX_ITERATION_COUNT
250                );
251                break;
252            }
253            match xml_reader.read_event_into(&mut buf) {
254                Ok(Event::Empty(e)) if e.name().as_ref() == b"package" => {
255                    if let Some(dep) = parse_packages_config_package(&e) {
256                        dependencies.push(dep);
257                    }
258                }
259                Ok(Event::Eof) => break,
260                Err(e) => {
261                    warn!("Error parsing packages.config at {:?}: {}", path, e);
262                    return vec![default_package_data(Some(
263                        DatasourceId::NugetPackagesConfig,
264                    ))];
265                }
266                _ => {}
267            }
268            buf.clear();
269        }
270
271        vec![PackageData {
272            datasource_id: Some(DatasourceId::NugetPackagesConfig),
273            package_type: Some(Self::PACKAGE_TYPE),
274            dependencies,
275            ..default_package_data(Some(DatasourceId::NugetPackagesConfig))
276        }]
277    }
278}
279
280/// Parser for .nuspec files (NuGet package specification)
281pub struct NuspecParser;
282
283impl PackageParser for NuspecParser {
284    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
285
286    fn is_match(path: &Path) -> bool {
287        path.extension()
288            .and_then(|ext| ext.to_str())
289            .is_some_and(|ext| ext == "nuspec")
290    }
291
292    fn extract_packages(path: &Path) -> Vec<PackageData> {
293        if let Err(e) = check_file_size(path) {
294            warn!("{}", e);
295            return vec![default_package_data(Some(DatasourceId::NugetNuspec))];
296        }
297
298        let file = match File::open(path) {
299            Ok(f) => f,
300            Err(e) => {
301                warn!("Failed to open .nuspec at {:?}: {}", path, e);
302                return vec![default_package_data(Some(DatasourceId::NugetNuspec))];
303            }
304        };
305
306        let reader = BufReader::new(file);
307        let mut xml_reader = Reader::from_reader(reader);
308        xml_reader.config_mut().trim_text(true);
309
310        let mut name = None;
311        let mut version = None;
312        let mut summary = None;
313        let mut description = None;
314        let mut title = None;
315        let mut homepage_url = None;
316        let mut parties = Vec::new();
317        let mut dependencies = Vec::new();
318        let mut extracted_license_statement = None;
319        let mut license_type = None;
320        let mut copyright = None;
321        let mut vcs_url = None;
322        let mut repository_branch = None;
323        let mut repository_commit = None;
324
325        let mut buf = Vec::new();
326        let mut current_element = String::new();
327        let mut in_metadata = false;
328        let mut in_dependencies = false;
329        let mut current_group_framework = None;
330        let mut iteration_count: usize = 0;
331
332        loop {
333            iteration_count += 1;
334            if iteration_count > MAX_ITERATION_COUNT {
335                warn!(
336                    "Iteration limit exceeded in .nuspec at {:?}; stopping at {} items",
337                    path, MAX_ITERATION_COUNT
338                );
339                break;
340            }
341            match xml_reader.read_event_into(&mut buf) {
342                Ok(Event::Start(e)) => {
343                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
344                    current_element = tag_name.clone();
345
346                    if tag_name == "metadata" {
347                        in_metadata = true;
348                    } else if tag_name == "dependencies" && in_metadata {
349                        in_dependencies = true;
350                    } else if tag_name == "group" && in_dependencies {
351                        current_group_framework = e
352                            .attributes()
353                            .filter_map(|a| a.ok())
354                            .find(|attr| attr.key.as_ref() == b"targetFramework")
355                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
356                    } else if tag_name == "repository" && in_metadata {
357                        let repository = parse_repository_metadata(&e);
358                        vcs_url = repository.vcs_url;
359                        repository_branch = repository.branch;
360                        repository_commit = repository.commit;
361                    } else if tag_name == "license" && in_metadata {
362                        license_type = e
363                            .attributes()
364                            .filter_map(|a| a.ok())
365                            .find(|attr| attr.key.as_ref() == b"type")
366                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
367                    }
368                }
369                Ok(Event::Empty(e)) => {
370                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
371
372                    if tag_name == "dependency" && in_dependencies {
373                        if let Some(dep) =
374                            parse_nuspec_dependency(&e, current_group_framework.as_deref())
375                        {
376                            dependencies.push(dep);
377                        }
378                    } else if tag_name == "repository" && in_metadata {
379                        let repository = parse_repository_metadata(&e);
380                        vcs_url = repository.vcs_url;
381                        repository_branch = repository.branch;
382                        repository_commit = repository.commit;
383                    }
384                }
385                Ok(Event::Text(e)) => {
386                    if !in_metadata {
387                        continue;
388                    }
389
390                    let text = e.decode().ok().map(|s| s.trim().to_string());
391                    if let Some(text) = text.filter(|s| !s.is_empty()) {
392                        match current_element.as_str() {
393                            "id" => name = Some(text),
394                            "version" => version = Some(text),
395                            "summary" => summary = Some(text),
396                            "description" => description = Some(text),
397                            "title" => title = Some(text),
398                            "projectUrl" => homepage_url = Some(text),
399                            "authors" => {
400                                parties.push(build_nuget_party("author", text));
401                            }
402                            "owners" => {
403                                parties.push(build_nuget_party("owner", text));
404                            }
405                            "license" => {
406                                extracted_license_statement = Some(text);
407                            }
408                            "licenseUrl" => {
409                                if extracted_license_statement.is_none() {
410                                    extracted_license_statement = Some(text);
411                                }
412                            }
413                            "copyright" => copyright = Some(text),
414                            _ => {}
415                        }
416                    }
417                }
418                Ok(Event::End(e)) => {
419                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
420
421                    if tag_name == "metadata" {
422                        in_metadata = false;
423                    } else if tag_name == "dependencies" {
424                        in_dependencies = false;
425                    } else if tag_name == "group" {
426                        current_group_framework = None;
427                    }
428
429                    current_element.clear();
430                }
431                Ok(Event::Eof) => break,
432                Err(e) => {
433                    warn!("Error parsing .nuspec at {:?}: {}", path, e);
434                    return vec![default_package_data(Some(DatasourceId::NugetNuspec))];
435                }
436                _ => {}
437            }
438            buf.clear();
439        }
440
441        // Build description from summary, description, and title fields
442        // Following Python ScanCode's build_description logic
443        let final_description = build_nuget_description(
444            summary.as_deref(),
445            description.as_deref(),
446            title.as_deref(),
447            name.as_deref(),
448        );
449
450        let (repository_homepage_url, repository_download_url, api_data_url) =
451            build_nuget_urls(name.as_deref(), version.as_deref());
452
453        let purl = build_nuget_purl(name.as_deref(), version.as_deref());
454
455        let (declared_license_expression, declared_license_expression_spdx, license_detections) =
456            if license_type.as_deref() == Some("expression") {
457                normalize_spdx_declared_license(extracted_license_statement.as_deref())
458            } else {
459                empty_declared_license_data()
460            };
461
462        let holder = None;
463
464        let mut extra_data = serde_json::Map::new();
465        insert_extra_string(&mut extra_data, "license_type", license_type.clone());
466        if license_type.as_deref() == Some("file") {
467            insert_extra_string(
468                &mut extra_data,
469                "license_file",
470                extracted_license_statement.clone(),
471            );
472        }
473        insert_extra_string(&mut extra_data, "repository_branch", repository_branch);
474        insert_extra_string(&mut extra_data, "repository_commit", repository_commit);
475
476        vec![PackageData {
477            datasource_id: Some(DatasourceId::NugetNuspec),
478            package_type: Some(Self::PACKAGE_TYPE),
479            name: name.map(truncate_field),
480            version: version.map(truncate_field),
481            purl,
482            description: final_description.map(truncate_field),
483            homepage_url: homepage_url.map(truncate_field),
484            parties,
485            dependencies,
486            declared_license_expression,
487            declared_license_expression_spdx,
488            license_detections,
489            extracted_license_statement: extracted_license_statement.map(truncate_field),
490            copyright: copyright.map(truncate_field),
491            holder,
492            vcs_url: vcs_url.map(truncate_field),
493            extra_data: if extra_data.is_empty() {
494                None
495            } else {
496                Some(extra_data.into_iter().collect())
497            },
498            repository_homepage_url,
499            repository_download_url,
500            api_data_url,
501            ..default_package_data(Some(DatasourceId::NugetNuspec))
502        }]
503    }
504}
505
506fn parse_packages_config_package(element: &quick_xml::events::BytesStart) -> Option<Dependency> {
507    let mut id = None;
508    let mut version = None;
509    let mut target_framework = None;
510
511    for attr in element.attributes().filter_map(|a| a.ok()) {
512        match attr.key.as_ref() {
513            b"id" => id = String::from_utf8(attr.value.to_vec()).ok(),
514            b"version" => version = String::from_utf8(attr.value.to_vec()).ok(),
515            b"targetFramework" => target_framework = String::from_utf8(attr.value.to_vec()).ok(),
516            _ => {}
517        }
518    }
519
520    let name = id?;
521    let purl = PackageUrl::new("nuget", &name).ok().map(|p| p.to_string());
522
523    Some(Dependency {
524        purl,
525        extracted_requirement: version,
526        scope: target_framework,
527        is_runtime: Some(true),
528        is_optional: Some(false),
529        is_pinned: Some(true),
530        is_direct: Some(true),
531        resolved_package: None,
532        extra_data: None,
533    })
534}
535
536fn parse_nuspec_dependency(
537    element: &quick_xml::events::BytesStart,
538    framework: Option<&str>,
539) -> Option<Dependency> {
540    let mut id = None;
541    let mut version = None;
542    let mut include = None;
543    let mut exclude = None;
544
545    for attr in element.attributes().filter_map(|a| a.ok()) {
546        match attr.key.as_ref() {
547            b"id" => id = String::from_utf8(attr.value.to_vec()).ok(),
548            b"version" => version = String::from_utf8(attr.value.to_vec()).ok(),
549            b"include" => include = String::from_utf8(attr.value.to_vec()).ok(),
550            b"exclude" => exclude = String::from_utf8(attr.value.to_vec()).ok(),
551            _ => {}
552        }
553    }
554
555    let name = id?;
556    let purl = PackageUrl::new("nuget", &name).ok().map(|p| p.to_string());
557
558    let mut extra_data = serde_json::Map::new();
559    if let Some(fw) = framework {
560        extra_data.insert(
561            "framework".to_string(),
562            serde_json::Value::String(fw.to_string()),
563        );
564    }
565    if let Some(inc) = include {
566        extra_data.insert("include".to_string(), serde_json::Value::String(inc));
567    }
568    if let Some(exc) = exclude {
569        extra_data.insert("exclude".to_string(), serde_json::Value::String(exc));
570    }
571
572    Some(Dependency {
573        purl,
574        extracted_requirement: version,
575        scope: Some("dependency".to_string()),
576        is_runtime: Some(true),
577        is_optional: Some(false),
578        is_pinned: Some(false),
579        is_direct: Some(true),
580        resolved_package: None,
581        extra_data: if extra_data.is_empty() {
582            None
583        } else {
584            Some(extra_data.into_iter().collect())
585        },
586    })
587}
588
589fn default_package_data(datasource_id: Option<DatasourceId>) -> PackageData {
590    PackageData {
591        package_type: Some(PackagesConfigParser::PACKAGE_TYPE),
592        datasource_id,
593        ..Default::default()
594    }
595}
596
597const MAX_ARCHIVE_SIZE: u64 = 100 * 1024 * 1024; // 100MB
598const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024; // 50MB
599const MAX_COMPRESSION_RATIO: f64 = 100.0; // 100:1
600
601/// Parser for packages.lock.json (NuGet lock file)
602pub struct PackagesLockParser;
603
604impl PackageParser for PackagesLockParser {
605    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
606
607    fn is_match(path: &Path) -> bool {
608        path.file_name()
609            .and_then(|name| name.to_str())
610            .is_some_and(|name| name.ends_with("packages.lock.json"))
611    }
612
613    fn extract_packages(path: &Path) -> Vec<PackageData> {
614        let content = match read_file_to_string(path, None) {
615            Ok(c) => c,
616            Err(e) => {
617                warn!("Failed to read packages.lock.json at {:?}: {}", path, e);
618                return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
619            }
620        };
621
622        let parsed: serde_json::Value = match serde_json::from_str(&content) {
623            Ok(v) => v,
624            Err(e) => {
625                warn!("Failed to parse packages.lock.json at {:?}: {}", path, e);
626                return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
627            }
628        };
629
630        let mut dependencies = Vec::new();
631        let mut iteration_count: usize = 0;
632
633        if let Some(deps_obj) = parsed.get("dependencies").and_then(|v| v.as_object()) {
634            for (target_framework, packages) in deps_obj.iter().take(MAX_ITERATION_COUNT) {
635                if let Some(packages_obj) = packages.as_object() {
636                    for (package_name, package_info) in
637                        packages_obj.iter().take(MAX_ITERATION_COUNT)
638                    {
639                        iteration_count += 1;
640                        if iteration_count > MAX_ITERATION_COUNT {
641                            warn!(
642                                "Iteration limit exceeded in packages.lock.json at {:?}; stopping at {} dependencies",
643                                path, MAX_ITERATION_COUNT
644                            );
645                            break;
646                        }
647                        if let Some(info_obj) = package_info.as_object() {
648                            let version = info_obj
649                                .get("resolved")
650                                .and_then(|v| v.as_str())
651                                .map(|s| s.to_string());
652
653                            let requested = info_obj
654                                .get("requested")
655                                .and_then(|v| v.as_str())
656                                .map(|s| s.to_string());
657
658                            let package_type = info_obj.get("type").and_then(|v| v.as_str());
659
660                            let is_direct = match package_type {
661                                Some("Direct") => Some(true),
662                                Some("Transitive") => Some(false),
663                                _ => None,
664                            };
665
666                            let purl = version.as_ref().and_then(|v| {
667                                PackageUrl::new("nuget", package_name).ok().map(|mut p| {
668                                    let _ = p.with_version(v);
669                                    p.to_string()
670                                })
671                            });
672
673                            let mut extra_data = serde_json::Map::new();
674                            extra_data.insert(
675                                "target_framework".to_string(),
676                                serde_json::Value::String(target_framework.clone()),
677                            );
678
679                            if let Some(content_hash) =
680                                info_obj.get("contentHash").and_then(|v| v.as_str())
681                            {
682                                extra_data.insert(
683                                    "content_hash".to_string(),
684                                    serde_json::Value::String(content_hash.to_string()),
685                                );
686                            }
687
688                            dependencies.push(Dependency {
689                                purl,
690                                extracted_requirement: requested.or(version),
691                                scope: Some(target_framework.clone()),
692                                is_runtime: Some(true),
693                                is_optional: Some(false),
694                                is_pinned: Some(true),
695                                is_direct,
696                                resolved_package: None,
697                                extra_data: if extra_data.is_empty() {
698                                    None
699                                } else {
700                                    Some(extra_data.into_iter().collect())
701                                },
702                            });
703                        }
704                    }
705                }
706            }
707        }
708
709        vec![PackageData {
710            datasource_id: Some(DatasourceId::NugetPackagesLock),
711            package_type: Some(Self::PACKAGE_TYPE),
712            dependencies,
713            ..default_package_data(Some(DatasourceId::NugetPackagesLock))
714        }]
715    }
716}
717
718pub struct DotNetDepsJsonParser;
719
720impl PackageParser for DotNetDepsJsonParser {
721    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
722
723    fn is_match(path: &Path) -> bool {
724        path.file_name()
725            .and_then(|name| name.to_str())
726            .is_some_and(|name| name.ends_with(".deps.json"))
727    }
728
729    fn extract_packages(path: &Path) -> Vec<PackageData> {
730        let content = match read_file_to_string(path, None) {
731            Ok(c) => c,
732            Err(e) => {
733                warn!("Failed to read .deps.json at {:?}: {}", path, e);
734                return vec![default_package_data(Some(DatasourceId::NugetDepsJson))];
735            }
736        };
737
738        let parsed: serde_json::Value = match serde_json::from_str(&content) {
739            Ok(value) => value,
740            Err(e) => {
741                warn!("Failed to parse .deps.json at {:?}: {}", path, e);
742                return vec![default_package_data(Some(DatasourceId::NugetDepsJson))];
743            }
744        };
745
746        vec![parse_dotnet_deps_json(&parsed, path)]
747    }
748}
749
750fn parse_dotnet_deps_json(parsed: &serde_json::Value, path: &Path) -> PackageData {
751    let Some(libraries) = parsed.get("libraries").and_then(|value| value.as_object()) else {
752        return default_package_data(Some(DatasourceId::NugetDepsJson));
753    };
754
755    let Some((selected_target_name, selected_target)) = select_deps_target(parsed) else {
756        return default_package_data(Some(DatasourceId::NugetDepsJson));
757    };
758
759    let root_key = select_root_library_key(path, libraries, &selected_target);
760    let root_dependencies = root_key
761        .as_deref()
762        .and_then(|root_key| selected_target.get(root_key))
763        .and_then(|value| value.get("dependencies"))
764        .and_then(|value| value.as_object())
765        .cloned()
766        .unwrap_or_default();
767
768    let mut dependencies = Vec::new();
769    let mut iteration_count: usize = 0;
770    for (library_key, target_entry) in selected_target.iter().take(MAX_ITERATION_COUNT) {
771        iteration_count += 1;
772        if iteration_count > MAX_ITERATION_COUNT {
773            warn!(
774                "Iteration limit exceeded in .deps.json at {:?}; stopping at {} dependencies",
775                path, MAX_ITERATION_COUNT
776            );
777            break;
778        }
779        if root_key.as_deref() == Some(library_key.as_str()) {
780            continue;
781        }
782
783        let Some((name, version)) = split_library_key(library_key) else {
784            continue;
785        };
786        let Some(library_metadata) = libraries
787            .get(library_key)
788            .and_then(|value| value.as_object())
789        else {
790            continue;
791        };
792
793        let mut extra_data = serde_json::Map::new();
794        extra_data.insert(
795            "target_name".to_string(),
796            serde_json::Value::String(selected_target_name.clone()),
797        );
798
799        for field in [
800            "type",
801            "sha512",
802            "path",
803            "hashPath",
804            "runtimeStoreManifestName",
805        ] {
806            if let Some(value) = library_metadata.get(field) {
807                extra_data.insert(field.to_string(), value.clone());
808            }
809        }
810
811        if let Some(value) = library_metadata.get("serviceable") {
812            extra_data.insert("serviceable".to_string(), value.clone());
813        }
814
815        if let Some(object) = target_entry.as_object() {
816            for field in ["runtime", "native", "runtimeTargets", "resources"] {
817                if let Some(value) = object.get(field) {
818                    extra_data.insert(field.to_string(), value.clone());
819                }
820            }
821            if let Some(value) = object.get("compileOnly") {
822                extra_data.insert("compileOnly".to_string(), value.clone());
823            }
824        }
825
826        let is_direct = if root_key.is_some() {
827            Some(root_dependencies.contains_key(name))
828        } else {
829            None
830        };
831
832        let compile_only = target_entry
833            .get("compileOnly")
834            .and_then(|value| value.as_bool())
835            .unwrap_or(false);
836
837        dependencies.push(Dependency {
838            purl: build_nuget_purl(Some(name), Some(version)),
839            extracted_requirement: Some(version.to_string()),
840            scope: Some(selected_target_name.clone()),
841            is_runtime: Some(!compile_only),
842            is_optional: Some(compile_only),
843            is_pinned: Some(true),
844            is_direct,
845            resolved_package: None,
846            extra_data: if extra_data.is_empty() {
847                None
848            } else {
849                Some(extra_data.into_iter().collect())
850            },
851        });
852    }
853
854    let mut package_data = if let Some(root_key) = root_key {
855        let (name, version) = split_library_key(&root_key).unwrap_or(("", ""));
856        let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
857        package.name = (!name.is_empty()).then(|| name.to_string());
858        package.version = (!version.is_empty()).then(|| version.to_string());
859        package.purl = build_nuget_purl(package.name.as_deref(), package.version.as_deref());
860        let (repository_homepage_url, repository_download_url, api_data_url) =
861            build_nuget_urls(package.name.as_deref(), package.version.as_deref());
862        package.repository_homepage_url = repository_homepage_url;
863        package.repository_download_url = repository_download_url;
864        package.api_data_url = api_data_url;
865        package
866    } else {
867        let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
868        let file_stem = path
869            .file_name()
870            .and_then(|name| name.to_str())
871            .and_then(|name| name.strip_suffix(".deps.json"))
872            .filter(|name| !name.trim().is_empty())
873            .map(|name| name.to_string());
874        package.name = file_stem.clone();
875        package.purl = build_nuget_purl(file_stem.as_deref(), None);
876        package
877    };
878
879    let mut extra_data = serde_json::Map::new();
880    if let Some(runtime_target) = parsed
881        .get("runtimeTarget")
882        .and_then(|value| value.as_object())
883    {
884        if let Some(name) = runtime_target.get("name").and_then(|value| value.as_str()) {
885            extra_data.insert(
886                "runtime_target_name".to_string(),
887                serde_json::Value::String(name.to_string()),
888            );
889            if let Some((framework, runtime_identifier)) = name.split_once('/') {
890                extra_data.insert(
891                    "target_framework".to_string(),
892                    serde_json::Value::String(framework.to_string()),
893                );
894                extra_data.insert(
895                    "runtime_identifier".to_string(),
896                    serde_json::Value::String(runtime_identifier.to_string()),
897                );
898            } else {
899                extra_data.insert(
900                    "target_framework".to_string(),
901                    serde_json::Value::String(name.to_string()),
902                );
903            }
904        }
905        if let Some(signature) = runtime_target.get("signature") {
906            extra_data.insert("runtime_signature".to_string(), signature.clone());
907        }
908    } else {
909        extra_data.insert(
910            "target_name".to_string(),
911            serde_json::Value::String(selected_target_name.clone()),
912        );
913        if let Some((framework, runtime_identifier)) = selected_target_name.split_once('/') {
914            extra_data.insert(
915                "target_framework".to_string(),
916                serde_json::Value::String(framework.to_string()),
917            );
918            extra_data.insert(
919                "runtime_identifier".to_string(),
920                serde_json::Value::String(runtime_identifier.to_string()),
921            );
922        } else {
923            extra_data.insert(
924                "target_framework".to_string(),
925                serde_json::Value::String(selected_target_name.clone()),
926            );
927        }
928    }
929
930    package_data.dependencies = dependencies;
931    package_data.extra_data = if extra_data.is_empty() {
932        None
933    } else {
934        Some(extra_data.into_iter().collect())
935    };
936    package_data
937}
938
939fn select_deps_target(
940    parsed: &serde_json::Value,
941) -> Option<(String, serde_json::Map<String, serde_json::Value>)> {
942    let targets = parsed.get("targets")?.as_object()?;
943
944    if let Some(runtime_target_name) = parsed
945        .get("runtimeTarget")
946        .and_then(|value| value.get("name"))
947        .and_then(|value| value.as_str())
948        && let Some(target) = targets
949            .get(runtime_target_name)
950            .and_then(|value| value.as_object())
951    {
952        return Some((runtime_target_name.to_string(), target.clone()));
953    }
954
955    if let Some((name, value)) = targets
956        .iter()
957        .find(|(name, value)| name.contains('/') && value.is_object())
958        && let Some(target) = value.as_object()
959    {
960        return Some((name.clone(), target.clone()));
961    }
962
963    targets.iter().find_map(|(name, value)| {
964        value
965            .as_object()
966            .map(|target| (name.clone(), target.clone()))
967    })
968}
969
970fn select_root_library_key(
971    path: &Path,
972    libraries: &serde_json::Map<String, serde_json::Value>,
973    target: &serde_json::Map<String, serde_json::Value>,
974) -> Option<String> {
975    let base_name = path
976        .file_name()
977        .and_then(|name| name.to_str())
978        .and_then(|name| name.strip_suffix(".deps.json"));
979
980    let project_keys: Vec<String> = target
981        .keys()
982        .filter(|key| {
983            libraries
984                .get(*key)
985                .and_then(|value| value.get("type"))
986                .and_then(|value| value.as_str())
987                == Some("project")
988        })
989        .cloned()
990        .collect();
991
992    if let Some(base_name) = base_name
993        && let Some(matched) = project_keys.iter().find(|key| {
994            split_library_key(key)
995                .map(|(name, _)| name.eq_ignore_ascii_case(base_name))
996                .unwrap_or(false)
997        })
998    {
999        return Some(matched.clone());
1000    }
1001
1002    project_keys.into_iter().next()
1003}
1004
1005fn split_library_key(key: &str) -> Option<(&str, &str)> {
1006    key.rsplit_once('/')
1007}
1008
1009#[derive(Default)]
1010struct ProjectReferenceData {
1011    name: Option<String>,
1012    version: Option<String>,
1013    version_override: Option<String>,
1014    condition: Option<String>,
1015}
1016
1017#[derive(Default)]
1018struct CentralPackagePropsData {
1019    dependencies: Vec<Dependency>,
1020    properties: HashMap<String, String>,
1021    import_projects: Vec<String>,
1022    manage_package_versions_centrally: Option<bool>,
1023    central_package_transitive_pinning_enabled: Option<bool>,
1024    central_package_version_override_enabled: Option<bool>,
1025}
1026
1027pub struct ProjectJsonParser;
1028
1029impl PackageParser for ProjectJsonParser {
1030    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
1031
1032    fn is_match(path: &Path) -> bool {
1033        path.file_name()
1034            .and_then(|name| name.to_str())
1035            .is_some_and(|name| name == "project.json")
1036    }
1037
1038    fn extract_packages(path: &Path) -> Vec<PackageData> {
1039        let content = match read_file_to_string(path, None) {
1040            Ok(c) => c,
1041            Err(e) => {
1042                warn!("Failed to read project.json at {:?}: {}", path, e);
1043                return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
1044            }
1045        };
1046
1047        let parsed: serde_json::Value = match serde_json::from_str(&content) {
1048            Ok(value) => value,
1049            Err(e) => {
1050                warn!("Failed to parse project.json at {:?}: {}", path, e);
1051                return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
1052            }
1053        };
1054
1055        vec![parse_project_json_manifest(&parsed)]
1056    }
1057}
1058
1059pub struct ProjectLockJsonParser;
1060
1061impl PackageParser for ProjectLockJsonParser {
1062    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
1063
1064    fn is_match(path: &Path) -> bool {
1065        path.file_name()
1066            .and_then(|name| name.to_str())
1067            .is_some_and(|name| name == "project.lock.json")
1068    }
1069
1070    fn extract_packages(path: &Path) -> Vec<PackageData> {
1071        let content = match read_file_to_string(path, None) {
1072            Ok(c) => c,
1073            Err(e) => {
1074                warn!("Failed to read project.lock.json at {:?}: {}", path, e);
1075                return vec![default_package_data(Some(
1076                    DatasourceId::NugetProjectLockJson,
1077                ))];
1078            }
1079        };
1080
1081        let parsed: serde_json::Value = match serde_json::from_str(&content) {
1082            Ok(value) => value,
1083            Err(e) => {
1084                warn!("Failed to parse project.lock.json at {:?}: {}", path, e);
1085                return vec![default_package_data(Some(
1086                    DatasourceId::NugetProjectLockJson,
1087                ))];
1088            }
1089        };
1090
1091        vec![parse_project_lock_manifest(&parsed)]
1092    }
1093}
1094
1095pub struct PackageReferenceProjectParser;
1096
1097impl PackageParser for PackageReferenceProjectParser {
1098    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
1099
1100    fn is_match(path: &Path) -> bool {
1101        path.extension()
1102            .and_then(|ext| ext.to_str())
1103            .is_some_and(|ext| PROJECT_FILE_EXTENSIONS.contains(&ext))
1104    }
1105
1106    fn extract_packages(path: &Path) -> Vec<PackageData> {
1107        let Some(datasource_id) = project_file_datasource_id(path) else {
1108            return vec![default_package_data(None)];
1109        };
1110
1111        if let Err(e) = check_file_size(path) {
1112            warn!("{}", e);
1113            return vec![default_package_data(Some(datasource_id))];
1114        }
1115
1116        let file = match File::open(path) {
1117            Ok(file) => file,
1118            Err(e) => {
1119                warn!("Failed to open project file at {:?}: {}", path, e);
1120                return vec![default_package_data(Some(datasource_id))];
1121            }
1122        };
1123
1124        let reader = BufReader::new(file);
1125        let mut xml_reader = Reader::from_reader(reader);
1126        xml_reader.config_mut().trim_text(true);
1127
1128        let mut name = None;
1129        let mut fallback_name = path
1130            .file_stem()
1131            .and_then(|stem| stem.to_str())
1132            .map(|stem| stem.to_string());
1133        let mut version = None;
1134        let mut description = None;
1135        let mut homepage_url = None;
1136        let mut authors = None;
1137        let mut repository_url = None;
1138        let mut repository_type = None;
1139        let mut repository_branch = None;
1140        let mut repository_commit = None;
1141        let mut extracted_license_statement = None;
1142        let mut license_type = None;
1143        let mut copyright = None;
1144        let mut readme_file = None;
1145        let mut icon_file = None;
1146        let mut package_references = Vec::new();
1147        let mut project_properties = HashMap::new();
1148
1149        let mut buf = Vec::new();
1150        let mut current_element = String::new();
1151        let mut in_property_group = false;
1152        let mut current_property_group_condition = None;
1153        let mut current_item_group_condition = None;
1154        let mut current_package_reference: Option<ProjectReferenceData> = None;
1155        let mut iteration_count: usize = 0;
1156
1157        loop {
1158            iteration_count += 1;
1159            if iteration_count > MAX_ITERATION_COUNT {
1160                warn!(
1161                    "Iteration limit exceeded in project file at {:?}; stopping at {} items",
1162                    path, MAX_ITERATION_COUNT
1163                );
1164                break;
1165            }
1166            match xml_reader.read_event_into(&mut buf) {
1167                Ok(Event::Start(e)) => {
1168                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1169                    current_element = tag_name.clone();
1170
1171                    match tag_name.as_str() {
1172                        "PropertyGroup" => {
1173                            in_property_group = true;
1174                            current_property_group_condition = e
1175                                .attributes()
1176                                .filter_map(|a| a.ok())
1177                                .find(|attr| attr.key.as_ref() == b"Condition")
1178                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1179                        }
1180                        "ItemGroup" => {
1181                            current_item_group_condition = e
1182                                .attributes()
1183                                .filter_map(|a| a.ok())
1184                                .find(|attr| attr.key.as_ref() == b"Condition")
1185                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1186                        }
1187                        "PackageReference" => {
1188                            let name = e
1189                                .attributes()
1190                                .filter_map(|a| a.ok())
1191                                .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1192                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1193                            let version = e
1194                                .attributes()
1195                                .filter_map(|a| a.ok())
1196                                .find(|attr| attr.key.as_ref() == b"Version")
1197                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1198                            let version_override = e
1199                                .attributes()
1200                                .filter_map(|a| a.ok())
1201                                .find(|attr| attr.key.as_ref() == b"VersionOverride")
1202                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1203                            let condition = e
1204                                .attributes()
1205                                .filter_map(|a| a.ok())
1206                                .find(|attr| attr.key.as_ref() == b"Condition")
1207                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1208                                .or_else(|| current_item_group_condition.clone());
1209
1210                            current_package_reference = Some(ProjectReferenceData {
1211                                name,
1212                                version,
1213                                version_override,
1214                                condition,
1215                            });
1216                        }
1217                        _ => {}
1218                    }
1219                }
1220                Ok(Event::Empty(e)) => {
1221                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1222
1223                    if tag_name == "PackageReference" {
1224                        let name = e
1225                            .attributes()
1226                            .filter_map(|a| a.ok())
1227                            .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1228                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1229                        let version = e
1230                            .attributes()
1231                            .filter_map(|a| a.ok())
1232                            .find(|attr| attr.key.as_ref() == b"Version")
1233                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1234                        let version_override = e
1235                            .attributes()
1236                            .filter_map(|a| a.ok())
1237                            .find(|attr| attr.key.as_ref() == b"VersionOverride")
1238                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1239                        let condition = e
1240                            .attributes()
1241                            .filter_map(|a| a.ok())
1242                            .find(|attr| attr.key.as_ref() == b"Condition")
1243                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1244                            .or_else(|| current_item_group_condition.clone());
1245
1246                        package_references.push(ProjectReferenceData {
1247                            name,
1248                            version,
1249                            version_override,
1250                            condition,
1251                        });
1252                    }
1253                }
1254                Ok(Event::Text(e)) => {
1255                    let text = e.decode().ok().map(|s| s.trim().to_string());
1256                    let Some(text) = text.filter(|value| !value.is_empty()) else {
1257                        buf.clear();
1258                        continue;
1259                    };
1260
1261                    if current_package_reference.is_some() {
1262                        if current_element.as_str() == "Version"
1263                            && let Some(reference) = &mut current_package_reference
1264                        {
1265                            reference.version = Some(text);
1266                        } else if current_element.as_str() == "VersionOverride"
1267                            && let Some(reference) = &mut current_package_reference
1268                        {
1269                            reference.version_override = Some(text);
1270                        }
1271                    } else if in_property_group && current_property_group_condition.is_none() {
1272                        project_properties.insert(current_element.clone(), text.clone());
1273                        match current_element.as_str() {
1274                            "PackageId" => name = Some(text),
1275                            "AssemblyName" if fallback_name.is_none() => fallback_name = Some(text),
1276                            "Version" if version.is_none() => version = Some(text),
1277                            "PackageVersion" => version = Some(text),
1278                            "Description" => description = Some(text),
1279                            "PackageProjectUrl" | "ProjectUrl" => homepage_url = Some(text),
1280                            "Authors" => authors = Some(text),
1281                            "RepositoryUrl" => repository_url = Some(text),
1282                            "RepositoryType" => repository_type = Some(text),
1283                            "RepositoryBranch" => repository_branch = Some(text),
1284                            "RepositoryCommit" => repository_commit = Some(text),
1285                            "PackageLicenseExpression" => {
1286                                extracted_license_statement = Some(text);
1287                                license_type = Some("expression".to_string());
1288                            }
1289                            "PackageLicenseFile" => {
1290                                extracted_license_statement = Some(text);
1291                                license_type = Some("file".to_string());
1292                            }
1293                            "PackageReadmeFile" => readme_file = Some(text),
1294                            "PackageIcon" => icon_file = Some(text),
1295                            "Copyright" => copyright = Some(text),
1296                            _ => {}
1297                        }
1298                    }
1299                }
1300                Ok(Event::End(e)) => {
1301                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1302
1303                    match tag_name.as_str() {
1304                        "PropertyGroup" => {
1305                            in_property_group = false;
1306                            current_property_group_condition = None;
1307                        }
1308                        "ItemGroup" => current_item_group_condition = None,
1309                        "PackageReference" => {
1310                            if let Some(reference) = current_package_reference.take() {
1311                                package_references.push(reference);
1312                            }
1313                        }
1314                        _ => {}
1315                    }
1316
1317                    current_element.clear();
1318                }
1319                Ok(Event::Eof) => break,
1320                Err(e) => {
1321                    warn!("Error parsing project file at {:?}: {}", path, e);
1322                    return vec![default_package_data(Some(datasource_id))];
1323                }
1324                _ => {}
1325            }
1326
1327            buf.clear();
1328        }
1329
1330        let name = name.or(fallback_name);
1331        let vcs_url = repository_url.map(|url| match repository_type {
1332            Some(repo_type) if !repo_type.trim().is_empty() => format!("{}+{}", repo_type, url),
1333            _ => url,
1334        });
1335        let dependencies = package_references
1336            .into_iter()
1337            .filter_map(|reference| {
1338                build_project_file_dependency(
1339                    reference.name,
1340                    reference.version,
1341                    reference.version_override,
1342                    reference.condition,
1343                    &project_properties,
1344                )
1345            })
1346            .collect::<Vec<_>>();
1347        let (repository_homepage_url, repository_download_url, api_data_url) =
1348            build_nuget_urls(name.as_deref(), version.as_deref());
1349
1350        let mut parties = Vec::new();
1351        if let Some(authors) = authors {
1352            parties.push(build_nuget_party("author", authors));
1353        }
1354
1355        let mut extra_data = serde_json::Map::new();
1356        insert_extra_string(&mut extra_data, "license_type", license_type.clone());
1357        if license_type.as_deref() == Some("file") {
1358            insert_extra_string(
1359                &mut extra_data,
1360                "license_file",
1361                extracted_license_statement.clone(),
1362            );
1363        }
1364        insert_extra_string(&mut extra_data, "repository_branch", repository_branch);
1365        insert_extra_string(&mut extra_data, "repository_commit", repository_commit);
1366        insert_extra_string(&mut extra_data, "readme_file", readme_file);
1367        insert_extra_string(&mut extra_data, "icon_file", icon_file);
1368        if let Some(value) = project_properties
1369            .get("CentralPackageVersionOverrideEnabled")
1370            .cloned()
1371        {
1372            extra_data.insert(
1373                "central_package_version_override_enabled_raw".to_string(),
1374                serde_json::Value::String(value),
1375            );
1376        }
1377        if let Some(value) = resolve_bool_property_reference(
1378            project_properties
1379                .get("CentralPackageVersionOverrideEnabled")
1380                .map(String::as_str),
1381            &project_properties,
1382        ) {
1383            extra_data.insert(
1384                "central_package_version_override_enabled".to_string(),
1385                serde_json::Value::Bool(value),
1386            );
1387        }
1388
1389        let (declared_license_expression, declared_license_expression_spdx, license_detections) =
1390            if license_type.as_deref() == Some("expression") {
1391                normalize_spdx_declared_license(extracted_license_statement.as_deref())
1392            } else {
1393                empty_declared_license_data()
1394            };
1395
1396        vec![PackageData {
1397            datasource_id: Some(datasource_id),
1398            package_type: Some(Self::PACKAGE_TYPE),
1399            name: name.clone().map(truncate_field),
1400            version: version.clone().map(truncate_field),
1401            purl: build_nuget_purl(name.as_deref(), version.as_deref()),
1402            description: description.map(truncate_field),
1403            homepage_url: homepage_url.map(truncate_field),
1404            parties,
1405            dependencies,
1406            declared_license_expression,
1407            declared_license_expression_spdx,
1408            license_detections,
1409            extracted_license_statement: extracted_license_statement.map(truncate_field),
1410            copyright: copyright.map(truncate_field),
1411            vcs_url: vcs_url.map(truncate_field),
1412            extra_data: if extra_data.is_empty() {
1413                None
1414            } else {
1415                Some(extra_data.into_iter().collect())
1416            },
1417            repository_homepage_url,
1418            repository_download_url,
1419            api_data_url,
1420            ..default_package_data(Some(datasource_id))
1421        }]
1422    }
1423}
1424
1425fn parse_project_json_manifest(parsed: &serde_json::Value) -> PackageData {
1426    let name = parsed
1427        .get("name")
1428        .and_then(|value| value.as_str())
1429        .map(|value| value.to_string());
1430    let version = parsed
1431        .get("version")
1432        .and_then(|value| value.as_str())
1433        .map(|value| value.to_string());
1434    let description = parsed
1435        .get("description")
1436        .and_then(|value| value.as_str())
1437        .map(|value| value.to_string());
1438    let homepage_url = parsed
1439        .get("projectUrl")
1440        .and_then(|value| value.as_str())
1441        .map(|value| value.to_string());
1442    let extracted_license_statement = parsed
1443        .get("license")
1444        .or_else(|| parsed.get("licenseUrl"))
1445        .and_then(|value| value.as_str())
1446        .map(|value| value.to_string());
1447
1448    let mut parties = Vec::new();
1449    if let Some(authors) = parsed.get("authors") {
1450        let author_name = if let Some(value) = authors.as_str() {
1451            Some(value.to_string())
1452        } else {
1453            authors.as_array().map(|entries| {
1454                entries
1455                    .iter()
1456                    .filter_map(|entry| entry.as_str())
1457                    .collect::<Vec<_>>()
1458                    .join(", ")
1459            })
1460        };
1461
1462        if let Some(author_name) = author_name.filter(|value| !value.is_empty()) {
1463            parties.push(build_nuget_party("author", author_name));
1464        }
1465    }
1466
1467    let mut dependencies = Vec::new();
1468
1469    if let Some(root_dependencies) = parsed
1470        .get("dependencies")
1471        .and_then(|value| value.as_object())
1472    {
1473        for (dependency_name, dependency_spec) in root_dependencies.iter().take(MAX_ITERATION_COUNT)
1474        {
1475            if let Some(dependency) =
1476                parse_project_json_dependency(dependency_name, dependency_spec, None)
1477            {
1478                dependencies.push(dependency);
1479            }
1480        }
1481    }
1482
1483    if let Some(frameworks) = parsed.get("frameworks").and_then(|value| value.as_object()) {
1484        for (framework, framework_value) in frameworks.iter().take(MAX_ITERATION_COUNT) {
1485            let Some(framework_dependencies) = framework_value
1486                .get("dependencies")
1487                .and_then(|value| value.as_object())
1488            else {
1489                continue;
1490            };
1491
1492            for (dependency_name, dependency_spec) in
1493                framework_dependencies.iter().take(MAX_ITERATION_COUNT)
1494            {
1495                if let Some(dependency) = parse_project_json_dependency(
1496                    dependency_name,
1497                    dependency_spec,
1498                    Some(framework.clone()),
1499                ) {
1500                    dependencies.push(dependency);
1501                }
1502            }
1503        }
1504    }
1505
1506    let (repository_homepage_url, repository_download_url, api_data_url) =
1507        build_nuget_urls(name.as_deref(), version.as_deref());
1508
1509    PackageData {
1510        datasource_id: Some(DatasourceId::NugetProjectJson),
1511        package_type: Some(PackageType::Nuget),
1512        name: name.clone().map(truncate_field),
1513        version: version.clone().map(truncate_field),
1514        purl: build_nuget_purl(name.as_deref(), version.as_deref()),
1515        description: description.map(truncate_field),
1516        homepage_url: homepage_url.map(truncate_field),
1517        parties,
1518        dependencies,
1519        extracted_license_statement: extracted_license_statement.map(truncate_field),
1520        repository_homepage_url,
1521        repository_download_url,
1522        api_data_url,
1523        ..default_package_data(Some(DatasourceId::NugetProjectJson))
1524    }
1525}
1526
1527fn parse_project_json_dependency(
1528    dependency_name: &str,
1529    dependency_spec: &serde_json::Value,
1530    scope: Option<String>,
1531) -> Option<Dependency> {
1532    let mut extra_data = serde_json::Map::new();
1533
1534    let requirement = match dependency_spec {
1535        serde_json::Value::String(version) => Some(version.clone()),
1536        serde_json::Value::Object(object) => {
1537            let requirement = object
1538                .get("version")
1539                .and_then(|value| value.as_str())
1540                .map(|value| value.to_string());
1541            insert_extra_string(
1542                &mut extra_data,
1543                "include",
1544                object
1545                    .get("include")
1546                    .and_then(|value| value.as_str())
1547                    .map(|value| value.to_string()),
1548            );
1549            insert_extra_string(
1550                &mut extra_data,
1551                "exclude",
1552                object
1553                    .get("exclude")
1554                    .and_then(|value| value.as_str())
1555                    .map(|value| value.to_string()),
1556            );
1557            insert_extra_string(
1558                &mut extra_data,
1559                "type",
1560                object
1561                    .get("type")
1562                    .and_then(|value| value.as_str())
1563                    .map(|value| value.to_string()),
1564            );
1565            requirement
1566        }
1567        _ => return None,
1568    };
1569
1570    Some(Dependency {
1571        purl: build_nuget_purl(Some(dependency_name), None),
1572        extracted_requirement: requirement,
1573        scope,
1574        is_runtime: Some(true),
1575        is_optional: Some(false),
1576        is_pinned: Some(false),
1577        is_direct: Some(true),
1578        resolved_package: None,
1579        extra_data: if extra_data.is_empty() {
1580            None
1581        } else {
1582            Some(extra_data.into_iter().collect())
1583        },
1584    })
1585}
1586
1587fn parse_project_lock_manifest(parsed: &serde_json::Value) -> PackageData {
1588    let mut dependencies = Vec::new();
1589
1590    if let Some(groups) = parsed
1591        .get("projectFileDependencyGroups")
1592        .and_then(|value| value.as_object())
1593    {
1594        for (framework, entries) in groups.iter().take(MAX_ITERATION_COUNT) {
1595            let Some(entries) = entries.as_array() else {
1596                continue;
1597            };
1598
1599            for entry in entries
1600                .iter()
1601                .take(MAX_ITERATION_COUNT)
1602                .filter_map(|value| value.as_str())
1603            {
1604                if let Some(dependency) = parse_project_lock_dependency(
1605                    entry,
1606                    (!framework.is_empty()).then(|| framework.clone()),
1607                ) {
1608                    dependencies.push(dependency);
1609                }
1610            }
1611        }
1612    }
1613
1614    PackageData {
1615        datasource_id: Some(DatasourceId::NugetProjectLockJson),
1616        package_type: Some(PackageType::Nuget),
1617        dependencies,
1618        ..default_package_data(Some(DatasourceId::NugetProjectLockJson))
1619    }
1620}
1621
1622fn parse_project_lock_dependency(entry: &str, scope: Option<String>) -> Option<Dependency> {
1623    let trimmed = entry.trim();
1624    if trimmed.is_empty() {
1625        return None;
1626    }
1627
1628    let mut parts = trimmed.split_whitespace();
1629    let name = parts.next()?;
1630    let requirement = parts.collect::<Vec<_>>().join(" ");
1631
1632    Some(Dependency {
1633        purl: build_nuget_purl(Some(name), None),
1634        extracted_requirement: (!requirement.is_empty()).then_some(requirement),
1635        scope,
1636        is_runtime: Some(true),
1637        is_optional: Some(false),
1638        is_pinned: Some(false),
1639        is_direct: Some(true),
1640        resolved_package: None,
1641        extra_data: None,
1642    })
1643}
1644
1645fn build_project_file_dependency(
1646    name: Option<String>,
1647    version: Option<String>,
1648    version_override: Option<String>,
1649    condition: Option<String>,
1650    project_properties: &HashMap<String, String>,
1651) -> Option<Dependency> {
1652    let name = name?.trim().to_string();
1653    if name.is_empty() {
1654        return None;
1655    }
1656
1657    let mut extra_data = serde_json::Map::new();
1658    insert_extra_string(&mut extra_data, "condition", condition);
1659    insert_extra_string(
1660        &mut extra_data,
1661        "version_override",
1662        version_override.clone(),
1663    );
1664    insert_extra_string(
1665        &mut extra_data,
1666        "version_override_resolved",
1667        version_override
1668            .as_deref()
1669            .and_then(|value| resolve_string_property_reference(value, project_properties)),
1670    );
1671
1672    Some(Dependency {
1673        purl: build_nuget_purl(Some(&name), None),
1674        extracted_requirement: version,
1675        scope: None,
1676        is_runtime: Some(true),
1677        is_optional: Some(false),
1678        is_pinned: Some(false),
1679        is_direct: Some(true),
1680        resolved_package: None,
1681        extra_data: if extra_data.is_empty() {
1682            None
1683        } else {
1684            Some(extra_data.into_iter().collect())
1685        },
1686    })
1687}
1688
1689#[derive(Default)]
1690struct CentralPackageVersionData {
1691    name: Option<String>,
1692    version: Option<String>,
1693    condition: Option<String>,
1694}
1695
1696#[derive(Default)]
1697struct RawCentralPackagePropsData {
1698    package_versions: Vec<CentralPackageVersionData>,
1699    property_values: HashMap<String, String>,
1700    import_projects: Vec<String>,
1701    manage_package_versions_centrally: Option<String>,
1702    central_package_transitive_pinning_enabled: Option<String>,
1703    central_package_version_override_enabled: Option<String>,
1704}
1705
1706#[derive(Default)]
1707struct RawBuildPropsData {
1708    property_values: HashMap<String, String>,
1709    import_projects: Vec<String>,
1710    manage_package_versions_centrally: Option<String>,
1711    central_package_transitive_pinning_enabled: Option<String>,
1712    central_package_version_override_enabled: Option<String>,
1713}
1714
1715#[derive(Default)]
1716struct BuildPropsData {
1717    property_values: HashMap<String, String>,
1718    import_projects: Vec<String>,
1719    manage_package_versions_centrally: Option<bool>,
1720    central_package_transitive_pinning_enabled: Option<bool>,
1721    central_package_version_override_enabled: Option<bool>,
1722}
1723
1724fn build_directory_packages_dependency(
1725    name: Option<String>,
1726    version: Option<String>,
1727    raw_version: Option<String>,
1728    condition: Option<String>,
1729) -> Option<Dependency> {
1730    let name = name?.trim().to_string();
1731    if name.is_empty() {
1732        return None;
1733    }
1734    let version = version
1735        .map(|value| value.trim().to_string())
1736        .filter(|value| !value.is_empty())?;
1737
1738    let mut extra_data = serde_json::Map::new();
1739    insert_extra_string(&mut extra_data, "condition", condition);
1740    insert_extra_string(&mut extra_data, "version_expression", raw_version);
1741
1742    Some(Dependency {
1743        purl: build_nuget_purl(Some(&name), None),
1744        extracted_requirement: Some(version),
1745        scope: Some("package_version".to_string()),
1746        is_runtime: Some(true),
1747        is_optional: Some(false),
1748        is_pinned: Some(false),
1749        is_direct: Some(true),
1750        resolved_package: None,
1751        extra_data: if extra_data.is_empty() {
1752            None
1753        } else {
1754            Some(extra_data.into_iter().collect())
1755        },
1756    })
1757}
1758
1759fn resolve_directory_packages_props(
1760    path: &Path,
1761    visited: &mut HashSet<PathBuf>,
1762    depth: usize,
1763) -> Result<CentralPackagePropsData, String> {
1764    if depth > MAX_RECURSION_DEPTH {
1765        return Err(format!(
1766            "Recursion depth exceeded ({}) resolving Directory.Packages.props at {:?}",
1767            depth, path
1768        ));
1769    }
1770
1771    let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
1772    if !visited.insert(canonical.clone()) {
1773        return Ok(CentralPackagePropsData::default());
1774    }
1775
1776    let raw = parse_directory_packages_props_file(path)?;
1777    let mut merged = CentralPackagePropsData::default();
1778
1779    for import_project in &raw.import_projects {
1780        let Some(import_path) =
1781            resolve_import_project_for_directory_packages(path, import_project, &HashMap::new())
1782        else {
1783            continue;
1784        };
1785        let imported = resolve_directory_packages_props(&import_path, visited, depth + 1)?;
1786        merge_central_package_props(&mut merged, imported);
1787    }
1788
1789    merged.import_projects.extend(raw.import_projects.clone());
1790    merged.properties.extend(raw.property_values.clone());
1791
1792    if let Some(value) = resolve_bool_property_reference(
1793        raw.manage_package_versions_centrally.as_deref(),
1794        &merged.properties,
1795    ) {
1796        merged.manage_package_versions_centrally = Some(value);
1797    }
1798    if let Some(value) = resolve_bool_property_reference(
1799        raw.central_package_transitive_pinning_enabled.as_deref(),
1800        &merged.properties,
1801    ) {
1802        merged.central_package_transitive_pinning_enabled = Some(value);
1803    }
1804    if let Some(value) = resolve_bool_property_reference(
1805        raw.central_package_version_override_enabled.as_deref(),
1806        &merged.properties,
1807    ) {
1808        merged.central_package_version_override_enabled = Some(value);
1809    }
1810
1811    for entry in raw.package_versions {
1812        let resolved_version =
1813            resolve_optional_property_value(entry.version.as_deref(), &merged.properties);
1814        if let Some(dependency) = build_directory_packages_dependency(
1815            entry.name,
1816            resolved_version,
1817            entry.version,
1818            entry.condition,
1819        ) {
1820            replace_matching_dependency_group(
1821                &mut merged.dependencies,
1822                std::slice::from_ref(&dependency),
1823            );
1824            merged.dependencies.push(dependency);
1825        }
1826    }
1827
1828    Ok(merged)
1829}
1830
1831fn resolve_directory_build_props(
1832    path: &Path,
1833    visited: &mut HashSet<PathBuf>,
1834    depth: usize,
1835) -> Result<BuildPropsData, String> {
1836    if depth > MAX_RECURSION_DEPTH {
1837        return Err(format!(
1838            "Recursion depth exceeded ({}) resolving Directory.Build.props at {:?}",
1839            depth, path
1840        ));
1841    }
1842
1843    let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
1844    if !visited.insert(canonical.clone()) {
1845        return Ok(BuildPropsData::default());
1846    }
1847
1848    let raw = parse_directory_build_props_file(path)?;
1849    let mut merged = BuildPropsData::default();
1850
1851    for import_project in &raw.import_projects {
1852        let Some(import_path) =
1853            resolve_import_project_for_directory_build(path, import_project, &HashMap::new())
1854        else {
1855            continue;
1856        };
1857        let imported = resolve_directory_build_props(&import_path, visited, depth + 1)?;
1858        merge_build_props_data(&mut merged, imported);
1859    }
1860
1861    merged.import_projects.extend(raw.import_projects.clone());
1862    merged.property_values.extend(raw.property_values.clone());
1863
1864    if let Some(value) = resolve_bool_property_reference(
1865        raw.manage_package_versions_centrally.as_deref(),
1866        &merged.property_values,
1867    ) {
1868        merged.manage_package_versions_centrally = Some(value);
1869    }
1870    if let Some(value) = resolve_bool_property_reference(
1871        raw.central_package_transitive_pinning_enabled.as_deref(),
1872        &merged.property_values,
1873    ) {
1874        merged.central_package_transitive_pinning_enabled = Some(value);
1875    }
1876    if let Some(value) = resolve_bool_property_reference(
1877        raw.central_package_version_override_enabled.as_deref(),
1878        &merged.property_values,
1879    ) {
1880        merged.central_package_version_override_enabled = Some(value);
1881    }
1882
1883    Ok(merged)
1884}
1885
1886fn parse_directory_packages_props_file(path: &Path) -> Result<RawCentralPackagePropsData, String> {
1887    check_file_size(path)?;
1888
1889    let file = File::open(path).map_err(|e| {
1890        format!(
1891            "Failed to open Directory.Packages.props at {:?}: {}",
1892            path, e
1893        )
1894    })?;
1895
1896    let reader = BufReader::new(file);
1897    let mut xml_reader = Reader::from_reader(reader);
1898    xml_reader.config_mut().trim_text(true);
1899
1900    let mut raw = RawCentralPackagePropsData::default();
1901    let mut buf = Vec::new();
1902    let mut current_element = String::new();
1903    let mut current_property_group_condition = None;
1904    let mut current_item_group_condition = None;
1905    let mut current_package_version: Option<CentralPackageVersionData> = None;
1906    let mut iteration_count: usize = 0;
1907
1908    loop {
1909        iteration_count += 1;
1910        if iteration_count > MAX_ITERATION_COUNT {
1911            return Err(format!(
1912                "Iteration limit exceeded in Directory.Packages.props at {:?}; stopping at {} items",
1913                path, MAX_ITERATION_COUNT
1914            ));
1915        }
1916        match xml_reader.read_event_into(&mut buf) {
1917            Ok(Event::Start(e)) => {
1918                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1919                current_element = tag_name.clone();
1920
1921                match tag_name.as_str() {
1922                    "ItemGroup" => {
1923                        current_item_group_condition = e
1924                            .attributes()
1925                            .filter_map(|a| a.ok())
1926                            .find(|attr| attr.key.as_ref() == b"Condition")
1927                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1928                    }
1929                    "PackageVersion" => {
1930                        let name = e
1931                            .attributes()
1932                            .filter_map(|a| a.ok())
1933                            .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1934                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1935                        let version = e
1936                            .attributes()
1937                            .filter_map(|a| a.ok())
1938                            .find(|attr| attr.key.as_ref() == b"Version")
1939                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1940                        let condition = e
1941                            .attributes()
1942                            .filter_map(|a| a.ok())
1943                            .find(|attr| attr.key.as_ref() == b"Condition")
1944                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1945                            .or_else(|| current_item_group_condition.clone());
1946
1947                        current_package_version = Some(CentralPackageVersionData {
1948                            name,
1949                            version,
1950                            condition,
1951                        });
1952                    }
1953                    "PropertyGroup" => {
1954                        current_property_group_condition = e
1955                            .attributes()
1956                            .filter_map(|a| a.ok())
1957                            .find(|attr| attr.key.as_ref() == b"Condition")
1958                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1959                    }
1960                    _ => {}
1961                }
1962            }
1963            Ok(Event::Empty(e)) => {
1964                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1965                if tag_name == "PackageVersion" {
1966                    let name = e
1967                        .attributes()
1968                        .filter_map(|a| a.ok())
1969                        .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1970                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1971                    let version = e
1972                        .attributes()
1973                        .filter_map(|a| a.ok())
1974                        .find(|attr| attr.key.as_ref() == b"Version")
1975                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1976                    let condition = e
1977                        .attributes()
1978                        .filter_map(|a| a.ok())
1979                        .find(|attr| attr.key.as_ref() == b"Condition")
1980                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1981                        .or_else(|| current_item_group_condition.clone());
1982
1983                    raw.package_versions.push(CentralPackageVersionData {
1984                        name,
1985                        version,
1986                        condition,
1987                    });
1988                } else if tag_name == "Import"
1989                    && let Some(project) = e
1990                        .attributes()
1991                        .filter_map(|a| a.ok())
1992                        .find(|attr| attr.key.as_ref() == b"Project")
1993                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1994                    && !e
1995                        .attributes()
1996                        .filter_map(|a| a.ok())
1997                        .any(|attr| attr.key.as_ref() == b"Condition")
1998                    && is_supported_directory_packages_import(&project)
1999                {
2000                    raw.import_projects.push(project.trim().to_string());
2001                }
2002            }
2003            Ok(Event::Text(e)) => {
2004                let text = e.decode().ok().map(|s| s.trim().to_string());
2005                let Some(text) = text.filter(|value| !value.is_empty()) else {
2006                    buf.clear();
2007                    continue;
2008                };
2009
2010                if current_package_version.is_some() {
2011                    if current_element.as_str() == "Version"
2012                        && let Some(entry) = &mut current_package_version
2013                    {
2014                        entry.version = Some(text);
2015                    }
2016                } else if current_property_group_condition.is_none() {
2017                    raw.property_values
2018                        .insert(current_element.clone(), text.clone());
2019                    match current_element.as_str() {
2020                        "ManagePackageVersionsCentrally" => {
2021                            raw.manage_package_versions_centrally = Some(text)
2022                        }
2023                        "CentralPackageTransitivePinningEnabled" => {
2024                            raw.central_package_transitive_pinning_enabled = Some(text)
2025                        }
2026                        "CentralPackageVersionOverrideEnabled" => {
2027                            raw.central_package_version_override_enabled = Some(text)
2028                        }
2029                        _ => {}
2030                    }
2031                }
2032            }
2033            Ok(Event::End(e)) => {
2034                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2035
2036                match tag_name.as_str() {
2037                    "PropertyGroup" => current_property_group_condition = None,
2038                    "ItemGroup" => current_item_group_condition = None,
2039                    "PackageVersion" => {
2040                        if let Some(entry) = current_package_version.take() {
2041                            raw.package_versions.push(entry);
2042                        }
2043                    }
2044                    _ => {}
2045                }
2046
2047                current_element.clear();
2048            }
2049            Ok(Event::Eof) => break,
2050            Err(e) => {
2051                return Err(format!(
2052                    "Error parsing Directory.Packages.props at {:?}: {}",
2053                    path, e
2054                ));
2055            }
2056            _ => {}
2057        }
2058
2059        buf.clear();
2060    }
2061
2062    Ok(raw)
2063}
2064
2065fn parse_directory_build_props_file(path: &Path) -> Result<RawBuildPropsData, String> {
2066    check_file_size(path)?;
2067
2068    let file = File::open(path)
2069        .map_err(|e| format!("Failed to open Directory.Build.props at {:?}: {}", path, e))?;
2070
2071    let reader = BufReader::new(file);
2072    let mut xml_reader = Reader::from_reader(reader);
2073    xml_reader.config_mut().trim_text(true);
2074
2075    let mut raw = RawBuildPropsData::default();
2076    let mut buf = Vec::new();
2077    let mut current_element = String::new();
2078    let mut in_property_group = false;
2079    let mut current_property_group_condition = None;
2080    let mut iteration_count: usize = 0;
2081
2082    loop {
2083        iteration_count += 1;
2084        if iteration_count > MAX_ITERATION_COUNT {
2085            return Err(format!(
2086                "Iteration limit exceeded in Directory.Build.props at {:?}; stopping at {} items",
2087                path, MAX_ITERATION_COUNT
2088            ));
2089        }
2090        match xml_reader.read_event_into(&mut buf) {
2091            Ok(Event::Start(e)) => {
2092                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2093                current_element = tag_name.clone();
2094                if tag_name == "PropertyGroup" {
2095                    in_property_group = true;
2096                    current_property_group_condition = e
2097                        .attributes()
2098                        .filter_map(|a| a.ok())
2099                        .find(|attr| attr.key.as_ref() == b"Condition")
2100                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
2101                }
2102            }
2103            Ok(Event::Empty(e)) => {
2104                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2105                if tag_name == "Import"
2106                    && let Some(project) = e
2107                        .attributes()
2108                        .filter_map(|a| a.ok())
2109                        .find(|attr| attr.key.as_ref() == b"Project")
2110                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
2111                    && !e
2112                        .attributes()
2113                        .filter_map(|a| a.ok())
2114                        .any(|attr| attr.key.as_ref() == b"Condition")
2115                    && is_supported_directory_build_import(&project)
2116                {
2117                    raw.import_projects.push(project.trim().to_string());
2118                }
2119            }
2120            Ok(Event::Text(e)) => {
2121                let text = e.decode().ok().map(|s| s.trim().to_string());
2122                let Some(text) = text.filter(|value| !value.is_empty()) else {
2123                    buf.clear();
2124                    continue;
2125                };
2126
2127                if in_property_group && current_property_group_condition.is_none() {
2128                    raw.property_values
2129                        .insert(current_element.clone(), text.clone());
2130                    match current_element.as_str() {
2131                        "ManagePackageVersionsCentrally" => {
2132                            raw.manage_package_versions_centrally = Some(text)
2133                        }
2134                        "CentralPackageTransitivePinningEnabled" => {
2135                            raw.central_package_transitive_pinning_enabled = Some(text)
2136                        }
2137                        "CentralPackageVersionOverrideEnabled" => {
2138                            raw.central_package_version_override_enabled = Some(text)
2139                        }
2140                        _ => {}
2141                    }
2142                }
2143            }
2144            Ok(Event::End(e)) => {
2145                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2146                if tag_name == "PropertyGroup" {
2147                    in_property_group = false;
2148                    current_property_group_condition = None;
2149                }
2150                current_element.clear();
2151            }
2152            Ok(Event::Eof) => break,
2153            Err(e) => {
2154                return Err(format!(
2155                    "Error parsing Directory.Build.props at {:?}: {}",
2156                    path, e
2157                ));
2158            }
2159            _ => {}
2160        }
2161
2162        buf.clear();
2163    }
2164
2165    Ok(raw)
2166}
2167
2168fn build_directory_packages_package_data(
2169    data: CentralPackagePropsData,
2170    raw: RawCentralPackagePropsData,
2171) -> PackageData {
2172    let mut extra_data = serde_json::Map::new();
2173    if !data.properties.is_empty() {
2174        extra_data.insert(
2175            "property_values".to_string(),
2176            serde_json::Value::Object(
2177                data.properties
2178                    .iter()
2179                    .map(|(key, value)| (key.clone(), serde_json::Value::String(value.clone())))
2180                    .collect(),
2181            ),
2182        );
2183    }
2184    if let Some(value) = data.manage_package_versions_centrally {
2185        extra_data.insert(
2186            "manage_package_versions_centrally".to_string(),
2187            serde_json::Value::Bool(value),
2188        );
2189    }
2190    if let Some(value) = data.central_package_transitive_pinning_enabled {
2191        extra_data.insert(
2192            "central_package_transitive_pinning_enabled".to_string(),
2193            serde_json::Value::Bool(value),
2194        );
2195    }
2196    if let Some(value) = data.central_package_version_override_enabled {
2197        extra_data.insert(
2198            "central_package_version_override_enabled".to_string(),
2199            serde_json::Value::Bool(value),
2200        );
2201    }
2202    if !data.import_projects.is_empty() {
2203        extra_data.insert(
2204            "import_projects".to_string(),
2205            serde_json::Value::Array(
2206                data.import_projects
2207                    .into_iter()
2208                    .map(serde_json::Value::String)
2209                    .collect(),
2210            ),
2211        );
2212    }
2213    extra_data.insert(
2214        "package_versions".to_string(),
2215        serde_json::Value::Array(
2216            raw.package_versions
2217                .into_iter()
2218                .map(|entry| {
2219                    serde_json::json!({
2220                        "name": entry.name,
2221                        "version": entry.version,
2222                        "condition": entry.condition,
2223                    })
2224                })
2225                .collect(),
2226        ),
2227    );
2228
2229    PackageData {
2230        datasource_id: Some(DatasourceId::NugetDirectoryPackagesProps),
2231        package_type: Some(PackageType::Nuget),
2232        dependencies: data.dependencies,
2233        extra_data: if extra_data.is_empty() {
2234            None
2235        } else {
2236            Some(extra_data.into_iter().collect())
2237        },
2238        ..default_package_data(Some(DatasourceId::NugetDirectoryPackagesProps))
2239    }
2240}
2241
2242fn build_directory_build_props_package_data(
2243    data: BuildPropsData,
2244    _raw: RawBuildPropsData,
2245) -> PackageData {
2246    let mut extra_data = serde_json::Map::new();
2247    if !data.property_values.is_empty() {
2248        extra_data.insert(
2249            "property_values".to_string(),
2250            serde_json::Value::Object(
2251                data.property_values
2252                    .iter()
2253                    .map(|(key, value)| (key.clone(), serde_json::Value::String(value.clone())))
2254                    .collect(),
2255            ),
2256        );
2257    }
2258    if let Some(value) = data.manage_package_versions_centrally {
2259        extra_data.insert(
2260            "manage_package_versions_centrally".to_string(),
2261            serde_json::Value::Bool(value),
2262        );
2263    }
2264    if let Some(value) = data.central_package_transitive_pinning_enabled {
2265        extra_data.insert(
2266            "central_package_transitive_pinning_enabled".to_string(),
2267            serde_json::Value::Bool(value),
2268        );
2269    }
2270    if let Some(value) = data.central_package_version_override_enabled {
2271        extra_data.insert(
2272            "central_package_version_override_enabled".to_string(),
2273            serde_json::Value::Bool(value),
2274        );
2275    }
2276    if !data.import_projects.is_empty() {
2277        extra_data.insert(
2278            "import_projects".to_string(),
2279            serde_json::Value::Array(
2280                data.import_projects
2281                    .into_iter()
2282                    .map(serde_json::Value::String)
2283                    .collect(),
2284            ),
2285        );
2286    }
2287
2288    PackageData {
2289        datasource_id: Some(DatasourceId::NugetDirectoryBuildProps),
2290        package_type: Some(PackageType::Nuget),
2291        extra_data: if extra_data.is_empty() {
2292            None
2293        } else {
2294            Some(extra_data.into_iter().collect())
2295        },
2296        ..default_package_data(Some(DatasourceId::NugetDirectoryBuildProps))
2297    }
2298}
2299
2300fn merge_central_package_props(
2301    target: &mut CentralPackagePropsData,
2302    source: CentralPackagePropsData,
2303) {
2304    target.import_projects.extend(source.import_projects);
2305    target.properties.extend(source.properties);
2306    if target.manage_package_versions_centrally.is_none() {
2307        target.manage_package_versions_centrally = source.manage_package_versions_centrally;
2308    }
2309    if target.central_package_transitive_pinning_enabled.is_none() {
2310        target.central_package_transitive_pinning_enabled =
2311            source.central_package_transitive_pinning_enabled;
2312    }
2313    if target.central_package_version_override_enabled.is_none() {
2314        target.central_package_version_override_enabled =
2315            source.central_package_version_override_enabled;
2316    }
2317    replace_matching_dependency_group(&mut target.dependencies, &source.dependencies);
2318    target.dependencies.extend(source.dependencies);
2319}
2320
2321fn replace_matching_dependency_group(target: &mut Vec<Dependency>, source: &[Dependency]) {
2322    if source.is_empty() {
2323        return;
2324    }
2325
2326    let source_keys = source.iter().map(dependency_key).collect::<Vec<_>>();
2327    target.retain(|candidate| {
2328        !source_keys
2329            .iter()
2330            .any(|key| *key == dependency_key(candidate))
2331    });
2332}
2333
2334fn dependency_key(dependency: &Dependency) -> (Option<String>, Option<String>, Option<String>) {
2335    (
2336        dependency.purl.clone(),
2337        dependency.scope.clone(),
2338        dependency
2339            .extra_data
2340            .as_ref()
2341            .and_then(|data| data.get("condition"))
2342            .and_then(|value| value.as_str())
2343            .map(ToOwned::to_owned),
2344    )
2345}
2346
2347fn is_supported_directory_packages_import(project: &str) -> bool {
2348    let trimmed = project.trim();
2349    if trimmed.is_empty() {
2350        return false;
2351    }
2352
2353    if is_get_path_of_file_above_import(trimmed) {
2354        return true;
2355    }
2356
2357    let candidate = PathBuf::from(trimmed);
2358    candidate.file_name().and_then(|name| name.to_str()) == Some("Directory.Packages.props")
2359}
2360
2361fn is_supported_directory_build_import(project: &str) -> bool {
2362    let trimmed = project.trim();
2363    if trimmed.is_empty() {
2364        return false;
2365    }
2366
2367    if is_get_path_of_file_above_build_import(trimmed) {
2368        return true;
2369    }
2370
2371    let candidate = PathBuf::from(trimmed);
2372    candidate.file_name().and_then(|name| name.to_str()) == Some("Directory.Build.props")
2373}
2374
2375fn is_get_path_of_file_above_import(project: &str) -> bool {
2376    let normalized = project.replace(' ', "");
2377    normalized
2378        == "$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props,$(MSBuildThisFileDirectory)..))"
2379}
2380
2381fn is_get_path_of_file_above_build_import(project: &str) -> bool {
2382    let normalized = project.replace(' ', "");
2383    normalized
2384        == "$([MSBuild]::GetPathOfFileAbove(Directory.Build.props,$(MSBuildThisFileDirectory)..))"
2385}
2386
2387fn resolve_import_project_for_directory_build(
2388    current_path: &Path,
2389    project: &str,
2390    known_props_paths: &HashMap<PathBuf, &PackageData>,
2391) -> Option<PathBuf> {
2392    let trimmed = project.trim();
2393    if is_get_path_of_file_above_build_import(trimmed) {
2394        let start_dir = current_path.parent()?.parent()?;
2395        for ancestor in start_dir.ancestors() {
2396            let candidate = ancestor.join("Directory.Build.props");
2397            if known_props_paths.is_empty() {
2398                if candidate.exists() {
2399                    return Some(candidate);
2400                }
2401            } else if known_props_paths.contains_key(&candidate) {
2402                return Some(candidate);
2403            }
2404        }
2405        return None;
2406    }
2407
2408    if !is_supported_directory_build_import(trimmed) {
2409        return None;
2410    }
2411
2412    let candidate = PathBuf::from(trimmed);
2413    if candidate.is_absolute() {
2414        if known_props_paths.is_empty() {
2415            candidate.exists().then_some(candidate)
2416        } else {
2417            known_props_paths
2418                .contains_key(&candidate)
2419                .then_some(candidate)
2420        }
2421    } else {
2422        let resolved = current_path.parent()?.join(candidate);
2423        if known_props_paths.is_empty() {
2424            resolved.exists().then_some(resolved)
2425        } else {
2426            known_props_paths
2427                .contains_key(&resolved)
2428                .then_some(resolved)
2429        }
2430    }
2431}
2432
2433fn merge_build_props_data(target: &mut BuildPropsData, source: BuildPropsData) {
2434    target.import_projects.extend(source.import_projects);
2435    target.property_values.extend(source.property_values);
2436    if target.manage_package_versions_centrally.is_none() {
2437        target.manage_package_versions_centrally = source.manage_package_versions_centrally;
2438    }
2439    if target.central_package_transitive_pinning_enabled.is_none() {
2440        target.central_package_transitive_pinning_enabled =
2441            source.central_package_transitive_pinning_enabled;
2442    }
2443    if target.central_package_version_override_enabled.is_none() {
2444        target.central_package_version_override_enabled =
2445            source.central_package_version_override_enabled;
2446    }
2447}
2448
2449fn resolve_import_project_for_directory_packages(
2450    current_path: &Path,
2451    project: &str,
2452    known_props_paths: &HashMap<PathBuf, &PackageData>,
2453) -> Option<PathBuf> {
2454    let trimmed = project.trim();
2455    if is_get_path_of_file_above_import(trimmed) {
2456        let start_dir = current_path.parent()?.parent()?;
2457        for ancestor in start_dir.ancestors() {
2458            let candidate = ancestor.join("Directory.Packages.props");
2459            if known_props_paths.is_empty() {
2460                if candidate.exists() {
2461                    return Some(candidate);
2462                }
2463            } else if known_props_paths.contains_key(&candidate) {
2464                return Some(candidate);
2465            }
2466        }
2467        return None;
2468    }
2469
2470    if !is_supported_directory_packages_import(trimmed) {
2471        return None;
2472    }
2473
2474    let candidate = PathBuf::from(trimmed);
2475    if candidate.is_absolute() {
2476        if known_props_paths.is_empty() {
2477            candidate.exists().then_some(candidate)
2478        } else {
2479            known_props_paths
2480                .contains_key(&candidate)
2481                .then_some(candidate)
2482        }
2483    } else {
2484        let resolved = current_path.parent()?.join(candidate);
2485        if known_props_paths.is_empty() {
2486            resolved.exists().then_some(resolved)
2487        } else {
2488            known_props_paths
2489                .contains_key(&resolved)
2490                .then_some(resolved)
2491        }
2492    }
2493}
2494
2495fn resolve_string_property_reference(
2496    value: &str,
2497    properties: &HashMap<String, String>,
2498) -> Option<String> {
2499    let trimmed = value.trim();
2500    if let Some(property_name) = trimmed
2501        .strip_prefix("$(")
2502        .and_then(|value| value.strip_suffix(')'))
2503    {
2504        properties.get(property_name).cloned()
2505    } else {
2506        Some(trimmed.to_string())
2507    }
2508}
2509
2510fn resolve_bool_property_reference(
2511    value: Option<&str>,
2512    properties: &HashMap<String, String>,
2513) -> Option<bool> {
2514    let resolved = resolve_string_property_reference(value?, properties)?;
2515    Some(resolved.eq_ignore_ascii_case("true"))
2516}
2517
2518fn resolve_optional_property_value(
2519    value: Option<&str>,
2520    properties: &HashMap<String, String>,
2521) -> Option<String> {
2522    let value = value?.trim();
2523    if value.is_empty() {
2524        return None;
2525    }
2526
2527    if value.starts_with("$(") && value.ends_with(')') {
2528        resolve_string_property_reference(value, properties)
2529    } else {
2530        Some(value.to_string())
2531    }
2532}
2533
2534pub struct CentralPackageManagementPropsParser;
2535
2536pub struct DirectoryBuildPropsParser;
2537
2538impl PackageParser for DirectoryBuildPropsParser {
2539    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
2540
2541    fn is_match(path: &Path) -> bool {
2542        path.file_name().and_then(|name| name.to_str()) == Some("Directory.Build.props")
2543    }
2544
2545    fn extract_packages(path: &Path) -> Vec<PackageData> {
2546        vec![match (
2547            resolve_directory_build_props(path, &mut HashSet::new(), 0),
2548            parse_directory_build_props_file(path),
2549        ) {
2550            (Ok(data), Ok(raw)) => build_directory_build_props_package_data(data, raw),
2551            (Err(e), _) | (_, Err(e)) => {
2552                warn!("Error parsing Directory.Build.props at {:?}: {}", path, e);
2553                default_package_data(Some(DatasourceId::NugetDirectoryBuildProps))
2554            }
2555        }]
2556    }
2557}
2558
2559impl PackageParser for CentralPackageManagementPropsParser {
2560    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
2561
2562    fn is_match(path: &Path) -> bool {
2563        path.file_name().and_then(|name| name.to_str()) == Some("Directory.Packages.props")
2564    }
2565
2566    fn extract_packages(path: &Path) -> Vec<PackageData> {
2567        vec![match (
2568            resolve_directory_packages_props(path, &mut HashSet::new(), 0),
2569            parse_directory_packages_props_file(path),
2570        ) {
2571            (Ok(data), Ok(raw)) => build_directory_packages_package_data(data, raw),
2572            (Err(e), _) | (_, Err(e)) => {
2573                warn!(
2574                    "Error parsing Directory.Packages.props at {:?}: {}",
2575                    path, e
2576                );
2577                default_package_data(Some(DatasourceId::NugetDirectoryPackagesProps))
2578            }
2579        }]
2580    }
2581}
2582
2583/// Parser for .nupkg files (NuGet package archives)
2584pub struct NupkgParser;
2585
2586impl PackageParser for NupkgParser {
2587    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
2588
2589    fn is_match(path: &Path) -> bool {
2590        path.extension()
2591            .and_then(|ext| ext.to_str())
2592            .is_some_and(|ext| ext == "nupkg")
2593    }
2594
2595    fn extract_packages(path: &Path) -> Vec<PackageData> {
2596        vec![match extract_nupkg_archive(path) {
2597            Ok(data) => data,
2598            Err(e) => {
2599                warn!("Failed to extract .nupkg at {:?}: {}", path, e);
2600                default_package_data(Some(DatasourceId::NugetNupkg))
2601            }
2602        }]
2603    }
2604}
2605
2606fn extract_nupkg_archive(path: &Path) -> Result<PackageData, String> {
2607    use std::fs;
2608    use zip::ZipArchive;
2609
2610    let file_metadata =
2611        fs::metadata(path).map_err(|e| format!("Failed to read file metadata: {}", e))?;
2612    let archive_size = file_metadata.len();
2613
2614    if archive_size > MAX_ARCHIVE_SIZE {
2615        return Err(format!(
2616            "Archive too large: {} bytes (limit: {} bytes)",
2617            archive_size, MAX_ARCHIVE_SIZE
2618        ));
2619    }
2620
2621    let file = File::open(path).map_err(|e| format!("Failed to open archive: {}", e))?;
2622    let mut archive =
2623        ZipArchive::new(file).map_err(|e| format!("Failed to read ZIP archive: {}", e))?;
2624
2625    for i in 0..archive.len() {
2626        let content = {
2627            let mut entry = archive
2628                .by_index(i)
2629                .map_err(|e| format!("Failed to read ZIP entry: {}", e))?;
2630
2631            let entry_name = entry.name().to_string();
2632            if !entry_name.ends_with(".nuspec") {
2633                continue;
2634            }
2635
2636            let entry_size = entry.size();
2637            if entry_size > MAX_FILE_SIZE {
2638                return Err(format!(
2639                    ".nuspec too large: {} bytes (limit: {} bytes)",
2640                    entry_size, MAX_FILE_SIZE
2641                ));
2642            }
2643
2644            let compressed_size = entry.compressed_size();
2645            if compressed_size > 0 {
2646                let ratio = entry_size as f64 / compressed_size as f64;
2647                if ratio > MAX_COMPRESSION_RATIO {
2648                    return Err(format!(
2649                        "Suspicious compression ratio: {:.2}:1 (limit: {:.0}:1)",
2650                        ratio, MAX_COMPRESSION_RATIO
2651                    ));
2652                }
2653            }
2654
2655            let mut content = String::new();
2656            entry
2657                .read_to_string(&mut content)
2658                .map_err(|e| format!("Failed to read .nuspec: {}", e))?;
2659            content
2660        };
2661
2662        let mut package_data = parse_nuspec_content(&content)?;
2663
2664        let license_file = package_data.extra_data.as_ref().and_then(|extra| {
2665            extra
2666                .get("license_file")
2667                .and_then(|value| value.as_str())
2668                .map(|value| value.to_string())
2669        });
2670
2671        if let Some(license_file) = license_file
2672            && let Some(license_text) = read_nupkg_license_file(&mut archive, &license_file)?
2673        {
2674            package_data.extracted_license_statement = Some(license_text);
2675        }
2676
2677        return Ok(package_data);
2678    }
2679
2680    Err("No .nuspec file found in archive".to_string())
2681}
2682
2683fn read_nupkg_license_file(
2684    archive: &mut zip::ZipArchive<File>,
2685    license_file: &str,
2686) -> Result<Option<String>, String> {
2687    let normalized_target = license_file.replace('\\', "/");
2688
2689    for i in 0..archive.len() {
2690        let mut entry = archive
2691            .by_index(i)
2692            .map_err(|e| format!("Failed to read ZIP entry: {}", e))?;
2693        let entry_name = entry.name().replace('\\', "/");
2694
2695        if entry_name != normalized_target
2696            && !entry_name.ends_with(&format!("/{}", normalized_target))
2697        {
2698            continue;
2699        }
2700
2701        let entry_size = entry.size();
2702        if entry_size > MAX_FILE_SIZE {
2703            return Err(format!(
2704                "License file too large: {} bytes (limit: {} bytes)",
2705                entry_size, MAX_FILE_SIZE
2706            ));
2707        }
2708
2709        let mut content = Vec::new();
2710        entry
2711            .read_to_end(&mut content)
2712            .map_err(|e| format!("Failed to read license file from archive: {}", e))?;
2713
2714        return Ok(Some(String::from_utf8_lossy(&content).to_string()));
2715    }
2716
2717    Ok(None)
2718}
2719
2720fn parse_nuspec_content(content: &str) -> Result<PackageData, String> {
2721    use quick_xml::Reader;
2722
2723    let mut xml_reader = Reader::from_str(content);
2724    xml_reader.config_mut().trim_text(true);
2725
2726    let mut name = None;
2727    let mut version = None;
2728    let mut description = None;
2729    let mut homepage_url = None;
2730    let mut parties = Vec::new();
2731    let mut dependencies = Vec::new();
2732    let mut extracted_license_statement = None;
2733    let mut license_type = None;
2734    let mut copyright = None;
2735    let mut vcs_url = None;
2736    let mut repository_branch = None;
2737    let mut repository_commit = None;
2738
2739    let mut buf = Vec::new();
2740    let mut current_element = String::new();
2741    let mut in_metadata = false;
2742    let mut in_dependencies = false;
2743    let mut current_group_framework = None;
2744    let mut iteration_count: usize = 0;
2745
2746    loop {
2747        iteration_count += 1;
2748        if iteration_count > MAX_ITERATION_COUNT {
2749            return Err(format!(
2750                "Iteration limit exceeded parsing .nuspec content; stopping at {} items",
2751                MAX_ITERATION_COUNT
2752            ));
2753        }
2754        match xml_reader.read_event_into(&mut buf) {
2755            Ok(Event::Start(e)) => {
2756                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2757                current_element = tag_name.clone();
2758
2759                if tag_name == "metadata" {
2760                    in_metadata = true;
2761                } else if tag_name == "dependencies" && in_metadata {
2762                    in_dependencies = true;
2763                } else if tag_name == "group" && in_dependencies {
2764                    current_group_framework = e
2765                        .attributes()
2766                        .filter_map(|a| a.ok())
2767                        .find(|attr| attr.key.as_ref() == b"targetFramework")
2768                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
2769                } else if tag_name == "repository" && in_metadata {
2770                    let repository = parse_repository_metadata(&e);
2771                    vcs_url = repository.vcs_url;
2772                    repository_branch = repository.branch;
2773                    repository_commit = repository.commit;
2774                } else if tag_name == "license" && in_metadata {
2775                    license_type = e
2776                        .attributes()
2777                        .filter_map(|a| a.ok())
2778                        .find(|attr| attr.key.as_ref() == b"type")
2779                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
2780                }
2781            }
2782            Ok(Event::Empty(e)) => {
2783                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2784
2785                if tag_name == "dependency" && in_dependencies {
2786                    if let Some(dep) =
2787                        parse_nuspec_dependency(&e, current_group_framework.as_deref())
2788                    {
2789                        dependencies.push(dep);
2790                    }
2791                } else if tag_name == "repository" && in_metadata {
2792                    let repository = parse_repository_metadata(&e);
2793                    vcs_url = repository.vcs_url;
2794                    repository_branch = repository.branch;
2795                    repository_commit = repository.commit;
2796                }
2797            }
2798            Ok(Event::Text(e)) => {
2799                if !in_metadata {
2800                    continue;
2801                }
2802
2803                let text = e.decode().ok().map(|s| s.trim().to_string());
2804                if let Some(text) = text.filter(|s| !s.is_empty()) {
2805                    match current_element.as_str() {
2806                        "id" => name = Some(text),
2807                        "version" => version = Some(text),
2808                        "description" => description = Some(text),
2809                        "projectUrl" => homepage_url = Some(text),
2810                        "authors" => {
2811                            parties.push(build_nuget_party("author", text));
2812                        }
2813                        "owners" => {
2814                            parties.push(build_nuget_party("owner", text));
2815                        }
2816                        "license" => {
2817                            extracted_license_statement = Some(text);
2818                        }
2819                        "licenseUrl" => {
2820                            if extracted_license_statement.is_none() {
2821                                extracted_license_statement = Some(text);
2822                            }
2823                        }
2824                        "copyright" => copyright = Some(text),
2825                        _ => {}
2826                    }
2827                }
2828            }
2829            Ok(Event::End(e)) => {
2830                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2831
2832                if tag_name == "metadata" {
2833                    in_metadata = false;
2834                } else if tag_name == "dependencies" {
2835                    in_dependencies = false;
2836                } else if tag_name == "group" {
2837                    current_group_framework = None;
2838                }
2839
2840                current_element.clear();
2841            }
2842            Ok(Event::Eof) => break,
2843            Err(e) => {
2844                return Err(format!("XML parsing error: {}", e));
2845            }
2846            _ => {}
2847        }
2848        buf.clear();
2849    }
2850
2851    let (repository_homepage_url, repository_download_url, api_data_url) =
2852        build_nuget_urls(name.as_deref(), version.as_deref());
2853
2854    let (declared_license_expression, declared_license_expression_spdx, license_detections) =
2855        if license_type.as_deref() == Some("expression") {
2856            normalize_spdx_declared_license(extracted_license_statement.as_deref())
2857        } else {
2858            empty_declared_license_data()
2859        };
2860
2861    let holder = None;
2862
2863    let mut extra_data = serde_json::Map::new();
2864    insert_extra_string(&mut extra_data, "license_type", license_type.clone());
2865    if license_type.as_deref() == Some("file") {
2866        insert_extra_string(
2867            &mut extra_data,
2868            "license_file",
2869            extracted_license_statement.clone(),
2870        );
2871    }
2872    insert_extra_string(&mut extra_data, "repository_branch", repository_branch);
2873    insert_extra_string(&mut extra_data, "repository_commit", repository_commit);
2874
2875    Ok(PackageData {
2876        datasource_id: Some(DatasourceId::NugetNupkg),
2877        package_type: Some(NupkgParser::PACKAGE_TYPE),
2878        name: name.map(truncate_field),
2879        version: version.map(truncate_field),
2880        description: description.map(truncate_field),
2881        homepage_url: homepage_url.map(truncate_field),
2882        parties,
2883        dependencies,
2884        declared_license_expression,
2885        declared_license_expression_spdx,
2886        license_detections,
2887        extracted_license_statement: extracted_license_statement.map(truncate_field),
2888        copyright: copyright.map(truncate_field),
2889        holder,
2890        vcs_url: vcs_url.map(truncate_field),
2891        extra_data: if extra_data.is_empty() {
2892            None
2893        } else {
2894            Some(extra_data.into_iter().collect())
2895        },
2896        repository_homepage_url,
2897        repository_download_url,
2898        api_data_url,
2899        ..default_package_data(Some(DatasourceId::NugetNupkg))
2900    })
2901}
2902
2903crate::register_parser!(
2904    ".NET Directory.Build.props property source",
2905    &["**/Directory.Build.props"],
2906    "nuget",
2907    "C#",
2908    Some(
2909        "https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022"
2910    ),
2911);
2912
2913crate::register_parser!(
2914    ".NET Directory.Packages.props central package management manifest",
2915    &["**/Directory.Packages.props"],
2916    "nuget",
2917    "C#",
2918    Some("https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management"),
2919);
2920
2921crate::register_parser!(
2922    ".NET packages.config manifest",
2923    &["**/packages.config"],
2924    "nuget",
2925    "C#",
2926    Some("https://learn.microsoft.com/en-us/nuget/reference/packages-config"),
2927);
2928
2929crate::register_parser!(
2930    ".NET .nuspec package specification",
2931    &["**/*.nuspec"],
2932    "nuget",
2933    "C#",
2934    Some("https://learn.microsoft.com/en-us/nuget/reference/nuspec"),
2935);
2936
2937crate::register_parser!(
2938    ".NET packages.lock.json lockfile",
2939    &["**/packages.lock.json"],
2940    "nuget",
2941    "C#",
2942    Some(
2943        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies"
2944    ),
2945);
2946
2947crate::register_parser!(
2948    ".NET project.json manifest",
2949    &["**/project.json"],
2950    "nuget",
2951    "C#",
2952    Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
2953);
2954
2955crate::register_parser!(
2956    ".NET project.lock.json lockfile",
2957    &["**/project.lock.json"],
2958    "nuget",
2959    "C#",
2960    Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
2961);
2962
2963crate::register_parser!(
2964    ".NET .deps.json runtime dependency graph",
2965    &["**/*.deps.json"],
2966    "nuget",
2967    "C#",
2968    Some("https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing"),
2969);
2970
2971crate::register_parser!(
2972    ".NET PackageReference C# project file",
2973    &["**/*.csproj"],
2974    "nuget",
2975    "C#",
2976    Some(
2977        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files"
2978    ),
2979);
2980
2981crate::register_parser!(
2982    ".NET PackageReference Visual Basic project file",
2983    &["**/*.vbproj"],
2984    "nuget",
2985    "Visual Basic .NET",
2986    Some(
2987        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files"
2988    ),
2989);
2990
2991crate::register_parser!(
2992    ".NET PackageReference F# project file",
2993    &["**/*.fsproj"],
2994    "nuget",
2995    "F#",
2996    Some(
2997        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files"
2998    ),
2999);
3000
3001crate::register_parser!(
3002    ".NET .nupkg package archive",
3003    &["**/*.nupkg"],
3004    "nuget",
3005    "C#",
3006    Some("https://learn.microsoft.com/en-us/nuget/create-packages/creating-a-package"),
3007);