1use std::collections::HashMap;
5use std::fs::File;
6use std::io::BufReader;
7use std::path::{Path, PathBuf};
8
9use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
10use crate::parser_warn as warn;
11use crate::parsers::active_parser_scan_root;
12use quick_xml::Reader;
13use quick_xml::events::Event;
14
15use super::super::PackageParser;
16use super::super::utils::{MAX_ITERATION_COUNT, RecursionGuard};
17use super::utils::{resolve_bool_property_reference, resolve_optional_property_value};
18use super::{build_nuget_purl, check_file_size, default_package_data, insert_extra_string};
19
20pub struct CentralPackageManagementPropsParser;
21
22pub struct DirectoryBuildPropsParser;
23
24impl PackageParser for DirectoryBuildPropsParser {
25 const PACKAGE_TYPE: PackageType = PackageType::Nuget;
26
27 fn is_match(path: &Path) -> bool {
28 path.file_name().and_then(|name| name.to_str()) == Some("Directory.Build.props")
29 }
30
31 fn extract_packages(path: &Path) -> Vec<PackageData> {
32 let allowed_root = detect_props_resolution_root(path);
33 vec![match (
34 resolve_directory_build_props(path, &allowed_root, &mut RecursionGuard::new()),
35 parse_directory_build_props_file(path),
36 ) {
37 (Ok(data), Ok(raw)) => build_directory_build_props_package_data(data, raw),
38 (Err(e), _) | (_, Err(e)) => {
39 warn!("Error parsing Directory.Build.props at {:?}: {}", path, e);
40 default_package_data(Some(DatasourceId::NugetDirectoryBuildProps))
41 }
42 }]
43 }
44
45 fn metadata() -> Vec<super::super::metadata::ParserMetadata> {
46 vec![super::super::metadata::ParserMetadata {
47 description: ".NET Directory.Build.props property source",
48 file_patterns: &["**/Directory.Build.props"],
49 package_type: "nuget",
50 primary_language: "C#",
51 documentation_url: Some(
52 "https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022",
53 ),
54 }]
55 }
56}
57
58impl PackageParser for CentralPackageManagementPropsParser {
59 const PACKAGE_TYPE: PackageType = PackageType::Nuget;
60
61 fn is_match(path: &Path) -> bool {
62 path.file_name().and_then(|name| name.to_str()) == Some("Directory.Packages.props")
63 }
64
65 fn extract_packages(path: &Path) -> Vec<PackageData> {
66 let allowed_root = detect_props_resolution_root(path);
67 vec![match (
68 resolve_directory_packages_props(path, &allowed_root, &mut RecursionGuard::new()),
69 parse_directory_packages_props_file(path),
70 ) {
71 (Ok(data), Ok(raw)) => build_directory_packages_package_data(data, raw),
72 (Err(e), _) | (_, Err(e)) => {
73 warn!(
74 "Error parsing Directory.Packages.props at {:?}: {}",
75 path, e
76 );
77 default_package_data(Some(DatasourceId::NugetDirectoryPackagesProps))
78 }
79 }]
80 }
81
82 fn metadata() -> Vec<super::super::metadata::ParserMetadata> {
83 vec![super::super::metadata::ParserMetadata {
84 description: ".NET Directory.Packages.props central package management manifest",
85 file_patterns: &["**/Directory.Packages.props"],
86 package_type: "nuget",
87 primary_language: "C#",
88 documentation_url: Some(
89 "https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management",
90 ),
91 }]
92 }
93}
94
95#[derive(Default)]
96struct CentralPackageVersionData {
97 name: Option<String>,
98 version: Option<String>,
99 condition: Option<String>,
100}
101
102#[derive(Default)]
103struct RawCentralPackagePropsData {
104 package_versions: Vec<CentralPackageVersionData>,
105 property_values: HashMap<String, String>,
106 import_projects: Vec<String>,
107 manage_package_versions_centrally: Option<String>,
108 central_package_transitive_pinning_enabled: Option<String>,
109 central_package_version_override_enabled: Option<String>,
110}
111
112#[derive(Default)]
113struct RawBuildPropsData {
114 property_values: HashMap<String, String>,
115 import_projects: Vec<String>,
116 manage_package_versions_centrally: Option<String>,
117 central_package_transitive_pinning_enabled: Option<String>,
118 central_package_version_override_enabled: Option<String>,
119}
120
121#[derive(Default)]
122struct BuildPropsData {
123 property_values: HashMap<String, String>,
124 import_projects: Vec<String>,
125 manage_package_versions_centrally: Option<bool>,
126 central_package_transitive_pinning_enabled: Option<bool>,
127 central_package_version_override_enabled: Option<bool>,
128}
129
130#[derive(Default)]
131pub(super) struct CentralPackagePropsData {
132 dependencies: Vec<Dependency>,
133 properties: HashMap<String, String>,
134 import_projects: Vec<String>,
135 manage_package_versions_centrally: Option<bool>,
136 central_package_transitive_pinning_enabled: Option<bool>,
137 central_package_version_override_enabled: Option<bool>,
138}
139
140fn build_directory_packages_dependency(
141 name: Option<String>,
142 version: Option<String>,
143 raw_version: Option<String>,
144 condition: Option<String>,
145) -> Option<Dependency> {
146 let name = name?.trim().to_string();
147 if name.is_empty() {
148 return None;
149 }
150 let version = version
151 .map(|value| value.trim().to_string())
152 .filter(|value| !value.is_empty())?;
153
154 let mut extra_data = serde_json::Map::new();
155 insert_extra_string(&mut extra_data, "condition", condition);
156 insert_extra_string(&mut extra_data, "version_expression", raw_version);
157
158 Some(Dependency {
159 purl: build_nuget_purl(Some(&name), None),
160 extracted_requirement: Some(version),
161 scope: Some("package_version".to_string()),
162 is_runtime: Some(true),
163 is_optional: Some(false),
164 is_pinned: Some(false),
165 is_direct: Some(true),
166 resolved_package: None,
167 extra_data: if extra_data.is_empty() {
168 None
169 } else {
170 Some(extra_data.into_iter().collect())
171 },
172 })
173}
174
175fn resolve_directory_packages_props(
176 path: &Path,
177 allowed_root: &Path,
178 guard: &mut RecursionGuard<PathBuf>,
179) -> Result<CentralPackagePropsData, String> {
180 if guard.exceeded() {
181 return Err(format!(
182 "Recursion depth exceeded resolving Directory.Packages.props at {:?}",
183 path
184 ));
185 }
186
187 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
188 if guard.enter(canonical.clone()) {
189 return Ok(CentralPackagePropsData::default());
190 }
191
192 let raw = parse_directory_packages_props_file(path)?;
193 let mut merged = CentralPackagePropsData::default();
194
195 for import_project in &raw.import_projects {
196 let Some(import_path) = resolve_import_project_for_directory_packages(
197 path,
198 import_project,
199 allowed_root,
200 &HashMap::new(),
201 ) else {
202 continue;
203 };
204 let imported = resolve_directory_packages_props(&import_path, allowed_root, guard)?;
205 merge_central_package_props(&mut merged, imported);
206 }
207
208 merged.import_projects.extend(raw.import_projects.clone());
209 merged.properties.extend(raw.property_values.clone());
210
211 if let Some(value) = resolve_bool_property_reference(
212 raw.manage_package_versions_centrally.as_deref(),
213 &merged.properties,
214 ) {
215 merged.manage_package_versions_centrally = Some(value);
216 }
217 if let Some(value) = resolve_bool_property_reference(
218 raw.central_package_transitive_pinning_enabled.as_deref(),
219 &merged.properties,
220 ) {
221 merged.central_package_transitive_pinning_enabled = Some(value);
222 }
223 if let Some(value) = resolve_bool_property_reference(
224 raw.central_package_version_override_enabled.as_deref(),
225 &merged.properties,
226 ) {
227 merged.central_package_version_override_enabled = Some(value);
228 }
229
230 for entry in raw.package_versions {
231 let resolved_version =
232 resolve_optional_property_value(entry.version.as_deref(), &merged.properties);
233 if let Some(dependency) = build_directory_packages_dependency(
234 entry.name,
235 resolved_version,
236 entry.version,
237 entry.condition,
238 ) {
239 replace_matching_dependency_group(
240 &mut merged.dependencies,
241 std::slice::from_ref(&dependency),
242 );
243 merged.dependencies.push(dependency);
244 }
245 }
246
247 guard.leave(canonical);
248 Ok(merged)
249}
250
251fn resolve_directory_build_props(
252 path: &Path,
253 allowed_root: &Path,
254 guard: &mut RecursionGuard<PathBuf>,
255) -> Result<BuildPropsData, String> {
256 if guard.exceeded() {
257 return Err(format!(
258 "Recursion depth exceeded resolving Directory.Build.props at {:?}",
259 path
260 ));
261 }
262
263 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
264 if guard.enter(canonical.clone()) {
265 return Ok(BuildPropsData::default());
266 }
267
268 let raw = parse_directory_build_props_file(path)?;
269 let mut merged = BuildPropsData::default();
270
271 for import_project in &raw.import_projects {
272 let Some(import_path) = resolve_import_project_for_directory_build(
273 path,
274 import_project,
275 allowed_root,
276 &HashMap::new(),
277 ) else {
278 continue;
279 };
280 let imported = resolve_directory_build_props(&import_path, allowed_root, guard)?;
281 merge_build_props_data(&mut merged, imported);
282 }
283
284 merged.import_projects.extend(raw.import_projects.clone());
285 merged.property_values.extend(raw.property_values.clone());
286
287 if let Some(value) = resolve_bool_property_reference(
288 raw.manage_package_versions_centrally.as_deref(),
289 &merged.property_values,
290 ) {
291 merged.manage_package_versions_centrally = Some(value);
292 }
293 if let Some(value) = resolve_bool_property_reference(
294 raw.central_package_transitive_pinning_enabled.as_deref(),
295 &merged.property_values,
296 ) {
297 merged.central_package_transitive_pinning_enabled = Some(value);
298 }
299 if let Some(value) = resolve_bool_property_reference(
300 raw.central_package_version_override_enabled.as_deref(),
301 &merged.property_values,
302 ) {
303 merged.central_package_version_override_enabled = Some(value);
304 }
305
306 guard.leave(canonical);
307 Ok(merged)
308}
309
310fn parse_directory_packages_props_file(path: &Path) -> Result<RawCentralPackagePropsData, String> {
311 check_file_size(path)?;
312
313 let file = File::open(path).map_err(|e| {
314 format!(
315 "Failed to open Directory.Packages.props at {:?}: {}",
316 path, e
317 )
318 })?;
319
320 let reader = BufReader::new(file);
321 let mut xml_reader = Reader::from_reader(reader);
322 xml_reader.config_mut().trim_text(true);
323
324 let mut raw = RawCentralPackagePropsData::default();
325 let mut buf = Vec::new();
326 let mut current_element = String::new();
327 let mut current_property_group_condition = None;
328 let mut current_item_group_condition = None;
329 let mut current_package_version: Option<CentralPackageVersionData> = None;
330 let mut iteration_count: usize = 0;
331
332 loop {
333 iteration_count += 1;
334 if iteration_count > MAX_ITERATION_COUNT {
335 return Err(format!(
336 "Iteration limit exceeded in Directory.Packages.props at {:?}; stopping at {} items",
337 path, MAX_ITERATION_COUNT
338 ));
339 }
340 match xml_reader.read_event_into(&mut buf) {
341 Ok(Event::Start(e)) => {
342 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
343 current_element = tag_name.clone();
344
345 match tag_name.as_str() {
346 "ItemGroup" => {
347 current_item_group_condition = e
348 .attributes()
349 .filter_map(|a| a.ok())
350 .find(|attr| attr.key.as_ref() == b"Condition")
351 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
352 }
353 "PackageVersion" => {
354 let name = e
355 .attributes()
356 .filter_map(|a| a.ok())
357 .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
358 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
359 let version = e
360 .attributes()
361 .filter_map(|a| a.ok())
362 .find(|attr| attr.key.as_ref() == b"Version")
363 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
364 let condition = e
365 .attributes()
366 .filter_map(|a| a.ok())
367 .find(|attr| attr.key.as_ref() == b"Condition")
368 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
369 .or_else(|| current_item_group_condition.clone());
370
371 current_package_version = Some(CentralPackageVersionData {
372 name,
373 version,
374 condition,
375 });
376 }
377 "PropertyGroup" => {
378 current_property_group_condition = e
379 .attributes()
380 .filter_map(|a| a.ok())
381 .find(|attr| attr.key.as_ref() == b"Condition")
382 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
383 }
384 _ => {}
385 }
386 }
387 Ok(Event::Empty(e)) => {
388 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
389 if tag_name == "PackageVersion" {
390 let name = e
391 .attributes()
392 .filter_map(|a| a.ok())
393 .find(|attr| matches!(attr.key.as_ref(), b"Include" | b"Update"))
394 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
395 let version = e
396 .attributes()
397 .filter_map(|a| a.ok())
398 .find(|attr| attr.key.as_ref() == b"Version")
399 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
400 let condition = e
401 .attributes()
402 .filter_map(|a| a.ok())
403 .find(|attr| attr.key.as_ref() == b"Condition")
404 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
405 .or_else(|| current_item_group_condition.clone());
406
407 raw.package_versions.push(CentralPackageVersionData {
408 name,
409 version,
410 condition,
411 });
412 } else if tag_name == "Import"
413 && let Some(project) = e
414 .attributes()
415 .filter_map(|a| a.ok())
416 .find(|attr| attr.key.as_ref() == b"Project")
417 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
418 && !e
419 .attributes()
420 .filter_map(|a| a.ok())
421 .any(|attr| attr.key.as_ref() == b"Condition")
422 && is_supported_directory_packages_import(&project)
423 {
424 raw.import_projects.push(project.trim().to_string());
425 }
426 }
427 Ok(Event::Text(e)) => {
428 let text = e.decode().ok().map(|s| s.trim().to_string());
429 let Some(text) = text.filter(|value| !value.is_empty()) else {
430 buf.clear();
431 continue;
432 };
433
434 if current_package_version.is_some() {
435 if current_element.as_str() == "Version"
436 && let Some(entry) = &mut current_package_version
437 {
438 entry.version = Some(text);
439 }
440 } else if current_property_group_condition.is_none() {
441 raw.property_values
442 .insert(current_element.clone(), text.clone());
443 match current_element.as_str() {
444 "ManagePackageVersionsCentrally" => {
445 raw.manage_package_versions_centrally = Some(text)
446 }
447 "CentralPackageTransitivePinningEnabled" => {
448 raw.central_package_transitive_pinning_enabled = Some(text)
449 }
450 "CentralPackageVersionOverrideEnabled" => {
451 raw.central_package_version_override_enabled = Some(text)
452 }
453 _ => {}
454 }
455 }
456 }
457 Ok(Event::End(e)) => {
458 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
459
460 match tag_name.as_str() {
461 "PropertyGroup" => current_property_group_condition = None,
462 "ItemGroup" => current_item_group_condition = None,
463 "PackageVersion" => {
464 if let Some(entry) = current_package_version.take() {
465 raw.package_versions.push(entry);
466 }
467 }
468 _ => {}
469 }
470
471 current_element.clear();
472 }
473 Ok(Event::Eof) => break,
474 Err(e) => {
475 return Err(format!(
476 "Error parsing Directory.Packages.props at {:?}: {}",
477 path, e
478 ));
479 }
480 _ => {}
481 }
482
483 buf.clear();
484 }
485
486 Ok(raw)
487}
488
489fn parse_directory_build_props_file(path: &Path) -> Result<RawBuildPropsData, String> {
490 check_file_size(path)?;
491
492 let file = File::open(path)
493 .map_err(|e| format!("Failed to open Directory.Build.props at {:?}: {}", path, e))?;
494
495 let reader = BufReader::new(file);
496 let mut xml_reader = Reader::from_reader(reader);
497 xml_reader.config_mut().trim_text(true);
498
499 let mut raw = RawBuildPropsData::default();
500 let mut buf = Vec::new();
501 let mut current_element = String::new();
502 let mut in_property_group = false;
503 let mut current_property_group_condition = None;
504 let mut iteration_count: usize = 0;
505
506 loop {
507 iteration_count += 1;
508 if iteration_count > MAX_ITERATION_COUNT {
509 return Err(format!(
510 "Iteration limit exceeded in Directory.Build.props at {:?}; stopping at {} items",
511 path, MAX_ITERATION_COUNT
512 ));
513 }
514 match xml_reader.read_event_into(&mut buf) {
515 Ok(Event::Start(e)) => {
516 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
517 current_element = tag_name.clone();
518 if tag_name == "PropertyGroup" {
519 in_property_group = true;
520 current_property_group_condition = e
521 .attributes()
522 .filter_map(|a| a.ok())
523 .find(|attr| attr.key.as_ref() == b"Condition")
524 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok());
525 }
526 }
527 Ok(Event::Empty(e)) => {
528 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
529 if tag_name == "Import"
530 && let Some(project) = e
531 .attributes()
532 .filter_map(|a| a.ok())
533 .find(|attr| attr.key.as_ref() == b"Project")
534 .and_then(|attr| String::from_utf8(attr.value.to_vec()).ok())
535 && !e
536 .attributes()
537 .filter_map(|a| a.ok())
538 .any(|attr| attr.key.as_ref() == b"Condition")
539 && is_supported_directory_build_import(&project)
540 {
541 raw.import_projects.push(project.trim().to_string());
542 }
543 }
544 Ok(Event::Text(e)) => {
545 let text = e.decode().ok().map(|s| s.trim().to_string());
546 let Some(text) = text.filter(|value| !value.is_empty()) else {
547 buf.clear();
548 continue;
549 };
550
551 if in_property_group && current_property_group_condition.is_none() {
552 raw.property_values
553 .insert(current_element.clone(), text.clone());
554 match current_element.as_str() {
555 "ManagePackageVersionsCentrally" => {
556 raw.manage_package_versions_centrally = Some(text)
557 }
558 "CentralPackageTransitivePinningEnabled" => {
559 raw.central_package_transitive_pinning_enabled = Some(text)
560 }
561 "CentralPackageVersionOverrideEnabled" => {
562 raw.central_package_version_override_enabled = Some(text)
563 }
564 _ => {}
565 }
566 }
567 }
568 Ok(Event::End(e)) => {
569 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
570 if tag_name == "PropertyGroup" {
571 in_property_group = false;
572 current_property_group_condition = None;
573 }
574 current_element.clear();
575 }
576 Ok(Event::Eof) => break,
577 Err(e) => {
578 return Err(format!(
579 "Error parsing Directory.Build.props at {:?}: {}",
580 path, e
581 ));
582 }
583 _ => {}
584 }
585
586 buf.clear();
587 }
588
589 Ok(raw)
590}
591
592fn build_directory_packages_package_data(
593 data: CentralPackagePropsData,
594 raw: RawCentralPackagePropsData,
595) -> PackageData {
596 let mut extra_data = serde_json::Map::new();
597 if !data.properties.is_empty() {
598 extra_data.insert(
599 "property_values".to_string(),
600 serde_json::Value::Object(
601 data.properties
602 .iter()
603 .map(|(key, value)| (key.clone(), serde_json::Value::String(value.clone())))
604 .collect(),
605 ),
606 );
607 }
608 if let Some(value) = data.manage_package_versions_centrally {
609 extra_data.insert(
610 "manage_package_versions_centrally".to_string(),
611 serde_json::Value::Bool(value),
612 );
613 }
614 if let Some(value) = data.central_package_transitive_pinning_enabled {
615 extra_data.insert(
616 "central_package_transitive_pinning_enabled".to_string(),
617 serde_json::Value::Bool(value),
618 );
619 }
620 if let Some(value) = data.central_package_version_override_enabled {
621 extra_data.insert(
622 "central_package_version_override_enabled".to_string(),
623 serde_json::Value::Bool(value),
624 );
625 }
626 if !data.import_projects.is_empty() {
627 extra_data.insert(
628 "import_projects".to_string(),
629 serde_json::Value::Array(
630 data.import_projects
631 .into_iter()
632 .map(serde_json::Value::String)
633 .collect(),
634 ),
635 );
636 }
637 extra_data.insert(
638 "package_versions".to_string(),
639 serde_json::Value::Array(
640 raw.package_versions
641 .into_iter()
642 .map(|entry| {
643 serde_json::json!({
644 "name": entry.name,
645 "version": entry.version,
646 "condition": entry.condition,
647 })
648 })
649 .collect(),
650 ),
651 );
652
653 PackageData {
654 datasource_id: Some(DatasourceId::NugetDirectoryPackagesProps),
655 package_type: Some(PackageType::Nuget),
656 dependencies: data.dependencies,
657 extra_data: if extra_data.is_empty() {
658 None
659 } else {
660 Some(extra_data.into_iter().collect())
661 },
662 ..default_package_data(Some(DatasourceId::NugetDirectoryPackagesProps))
663 }
664}
665
666fn build_directory_build_props_package_data(
667 data: BuildPropsData,
668 _raw: RawBuildPropsData,
669) -> PackageData {
670 let mut extra_data = serde_json::Map::new();
671 if !data.property_values.is_empty() {
672 extra_data.insert(
673 "property_values".to_string(),
674 serde_json::Value::Object(
675 data.property_values
676 .iter()
677 .map(|(key, value)| (key.clone(), serde_json::Value::String(value.clone())))
678 .collect(),
679 ),
680 );
681 }
682 if let Some(value) = data.manage_package_versions_centrally {
683 extra_data.insert(
684 "manage_package_versions_centrally".to_string(),
685 serde_json::Value::Bool(value),
686 );
687 }
688 if let Some(value) = data.central_package_transitive_pinning_enabled {
689 extra_data.insert(
690 "central_package_transitive_pinning_enabled".to_string(),
691 serde_json::Value::Bool(value),
692 );
693 }
694 if let Some(value) = data.central_package_version_override_enabled {
695 extra_data.insert(
696 "central_package_version_override_enabled".to_string(),
697 serde_json::Value::Bool(value),
698 );
699 }
700 if !data.import_projects.is_empty() {
701 extra_data.insert(
702 "import_projects".to_string(),
703 serde_json::Value::Array(
704 data.import_projects
705 .into_iter()
706 .map(serde_json::Value::String)
707 .collect(),
708 ),
709 );
710 }
711
712 PackageData {
713 datasource_id: Some(DatasourceId::NugetDirectoryBuildProps),
714 package_type: Some(PackageType::Nuget),
715 extra_data: if extra_data.is_empty() {
716 None
717 } else {
718 Some(extra_data.into_iter().collect())
719 },
720 ..default_package_data(Some(DatasourceId::NugetDirectoryBuildProps))
721 }
722}
723
724fn merge_central_package_props(
725 target: &mut CentralPackagePropsData,
726 source: CentralPackagePropsData,
727) {
728 target.import_projects.extend(source.import_projects);
729 target.properties.extend(source.properties);
730 if target.manage_package_versions_centrally.is_none() {
731 target.manage_package_versions_centrally = source.manage_package_versions_centrally;
732 }
733 if target.central_package_transitive_pinning_enabled.is_none() {
734 target.central_package_transitive_pinning_enabled =
735 source.central_package_transitive_pinning_enabled;
736 }
737 if target.central_package_version_override_enabled.is_none() {
738 target.central_package_version_override_enabled =
739 source.central_package_version_override_enabled;
740 }
741 replace_matching_dependency_group(&mut target.dependencies, &source.dependencies);
742 target.dependencies.extend(source.dependencies);
743}
744
745fn replace_matching_dependency_group(target: &mut Vec<Dependency>, source: &[Dependency]) {
746 if source.is_empty() {
747 return;
748 }
749
750 let source_keys = source.iter().map(dependency_key).collect::<Vec<_>>();
751 target.retain(|candidate| {
752 !source_keys
753 .iter()
754 .any(|key| *key == dependency_key(candidate))
755 });
756}
757
758fn dependency_key(dependency: &Dependency) -> (Option<String>, Option<String>, Option<String>) {
759 (
760 dependency.purl.clone(),
761 dependency.scope.clone(),
762 dependency
763 .extra_data
764 .as_ref()
765 .and_then(|data| data.get("condition"))
766 .and_then(|value| value.as_str())
767 .map(ToOwned::to_owned),
768 )
769}
770
771fn is_supported_directory_packages_import(project: &str) -> bool {
772 let trimmed = project.trim();
773 if trimmed.is_empty() {
774 return false;
775 }
776
777 if is_get_path_of_file_above_import(trimmed) {
778 return true;
779 }
780
781 let candidate = PathBuf::from(trimmed);
782 candidate.extension().and_then(|ext| ext.to_str()) == Some("props")
783}
784
785fn is_supported_directory_build_import(project: &str) -> bool {
786 let trimmed = project.trim();
787 if trimmed.is_empty() {
788 return false;
789 }
790
791 if is_get_path_of_file_above_build_import(trimmed) {
792 return true;
793 }
794
795 let candidate = PathBuf::from(trimmed);
796 candidate.extension().and_then(|ext| ext.to_str()) == Some("props")
797}
798
799fn is_get_path_of_file_above_import(project: &str) -> bool {
800 let normalized = project.replace(' ', "");
801 normalized
802 == "$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props,$(MSBuildThisFileDirectory)..))"
803}
804
805fn is_get_path_of_file_above_build_import(project: &str) -> bool {
806 let normalized = project.replace(' ', "");
807 normalized
808 == "$([MSBuild]::GetPathOfFileAbove(Directory.Build.props,$(MSBuildThisFileDirectory)..))"
809}
810
811fn resolve_import_project_for_directory_build(
812 current_path: &Path,
813 project: &str,
814 allowed_root: &Path,
815 known_props_paths: &HashMap<PathBuf, &PackageData>,
816) -> Option<PathBuf> {
817 let trimmed = project.trim();
818 if is_get_path_of_file_above_build_import(trimmed) {
819 let start_dir = current_path.parent()?.parent()?;
820 for ancestor in start_dir.ancestors() {
821 let candidate = ancestor.join("Directory.Build.props");
822 if let Some(resolved) = resolve_checked_props_import(
823 candidate,
824 current_path,
825 allowed_root,
826 known_props_paths,
827 ) {
828 return Some(resolved);
829 }
830 }
831 return None;
832 }
833
834 if !is_supported_directory_build_import(trimmed) {
835 return None;
836 }
837
838 let candidate = resolve_msbuild_props_import_path(current_path, trimmed)?;
839 resolve_checked_props_import(candidate, current_path, allowed_root, known_props_paths)
840}
841
842fn merge_build_props_data(target: &mut BuildPropsData, source: BuildPropsData) {
843 target.import_projects.extend(source.import_projects);
844 target.property_values.extend(source.property_values);
845 if target.manage_package_versions_centrally.is_none() {
846 target.manage_package_versions_centrally = source.manage_package_versions_centrally;
847 }
848 if target.central_package_transitive_pinning_enabled.is_none() {
849 target.central_package_transitive_pinning_enabled =
850 source.central_package_transitive_pinning_enabled;
851 }
852 if target.central_package_version_override_enabled.is_none() {
853 target.central_package_version_override_enabled =
854 source.central_package_version_override_enabled;
855 }
856}
857
858fn resolve_import_project_for_directory_packages(
859 current_path: &Path,
860 project: &str,
861 allowed_root: &Path,
862 known_props_paths: &HashMap<PathBuf, &PackageData>,
863) -> Option<PathBuf> {
864 let trimmed = project.trim();
865 if is_get_path_of_file_above_import(trimmed) {
866 let start_dir = current_path.parent()?.parent()?;
867 for ancestor in start_dir.ancestors() {
868 let candidate = ancestor.join("Directory.Packages.props");
869 if let Some(resolved) = resolve_checked_props_import(
870 candidate,
871 current_path,
872 allowed_root,
873 known_props_paths,
874 ) {
875 return Some(resolved);
876 }
877 }
878 return None;
879 }
880
881 if !is_supported_directory_packages_import(trimmed) {
882 return None;
883 }
884
885 let candidate = resolve_msbuild_props_import_path(current_path, trimmed)?;
886 resolve_checked_props_import(candidate, current_path, allowed_root, known_props_paths)
887}
888
889fn resolve_msbuild_props_import_path(current_path: &Path, project: &str) -> Option<PathBuf> {
890 let current_dir = current_path.parent()?;
891 let current_dir_prefix = format!("{}{}", current_dir.display(), std::path::MAIN_SEPARATOR);
892 let normalized = project
893 .replace("$(MSBuildThisFileDirectory)", ¤t_dir_prefix)
894 .replace('\\', "/");
895 Some(PathBuf::from(normalized.trim()))
896}
897
898fn resolve_checked_props_import(
899 candidate: PathBuf,
900 current_path: &Path,
901 allowed_root: &Path,
902 known_props_paths: &HashMap<PathBuf, &PackageData>,
903) -> Option<PathBuf> {
904 let resolved = if candidate.is_absolute() {
905 candidate
906 } else {
907 current_path.parent()?.join(candidate)
908 };
909
910 let canonical_allowed_root = allowed_root.canonicalize().ok()?;
911 let canonical_resolved = resolved.canonicalize().ok()?;
912 if !canonical_resolved.starts_with(&canonical_allowed_root) {
913 return None;
914 }
915
916 if known_props_paths.is_empty() {
917 Some(canonical_resolved)
918 } else {
919 known_props_paths
920 .contains_key(&canonical_resolved)
921 .then_some(canonical_resolved)
922 }
923}
924
925fn detect_props_resolution_root(path: &Path) -> PathBuf {
926 if let Some(scan_root) = active_parser_scan_root() {
927 return scan_root;
928 }
929 path.parent()
930 .map(Path::to_path_buf)
931 .unwrap_or_else(|| path.to_path_buf())
932}