Skip to main content

provenant/parsers/nuget/
directory_props.rs

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);