Skip to main content

provenant/assembly/
file_ref_resolve.rs

1// SPDX-FileCopyrightText: nexB Inc. and others
2// SPDX-FileCopyrightText: Provenant contributors
3// SPDX-License-Identifier: Apache-2.0
4// Derived from ScanCode Toolkit (Apache-2.0); modified. See NOTICE.
5
6use std::collections::{HashMap, HashSet};
7use std::path::Path;
8use std::str::FromStr;
9
10use crate::models::{DatasourceId, FileInfo, Package, PackageData, TopLevelDependency};
11use packageurl::PackageUrl;
12use strum::EnumIter;
13
14struct DbPathConfig {
15    datasource_ids: &'static [DatasourceId],
16    path_suffix: &'static str,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, EnumIter)]
20enum FileReferenceResolverKind {
21    About,
22    AttachedManifest,
23    CondaMeta,
24    DebianExtractedDeb,
25    InstalledDb,
26    PythonMetadata,
27    RelativeToDatafileParent,
28}
29
30struct FileReferenceResolverConfig {
31    datasource_ids: &'static [DatasourceId],
32    kind: FileReferenceResolverKind,
33}
34
35const DB_PATH_CONFIGS: &[DbPathConfig] = &[
36    DbPathConfig {
37        datasource_ids: &[DatasourceId::AlpineInstalledDb],
38        path_suffix: "lib/apk/db/installed",
39    },
40    DbPathConfig {
41        datasource_ids: &[DatasourceId::RpmInstalledDatabaseBdb],
42        path_suffix: "var/lib/rpm/Packages",
43    },
44    DbPathConfig {
45        datasource_ids: &[DatasourceId::RpmInstalledDatabaseBdb],
46        path_suffix: "usr/lib/sysimage/rpm/Packages",
47    },
48    DbPathConfig {
49        datasource_ids: &[DatasourceId::RpmInstalledDatabaseNdb],
50        path_suffix: "var/lib/rpm/Packages.db",
51    },
52    DbPathConfig {
53        datasource_ids: &[DatasourceId::RpmInstalledDatabaseNdb],
54        path_suffix: "usr/lib/sysimage/rpm/Packages.db",
55    },
56    DbPathConfig {
57        datasource_ids: &[DatasourceId::RpmInstalledDatabaseSqlite],
58        path_suffix: "usr/lib/sysimage/rpm/rpmdb.sqlite",
59    },
60    DbPathConfig {
61        datasource_ids: &[DatasourceId::RpmInstalledDatabaseSqlite],
62        path_suffix: "var/lib/rpm/rpmdb.sqlite",
63    },
64    DbPathConfig {
65        datasource_ids: &[DatasourceId::RpmMarinerManifest],
66        path_suffix: "var/lib/rpmmanifest/container-manifest-2",
67    },
68    DbPathConfig {
69        datasource_ids: &[DatasourceId::DebianInstalledStatusDb],
70        path_suffix: "var/lib/dpkg/status",
71    },
72    DbPathConfig {
73        datasource_ids: &[DatasourceId::DebianDistrolessInstalledDb],
74        path_suffix: "var/lib/dpkg/status.d/",
75    },
76];
77
78const RPM_DATASOURCE_IDS: &[DatasourceId] = &[
79    DatasourceId::RpmInstalledDatabaseBdb,
80    DatasourceId::RpmInstalledDatabaseNdb,
81    DatasourceId::RpmInstalledDatabaseSqlite,
82    DatasourceId::RpmMarinerManifest,
83];
84const RPM_YUMDB_PATH_SUFFIX: &str = "var/lib/yum/yumdb/";
85const CONDA_META_PATH_SEGMENT: &str = "conda-meta/";
86const PYTHON_METADATA_DATASOURCE_IDS: &[DatasourceId] = &[
87    DatasourceId::PypiWheelMetadata,
88    DatasourceId::PypiSdistPkginfo,
89    DatasourceId::PypiEggPkginfo,
90    DatasourceId::PypiEditableEggPkginfo,
91];
92const PYTHON_SITE_PACKAGES_SEGMENTS: &[&str] = &["site-packages/", "dist-packages/"];
93const DEBIAN_INSTALLED_SUPPLEMENTAL_DATASOURCE_IDS: &[DatasourceId] = &[
94    DatasourceId::DebianInstalledFilesList,
95    DatasourceId::DebianInstalledMd5Sums,
96];
97const RPM_INSTALLED_SUPPLEMENTAL_DATASOURCE_IDS: &[DatasourceId] =
98    &[DatasourceId::RpmPackageLicenses];
99
100const INSTALLED_DB_DATASOURCE_IDS: &[DatasourceId] = &[
101    DatasourceId::AlpineInstalledDb,
102    DatasourceId::RpmInstalledDatabaseBdb,
103    DatasourceId::RpmInstalledDatabaseNdb,
104    DatasourceId::RpmInstalledDatabaseSqlite,
105    DatasourceId::RpmMarinerManifest,
106    DatasourceId::DebianInstalledStatusDb,
107    DatasourceId::DebianDistrolessInstalledDb,
108];
109
110const FILE_REFERENCE_RESOLVER_CONFIGS: &[FileReferenceResolverConfig] = &[
111    FileReferenceResolverConfig {
112        datasource_ids: &[DatasourceId::AboutFile],
113        kind: FileReferenceResolverKind::About,
114    },
115    FileReferenceResolverConfig {
116        datasource_ids: &[DatasourceId::CpanManifest],
117        kind: FileReferenceResolverKind::AttachedManifest,
118    },
119    FileReferenceResolverConfig {
120        datasource_ids: &[DatasourceId::CondaMetaJson],
121        kind: FileReferenceResolverKind::CondaMeta,
122    },
123    FileReferenceResolverConfig {
124        datasource_ids: &[DatasourceId::DebianMd5SumsInExtractedDeb],
125        kind: FileReferenceResolverKind::DebianExtractedDeb,
126    },
127    FileReferenceResolverConfig {
128        datasource_ids: INSTALLED_DB_DATASOURCE_IDS,
129        kind: FileReferenceResolverKind::InstalledDb,
130    },
131    FileReferenceResolverConfig {
132        datasource_ids: PYTHON_METADATA_DATASOURCE_IDS,
133        kind: FileReferenceResolverKind::PythonMetadata,
134    },
135    FileReferenceResolverConfig {
136        datasource_ids: &[
137            DatasourceId::GradleModule,
138            DatasourceId::BitbakeRecipe,
139            DatasourceId::BitbakeRecipeAppend,
140        ],
141        kind: FileReferenceResolverKind::RelativeToDatafileParent,
142    },
143];
144
145struct PythonMetadataResolution {
146    base_path: String,
147    allowed_root: String,
148}
149
150pub fn resolve_file_references(
151    files: &mut [FileInfo],
152    packages: &mut [Package],
153    dependencies: &mut [TopLevelDependency],
154) {
155    if packages.is_empty() || !has_relevant_file_reference_inputs(files) {
156        return;
157    }
158
159    let path_index = build_path_index(&*files);
160
161    for package in packages.iter_mut() {
162        let Some(config) = find_file_reference_resolver(files, package) else {
163            continue;
164        };
165
166        match config.kind {
167            FileReferenceResolverKind::About
168            | FileReferenceResolverKind::RelativeToDatafileParent => {
169                resolve_relative_to_datafile_parent(
170                    files,
171                    &path_index,
172                    package,
173                    config.datasource_ids,
174                );
175            }
176            FileReferenceResolverKind::AttachedManifest => {
177                resolve_attached_manifest_file_references(
178                    files,
179                    &path_index,
180                    package,
181                    config.datasource_ids[0],
182                );
183            }
184            FileReferenceResolverKind::CondaMeta => {
185                resolve_conda_file_references(files, &path_index, package);
186            }
187            FileReferenceResolverKind::DebianExtractedDeb => {
188                resolve_debian_extracted_deb_file_references(files, &path_index, package)
189            }
190            FileReferenceResolverKind::InstalledDb => {
191                resolve_installed_db_file_references(files, &path_index, package, dependencies);
192            }
193            FileReferenceResolverKind::PythonMetadata => {
194                resolve_python_metadata_file_references(files, &path_index, package);
195            }
196        }
197    }
198}
199
200pub(super) fn has_relevant_file_reference_datasource_ids(
201    file_datasource_ids: &HashSet<DatasourceId>,
202) -> bool {
203    FILE_REFERENCE_RESOLVER_CONFIGS.iter().any(|config| {
204        config
205            .datasource_ids
206            .iter()
207            .any(|datasource_id| file_datasource_ids.contains(datasource_id))
208    })
209}
210
211fn has_relevant_file_reference_inputs(files: &[FileInfo]) -> bool {
212    let file_datasource_ids: HashSet<DatasourceId> = files
213        .iter()
214        .flat_map(|file| {
215            file.package_data
216                .iter()
217                .filter_map(|package_data| package_data.datasource_id)
218        })
219        .collect();
220
221    has_relevant_file_reference_datasource_ids(&file_datasource_ids)
222}
223
224fn resolve_relative_to_datafile_parent(
225    files: &mut [FileInfo],
226    path_index: &HashMap<String, usize>,
227    package: &mut Package,
228    datasource_ids: &[DatasourceId],
229) {
230    let mut missing_refs = Vec::new();
231    for datafile_path in &package.datafile_paths {
232        let root = Path::new(datafile_path)
233            .parent()
234            .map(|p| p.to_string_lossy().to_string())
235            .unwrap_or_default();
236
237        let file_references = collect_file_references(
238            files,
239            path_index,
240            datafile_path,
241            &package.datasource_ids,
242            datasource_ids,
243            package.purl.as_deref(),
244        );
245
246        for file_ref in &file_references {
247            let resolved_path = if root.is_empty() {
248                file_ref.path.clone()
249            } else {
250                format!("{}/{}", root, file_ref.path.trim_start_matches('/'))
251            };
252            if let Some(&file_idx) = path_index.get(&resolved_path) {
253                let package_uid = package.package_uid.clone();
254                if !files[file_idx].for_packages.contains(&package_uid) {
255                    files[file_idx].for_packages.push(package_uid);
256                }
257            } else {
258                missing_refs.push(file_ref.path.clone());
259            }
260        }
261    }
262
263    record_missing_file_references(package, missing_refs);
264}
265
266fn resolve_attached_manifest_file_references(
267    files: &mut [FileInfo],
268    path_index: &HashMap<String, usize>,
269    package: &mut Package,
270    datasource_id: DatasourceId,
271) {
272    let Some((datafile_path, file_references)) =
273        find_attached_manifest_file_references(files, package, datasource_id)
274    else {
275        return;
276    };
277
278    let root = Path::new(datafile_path)
279        .parent()
280        .map(|p| p.to_string_lossy().to_string())
281        .unwrap_or_default();
282
283    let mut missing_refs = Vec::new();
284    for file_ref in &file_references {
285        let resolved_path = if root.is_empty() {
286            file_ref.path.clone()
287        } else {
288            format!("{}/{}", root, file_ref.path.trim_start_matches('/'))
289        };
290
291        if let Some(&file_idx) = path_index.get(&resolved_path) {
292            let package_uid = package.package_uid.clone();
293            if !files[file_idx].for_packages.contains(&package_uid) {
294                files[file_idx].for_packages.push(package_uid);
295            }
296        } else {
297            missing_refs.push(file_ref.path.clone());
298        }
299    }
300
301    record_missing_file_references(package, missing_refs);
302}
303
304fn resolve_conda_file_references(
305    files: &mut [FileInfo],
306    path_index: &HashMap<String, usize>,
307    package: &mut Package,
308) {
309    let Some(conda_meta_path) = package
310        .datafile_paths
311        .iter()
312        .find(|path| path.contains(CONDA_META_PATH_SEGMENT))
313    else {
314        return;
315    };
316    let Some(root) = compute_conda_root(Some(conda_meta_path.as_str())) else {
317        return;
318    };
319
320    let file_references = collect_file_references(
321        files,
322        path_index,
323        conda_meta_path,
324        &package.datasource_ids,
325        &[DatasourceId::CondaMetaJson],
326        package.purl.as_deref(),
327    );
328
329    let mut missing_refs = Vec::new();
330    for file_ref in &file_references {
331        let resolved_path = format!("{}{}", root, file_ref.path.trim_start_matches('/'));
332        if let Some(&file_idx) = path_index.get(&resolved_path) {
333            let package_uid = package.package_uid.clone();
334            if !files[file_idx].for_packages.contains(&package_uid) {
335                files[file_idx].for_packages.push(package_uid);
336            }
337        } else {
338            missing_refs.push(file_ref.path.clone());
339        }
340    }
341
342    record_missing_file_references(package, missing_refs);
343}
344
345fn resolve_installed_db_file_references(
346    files: &mut [FileInfo],
347    path_index: &HashMap<String, usize>,
348    package: &mut Package,
349    dependencies: &mut [TopLevelDependency],
350) {
351    let Some(config) = find_db_config(package) else {
352        return;
353    };
354    let Some(datafile_path) = package.datafile_paths.first() else {
355        return;
356    };
357
358    let root = compute_root(datafile_path, config.path_suffix);
359
360    let mut file_references = collect_file_references(
361        files,
362        path_index,
363        datafile_path,
364        &package.datasource_ids,
365        config.datasource_ids,
366        package.purl.as_deref(),
367    );
368
369    if is_debian_installed_package(package) {
370        merge_file_references(
371            &mut file_references,
372            collect_debian_installed_file_references(files, package),
373        );
374    }
375
376    if is_rpm_package(package) {
377        merge_rpm_installed_supplemental_package_data(files, package, dependencies, &root);
378    }
379
380    let mut missing_refs = Vec::new();
381    for file_ref in &file_references {
382        let ref_path = file_ref.path.trim_start_matches('/');
383        let resolved_path = if root.is_empty() {
384            ref_path.to_string()
385        } else {
386            format!("{}{}", root, ref_path)
387        };
388
389        if let Some(&file_idx) = path_index.get(&resolved_path) {
390            let package_uid = package.package_uid.clone();
391            if !files[file_idx].for_packages.contains(&package_uid) {
392                files[file_idx].for_packages.push(package_uid);
393            }
394        } else {
395            missing_refs.push(file_ref.path.clone());
396        }
397    }
398
399    record_missing_file_references(package, missing_refs);
400
401    if is_rpm_package(package)
402        && let Some(namespace) = resolve_rpm_namespace(files, path_index, &root)
403    {
404        apply_rpm_namespace(files, package, dependencies, &namespace);
405    }
406}
407
408fn resolve_debian_extracted_deb_file_references(
409    files: &mut [FileInfo],
410    path_index: &HashMap<String, usize>,
411    package: &mut Package,
412) {
413    let Some(datafile_path) = package
414        .datafile_paths
415        .iter()
416        .find(|path| path.ends_with("/md5sums"))
417    else {
418        return;
419    };
420
421    let Some(md5sums_parent) = Path::new(datafile_path).parent() else {
422        return;
423    };
424    let Some(extracted_root) = md5sums_parent.parent() else {
425        return;
426    };
427    let root = extracted_root.to_string_lossy().to_string();
428
429    let Some(&file_idx) = path_index.get(datafile_path) else {
430        return;
431    };
432    let file_references: Vec<_> = files[file_idx]
433        .package_data
434        .iter()
435        .filter(|pkg_data| {
436            pkg_data.datasource_id == Some(DatasourceId::DebianMd5SumsInExtractedDeb)
437        })
438        .flat_map(|pkg_data| pkg_data.file_references.clone())
439        .collect();
440
441    let mut missing_refs = Vec::new();
442    for file_ref in &file_references {
443        let resolved_path = if root.is_empty() {
444            file_ref.path.trim_start_matches('/').to_string()
445        } else {
446            format!("{}/{}", root, file_ref.path.trim_start_matches('/'))
447        };
448
449        if let Some(&file_idx) = path_index.get(&resolved_path) {
450            let package_uid = package.package_uid.clone();
451            if !files[file_idx].for_packages.contains(&package_uid) {
452                files[file_idx].for_packages.push(package_uid);
453            }
454        } else {
455            missing_refs.push(file_ref.path.clone());
456        }
457    }
458
459    record_missing_file_references(package, missing_refs);
460}
461
462fn resolve_python_metadata_file_references(
463    files: &mut [FileInfo],
464    path_index: &HashMap<String, usize>,
465    package: &mut Package,
466) {
467    let Some(python_resolution) = find_python_metadata_root(package) else {
468        return;
469    };
470    let Some(datafile_path) = package
471        .datafile_paths
472        .iter()
473        .find(|path| is_python_metadata_layout(path))
474    else {
475        return;
476    };
477
478    let file_references = collect_file_references(
479        files,
480        path_index,
481        datafile_path,
482        &package.datasource_ids,
483        PYTHON_METADATA_DATASOURCE_IDS,
484        package.purl.as_deref(),
485    );
486
487    let mut missing_refs = Vec::new();
488    for file_ref in &file_references {
489        let Some(resolved_path) = normalize_relative_path(
490            &python_resolution.base_path,
491            &python_resolution.allowed_root,
492            &file_ref.path,
493        ) else {
494            missing_refs.push(file_ref.path.clone());
495            continue;
496        };
497
498        if let Some(&file_idx) = path_index.get(&resolved_path) {
499            let package_uid = package.package_uid.clone();
500            if !files[file_idx].for_packages.contains(&package_uid) {
501                files[file_idx].for_packages.push(package_uid);
502            }
503        } else {
504            missing_refs.push(file_ref.path.clone());
505        }
506    }
507
508    record_missing_file_references(package, missing_refs);
509}
510
511fn record_missing_file_references(package: &mut Package, mut missing_refs: Vec<String>) {
512    if missing_refs.is_empty() {
513        return;
514    }
515
516    missing_refs.sort();
517    let missing_refs_json: Vec<serde_json::Value> = missing_refs
518        .into_iter()
519        .map(|path| serde_json::json!({"path": path}))
520        .collect();
521
522    let extra_data = package.extra_data.get_or_insert_with(HashMap::new);
523    extra_data.insert(
524        "missing_file_references".to_string(),
525        serde_json::Value::Array(missing_refs_json),
526    );
527}
528
529fn find_file_reference_resolver(
530    files: &[FileInfo],
531    package: &Package,
532) -> Option<&'static FileReferenceResolverConfig> {
533    FILE_REFERENCE_RESOLVER_CONFIGS
534        .iter()
535        .find(|config| match config.kind {
536            FileReferenceResolverKind::AttachedManifest => {
537                config.datasource_ids.iter().any(|datasource_id| {
538                    files.iter().any(|file| {
539                        file.for_packages.contains(&package.package_uid)
540                            && file
541                                .package_data
542                                .iter()
543                                .any(|pkg_data| pkg_data.datasource_id == Some(*datasource_id))
544                    })
545                })
546            }
547            _ => config
548                .datasource_ids
549                .iter()
550                .any(|datasource_id| package.datasource_ids.contains(datasource_id)),
551        })
552}
553
554fn is_python_metadata_layout(path: &str) -> bool {
555    path.ends_with("/METADATA") || path.ends_with("/PKG-INFO")
556}
557
558fn find_python_metadata_root(package: &Package) -> Option<PythonMetadataResolution> {
559    let datafile_path = package
560        .datafile_paths
561        .iter()
562        .find(|path| is_python_metadata_layout(path))?;
563
564    if !package
565        .datasource_ids
566        .iter()
567        .any(|datasource_id| PYTHON_METADATA_DATASOURCE_IDS.contains(datasource_id))
568    {
569        return None;
570    }
571
572    for segment in PYTHON_SITE_PACKAGES_SEGMENTS {
573        if let Some(idx) = datafile_path.rfind(segment) {
574            if datafile_path.ends_with("/METADATA") {
575                let root_end = idx + segment.len();
576                let root = datafile_path[..root_end].to_string();
577                return Some(PythonMetadataResolution {
578                    base_path: root.clone(),
579                    allowed_root: root,
580                });
581            }
582
583            if datafile_path.ends_with("/PKG-INFO") {
584                let parent = Path::new(datafile_path).parent()?;
585                let allowed_root = datafile_path[..idx + segment.len()].to_string();
586                return Some(PythonMetadataResolution {
587                    base_path: parent.to_string_lossy().to_string(),
588                    allowed_root,
589                });
590            }
591        }
592    }
593
594    if datafile_path.ends_with(".egg-info/PKG-INFO") {
595        let metadata_parent = Path::new(datafile_path).parent()?;
596        let project_root = metadata_parent.parent()?;
597        let project_root = project_root.to_string_lossy().to_string();
598        return Some(PythonMetadataResolution {
599            base_path: project_root.clone(),
600            allowed_root: project_root,
601        });
602    }
603
604    None
605}
606
607fn normalize_relative_path(base: &str, allowed_root: &str, relative: &str) -> Option<String> {
608    let joined = Path::new(base).join(relative.trim_start_matches('/'));
609    let mut normalized = Path::new("").to_path_buf();
610
611    for component in joined.components() {
612        match component {
613            std::path::Component::CurDir => {}
614            std::path::Component::ParentDir => {
615                normalized.pop();
616            }
617            _ => normalized.push(component.as_os_str()),
618        }
619    }
620
621    let normalized_str = normalized.to_string_lossy().to_string();
622    if Path::new(&normalized_str).starts_with(Path::new(allowed_root)) {
623        Some(normalized_str)
624    } else {
625        None
626    }
627}
628
629fn compute_conda_root(datafile_path: Option<&str>) -> Option<String> {
630    let path = datafile_path?;
631    let idx = path.rfind(CONDA_META_PATH_SEGMENT)?;
632    Some(path[..idx].to_string())
633}
634
635pub fn merge_rpm_yumdb_metadata(files: &mut [FileInfo], packages: &mut Vec<Package>) {
636    let yumdb_indices: Vec<usize> = packages
637        .iter()
638        .enumerate()
639        .filter_map(|(idx, package)| {
640            package
641                .datasource_ids
642                .contains(&DatasourceId::RpmYumdb)
643                .then_some(idx)
644        })
645        .collect();
646    let mut removal_indices = Vec::new();
647
648    for yumdb_idx in yumdb_indices {
649        let yumdb_package = packages[yumdb_idx].clone();
650        let Some(yumdb_path) = yumdb_package.datafile_paths.first() else {
651            continue;
652        };
653        let yumdb_root = compute_root(yumdb_path, RPM_YUMDB_PATH_SUFFIX);
654        let yumdb_arch = yumdb_package
655            .qualifiers
656            .as_ref()
657            .and_then(|qualifiers| qualifiers.get("arch"));
658
659        let Some(target_idx) = packages.iter().enumerate().find_map(|(idx, package)| {
660            if idx == yumdb_idx || !is_rpm_package(package) {
661                return None;
662            }
663
664            let config = find_db_config(package)?;
665            let datafile_path = package.datafile_paths.first()?;
666            let target_root = compute_root(datafile_path, config.path_suffix);
667            let target_arch = package
668                .qualifiers
669                .as_ref()
670                .and_then(|qualifiers| qualifiers.get("arch"));
671
672            (target_root == yumdb_root
673                && package.name == yumdb_package.name
674                && rpm_yumdb_versions_match(&package.version, &yumdb_package.version)
675                && target_arch == yumdb_arch)
676                .then_some(idx)
677        }) else {
678            continue;
679        };
680
681        let target_package_uid = packages[target_idx].package_uid.clone();
682        {
683            let target = &mut packages[target_idx];
684            target
685                .datafile_paths
686                .extend(yumdb_package.datafile_paths.clone());
687            target
688                .datasource_ids
689                .extend(yumdb_package.datasource_ids.clone());
690
691            if let Some(yumdb_extra) = yumdb_package.extra_data.clone()
692                && !yumdb_extra.is_empty()
693            {
694                let extra_data = target.extra_data.get_or_insert_with(HashMap::new);
695                let mut merged_yumdb = extra_data
696                    .get("yumdb")
697                    .and_then(|value| value.as_object().cloned())
698                    .unwrap_or_default();
699                for (key, value) in yumdb_extra {
700                    merged_yumdb.insert(key, value);
701                }
702                extra_data.insert("yumdb".to_string(), serde_json::Value::Object(merged_yumdb));
703            }
704        }
705
706        for file in files.iter_mut() {
707            for package_uid in &mut file.for_packages {
708                if *package_uid == yumdb_package.package_uid {
709                    *package_uid = target_package_uid.clone();
710                }
711            }
712        }
713
714        removal_indices.push(yumdb_idx);
715    }
716
717    removal_indices.sort_unstable();
718    removal_indices.dedup();
719    for idx in removal_indices.into_iter().rev() {
720        packages.remove(idx);
721    }
722}
723
724fn rpm_yumdb_versions_match(
725    rpmdb_version: &Option<String>,
726    yumdb_version: &Option<String>,
727) -> bool {
728    match (rpmdb_version.as_deref(), yumdb_version.as_deref()) {
729        (Some(rpmdb), Some(yumdb)) => {
730            rpmdb == yumdb
731                || rpmdb
732                    .split_once(':')
733                    .is_some_and(|(_, version)| version == yumdb)
734        }
735        (None, None) => true,
736        _ => false,
737    }
738}
739
740fn build_path_index(files: &[FileInfo]) -> HashMap<String, usize> {
741    files
742        .iter()
743        .enumerate()
744        .map(|(idx, file)| (file.path.clone(), idx))
745        .collect()
746}
747
748fn find_db_config(package: &Package) -> Option<&'static DbPathConfig> {
749    let datafile_paths = &package.datafile_paths;
750
751    for config in DB_PATH_CONFIGS {
752        if !datafile_paths.is_empty()
753            && !datafile_paths
754                .iter()
755                .any(|path| path.ends_with(config.path_suffix))
756        {
757            continue;
758        }
759
760        for &config_dsid in config.datasource_ids {
761            for &pkg_dsid in &package.datasource_ids {
762                if config_dsid == pkg_dsid {
763                    return Some(config);
764                }
765            }
766        }
767    }
768
769    for config in DB_PATH_CONFIGS {
770        for &config_dsid in config.datasource_ids {
771            for &pkg_dsid in &package.datasource_ids {
772                if config_dsid == pkg_dsid {
773                    return Some(config);
774                }
775            }
776        }
777    }
778
779    None
780}
781
782fn compute_root(datafile_path: &str, suffix: &str) -> String {
783    if let Some(pos) = datafile_path.rfind(suffix) {
784        let root = &datafile_path[..pos];
785        if root.is_empty() {
786            String::new()
787        } else {
788            root.to_string()
789        }
790    } else {
791        String::new()
792    }
793}
794
795fn collect_file_references(
796    files: &[FileInfo],
797    path_index: &HashMap<String, usize>,
798    datafile_path: &str,
799    package_datasource_ids: &[DatasourceId],
800    config_datasource_ids: &[DatasourceId],
801    package_purl: Option<&str>,
802) -> Vec<crate::models::FileReference> {
803    let file_idx = match path_index.get(datafile_path) {
804        Some(&idx) => idx,
805        None => return Vec::new(),
806    };
807
808    let file = &files[file_idx];
809    let mut refs = Vec::new();
810
811    for pkg_data in &file.package_data {
812        let dsid_matches = pkg_data.datasource_id.is_some_and(|dsid| {
813            package_datasource_ids.contains(&dsid) || config_datasource_ids.contains(&dsid)
814        });
815
816        if !dsid_matches {
817            continue;
818        }
819
820        let purl_matches = match (package_purl, pkg_data.purl.as_deref()) {
821            (Some(pkg_purl), Some(data_purl)) => pkg_purl == data_purl,
822            _ => true,
823        };
824
825        if purl_matches {
826            refs.extend(pkg_data.file_references.clone());
827        }
828    }
829
830    refs
831}
832
833fn is_rpm_package(package: &Package) -> bool {
834    for &dsid in &package.datasource_ids {
835        for &rpm_dsid in RPM_DATASOURCE_IDS {
836            if rpm_dsid == dsid {
837                return true;
838            }
839        }
840    }
841    false
842}
843
844fn is_debian_installed_package(package: &Package) -> bool {
845    package
846        .datasource_ids
847        .contains(&DatasourceId::DebianInstalledStatusDb)
848        || package
849            .datasource_ids
850            .contains(&DatasourceId::DebianDistrolessInstalledDb)
851}
852
853fn collect_debian_installed_file_references(
854    files: &[FileInfo],
855    package: &Package,
856) -> Vec<crate::models::FileReference> {
857    let mut refs = Vec::new();
858
859    for file in files {
860        for pkg_data in &file.package_data {
861            let Some(dsid) = pkg_data.datasource_id else {
862                continue;
863            };
864            if !DEBIAN_INSTALLED_SUPPLEMENTAL_DATASOURCE_IDS.contains(&dsid) {
865                continue;
866            }
867
868            if pkg_data.name != package.name {
869                continue;
870            }
871            if !debian_installed_namespace_matches(&pkg_data.namespace, &package.namespace) {
872                continue;
873            }
874            if !debian_installed_arch_matches(&pkg_data.qualifiers, &package.qualifiers) {
875                continue;
876            }
877
878            merge_file_references(&mut refs, pkg_data.file_references.clone());
879        }
880    }
881
882    refs
883}
884
885fn merge_rpm_installed_supplemental_package_data(
886    files: &mut [FileInfo],
887    package: &mut Package,
888    dependencies: &mut [TopLevelDependency],
889    root: &str,
890) {
891    let mut supplemental_package_data: Vec<(usize, PackageData, String)> = Vec::new();
892    for (file_idx, file) in files.iter().enumerate() {
893        for pkg_data in &file.package_data {
894            let Some(dsid) = pkg_data.datasource_id else {
895                continue;
896            };
897            if !RPM_INSTALLED_SUPPLEMENTAL_DATASOURCE_IDS.contains(&dsid) {
898                continue;
899            }
900            if !rpm_installed_license_file_in_root(&file.path, root) {
901                continue;
902            }
903            if pkg_data.name != package.name {
904                continue;
905            }
906            if !rpm_installed_namespace_matches(&pkg_data.namespace, &package.namespace) {
907                continue;
908            }
909
910            supplemental_package_data.push((file_idx, pkg_data.clone(), file.path.clone()));
911        }
912    }
913
914    for (file_idx, pkg_data, datafile_path) in supplemental_package_data {
915        let old_package_uid = package.package_uid.clone();
916        package.update(&pkg_data, datafile_path);
917        if package.package_uid != old_package_uid {
918            replace_package_uid(files, dependencies, &old_package_uid, &package.package_uid);
919        }
920
921        if !files[file_idx].for_packages.contains(&package.package_uid) {
922            files[file_idx]
923                .for_packages
924                .push(package.package_uid.clone());
925        }
926    }
927}
928
929fn rpm_installed_license_file_in_root(path: &str, root: &str) -> bool {
930    let expected_prefix = format!("{}usr/share/licenses/", root);
931    path.starts_with(&expected_prefix)
932}
933
934fn rpm_installed_namespace_matches(
935    supplemental_namespace: &Option<String>,
936    package_namespace: &Option<String>,
937) -> bool {
938    match (
939        supplemental_namespace.as_deref(),
940        package_namespace.as_deref(),
941    ) {
942        (Some(left), Some(right)) => left == right,
943        _ => true,
944    }
945}
946
947fn replace_package_uid(
948    files: &mut [FileInfo],
949    dependencies: &mut [TopLevelDependency],
950    old_package_uid: &crate::models::PackageUid,
951    new_package_uid: &crate::models::PackageUid,
952) {
953    for file in files.iter_mut() {
954        for package_uid in &mut file.for_packages {
955            if package_uid == old_package_uid {
956                *package_uid = new_package_uid.clone();
957            }
958        }
959    }
960
961    for dep in dependencies.iter_mut() {
962        if dep.for_package_uid.as_ref() == Some(old_package_uid) {
963            dep.for_package_uid = Some(new_package_uid.clone());
964        }
965    }
966}
967
968fn find_attached_manifest_file_references<'a>(
969    files: &'a [FileInfo],
970    package: &Package,
971    datasource_id: DatasourceId,
972) -> Option<(&'a str, Vec<crate::models::FileReference>)> {
973    for file in files {
974        if !file.for_packages.contains(&package.package_uid) {
975            continue;
976        }
977
978        for pkg_data in &file.package_data {
979            if pkg_data.datasource_id == Some(datasource_id) {
980                return Some((&file.path, pkg_data.file_references.clone()));
981            }
982        }
983    }
984
985    None
986}
987
988fn debian_installed_namespace_matches(
989    supplemental_namespace: &Option<String>,
990    package_namespace: &Option<String>,
991) -> bool {
992    match (
993        supplemental_namespace.as_deref(),
994        package_namespace.as_deref(),
995    ) {
996        (None, _) => true,
997        (Some("debian"), Some("ubuntu")) => true,
998        (Some(left), Some(right)) => left == right,
999        (Some(_), None) => true,
1000    }
1001}
1002
1003fn debian_installed_arch_matches(
1004    supplemental_qualifiers: &Option<HashMap<String, String>>,
1005    package_qualifiers: &Option<HashMap<String, String>>,
1006) -> bool {
1007    let supplemental_arch = supplemental_qualifiers
1008        .as_ref()
1009        .and_then(|qualifiers| qualifiers.get("arch"));
1010    let package_arch = package_qualifiers
1011        .as_ref()
1012        .and_then(|qualifiers| qualifiers.get("arch"));
1013
1014    match (supplemental_arch, package_arch) {
1015        (Some(left), Some(right)) => left == right,
1016        (Some(_), None) => false,
1017        _ => true,
1018    }
1019}
1020
1021fn merge_file_references(
1022    target: &mut Vec<crate::models::FileReference>,
1023    incoming: Vec<crate::models::FileReference>,
1024) {
1025    for file_ref in incoming {
1026        if let Some(existing) = target
1027            .iter_mut()
1028            .find(|existing| existing.path == file_ref.path)
1029        {
1030            if existing.size.is_none() {
1031                existing.size = file_ref.size;
1032            }
1033            if existing.sha1.is_none() {
1034                existing.sha1 = file_ref.sha1;
1035            }
1036            if existing.md5.is_none() {
1037                existing.md5 = file_ref.md5;
1038            }
1039            if existing.sha256.is_none() {
1040                existing.sha256 = file_ref.sha256;
1041            }
1042            if existing.sha512.is_none() {
1043                existing.sha512 = file_ref.sha512;
1044            }
1045            if existing.extra_data.is_none() {
1046                existing.extra_data = file_ref.extra_data.clone();
1047            }
1048        } else {
1049            target.push(file_ref);
1050        }
1051    }
1052}
1053
1054fn resolve_rpm_namespace(
1055    files: &[FileInfo],
1056    path_index: &HashMap<String, usize>,
1057    root: &str,
1058) -> Option<String> {
1059    let os_release_paths = [
1060        format!("{}etc/os-release", root),
1061        format!("{}usr/lib/os-release", root),
1062    ];
1063
1064    for os_release_path in &os_release_paths {
1065        if let Some(&file_idx) = path_index.get(os_release_path) {
1066            let file = &files[file_idx];
1067            for pkg_data in &file.package_data {
1068                if pkg_data.datasource_id == Some(DatasourceId::EtcOsRelease)
1069                    && let Some(namespace) = &pkg_data.namespace
1070                {
1071                    return Some(namespace.clone());
1072                }
1073            }
1074        }
1075    }
1076
1077    None
1078}
1079
1080fn rewrite_purl_namespace(existing_purl: &str, namespace: &str) -> Option<String> {
1081    let parsed = PackageUrl::from_str(existing_purl).ok()?;
1082    let mut updated = PackageUrl::new(parsed.ty(), parsed.name()).ok()?;
1083
1084    updated.with_namespace(namespace).ok()?;
1085
1086    if let Some(version) = parsed.version() {
1087        updated.with_version(version).ok()?;
1088    }
1089
1090    if let Some(subpath) = parsed.subpath() {
1091        updated.with_subpath(subpath).ok()?;
1092    }
1093
1094    for (key, value) in parsed.qualifiers() {
1095        updated
1096            .add_qualifier(key.to_string(), value.to_string())
1097            .ok()?;
1098    }
1099
1100    Some(updated.to_string())
1101}
1102
1103fn apply_rpm_namespace(
1104    files: &mut [FileInfo],
1105    package: &mut Package,
1106    dependencies: &mut [TopLevelDependency],
1107    namespace: &str,
1108) {
1109    if package.namespace.is_some() {
1110        return;
1111    }
1112
1113    let old_package_uid = package.package_uid.clone();
1114
1115    package.namespace = Some(namespace.to_string());
1116
1117    if let Some(current_purl) = package.purl.as_deref()
1118        && let Some(updated_purl) = rewrite_purl_namespace(current_purl, namespace)
1119    {
1120        package.purl = Some(updated_purl.clone());
1121        package.package_uid = old_package_uid.replace_base(&updated_purl);
1122    }
1123
1124    replace_package_uid(files, dependencies, &old_package_uid, &package.package_uid);
1125
1126    for dep in dependencies.iter_mut() {
1127        if dep.for_package_uid.as_ref() == Some(&package.package_uid) {
1128            dep.namespace = Some(namespace.to_string());
1129
1130            if let Some(current_purl) = dep.purl.as_deref()
1131                && let Some(updated_purl) = rewrite_purl_namespace(current_purl, namespace)
1132            {
1133                dep.purl = Some(updated_purl.clone());
1134                dep.dependency_uid = dep.dependency_uid.replace_base(&updated_purl);
1135            }
1136        }
1137    }
1138}
1139
1140#[cfg(test)]
1141#[path = "file_ref_resolve_test.rs"]
1142mod tests;