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;
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::{
40    MAX_ITERATION_COUNT, MAX_MANIFEST_SIZE, RecursionGuard, read_file_to_string, truncate_field,
41};
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
600const MAX_UNCOMPRESSED_SIZE: u64 = 1024 * 1024 * 1024; // 1GB
601
602/// Parser for packages.lock.json (NuGet lock file)
603pub struct PackagesLockParser;
604
605impl PackageParser for PackagesLockParser {
606    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
607
608    fn is_match(path: &Path) -> bool {
609        path.file_name()
610            .and_then(|name| name.to_str())
611            .is_some_and(|name| name.ends_with("packages.lock.json"))
612    }
613
614    fn extract_packages(path: &Path) -> Vec<PackageData> {
615        let content = match read_file_to_string(path, None) {
616            Ok(c) => c,
617            Err(e) => {
618                warn!("Failed to read packages.lock.json at {:?}: {}", path, e);
619                return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
620            }
621        };
622
623        let parsed: serde_json::Value = match serde_json::from_str(&content) {
624            Ok(v) => v,
625            Err(e) => {
626                warn!("Failed to parse packages.lock.json at {:?}: {}", path, e);
627                return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
628            }
629        };
630
631        let mut dependencies = Vec::new();
632        let mut iteration_count: usize = 0;
633
634        if let Some(deps_obj) = parsed.get("dependencies").and_then(|v| v.as_object()) {
635            for (target_framework, packages) in deps_obj.iter().take(MAX_ITERATION_COUNT) {
636                if let Some(packages_obj) = packages.as_object() {
637                    for (package_name, package_info) in
638                        packages_obj.iter().take(MAX_ITERATION_COUNT)
639                    {
640                        iteration_count += 1;
641                        if iteration_count > MAX_ITERATION_COUNT {
642                            warn!(
643                                "Iteration limit exceeded in packages.lock.json at {:?}; stopping at {} dependencies",
644                                path, MAX_ITERATION_COUNT
645                            );
646                            break;
647                        }
648                        if let Some(info_obj) = package_info.as_object() {
649                            let version = info_obj
650                                .get("resolved")
651                                .and_then(|v| v.as_str())
652                                .map(|s| s.to_string());
653
654                            let requested = info_obj
655                                .get("requested")
656                                .and_then(|v| v.as_str())
657                                .map(|s| s.to_string());
658
659                            let package_type = info_obj.get("type").and_then(|v| v.as_str());
660
661                            let is_direct = match package_type {
662                                Some("Direct") => Some(true),
663                                Some("Transitive") => Some(false),
664                                _ => None,
665                            };
666
667                            let purl = version.as_ref().and_then(|v| {
668                                PackageUrl::new("nuget", package_name).ok().map(|mut p| {
669                                    let _ = p.with_version(v);
670                                    p.to_string()
671                                })
672                            });
673
674                            let mut extra_data = serde_json::Map::new();
675                            extra_data.insert(
676                                "target_framework".to_string(),
677                                serde_json::Value::String(target_framework.clone()),
678                            );
679
680                            if let Some(content_hash) =
681                                info_obj.get("contentHash").and_then(|v| v.as_str())
682                            {
683                                extra_data.insert(
684                                    "content_hash".to_string(),
685                                    serde_json::Value::String(content_hash.to_string()),
686                                );
687                            }
688
689                            dependencies.push(Dependency {
690                                purl,
691                                extracted_requirement: requested.or(version),
692                                scope: Some(target_framework.clone()),
693                                is_runtime: Some(true),
694                                is_optional: Some(false),
695                                is_pinned: Some(true),
696                                is_direct,
697                                resolved_package: None,
698                                extra_data: if extra_data.is_empty() {
699                                    None
700                                } else {
701                                    Some(extra_data.into_iter().collect())
702                                },
703                            });
704                        }
705                    }
706                }
707            }
708        }
709
710        vec![PackageData {
711            datasource_id: Some(DatasourceId::NugetPackagesLock),
712            package_type: Some(Self::PACKAGE_TYPE),
713            dependencies,
714            ..default_package_data(Some(DatasourceId::NugetPackagesLock))
715        }]
716    }
717}
718
719pub struct DotNetDepsJsonParser;
720
721impl PackageParser for DotNetDepsJsonParser {
722    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
723
724    fn is_match(path: &Path) -> bool {
725        path.file_name()
726            .and_then(|name| name.to_str())
727            .is_some_and(|name| name.ends_with(".deps.json"))
728    }
729
730    fn extract_packages(path: &Path) -> Vec<PackageData> {
731        let content = match read_file_to_string(path, None) {
732            Ok(c) => c,
733            Err(e) => {
734                warn!("Failed to read .deps.json at {:?}: {}", path, e);
735                return vec![default_package_data(Some(DatasourceId::NugetDepsJson))];
736            }
737        };
738
739        let parsed: serde_json::Value = match serde_json::from_str(&content) {
740            Ok(value) => value,
741            Err(e) => {
742                warn!("Failed to parse .deps.json at {:?}: {}", path, e);
743                return vec![default_package_data(Some(DatasourceId::NugetDepsJson))];
744            }
745        };
746
747        vec![parse_dotnet_deps_json(&parsed, path)]
748    }
749}
750
751fn parse_dotnet_deps_json(parsed: &serde_json::Value, path: &Path) -> PackageData {
752    let Some(libraries) = parsed.get("libraries").and_then(|value| value.as_object()) else {
753        return default_package_data(Some(DatasourceId::NugetDepsJson));
754    };
755
756    let Some((selected_target_name, selected_target)) = select_deps_target(parsed) else {
757        return default_package_data(Some(DatasourceId::NugetDepsJson));
758    };
759
760    let root_key = select_root_library_key(path, libraries, &selected_target);
761    let root_dependencies = root_key
762        .as_deref()
763        .and_then(|root_key| selected_target.get(root_key))
764        .and_then(|value| value.get("dependencies"))
765        .and_then(|value| value.as_object())
766        .cloned()
767        .unwrap_or_default();
768
769    let mut dependencies = Vec::new();
770    let mut iteration_count: usize = 0;
771    for (library_key, target_entry) in selected_target.iter().take(MAX_ITERATION_COUNT) {
772        iteration_count += 1;
773        if iteration_count > MAX_ITERATION_COUNT {
774            warn!(
775                "Iteration limit exceeded in .deps.json at {:?}; stopping at {} dependencies",
776                path, MAX_ITERATION_COUNT
777            );
778            break;
779        }
780        if root_key.as_deref() == Some(library_key.as_str()) {
781            continue;
782        }
783
784        let Some((name, version)) = split_library_key(library_key) else {
785            continue;
786        };
787        let Some(library_metadata) = libraries
788            .get(library_key)
789            .and_then(|value| value.as_object())
790        else {
791            continue;
792        };
793
794        let mut extra_data = serde_json::Map::new();
795        extra_data.insert(
796            "target_name".to_string(),
797            serde_json::Value::String(selected_target_name.clone()),
798        );
799
800        for field in [
801            "type",
802            "sha512",
803            "path",
804            "hashPath",
805            "runtimeStoreManifestName",
806        ] {
807            if let Some(value) = library_metadata.get(field) {
808                extra_data.insert(field.to_string(), value.clone());
809            }
810        }
811
812        if let Some(value) = library_metadata.get("serviceable") {
813            extra_data.insert("serviceable".to_string(), value.clone());
814        }
815
816        if let Some(object) = target_entry.as_object() {
817            for field in ["runtime", "native", "runtimeTargets", "resources"] {
818                if let Some(value) = object.get(field) {
819                    extra_data.insert(field.to_string(), value.clone());
820                }
821            }
822            if let Some(value) = object.get("compileOnly") {
823                extra_data.insert("compileOnly".to_string(), value.clone());
824            }
825        }
826
827        let is_direct = if root_key.is_some() {
828            Some(root_dependencies.contains_key(name))
829        } else {
830            None
831        };
832
833        let compile_only = target_entry
834            .get("compileOnly")
835            .and_then(|value| value.as_bool())
836            .unwrap_or(false);
837
838        dependencies.push(Dependency {
839            purl: build_nuget_purl(Some(name), Some(version)),
840            extracted_requirement: Some(version.to_string()),
841            scope: Some(selected_target_name.clone()),
842            is_runtime: Some(!compile_only),
843            is_optional: Some(compile_only),
844            is_pinned: Some(true),
845            is_direct,
846            resolved_package: None,
847            extra_data: if extra_data.is_empty() {
848                None
849            } else {
850                Some(extra_data.into_iter().collect())
851            },
852        });
853    }
854
855    let mut package_data = if let Some(root_key) = root_key {
856        let (name, version) = split_library_key(&root_key).unwrap_or(("", ""));
857        let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
858        package.name = (!name.is_empty()).then(|| name.to_string());
859        package.version = (!version.is_empty()).then(|| version.to_string());
860        package.purl = build_nuget_purl(package.name.as_deref(), package.version.as_deref());
861        let (repository_homepage_url, repository_download_url, api_data_url) =
862            build_nuget_urls(package.name.as_deref(), package.version.as_deref());
863        package.repository_homepage_url = repository_homepage_url;
864        package.repository_download_url = repository_download_url;
865        package.api_data_url = api_data_url;
866        package
867    } else {
868        let mut package = default_package_data(Some(DatasourceId::NugetDepsJson));
869        let file_stem = path
870            .file_name()
871            .and_then(|name| name.to_str())
872            .and_then(|name| name.strip_suffix(".deps.json"))
873            .filter(|name| !name.trim().is_empty())
874            .map(|name| name.to_string());
875        package.name = file_stem.clone();
876        package.purl = build_nuget_purl(file_stem.as_deref(), None);
877        package
878    };
879
880    let mut extra_data = serde_json::Map::new();
881    if let Some(runtime_target) = parsed
882        .get("runtimeTarget")
883        .and_then(|value| value.as_object())
884    {
885        if let Some(name) = runtime_target.get("name").and_then(|value| value.as_str()) {
886            extra_data.insert(
887                "runtime_target_name".to_string(),
888                serde_json::Value::String(name.to_string()),
889            );
890            if let Some((framework, runtime_identifier)) = name.split_once('/') {
891                extra_data.insert(
892                    "target_framework".to_string(),
893                    serde_json::Value::String(framework.to_string()),
894                );
895                extra_data.insert(
896                    "runtime_identifier".to_string(),
897                    serde_json::Value::String(runtime_identifier.to_string()),
898                );
899            } else {
900                extra_data.insert(
901                    "target_framework".to_string(),
902                    serde_json::Value::String(name.to_string()),
903                );
904            }
905        }
906        if let Some(signature) = runtime_target.get("signature") {
907            extra_data.insert("runtime_signature".to_string(), signature.clone());
908        }
909    } else {
910        extra_data.insert(
911            "target_name".to_string(),
912            serde_json::Value::String(selected_target_name.clone()),
913        );
914        if let Some((framework, runtime_identifier)) = selected_target_name.split_once('/') {
915            extra_data.insert(
916                "target_framework".to_string(),
917                serde_json::Value::String(framework.to_string()),
918            );
919            extra_data.insert(
920                "runtime_identifier".to_string(),
921                serde_json::Value::String(runtime_identifier.to_string()),
922            );
923        } else {
924            extra_data.insert(
925                "target_framework".to_string(),
926                serde_json::Value::String(selected_target_name.clone()),
927            );
928        }
929    }
930
931    package_data.dependencies = dependencies;
932    package_data.extra_data = if extra_data.is_empty() {
933        None
934    } else {
935        Some(extra_data.into_iter().collect())
936    };
937    package_data
938}
939
940fn select_deps_target(
941    parsed: &serde_json::Value,
942) -> Option<(String, serde_json::Map<String, serde_json::Value>)> {
943    let targets = parsed.get("targets")?.as_object()?;
944
945    if let Some(runtime_target_name) = parsed
946        .get("runtimeTarget")
947        .and_then(|value| value.get("name"))
948        .and_then(|value| value.as_str())
949        && let Some(target) = targets
950            .get(runtime_target_name)
951            .and_then(|value| value.as_object())
952    {
953        return Some((runtime_target_name.to_string(), target.clone()));
954    }
955
956    if let Some((name, value)) = targets
957        .iter()
958        .find(|(name, value)| name.contains('/') && value.is_object())
959        && let Some(target) = value.as_object()
960    {
961        return Some((name.clone(), target.clone()));
962    }
963
964    targets.iter().find_map(|(name, value)| {
965        value
966            .as_object()
967            .map(|target| (name.clone(), target.clone()))
968    })
969}
970
971fn select_root_library_key(
972    path: &Path,
973    libraries: &serde_json::Map<String, serde_json::Value>,
974    target: &serde_json::Map<String, serde_json::Value>,
975) -> Option<String> {
976    let base_name = path
977        .file_name()
978        .and_then(|name| name.to_str())
979        .and_then(|name| name.strip_suffix(".deps.json"));
980
981    let project_keys: Vec<String> = target
982        .keys()
983        .filter(|key| {
984            libraries
985                .get(*key)
986                .and_then(|value| value.get("type"))
987                .and_then(|value| value.as_str())
988                == Some("project")
989        })
990        .cloned()
991        .collect();
992
993    if let Some(base_name) = base_name
994        && let Some(matched) = project_keys.iter().find(|key| {
995            split_library_key(key)
996                .map(|(name, _)| name.eq_ignore_ascii_case(base_name))
997                .unwrap_or(false)
998        })
999    {
1000        return Some(matched.clone());
1001    }
1002
1003    project_keys.into_iter().next()
1004}
1005
1006fn split_library_key(key: &str) -> Option<(&str, &str)> {
1007    key.rsplit_once('/')
1008}
1009
1010#[derive(Default)]
1011struct ProjectReferenceData {
1012    name: Option<String>,
1013    version: Option<String>,
1014    version_override: Option<String>,
1015    condition: Option<String>,
1016}
1017
1018#[derive(Default)]
1019struct CentralPackagePropsData {
1020    dependencies: Vec<Dependency>,
1021    properties: HashMap<String, String>,
1022    import_projects: Vec<String>,
1023    manage_package_versions_centrally: Option<bool>,
1024    central_package_transitive_pinning_enabled: Option<bool>,
1025    central_package_version_override_enabled: Option<bool>,
1026}
1027
1028pub struct ProjectJsonParser;
1029
1030impl PackageParser for ProjectJsonParser {
1031    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
1032
1033    fn is_match(path: &Path) -> bool {
1034        path.file_name()
1035            .and_then(|name| name.to_str())
1036            .is_some_and(|name| name == "project.json")
1037    }
1038
1039    fn extract_packages(path: &Path) -> Vec<PackageData> {
1040        let content = match read_file_to_string(path, None) {
1041            Ok(c) => c,
1042            Err(e) => {
1043                warn!("Failed to read project.json at {:?}: {}", path, e);
1044                return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
1045            }
1046        };
1047
1048        let parsed: serde_json::Value = match serde_json::from_str(&content) {
1049            Ok(value) => value,
1050            Err(e) => {
1051                warn!("Failed to parse project.json at {:?}: {}", path, e);
1052                return vec![default_package_data(Some(DatasourceId::NugetProjectJson))];
1053            }
1054        };
1055
1056        vec![parse_project_json_manifest(&parsed)]
1057    }
1058}
1059
1060pub struct ProjectLockJsonParser;
1061
1062impl PackageParser for ProjectLockJsonParser {
1063    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
1064
1065    fn is_match(path: &Path) -> bool {
1066        path.file_name()
1067            .and_then(|name| name.to_str())
1068            .is_some_and(|name| name == "project.lock.json")
1069    }
1070
1071    fn extract_packages(path: &Path) -> Vec<PackageData> {
1072        let content = match read_file_to_string(path, None) {
1073            Ok(c) => c,
1074            Err(e) => {
1075                warn!("Failed to read project.lock.json at {:?}: {}", path, e);
1076                return vec![default_package_data(Some(
1077                    DatasourceId::NugetProjectLockJson,
1078                ))];
1079            }
1080        };
1081
1082        let parsed: serde_json::Value = match serde_json::from_str(&content) {
1083            Ok(value) => value,
1084            Err(e) => {
1085                warn!("Failed to parse project.lock.json at {:?}: {}", path, e);
1086                return vec![default_package_data(Some(
1087                    DatasourceId::NugetProjectLockJson,
1088                ))];
1089            }
1090        };
1091
1092        vec![parse_project_lock_manifest(&parsed)]
1093    }
1094}
1095
1096pub struct PackageReferenceProjectParser;
1097
1098impl PackageParser for PackageReferenceProjectParser {
1099    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
1100
1101    fn is_match(path: &Path) -> bool {
1102        path.extension()
1103            .and_then(|ext| ext.to_str())
1104            .is_some_and(|ext| PROJECT_FILE_EXTENSIONS.contains(&ext))
1105    }
1106
1107    fn extract_packages(path: &Path) -> Vec<PackageData> {
1108        let Some(datasource_id) = project_file_datasource_id(path) else {
1109            return vec![default_package_data(None)];
1110        };
1111
1112        if let Err(e) = check_file_size(path) {
1113            warn!("{}", e);
1114            return vec![default_package_data(Some(datasource_id))];
1115        }
1116
1117        let file = match File::open(path) {
1118            Ok(file) => file,
1119            Err(e) => {
1120                warn!("Failed to open project file at {:?}: {}", path, e);
1121                return vec![default_package_data(Some(datasource_id))];
1122            }
1123        };
1124
1125        let reader = BufReader::new(file);
1126        let mut xml_reader = Reader::from_reader(reader);
1127        xml_reader.config_mut().trim_text(true);
1128
1129        let mut name = None;
1130        let mut fallback_name = path
1131            .file_stem()
1132            .and_then(|stem| stem.to_str())
1133            .map(|stem| stem.to_string());
1134        let mut version = None;
1135        let mut description = None;
1136        let mut homepage_url = None;
1137        let mut authors = None;
1138        let mut repository_url = None;
1139        let mut repository_type = None;
1140        let mut repository_branch = None;
1141        let mut repository_commit = None;
1142        let mut extracted_license_statement = None;
1143        let mut license_type = None;
1144        let mut copyright = None;
1145        let mut readme_file = None;
1146        let mut icon_file = None;
1147        let mut package_references = Vec::new();
1148        let mut project_properties = HashMap::new();
1149
1150        let mut buf = Vec::new();
1151        let mut current_element = String::new();
1152        let mut in_property_group = false;
1153        let mut current_property_group_condition = None;
1154        let mut current_item_group_condition = None;
1155        let mut current_package_reference: Option<ProjectReferenceData> = None;
1156        let mut iteration_count: usize = 0;
1157
1158        loop {
1159            iteration_count += 1;
1160            if iteration_count > MAX_ITERATION_COUNT {
1161                warn!(
1162                    "Iteration limit exceeded in project file at {:?}; stopping at {} items",
1163                    path, MAX_ITERATION_COUNT
1164                );
1165                break;
1166            }
1167            match xml_reader.read_event_into(&mut buf) {
1168                Ok(Event::Start(e)) => {
1169                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1170                    current_element = tag_name.clone();
1171
1172                    match tag_name.as_str() {
1173                        "PropertyGroup" => {
1174                            in_property_group = true;
1175                            current_property_group_condition = e
1176                                .attributes()
1177                                .filter_map(|a| a.ok())
1178                                .find(|attr| attr.key.as_ref() == b"Condition")
1179                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1180                        }
1181                        "ItemGroup" => {
1182                            current_item_group_condition = e
1183                                .attributes()
1184                                .filter_map(|a| a.ok())
1185                                .find(|attr| attr.key.as_ref() == b"Condition")
1186                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1187                        }
1188                        "PackageReference" => {
1189                            let name = e
1190                                .attributes()
1191                                .filter_map(|a| a.ok())
1192                                .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1193                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1194                            let version = e
1195                                .attributes()
1196                                .filter_map(|a| a.ok())
1197                                .find(|attr| attr.key.as_ref() == b"Version")
1198                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1199                            let version_override = e
1200                                .attributes()
1201                                .filter_map(|a| a.ok())
1202                                .find(|attr| attr.key.as_ref() == b"VersionOverride")
1203                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1204                            let condition = e
1205                                .attributes()
1206                                .filter_map(|a| a.ok())
1207                                .find(|attr| attr.key.as_ref() == b"Condition")
1208                                .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1209                                .or_else(|| current_item_group_condition.clone());
1210
1211                            current_package_reference = Some(ProjectReferenceData {
1212                                name,
1213                                version,
1214                                version_override,
1215                                condition,
1216                            });
1217                        }
1218                        _ => {}
1219                    }
1220                }
1221                Ok(Event::Empty(e)) => {
1222                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1223
1224                    if tag_name == "PackageReference" {
1225                        let name = e
1226                            .attributes()
1227                            .filter_map(|a| a.ok())
1228                            .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1229                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1230                        let version = e
1231                            .attributes()
1232                            .filter_map(|a| a.ok())
1233                            .find(|attr| attr.key.as_ref() == b"Version")
1234                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1235                        let version_override = e
1236                            .attributes()
1237                            .filter_map(|a| a.ok())
1238                            .find(|attr| attr.key.as_ref() == b"VersionOverride")
1239                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1240                        let condition = e
1241                            .attributes()
1242                            .filter_map(|a| a.ok())
1243                            .find(|attr| attr.key.as_ref() == b"Condition")
1244                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1245                            .or_else(|| current_item_group_condition.clone());
1246
1247                        package_references.push(ProjectReferenceData {
1248                            name,
1249                            version,
1250                            version_override,
1251                            condition,
1252                        });
1253                    }
1254                }
1255                Ok(Event::Text(e)) => {
1256                    let text = e.decode().ok().map(|s| s.trim().to_string());
1257                    let Some(text) = text.filter(|value| !value.is_empty()) else {
1258                        buf.clear();
1259                        continue;
1260                    };
1261
1262                    if current_package_reference.is_some() {
1263                        if current_element.as_str() == "Version"
1264                            && let Some(reference) = &mut current_package_reference
1265                        {
1266                            reference.version = Some(text);
1267                        } else if current_element.as_str() == "VersionOverride"
1268                            && let Some(reference) = &mut current_package_reference
1269                        {
1270                            reference.version_override = Some(text);
1271                        }
1272                    } else if in_property_group && current_property_group_condition.is_none() {
1273                        project_properties.insert(current_element.clone(), text.clone());
1274                        match current_element.as_str() {
1275                            "PackageId" => name = Some(text),
1276                            "AssemblyName" if fallback_name.is_none() => fallback_name = Some(text),
1277                            "Version" if version.is_none() => version = Some(text),
1278                            "PackageVersion" => version = Some(text),
1279                            "Description" => description = Some(text),
1280                            "PackageProjectUrl" | "ProjectUrl" => homepage_url = Some(text),
1281                            "Authors" => authors = Some(text),
1282                            "RepositoryUrl" => repository_url = Some(text),
1283                            "RepositoryType" => repository_type = Some(text),
1284                            "RepositoryBranch" => repository_branch = Some(text),
1285                            "RepositoryCommit" => repository_commit = Some(text),
1286                            "PackageLicenseExpression" => {
1287                                extracted_license_statement = Some(text);
1288                                license_type = Some("expression".to_string());
1289                            }
1290                            "PackageLicenseFile" => {
1291                                extracted_license_statement = Some(text);
1292                                license_type = Some("file".to_string());
1293                            }
1294                            "PackageReadmeFile" => readme_file = Some(text),
1295                            "PackageIcon" => icon_file = Some(text),
1296                            "Copyright" => copyright = Some(text),
1297                            _ => {}
1298                        }
1299                    }
1300                }
1301                Ok(Event::End(e)) => {
1302                    let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1303
1304                    match tag_name.as_str() {
1305                        "PropertyGroup" => {
1306                            in_property_group = false;
1307                            current_property_group_condition = None;
1308                        }
1309                        "ItemGroup" => current_item_group_condition = None,
1310                        "PackageReference" => {
1311                            if let Some(reference) = current_package_reference.take() {
1312                                package_references.push(reference);
1313                            }
1314                        }
1315                        _ => {}
1316                    }
1317
1318                    current_element.clear();
1319                }
1320                Ok(Event::Eof) => break,
1321                Err(e) => {
1322                    warn!("Error parsing project file at {:?}: {}", path, e);
1323                    return vec![default_package_data(Some(datasource_id))];
1324                }
1325                _ => {}
1326            }
1327
1328            buf.clear();
1329        }
1330
1331        let name = name.or(fallback_name);
1332        let vcs_url = repository_url.map(|url| match repository_type {
1333            Some(repo_type) if !repo_type.trim().is_empty() => format!("{}+{}", repo_type, url),
1334            _ => url,
1335        });
1336        let dependencies = package_references
1337            .into_iter()
1338            .filter_map(|reference| {
1339                build_project_file_dependency(
1340                    reference.name,
1341                    reference.version,
1342                    reference.version_override,
1343                    reference.condition,
1344                    &project_properties,
1345                )
1346            })
1347            .collect::<Vec<_>>();
1348        let (repository_homepage_url, repository_download_url, api_data_url) =
1349            build_nuget_urls(name.as_deref(), version.as_deref());
1350
1351        let mut parties = Vec::new();
1352        if let Some(authors) = authors {
1353            parties.push(build_nuget_party("author", authors));
1354        }
1355
1356        let mut extra_data = serde_json::Map::new();
1357        insert_extra_string(&mut extra_data, "license_type", license_type.clone());
1358        if license_type.as_deref() == Some("file") {
1359            insert_extra_string(
1360                &mut extra_data,
1361                "license_file",
1362                extracted_license_statement.clone(),
1363            );
1364        }
1365        insert_extra_string(&mut extra_data, "repository_branch", repository_branch);
1366        insert_extra_string(&mut extra_data, "repository_commit", repository_commit);
1367        insert_extra_string(&mut extra_data, "readme_file", readme_file);
1368        insert_extra_string(&mut extra_data, "icon_file", icon_file);
1369        if let Some(value) = project_properties
1370            .get("CentralPackageVersionOverrideEnabled")
1371            .cloned()
1372        {
1373            extra_data.insert(
1374                "central_package_version_override_enabled_raw".to_string(),
1375                serde_json::Value::String(value),
1376            );
1377        }
1378        if let Some(value) = resolve_bool_property_reference(
1379            project_properties
1380                .get("CentralPackageVersionOverrideEnabled")
1381                .map(String::as_str),
1382            &project_properties,
1383        ) {
1384            extra_data.insert(
1385                "central_package_version_override_enabled".to_string(),
1386                serde_json::Value::Bool(value),
1387            );
1388        }
1389
1390        let (declared_license_expression, declared_license_expression_spdx, license_detections) =
1391            if license_type.as_deref() == Some("expression") {
1392                normalize_spdx_declared_license(extracted_license_statement.as_deref())
1393            } else {
1394                empty_declared_license_data()
1395            };
1396
1397        vec![PackageData {
1398            datasource_id: Some(datasource_id),
1399            package_type: Some(Self::PACKAGE_TYPE),
1400            name: name.clone().map(truncate_field),
1401            version: version.clone().map(truncate_field),
1402            purl: build_nuget_purl(name.as_deref(), version.as_deref()),
1403            description: description.map(truncate_field),
1404            homepage_url: homepage_url.map(truncate_field),
1405            parties,
1406            dependencies,
1407            declared_license_expression,
1408            declared_license_expression_spdx,
1409            license_detections,
1410            extracted_license_statement: extracted_license_statement.map(truncate_field),
1411            copyright: copyright.map(truncate_field),
1412            vcs_url: vcs_url.map(truncate_field),
1413            extra_data: if extra_data.is_empty() {
1414                None
1415            } else {
1416                Some(extra_data.into_iter().collect())
1417            },
1418            repository_homepage_url,
1419            repository_download_url,
1420            api_data_url,
1421            ..default_package_data(Some(datasource_id))
1422        }]
1423    }
1424}
1425
1426fn parse_project_json_manifest(parsed: &serde_json::Value) -> PackageData {
1427    let name = parsed
1428        .get("name")
1429        .and_then(|value| value.as_str())
1430        .map(|value| value.to_string());
1431    let version = parsed
1432        .get("version")
1433        .and_then(|value| value.as_str())
1434        .map(|value| value.to_string());
1435    let description = parsed
1436        .get("description")
1437        .and_then(|value| value.as_str())
1438        .map(|value| value.to_string());
1439    let homepage_url = parsed
1440        .get("projectUrl")
1441        .and_then(|value| value.as_str())
1442        .map(|value| value.to_string());
1443    let extracted_license_statement = parsed
1444        .get("license")
1445        .or_else(|| parsed.get("licenseUrl"))
1446        .and_then(|value| value.as_str())
1447        .map(|value| value.to_string());
1448
1449    let mut parties = Vec::new();
1450    if let Some(authors) = parsed.get("authors") {
1451        let author_name = if let Some(value) = authors.as_str() {
1452            Some(value.to_string())
1453        } else {
1454            authors.as_array().map(|entries| {
1455                entries
1456                    .iter()
1457                    .filter_map(|entry| entry.as_str())
1458                    .collect::<Vec<_>>()
1459                    .join(", ")
1460            })
1461        };
1462
1463        if let Some(author_name) = author_name.filter(|value| !value.is_empty()) {
1464            parties.push(build_nuget_party("author", author_name));
1465        }
1466    }
1467
1468    let mut dependencies = Vec::new();
1469
1470    if let Some(root_dependencies) = parsed
1471        .get("dependencies")
1472        .and_then(|value| value.as_object())
1473    {
1474        for (dependency_name, dependency_spec) in root_dependencies.iter().take(MAX_ITERATION_COUNT)
1475        {
1476            if let Some(dependency) =
1477                parse_project_json_dependency(dependency_name, dependency_spec, None)
1478            {
1479                dependencies.push(dependency);
1480            }
1481        }
1482    }
1483
1484    if let Some(frameworks) = parsed.get("frameworks").and_then(|value| value.as_object()) {
1485        for (framework, framework_value) in frameworks.iter().take(MAX_ITERATION_COUNT) {
1486            let Some(framework_dependencies) = framework_value
1487                .get("dependencies")
1488                .and_then(|value| value.as_object())
1489            else {
1490                continue;
1491            };
1492
1493            for (dependency_name, dependency_spec) in
1494                framework_dependencies.iter().take(MAX_ITERATION_COUNT)
1495            {
1496                if let Some(dependency) = parse_project_json_dependency(
1497                    dependency_name,
1498                    dependency_spec,
1499                    Some(framework.clone()),
1500                ) {
1501                    dependencies.push(dependency);
1502                }
1503            }
1504        }
1505    }
1506
1507    let (repository_homepage_url, repository_download_url, api_data_url) =
1508        build_nuget_urls(name.as_deref(), version.as_deref());
1509
1510    PackageData {
1511        datasource_id: Some(DatasourceId::NugetProjectJson),
1512        package_type: Some(PackageType::Nuget),
1513        name: name.clone().map(truncate_field),
1514        version: version.clone().map(truncate_field),
1515        purl: build_nuget_purl(name.as_deref(), version.as_deref()),
1516        description: description.map(truncate_field),
1517        homepage_url: homepage_url.map(truncate_field),
1518        parties,
1519        dependencies,
1520        extracted_license_statement: extracted_license_statement.map(truncate_field),
1521        repository_homepage_url,
1522        repository_download_url,
1523        api_data_url,
1524        ..default_package_data(Some(DatasourceId::NugetProjectJson))
1525    }
1526}
1527
1528fn parse_project_json_dependency(
1529    dependency_name: &str,
1530    dependency_spec: &serde_json::Value,
1531    scope: Option<String>,
1532) -> Option<Dependency> {
1533    let mut extra_data = serde_json::Map::new();
1534
1535    let requirement = match dependency_spec {
1536        serde_json::Value::String(version) => Some(version.clone()),
1537        serde_json::Value::Object(object) => {
1538            let requirement = object
1539                .get("version")
1540                .and_then(|value| value.as_str())
1541                .map(|value| value.to_string());
1542            insert_extra_string(
1543                &mut extra_data,
1544                "include",
1545                object
1546                    .get("include")
1547                    .and_then(|value| value.as_str())
1548                    .map(|value| value.to_string()),
1549            );
1550            insert_extra_string(
1551                &mut extra_data,
1552                "exclude",
1553                object
1554                    .get("exclude")
1555                    .and_then(|value| value.as_str())
1556                    .map(|value| value.to_string()),
1557            );
1558            insert_extra_string(
1559                &mut extra_data,
1560                "type",
1561                object
1562                    .get("type")
1563                    .and_then(|value| value.as_str())
1564                    .map(|value| value.to_string()),
1565            );
1566            requirement
1567        }
1568        _ => return None,
1569    };
1570
1571    Some(Dependency {
1572        purl: build_nuget_purl(Some(dependency_name), None),
1573        extracted_requirement: requirement,
1574        scope,
1575        is_runtime: Some(true),
1576        is_optional: Some(false),
1577        is_pinned: Some(false),
1578        is_direct: Some(true),
1579        resolved_package: None,
1580        extra_data: if extra_data.is_empty() {
1581            None
1582        } else {
1583            Some(extra_data.into_iter().collect())
1584        },
1585    })
1586}
1587
1588fn parse_project_lock_manifest(parsed: &serde_json::Value) -> PackageData {
1589    let mut dependencies = Vec::new();
1590
1591    if let Some(groups) = parsed
1592        .get("projectFileDependencyGroups")
1593        .and_then(|value| value.as_object())
1594    {
1595        for (framework, entries) in groups.iter().take(MAX_ITERATION_COUNT) {
1596            let Some(entries) = entries.as_array() else {
1597                continue;
1598            };
1599
1600            for entry in entries
1601                .iter()
1602                .take(MAX_ITERATION_COUNT)
1603                .filter_map(|value| value.as_str())
1604            {
1605                if let Some(dependency) = parse_project_lock_dependency(
1606                    entry,
1607                    (!framework.is_empty()).then(|| framework.clone()),
1608                ) {
1609                    dependencies.push(dependency);
1610                }
1611            }
1612        }
1613    }
1614
1615    PackageData {
1616        datasource_id: Some(DatasourceId::NugetProjectLockJson),
1617        package_type: Some(PackageType::Nuget),
1618        dependencies,
1619        ..default_package_data(Some(DatasourceId::NugetProjectLockJson))
1620    }
1621}
1622
1623fn parse_project_lock_dependency(entry: &str, scope: Option<String>) -> Option<Dependency> {
1624    let trimmed = entry.trim();
1625    if trimmed.is_empty() {
1626        return None;
1627    }
1628
1629    let mut parts = trimmed.split_whitespace();
1630    let name = parts.next()?;
1631    let requirement = parts.collect::<Vec<_>>().join(" ");
1632
1633    Some(Dependency {
1634        purl: build_nuget_purl(Some(name), None),
1635        extracted_requirement: (!requirement.is_empty()).then_some(requirement),
1636        scope,
1637        is_runtime: Some(true),
1638        is_optional: Some(false),
1639        is_pinned: Some(false),
1640        is_direct: Some(true),
1641        resolved_package: None,
1642        extra_data: None,
1643    })
1644}
1645
1646fn build_project_file_dependency(
1647    name: Option<String>,
1648    version: Option<String>,
1649    version_override: Option<String>,
1650    condition: Option<String>,
1651    project_properties: &HashMap<String, String>,
1652) -> Option<Dependency> {
1653    let name = name?.trim().to_string();
1654    if name.is_empty() {
1655        return None;
1656    }
1657
1658    let mut extra_data = serde_json::Map::new();
1659    insert_extra_string(&mut extra_data, "condition", condition);
1660    insert_extra_string(
1661        &mut extra_data,
1662        "version_override",
1663        version_override.clone(),
1664    );
1665    insert_extra_string(
1666        &mut extra_data,
1667        "version_override_resolved",
1668        version_override
1669            .as_deref()
1670            .and_then(|value| resolve_string_property_reference(value, project_properties)),
1671    );
1672
1673    Some(Dependency {
1674        purl: build_nuget_purl(Some(&name), None),
1675        extracted_requirement: version,
1676        scope: None,
1677        is_runtime: Some(true),
1678        is_optional: Some(false),
1679        is_pinned: Some(false),
1680        is_direct: Some(true),
1681        resolved_package: None,
1682        extra_data: if extra_data.is_empty() {
1683            None
1684        } else {
1685            Some(extra_data.into_iter().collect())
1686        },
1687    })
1688}
1689
1690#[derive(Default)]
1691struct CentralPackageVersionData {
1692    name: Option<String>,
1693    version: Option<String>,
1694    condition: Option<String>,
1695}
1696
1697#[derive(Default)]
1698struct RawCentralPackagePropsData {
1699    package_versions: Vec<CentralPackageVersionData>,
1700    property_values: HashMap<String, String>,
1701    import_projects: Vec<String>,
1702    manage_package_versions_centrally: Option<String>,
1703    central_package_transitive_pinning_enabled: Option<String>,
1704    central_package_version_override_enabled: Option<String>,
1705}
1706
1707#[derive(Default)]
1708struct RawBuildPropsData {
1709    property_values: HashMap<String, String>,
1710    import_projects: Vec<String>,
1711    manage_package_versions_centrally: Option<String>,
1712    central_package_transitive_pinning_enabled: Option<String>,
1713    central_package_version_override_enabled: Option<String>,
1714}
1715
1716#[derive(Default)]
1717struct BuildPropsData {
1718    property_values: HashMap<String, String>,
1719    import_projects: Vec<String>,
1720    manage_package_versions_centrally: Option<bool>,
1721    central_package_transitive_pinning_enabled: Option<bool>,
1722    central_package_version_override_enabled: Option<bool>,
1723}
1724
1725fn build_directory_packages_dependency(
1726    name: Option<String>,
1727    version: Option<String>,
1728    raw_version: Option<String>,
1729    condition: Option<String>,
1730) -> Option<Dependency> {
1731    let name = name?.trim().to_string();
1732    if name.is_empty() {
1733        return None;
1734    }
1735    let version = version
1736        .map(|value| value.trim().to_string())
1737        .filter(|value| !value.is_empty())?;
1738
1739    let mut extra_data = serde_json::Map::new();
1740    insert_extra_string(&mut extra_data, "condition", condition);
1741    insert_extra_string(&mut extra_data, "version_expression", raw_version);
1742
1743    Some(Dependency {
1744        purl: build_nuget_purl(Some(&name), None),
1745        extracted_requirement: Some(version),
1746        scope: Some("package_version".to_string()),
1747        is_runtime: Some(true),
1748        is_optional: Some(false),
1749        is_pinned: Some(false),
1750        is_direct: Some(true),
1751        resolved_package: None,
1752        extra_data: if extra_data.is_empty() {
1753            None
1754        } else {
1755            Some(extra_data.into_iter().collect())
1756        },
1757    })
1758}
1759
1760fn resolve_directory_packages_props(
1761    path: &Path,
1762    guard: &mut RecursionGuard<PathBuf>,
1763) -> Result<CentralPackagePropsData, String> {
1764    if guard.exceeded() {
1765        return Err(format!(
1766            "Recursion depth exceeded resolving Directory.Packages.props at {:?}",
1767            path
1768        ));
1769    }
1770
1771    let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
1772    if guard.enter(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, guard)?;
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    guard.leave(canonical);
1829    Ok(merged)
1830}
1831
1832fn resolve_directory_build_props(
1833    path: &Path,
1834    guard: &mut RecursionGuard<PathBuf>,
1835) -> Result<BuildPropsData, String> {
1836    if guard.exceeded() {
1837        return Err(format!(
1838            "Recursion depth exceeded resolving Directory.Build.props at {:?}",
1839            path
1840        ));
1841    }
1842
1843    let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
1844    if guard.enter(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, guard)?;
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    guard.leave(canonical);
1884    Ok(merged)
1885}
1886
1887fn parse_directory_packages_props_file(path: &Path) -> Result<RawCentralPackagePropsData, String> {
1888    check_file_size(path)?;
1889
1890    let file = File::open(path).map_err(|e| {
1891        format!(
1892            "Failed to open Directory.Packages.props at {:?}: {}",
1893            path, e
1894        )
1895    })?;
1896
1897    let reader = BufReader::new(file);
1898    let mut xml_reader = Reader::from_reader(reader);
1899    xml_reader.config_mut().trim_text(true);
1900
1901    let mut raw = RawCentralPackagePropsData::default();
1902    let mut buf = Vec::new();
1903    let mut current_element = String::new();
1904    let mut current_property_group_condition = None;
1905    let mut current_item_group_condition = None;
1906    let mut current_package_version: Option<CentralPackageVersionData> = None;
1907    let mut iteration_count: usize = 0;
1908
1909    loop {
1910        iteration_count += 1;
1911        if iteration_count > MAX_ITERATION_COUNT {
1912            return Err(format!(
1913                "Iteration limit exceeded in Directory.Packages.props at {:?}; stopping at {} items",
1914                path, MAX_ITERATION_COUNT
1915            ));
1916        }
1917        match xml_reader.read_event_into(&mut buf) {
1918            Ok(Event::Start(e)) => {
1919                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1920                current_element = tag_name.clone();
1921
1922                match tag_name.as_str() {
1923                    "ItemGroup" => {
1924                        current_item_group_condition = e
1925                            .attributes()
1926                            .filter_map(|a| a.ok())
1927                            .find(|attr| attr.key.as_ref() == b"Condition")
1928                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1929                    }
1930                    "PackageVersion" => {
1931                        let name = e
1932                            .attributes()
1933                            .filter_map(|a| a.ok())
1934                            .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1935                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1936                        let version = e
1937                            .attributes()
1938                            .filter_map(|a| a.ok())
1939                            .find(|attr| attr.key.as_ref() == b"Version")
1940                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1941                        let condition = e
1942                            .attributes()
1943                            .filter_map(|a| a.ok())
1944                            .find(|attr| attr.key.as_ref() == b"Condition")
1945                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1946                            .or_else(|| current_item_group_condition.clone());
1947
1948                        current_package_version = Some(CentralPackageVersionData {
1949                            name,
1950                            version,
1951                            condition,
1952                        });
1953                    }
1954                    "PropertyGroup" => {
1955                        current_property_group_condition = e
1956                            .attributes()
1957                            .filter_map(|a| a.ok())
1958                            .find(|attr| attr.key.as_ref() == b"Condition")
1959                            .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1960                    }
1961                    _ => {}
1962                }
1963            }
1964            Ok(Event::Empty(e)) => {
1965                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
1966                if tag_name == "PackageVersion" {
1967                    let name = e
1968                        .attributes()
1969                        .filter_map(|a| a.ok())
1970                        .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
1971                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1972                    let version = e
1973                        .attributes()
1974                        .filter_map(|a| a.ok())
1975                        .find(|attr| attr.key.as_ref() == b"Version")
1976                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
1977                    let condition = e
1978                        .attributes()
1979                        .filter_map(|a| a.ok())
1980                        .find(|attr| attr.key.as_ref() == b"Condition")
1981                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1982                        .or_else(|| current_item_group_condition.clone());
1983
1984                    raw.package_versions.push(CentralPackageVersionData {
1985                        name,
1986                        version,
1987                        condition,
1988                    });
1989                } else if tag_name == "Import"
1990                    && let Some(project) = e
1991                        .attributes()
1992                        .filter_map(|a| a.ok())
1993                        .find(|attr| attr.key.as_ref() == b"Project")
1994                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
1995                    && !e
1996                        .attributes()
1997                        .filter_map(|a| a.ok())
1998                        .any(|attr| attr.key.as_ref() == b"Condition")
1999                    && is_supported_directory_packages_import(&project)
2000                {
2001                    raw.import_projects.push(project.trim().to_string());
2002                }
2003            }
2004            Ok(Event::Text(e)) => {
2005                let text = e.decode().ok().map(|s| s.trim().to_string());
2006                let Some(text) = text.filter(|value| !value.is_empty()) else {
2007                    buf.clear();
2008                    continue;
2009                };
2010
2011                if current_package_version.is_some() {
2012                    if current_element.as_str() == "Version"
2013                        && let Some(entry) = &mut current_package_version
2014                    {
2015                        entry.version = Some(text);
2016                    }
2017                } else if current_property_group_condition.is_none() {
2018                    raw.property_values
2019                        .insert(current_element.clone(), text.clone());
2020                    match current_element.as_str() {
2021                        "ManagePackageVersionsCentrally" => {
2022                            raw.manage_package_versions_centrally = Some(text)
2023                        }
2024                        "CentralPackageTransitivePinningEnabled" => {
2025                            raw.central_package_transitive_pinning_enabled = Some(text)
2026                        }
2027                        "CentralPackageVersionOverrideEnabled" => {
2028                            raw.central_package_version_override_enabled = Some(text)
2029                        }
2030                        _ => {}
2031                    }
2032                }
2033            }
2034            Ok(Event::End(e)) => {
2035                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2036
2037                match tag_name.as_str() {
2038                    "PropertyGroup" => current_property_group_condition = None,
2039                    "ItemGroup" => current_item_group_condition = None,
2040                    "PackageVersion" => {
2041                        if let Some(entry) = current_package_version.take() {
2042                            raw.package_versions.push(entry);
2043                        }
2044                    }
2045                    _ => {}
2046                }
2047
2048                current_element.clear();
2049            }
2050            Ok(Event::Eof) => break,
2051            Err(e) => {
2052                return Err(format!(
2053                    "Error parsing Directory.Packages.props at {:?}: {}",
2054                    path, e
2055                ));
2056            }
2057            _ => {}
2058        }
2059
2060        buf.clear();
2061    }
2062
2063    Ok(raw)
2064}
2065
2066fn parse_directory_build_props_file(path: &Path) -> Result<RawBuildPropsData, String> {
2067    check_file_size(path)?;
2068
2069    let file = File::open(path)
2070        .map_err(|e| format!("Failed to open Directory.Build.props at {:?}: {}", path, e))?;
2071
2072    let reader = BufReader::new(file);
2073    let mut xml_reader = Reader::from_reader(reader);
2074    xml_reader.config_mut().trim_text(true);
2075
2076    let mut raw = RawBuildPropsData::default();
2077    let mut buf = Vec::new();
2078    let mut current_element = String::new();
2079    let mut in_property_group = false;
2080    let mut current_property_group_condition = None;
2081    let mut iteration_count: usize = 0;
2082
2083    loop {
2084        iteration_count += 1;
2085        if iteration_count > MAX_ITERATION_COUNT {
2086            return Err(format!(
2087                "Iteration limit exceeded in Directory.Build.props at {:?}; stopping at {} items",
2088                path, MAX_ITERATION_COUNT
2089            ));
2090        }
2091        match xml_reader.read_event_into(&mut buf) {
2092            Ok(Event::Start(e)) => {
2093                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2094                current_element = tag_name.clone();
2095                if tag_name == "PropertyGroup" {
2096                    in_property_group = true;
2097                    current_property_group_condition = e
2098                        .attributes()
2099                        .filter_map(|a| a.ok())
2100                        .find(|attr| attr.key.as_ref() == b"Condition")
2101                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
2102                }
2103            }
2104            Ok(Event::Empty(e)) => {
2105                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2106                if tag_name == "Import"
2107                    && let Some(project) = e
2108                        .attributes()
2109                        .filter_map(|a| a.ok())
2110                        .find(|attr| attr.key.as_ref() == b"Project")
2111                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
2112                    && !e
2113                        .attributes()
2114                        .filter_map(|a| a.ok())
2115                        .any(|attr| attr.key.as_ref() == b"Condition")
2116                    && is_supported_directory_build_import(&project)
2117                {
2118                    raw.import_projects.push(project.trim().to_string());
2119                }
2120            }
2121            Ok(Event::Text(e)) => {
2122                let text = e.decode().ok().map(|s| s.trim().to_string());
2123                let Some(text) = text.filter(|value| !value.is_empty()) else {
2124                    buf.clear();
2125                    continue;
2126                };
2127
2128                if in_property_group && current_property_group_condition.is_none() {
2129                    raw.property_values
2130                        .insert(current_element.clone(), text.clone());
2131                    match current_element.as_str() {
2132                        "ManagePackageVersionsCentrally" => {
2133                            raw.manage_package_versions_centrally = Some(text)
2134                        }
2135                        "CentralPackageTransitivePinningEnabled" => {
2136                            raw.central_package_transitive_pinning_enabled = Some(text)
2137                        }
2138                        "CentralPackageVersionOverrideEnabled" => {
2139                            raw.central_package_version_override_enabled = Some(text)
2140                        }
2141                        _ => {}
2142                    }
2143                }
2144            }
2145            Ok(Event::End(e)) => {
2146                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2147                if tag_name == "PropertyGroup" {
2148                    in_property_group = false;
2149                    current_property_group_condition = None;
2150                }
2151                current_element.clear();
2152            }
2153            Ok(Event::Eof) => break,
2154            Err(e) => {
2155                return Err(format!(
2156                    "Error parsing Directory.Build.props at {:?}: {}",
2157                    path, e
2158                ));
2159            }
2160            _ => {}
2161        }
2162
2163        buf.clear();
2164    }
2165
2166    Ok(raw)
2167}
2168
2169fn build_directory_packages_package_data(
2170    data: CentralPackagePropsData,
2171    raw: RawCentralPackagePropsData,
2172) -> PackageData {
2173    let mut extra_data = serde_json::Map::new();
2174    if !data.properties.is_empty() {
2175        extra_data.insert(
2176            "property_values".to_string(),
2177            serde_json::Value::Object(
2178                data.properties
2179                    .iter()
2180                    .map(|(key, value)| (key.clone(), serde_json::Value::String(value.clone())))
2181                    .collect(),
2182            ),
2183        );
2184    }
2185    if let Some(value) = data.manage_package_versions_centrally {
2186        extra_data.insert(
2187            "manage_package_versions_centrally".to_string(),
2188            serde_json::Value::Bool(value),
2189        );
2190    }
2191    if let Some(value) = data.central_package_transitive_pinning_enabled {
2192        extra_data.insert(
2193            "central_package_transitive_pinning_enabled".to_string(),
2194            serde_json::Value::Bool(value),
2195        );
2196    }
2197    if let Some(value) = data.central_package_version_override_enabled {
2198        extra_data.insert(
2199            "central_package_version_override_enabled".to_string(),
2200            serde_json::Value::Bool(value),
2201        );
2202    }
2203    if !data.import_projects.is_empty() {
2204        extra_data.insert(
2205            "import_projects".to_string(),
2206            serde_json::Value::Array(
2207                data.import_projects
2208                    .into_iter()
2209                    .map(serde_json::Value::String)
2210                    .collect(),
2211            ),
2212        );
2213    }
2214    extra_data.insert(
2215        "package_versions".to_string(),
2216        serde_json::Value::Array(
2217            raw.package_versions
2218                .into_iter()
2219                .map(|entry| {
2220                    serde_json::json!({
2221                        "name": entry.name,
2222                        "version": entry.version,
2223                        "condition": entry.condition,
2224                    })
2225                })
2226                .collect(),
2227        ),
2228    );
2229
2230    PackageData {
2231        datasource_id: Some(DatasourceId::NugetDirectoryPackagesProps),
2232        package_type: Some(PackageType::Nuget),
2233        dependencies: data.dependencies,
2234        extra_data: if extra_data.is_empty() {
2235            None
2236        } else {
2237            Some(extra_data.into_iter().collect())
2238        },
2239        ..default_package_data(Some(DatasourceId::NugetDirectoryPackagesProps))
2240    }
2241}
2242
2243fn build_directory_build_props_package_data(
2244    data: BuildPropsData,
2245    _raw: RawBuildPropsData,
2246) -> PackageData {
2247    let mut extra_data = serde_json::Map::new();
2248    if !data.property_values.is_empty() {
2249        extra_data.insert(
2250            "property_values".to_string(),
2251            serde_json::Value::Object(
2252                data.property_values
2253                    .iter()
2254                    .map(|(key, value)| (key.clone(), serde_json::Value::String(value.clone())))
2255                    .collect(),
2256            ),
2257        );
2258    }
2259    if let Some(value) = data.manage_package_versions_centrally {
2260        extra_data.insert(
2261            "manage_package_versions_centrally".to_string(),
2262            serde_json::Value::Bool(value),
2263        );
2264    }
2265    if let Some(value) = data.central_package_transitive_pinning_enabled {
2266        extra_data.insert(
2267            "central_package_transitive_pinning_enabled".to_string(),
2268            serde_json::Value::Bool(value),
2269        );
2270    }
2271    if let Some(value) = data.central_package_version_override_enabled {
2272        extra_data.insert(
2273            "central_package_version_override_enabled".to_string(),
2274            serde_json::Value::Bool(value),
2275        );
2276    }
2277    if !data.import_projects.is_empty() {
2278        extra_data.insert(
2279            "import_projects".to_string(),
2280            serde_json::Value::Array(
2281                data.import_projects
2282                    .into_iter()
2283                    .map(serde_json::Value::String)
2284                    .collect(),
2285            ),
2286        );
2287    }
2288
2289    PackageData {
2290        datasource_id: Some(DatasourceId::NugetDirectoryBuildProps),
2291        package_type: Some(PackageType::Nuget),
2292        extra_data: if extra_data.is_empty() {
2293            None
2294        } else {
2295            Some(extra_data.into_iter().collect())
2296        },
2297        ..default_package_data(Some(DatasourceId::NugetDirectoryBuildProps))
2298    }
2299}
2300
2301fn merge_central_package_props(
2302    target: &mut CentralPackagePropsData,
2303    source: CentralPackagePropsData,
2304) {
2305    target.import_projects.extend(source.import_projects);
2306    target.properties.extend(source.properties);
2307    if target.manage_package_versions_centrally.is_none() {
2308        target.manage_package_versions_centrally = source.manage_package_versions_centrally;
2309    }
2310    if target.central_package_transitive_pinning_enabled.is_none() {
2311        target.central_package_transitive_pinning_enabled =
2312            source.central_package_transitive_pinning_enabled;
2313    }
2314    if target.central_package_version_override_enabled.is_none() {
2315        target.central_package_version_override_enabled =
2316            source.central_package_version_override_enabled;
2317    }
2318    replace_matching_dependency_group(&mut target.dependencies, &source.dependencies);
2319    target.dependencies.extend(source.dependencies);
2320}
2321
2322fn replace_matching_dependency_group(target: &mut Vec<Dependency>, source: &[Dependency]) {
2323    if source.is_empty() {
2324        return;
2325    }
2326
2327    let source_keys = source.iter().map(dependency_key).collect::<Vec<_>>();
2328    target.retain(|candidate| {
2329        !source_keys
2330            .iter()
2331            .any(|key| *key == dependency_key(candidate))
2332    });
2333}
2334
2335fn dependency_key(dependency: &Dependency) -> (Option<String>, Option<String>, Option<String>) {
2336    (
2337        dependency.purl.clone(),
2338        dependency.scope.clone(),
2339        dependency
2340            .extra_data
2341            .as_ref()
2342            .and_then(|data| data.get("condition"))
2343            .and_then(|value| value.as_str())
2344            .map(ToOwned::to_owned),
2345    )
2346}
2347
2348fn is_supported_directory_packages_import(project: &str) -> bool {
2349    let trimmed = project.trim();
2350    if trimmed.is_empty() {
2351        return false;
2352    }
2353
2354    if is_get_path_of_file_above_import(trimmed) {
2355        return true;
2356    }
2357
2358    let candidate = PathBuf::from(trimmed);
2359    candidate.file_name().and_then(|name| name.to_str()) == Some("Directory.Packages.props")
2360}
2361
2362fn is_supported_directory_build_import(project: &str) -> bool {
2363    let trimmed = project.trim();
2364    if trimmed.is_empty() {
2365        return false;
2366    }
2367
2368    if is_get_path_of_file_above_build_import(trimmed) {
2369        return true;
2370    }
2371
2372    let candidate = PathBuf::from(trimmed);
2373    candidate.file_name().and_then(|name| name.to_str()) == Some("Directory.Build.props")
2374}
2375
2376fn is_get_path_of_file_above_import(project: &str) -> bool {
2377    let normalized = project.replace(' ', "");
2378    normalized
2379        == "$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props,$(MSBuildThisFileDirectory)..))"
2380}
2381
2382fn is_get_path_of_file_above_build_import(project: &str) -> bool {
2383    let normalized = project.replace(' ', "");
2384    normalized
2385        == "$([MSBuild]::GetPathOfFileAbove(Directory.Build.props,$(MSBuildThisFileDirectory)..))"
2386}
2387
2388fn resolve_import_project_for_directory_build(
2389    current_path: &Path,
2390    project: &str,
2391    known_props_paths: &HashMap<PathBuf, &PackageData>,
2392) -> Option<PathBuf> {
2393    let trimmed = project.trim();
2394    if is_get_path_of_file_above_build_import(trimmed) {
2395        let start_dir = current_path.parent()?.parent()?;
2396        for ancestor in start_dir.ancestors() {
2397            let candidate = ancestor.join("Directory.Build.props");
2398            if known_props_paths.is_empty() {
2399                if candidate.exists() {
2400                    return Some(candidate);
2401                }
2402            } else if known_props_paths.contains_key(&candidate) {
2403                return Some(candidate);
2404            }
2405        }
2406        return None;
2407    }
2408
2409    if !is_supported_directory_build_import(trimmed) {
2410        return None;
2411    }
2412
2413    let candidate = PathBuf::from(trimmed);
2414    if candidate.is_absolute() {
2415        if known_props_paths.is_empty() {
2416            candidate.exists().then_some(candidate)
2417        } else {
2418            known_props_paths
2419                .contains_key(&candidate)
2420                .then_some(candidate)
2421        }
2422    } else {
2423        let resolved = current_path.parent()?.join(candidate);
2424        if known_props_paths.is_empty() {
2425            resolved.exists().then_some(resolved)
2426        } else {
2427            known_props_paths
2428                .contains_key(&resolved)
2429                .then_some(resolved)
2430        }
2431    }
2432}
2433
2434fn merge_build_props_data(target: &mut BuildPropsData, source: BuildPropsData) {
2435    target.import_projects.extend(source.import_projects);
2436    target.property_values.extend(source.property_values);
2437    if target.manage_package_versions_centrally.is_none() {
2438        target.manage_package_versions_centrally = source.manage_package_versions_centrally;
2439    }
2440    if target.central_package_transitive_pinning_enabled.is_none() {
2441        target.central_package_transitive_pinning_enabled =
2442            source.central_package_transitive_pinning_enabled;
2443    }
2444    if target.central_package_version_override_enabled.is_none() {
2445        target.central_package_version_override_enabled =
2446            source.central_package_version_override_enabled;
2447    }
2448}
2449
2450fn resolve_import_project_for_directory_packages(
2451    current_path: &Path,
2452    project: &str,
2453    known_props_paths: &HashMap<PathBuf, &PackageData>,
2454) -> Option<PathBuf> {
2455    let trimmed = project.trim();
2456    if is_get_path_of_file_above_import(trimmed) {
2457        let start_dir = current_path.parent()?.parent()?;
2458        for ancestor in start_dir.ancestors() {
2459            let candidate = ancestor.join("Directory.Packages.props");
2460            if known_props_paths.is_empty() {
2461                if candidate.exists() {
2462                    return Some(candidate);
2463                }
2464            } else if known_props_paths.contains_key(&candidate) {
2465                return Some(candidate);
2466            }
2467        }
2468        return None;
2469    }
2470
2471    if !is_supported_directory_packages_import(trimmed) {
2472        return None;
2473    }
2474
2475    let candidate = PathBuf::from(trimmed);
2476    if candidate.is_absolute() {
2477        if known_props_paths.is_empty() {
2478            candidate.exists().then_some(candidate)
2479        } else {
2480            known_props_paths
2481                .contains_key(&candidate)
2482                .then_some(candidate)
2483        }
2484    } else {
2485        let resolved = current_path.parent()?.join(candidate);
2486        if known_props_paths.is_empty() {
2487            resolved.exists().then_some(resolved)
2488        } else {
2489            known_props_paths
2490                .contains_key(&resolved)
2491                .then_some(resolved)
2492        }
2493    }
2494}
2495
2496fn resolve_string_property_reference(
2497    value: &str,
2498    properties: &HashMap<String, String>,
2499) -> Option<String> {
2500    let trimmed = value.trim();
2501    if let Some(property_name) = trimmed
2502        .strip_prefix("$(")
2503        .and_then(|value| value.strip_suffix(')'))
2504    {
2505        properties.get(property_name).cloned()
2506    } else {
2507        Some(trimmed.to_string())
2508    }
2509}
2510
2511fn resolve_bool_property_reference(
2512    value: Option<&str>,
2513    properties: &HashMap<String, String>,
2514) -> Option<bool> {
2515    let resolved = resolve_string_property_reference(value?, properties)?;
2516    Some(resolved.eq_ignore_ascii_case("true"))
2517}
2518
2519fn resolve_optional_property_value(
2520    value: Option<&str>,
2521    properties: &HashMap<String, String>,
2522) -> Option<String> {
2523    let value = value?.trim();
2524    if value.is_empty() {
2525        return None;
2526    }
2527
2528    if value.starts_with("$(") && value.ends_with(')') {
2529        resolve_string_property_reference(value, properties)
2530    } else {
2531        Some(value.to_string())
2532    }
2533}
2534
2535pub struct CentralPackageManagementPropsParser;
2536
2537pub struct DirectoryBuildPropsParser;
2538
2539impl PackageParser for DirectoryBuildPropsParser {
2540    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
2541
2542    fn is_match(path: &Path) -> bool {
2543        path.file_name().and_then(|name| name.to_str()) == Some("Directory.Build.props")
2544    }
2545
2546    fn extract_packages(path: &Path) -> Vec<PackageData> {
2547        vec![match (
2548            resolve_directory_build_props(path, &mut RecursionGuard::new()),
2549            parse_directory_build_props_file(path),
2550        ) {
2551            (Ok(data), Ok(raw)) => build_directory_build_props_package_data(data, raw),
2552            (Err(e), _) | (_, Err(e)) => {
2553                warn!("Error parsing Directory.Build.props at {:?}: {}", path, e);
2554                default_package_data(Some(DatasourceId::NugetDirectoryBuildProps))
2555            }
2556        }]
2557    }
2558}
2559
2560impl PackageParser for CentralPackageManagementPropsParser {
2561    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
2562
2563    fn is_match(path: &Path) -> bool {
2564        path.file_name().and_then(|name| name.to_str()) == Some("Directory.Packages.props")
2565    }
2566
2567    fn extract_packages(path: &Path) -> Vec<PackageData> {
2568        vec![match (
2569            resolve_directory_packages_props(path, &mut RecursionGuard::new()),
2570            parse_directory_packages_props_file(path),
2571        ) {
2572            (Ok(data), Ok(raw)) => build_directory_packages_package_data(data, raw),
2573            (Err(e), _) | (_, Err(e)) => {
2574                warn!(
2575                    "Error parsing Directory.Packages.props at {:?}: {}",
2576                    path, e
2577                );
2578                default_package_data(Some(DatasourceId::NugetDirectoryPackagesProps))
2579            }
2580        }]
2581    }
2582}
2583
2584/// Parser for .nupkg files (NuGet package archives)
2585pub struct NupkgParser;
2586
2587impl PackageParser for NupkgParser {
2588    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
2589
2590    fn is_match(path: &Path) -> bool {
2591        path.extension()
2592            .and_then(|ext| ext.to_str())
2593            .is_some_and(|ext| ext == "nupkg")
2594    }
2595
2596    fn extract_packages(path: &Path) -> Vec<PackageData> {
2597        vec![match extract_nupkg_archive(path) {
2598            Ok(data) => data,
2599            Err(e) => {
2600                warn!("Failed to extract .nupkg at {:?}: {}", path, e);
2601                default_package_data(Some(DatasourceId::NugetNupkg))
2602            }
2603        }]
2604    }
2605}
2606
2607fn extract_nupkg_archive(path: &Path) -> Result<PackageData, String> {
2608    use std::fs;
2609    use zip::ZipArchive;
2610
2611    let file_metadata =
2612        fs::metadata(path).map_err(|e| format!("Failed to read file metadata: {}", e))?;
2613    let archive_size = file_metadata.len();
2614
2615    if archive_size > MAX_ARCHIVE_SIZE {
2616        return Err(format!(
2617            "Archive too large: {} bytes (limit: {} bytes)",
2618            archive_size, MAX_ARCHIVE_SIZE
2619        ));
2620    }
2621
2622    let file = File::open(path).map_err(|e| format!("Failed to open archive: {}", e))?;
2623    let mut archive =
2624        ZipArchive::new(file).map_err(|e| format!("Failed to read ZIP archive: {}", e))?;
2625
2626    let mut total_uncompressed: u64 = 0;
2627
2628    for i in 0..archive.len() {
2629        let content = {
2630            let mut entry = archive
2631                .by_index(i)
2632                .map_err(|e| format!("Failed to read ZIP entry: {}", e))?;
2633
2634            let entry_name = entry.name().to_string();
2635            let entry_size = entry.size();
2636
2637            total_uncompressed += entry_size;
2638            if total_uncompressed > MAX_UNCOMPRESSED_SIZE {
2639                warn!(
2640                    "NuGet: total uncompressed size exceeds {} bytes for {:?}",
2641                    MAX_UNCOMPRESSED_SIZE, path
2642                );
2643                return Err(format!(
2644                    "Total uncompressed size exceeds limit: {} bytes (limit: {} bytes)",
2645                    total_uncompressed, MAX_UNCOMPRESSED_SIZE
2646                ));
2647            }
2648
2649            if !entry_name.ends_with(".nuspec") {
2650                continue;
2651            }
2652
2653            if entry_size > MAX_FILE_SIZE {
2654                return Err(format!(
2655                    ".nuspec too large: {} bytes (limit: {} bytes)",
2656                    entry_size, MAX_FILE_SIZE
2657                ));
2658            }
2659
2660            let compressed_size = entry.compressed_size();
2661            if compressed_size > 0 {
2662                let ratio = entry_size as f64 / compressed_size as f64;
2663                if ratio > MAX_COMPRESSION_RATIO {
2664                    return Err(format!(
2665                        "Suspicious compression ratio: {:.2}:1 (limit: {:.0}:1)",
2666                        ratio, MAX_COMPRESSION_RATIO
2667                    ));
2668                }
2669            }
2670
2671            let mut content = String::new();
2672            entry
2673                .read_to_string(&mut content)
2674                .map_err(|e| format!("Failed to read .nuspec: {}", e))?;
2675            content
2676        };
2677
2678        let mut package_data = parse_nuspec_content(&content)?;
2679
2680        let license_file = package_data.extra_data.as_ref().and_then(|extra| {
2681            extra
2682                .get("license_file")
2683                .and_then(|value| value.as_str())
2684                .map(|value| value.to_string())
2685        });
2686
2687        if let Some(license_file) = license_file
2688            && let Some(license_text) = read_nupkg_license_file(&mut archive, &license_file)?
2689        {
2690            package_data.extracted_license_statement = Some(license_text);
2691        }
2692
2693        return Ok(package_data);
2694    }
2695
2696    Err("No .nuspec file found in archive".to_string())
2697}
2698
2699fn read_nupkg_license_file(
2700    archive: &mut zip::ZipArchive<File>,
2701    license_file: &str,
2702) -> Result<Option<String>, String> {
2703    if license_file.split('/').any(|c| c == "..") || license_file.split('\\').any(|c| c == "..") {
2704        warn!(
2705            "NuGet: path traversal detected in license file path: {}",
2706            license_file
2707        );
2708        return Ok(None);
2709    }
2710
2711    let normalized_target = license_file.replace('\\', "/");
2712
2713    for i in 0..archive.len() {
2714        let mut entry = archive
2715            .by_index(i)
2716            .map_err(|e| format!("Failed to read ZIP entry: {}", e))?;
2717        let entry_name = entry.name().replace('\\', "/");
2718
2719        if entry_name != normalized_target
2720            && !entry_name.ends_with(&format!("/{}", normalized_target))
2721        {
2722            continue;
2723        }
2724
2725        let entry_size = entry.size();
2726        if entry_size > MAX_FILE_SIZE {
2727            return Err(format!(
2728                "License file too large: {} bytes (limit: {} bytes)",
2729                entry_size, MAX_FILE_SIZE
2730            ));
2731        }
2732
2733        let mut content = Vec::new();
2734        entry
2735            .read_to_end(&mut content)
2736            .map_err(|e| format!("Failed to read license file from archive: {}", e))?;
2737
2738        return Ok(Some(String::from_utf8_lossy(&content).to_string()));
2739    }
2740
2741    Ok(None)
2742}
2743
2744fn parse_nuspec_content(content: &str) -> Result<PackageData, String> {
2745    use quick_xml::Reader;
2746
2747    let mut xml_reader = Reader::from_str(content);
2748    xml_reader.config_mut().trim_text(true);
2749
2750    let mut name = None;
2751    let mut version = None;
2752    let mut description = None;
2753    let mut homepage_url = None;
2754    let mut parties = Vec::new();
2755    let mut dependencies = Vec::new();
2756    let mut extracted_license_statement = None;
2757    let mut license_type = None;
2758    let mut copyright = None;
2759    let mut vcs_url = None;
2760    let mut repository_branch = None;
2761    let mut repository_commit = None;
2762
2763    let mut buf = Vec::new();
2764    let mut current_element = String::new();
2765    let mut in_metadata = false;
2766    let mut in_dependencies = false;
2767    let mut current_group_framework = None;
2768    let mut iteration_count: usize = 0;
2769
2770    loop {
2771        iteration_count += 1;
2772        if iteration_count > MAX_ITERATION_COUNT {
2773            return Err(format!(
2774                "Iteration limit exceeded parsing .nuspec content; stopping at {} items",
2775                MAX_ITERATION_COUNT
2776            ));
2777        }
2778        match xml_reader.read_event_into(&mut buf) {
2779            Ok(Event::Start(e)) => {
2780                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2781                current_element = tag_name.clone();
2782
2783                if tag_name == "metadata" {
2784                    in_metadata = true;
2785                } else if tag_name == "dependencies" && in_metadata {
2786                    in_dependencies = true;
2787                } else if tag_name == "group" && in_dependencies {
2788                    current_group_framework = e
2789                        .attributes()
2790                        .filter_map(|a| a.ok())
2791                        .find(|attr| attr.key.as_ref() == b"targetFramework")
2792                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
2793                } else if tag_name == "repository" && in_metadata {
2794                    let repository = parse_repository_metadata(&e);
2795                    vcs_url = repository.vcs_url;
2796                    repository_branch = repository.branch;
2797                    repository_commit = repository.commit;
2798                } else if tag_name == "license" && in_metadata {
2799                    license_type = e
2800                        .attributes()
2801                        .filter_map(|a| a.ok())
2802                        .find(|attr| attr.key.as_ref() == b"type")
2803                        .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
2804                }
2805            }
2806            Ok(Event::Empty(e)) => {
2807                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2808
2809                if tag_name == "dependency" && in_dependencies {
2810                    if let Some(dep) =
2811                        parse_nuspec_dependency(&e, current_group_framework.as_deref())
2812                    {
2813                        dependencies.push(dep);
2814                    }
2815                } else if tag_name == "repository" && in_metadata {
2816                    let repository = parse_repository_metadata(&e);
2817                    vcs_url = repository.vcs_url;
2818                    repository_branch = repository.branch;
2819                    repository_commit = repository.commit;
2820                }
2821            }
2822            Ok(Event::Text(e)) => {
2823                if !in_metadata {
2824                    continue;
2825                }
2826
2827                let text = e.decode().ok().map(|s| s.trim().to_string());
2828                if let Some(text) = text.filter(|s| !s.is_empty()) {
2829                    match current_element.as_str() {
2830                        "id" => name = Some(text),
2831                        "version" => version = Some(text),
2832                        "description" => description = Some(text),
2833                        "projectUrl" => homepage_url = Some(text),
2834                        "authors" => {
2835                            parties.push(build_nuget_party("author", text));
2836                        }
2837                        "owners" => {
2838                            parties.push(build_nuget_party("owner", text));
2839                        }
2840                        "license" => {
2841                            extracted_license_statement = Some(text);
2842                        }
2843                        "licenseUrl" => {
2844                            if extracted_license_statement.is_none() {
2845                                extracted_license_statement = Some(text);
2846                            }
2847                        }
2848                        "copyright" => copyright = Some(text),
2849                        _ => {}
2850                    }
2851                }
2852            }
2853            Ok(Event::End(e)) => {
2854                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
2855
2856                if tag_name == "metadata" {
2857                    in_metadata = false;
2858                } else if tag_name == "dependencies" {
2859                    in_dependencies = false;
2860                } else if tag_name == "group" {
2861                    current_group_framework = None;
2862                }
2863
2864                current_element.clear();
2865            }
2866            Ok(Event::Eof) => break,
2867            Err(e) => {
2868                return Err(format!("XML parsing error: {}", e));
2869            }
2870            _ => {}
2871        }
2872        buf.clear();
2873    }
2874
2875    let (repository_homepage_url, repository_download_url, api_data_url) =
2876        build_nuget_urls(name.as_deref(), version.as_deref());
2877
2878    let (declared_license_expression, declared_license_expression_spdx, license_detections) =
2879        if license_type.as_deref() == Some("expression") {
2880            normalize_spdx_declared_license(extracted_license_statement.as_deref())
2881        } else {
2882            empty_declared_license_data()
2883        };
2884
2885    let holder = None;
2886
2887    let mut extra_data = serde_json::Map::new();
2888    insert_extra_string(&mut extra_data, "license_type", license_type.clone());
2889    if license_type.as_deref() == Some("file") {
2890        insert_extra_string(
2891            &mut extra_data,
2892            "license_file",
2893            extracted_license_statement.clone(),
2894        );
2895    }
2896    insert_extra_string(&mut extra_data, "repository_branch", repository_branch);
2897    insert_extra_string(&mut extra_data, "repository_commit", repository_commit);
2898
2899    Ok(PackageData {
2900        datasource_id: Some(DatasourceId::NugetNupkg),
2901        package_type: Some(NupkgParser::PACKAGE_TYPE),
2902        name: name.map(truncate_field),
2903        version: version.map(truncate_field),
2904        description: description.map(truncate_field),
2905        homepage_url: homepage_url.map(truncate_field),
2906        parties,
2907        dependencies,
2908        declared_license_expression,
2909        declared_license_expression_spdx,
2910        license_detections,
2911        extracted_license_statement: extracted_license_statement.map(truncate_field),
2912        copyright: copyright.map(truncate_field),
2913        holder,
2914        vcs_url: vcs_url.map(truncate_field),
2915        extra_data: if extra_data.is_empty() {
2916            None
2917        } else {
2918            Some(extra_data.into_iter().collect())
2919        },
2920        repository_homepage_url,
2921        repository_download_url,
2922        api_data_url,
2923        ..default_package_data(Some(DatasourceId::NugetNupkg))
2924    })
2925}
2926
2927crate::register_parser!(
2928    ".NET Directory.Build.props property source",
2929    &["**/Directory.Build.props"],
2930    "nuget",
2931    "C#",
2932    Some(
2933        "https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022"
2934    ),
2935);
2936
2937crate::register_parser!(
2938    ".NET Directory.Packages.props central package management manifest",
2939    &["**/Directory.Packages.props"],
2940    "nuget",
2941    "C#",
2942    Some("https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management"),
2943);
2944
2945crate::register_parser!(
2946    ".NET packages.config manifest",
2947    &["**/packages.config"],
2948    "nuget",
2949    "C#",
2950    Some("https://learn.microsoft.com/en-us/nuget/reference/packages-config"),
2951);
2952
2953crate::register_parser!(
2954    ".NET .nuspec package specification",
2955    &["**/*.nuspec"],
2956    "nuget",
2957    "C#",
2958    Some("https://learn.microsoft.com/en-us/nuget/reference/nuspec"),
2959);
2960
2961crate::register_parser!(
2962    ".NET packages.lock.json lockfile",
2963    &["**/packages.lock.json"],
2964    "nuget",
2965    "C#",
2966    Some(
2967        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies"
2968    ),
2969);
2970
2971crate::register_parser!(
2972    ".NET project.json manifest",
2973    &["**/project.json"],
2974    "nuget",
2975    "C#",
2976    Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
2977);
2978
2979crate::register_parser!(
2980    ".NET project.lock.json lockfile",
2981    &["**/project.lock.json"],
2982    "nuget",
2983    "C#",
2984    Some("https://learn.microsoft.com/en-us/nuget/archive/project-json"),
2985);
2986
2987crate::register_parser!(
2988    ".NET .deps.json runtime dependency graph",
2989    &["**/*.deps.json"],
2990    "nuget",
2991    "C#",
2992    Some("https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing"),
2993);
2994
2995crate::register_parser!(
2996    ".NET PackageReference C# project file",
2997    &["**/*.csproj"],
2998    "nuget",
2999    "C#",
3000    Some(
3001        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files"
3002    ),
3003);
3004
3005crate::register_parser!(
3006    ".NET PackageReference Visual Basic project file",
3007    &["**/*.vbproj"],
3008    "nuget",
3009    "Visual Basic .NET",
3010    Some(
3011        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files"
3012    ),
3013);
3014
3015crate::register_parser!(
3016    ".NET PackageReference F# project file",
3017    &["**/*.fsproj"],
3018    "nuget",
3019    "F#",
3020    Some(
3021        "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files"
3022    ),
3023);
3024
3025crate::register_parser!(
3026    ".NET .nupkg package archive",
3027    &["**/*.nupkg"],
3028    "nuget",
3029    "C#",
3030    Some("https://learn.microsoft.com/en-us/nuget/create-packages/creating-a-package"),
3031);