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