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