1use 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
206pub 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
280pub 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 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; const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024; const MAX_COMPRESSION_RATIO: f64 = 100.0; const MAX_UNCOMPRESSED_SIZE: u64 = 1024 * 1024 * 1024; pub 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
2584pub 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);