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