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