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