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
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)", &current_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);