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