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