Skip to main content

provenant/parsers/nuget/
directory_props.rs

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