Skip to main content

provenant/assembly/
file_ref_resolve.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::str::FromStr;
4
5use crate::models::{DatasourceId, FileInfo, Package, TopLevelDependency};
6use packageurl::PackageUrl;
7
8struct DbPathConfig {
9    datasource_ids: &'static [DatasourceId],
10    path_suffix: &'static str,
11}
12
13const DB_PATH_CONFIGS: &[DbPathConfig] = &[
14    DbPathConfig {
15        datasource_ids: &[DatasourceId::AlpineInstalledDb],
16        path_suffix: "lib/apk/db/installed",
17    },
18    DbPathConfig {
19        datasource_ids: &[DatasourceId::RpmInstalledDatabaseBdb],
20        path_suffix: "var/lib/rpm/Packages",
21    },
22    DbPathConfig {
23        datasource_ids: &[DatasourceId::RpmInstalledDatabaseNdb],
24        path_suffix: "usr/lib/sysimage/rpm/Packages.db",
25    },
26    DbPathConfig {
27        datasource_ids: &[DatasourceId::RpmInstalledDatabaseSqlite],
28        path_suffix: "usr/lib/sysimage/rpm/rpmdb.sqlite",
29    },
30    DbPathConfig {
31        datasource_ids: &[DatasourceId::DebianInstalledStatusDb],
32        path_suffix: "var/lib/dpkg/status",
33    },
34    DbPathConfig {
35        datasource_ids: &[DatasourceId::DebianDistrolessInstalledDb],
36        path_suffix: "var/lib/dpkg/status.d/",
37    },
38];
39
40const RPM_DATASOURCE_IDS: &[DatasourceId] = &[
41    DatasourceId::RpmInstalledDatabaseBdb,
42    DatasourceId::RpmInstalledDatabaseNdb,
43    DatasourceId::RpmInstalledDatabaseSqlite,
44];
45const RPM_YUMDB_PATH_SUFFIX: &str = "var/lib/yum/yumdb/";
46const CONDA_META_PATH_SEGMENT: &str = "conda-meta/";
47const PYTHON_METADATA_DATASOURCE_IDS: &[DatasourceId] = &[
48    DatasourceId::PypiWheelMetadata,
49    DatasourceId::PypiSdistPkginfo,
50];
51const PYTHON_SITE_PACKAGES_SEGMENTS: &[&str] = &["site-packages/", "dist-packages/"];
52const DEBIAN_INSTALLED_SUPPLEMENTAL_DATASOURCE_IDS: &[DatasourceId] = &[
53    DatasourceId::DebianInstalledFilesList,
54    DatasourceId::DebianInstalledMd5Sums,
55];
56
57struct PythonMetadataResolution {
58    base_path: String,
59    allowed_root: String,
60}
61
62pub fn resolve_file_references(
63    files: &mut [FileInfo],
64    packages: &mut [Package],
65    dependencies: &mut [TopLevelDependency],
66) {
67    let path_index = build_path_index(&*files);
68
69    for package in packages.iter_mut() {
70        if package.datasource_ids.contains(&DatasourceId::AboutFile)
71            && let Some(datafile_path) = package.datafile_paths.first()
72        {
73            let root = Path::new(datafile_path)
74                .parent()
75                .map(|p| p.to_string_lossy().to_string())
76                .unwrap_or_default();
77
78            let file_references = collect_file_references(
79                files,
80                &path_index,
81                datafile_path,
82                &package.datasource_ids,
83                &[DatasourceId::AboutFile],
84                package.purl.as_deref(),
85            );
86
87            let mut missing_refs = Vec::new();
88            for file_ref in &file_references {
89                let resolved_path = if root.is_empty() {
90                    file_ref.path.clone()
91                } else {
92                    format!("{}/{}", root, file_ref.path.trim_start_matches('/'))
93                };
94                if let Some(&file_idx) = path_index.get(&resolved_path) {
95                    let package_uid = package.package_uid.clone();
96                    if !files[file_idx].for_packages.contains(&package_uid) {
97                        files[file_idx].for_packages.push(package_uid);
98                    }
99                } else {
100                    missing_refs.push(file_ref.path.clone());
101                }
102            }
103
104            if !missing_refs.is_empty() {
105                missing_refs.sort();
106                let missing_refs_json: Vec<serde_json::Value> = missing_refs
107                    .into_iter()
108                    .map(|path| serde_json::json!({"path": path}))
109                    .collect();
110
111                let extra_data = package.extra_data.get_or_insert_with(HashMap::new);
112                extra_data.insert(
113                    "missing_file_references".to_string(),
114                    serde_json::Value::Array(missing_refs_json),
115                );
116            }
117            continue;
118        }
119
120        if is_conda_meta_package(package)
121            && let Some(conda_meta_path) = package
122                .datafile_paths
123                .iter()
124                .find(|path| path.contains(CONDA_META_PATH_SEGMENT))
125            && let Some(root) = compute_conda_root(Some(conda_meta_path.as_str()))
126        {
127            let file_references = collect_file_references(
128                files,
129                &path_index,
130                conda_meta_path,
131                &package.datasource_ids,
132                &[DatasourceId::CondaMetaJson],
133                package.purl.as_deref(),
134            );
135
136            let mut missing_refs = Vec::new();
137            for file_ref in &file_references {
138                let resolved_path = format!("{}{}", root, file_ref.path.trim_start_matches('/'));
139                if let Some(&file_idx) = path_index.get(&resolved_path) {
140                    let package_uid = package.package_uid.clone();
141                    if !files[file_idx].for_packages.contains(&package_uid) {
142                        files[file_idx].for_packages.push(package_uid);
143                    }
144                } else {
145                    missing_refs.push(file_ref.path.clone());
146                }
147            }
148
149            if !missing_refs.is_empty() {
150                missing_refs.sort();
151                let missing_refs_json: Vec<serde_json::Value> = missing_refs
152                    .into_iter()
153                    .map(|path| serde_json::json!({"path": path}))
154                    .collect();
155
156                let extra_data = package.extra_data.get_or_insert_with(HashMap::new);
157                extra_data.insert(
158                    "missing_file_references".to_string(),
159                    serde_json::Value::Array(missing_refs_json),
160                );
161            }
162            continue;
163        }
164
165        if let Some(config) = find_db_config(package) {
166            let datafile_path = match package.datafile_paths.first() {
167                Some(path) => path,
168                None => continue,
169            };
170
171            let root = compute_root(datafile_path, config.path_suffix);
172
173            let mut file_references = collect_file_references(
174                files,
175                &path_index,
176                datafile_path,
177                &package.datasource_ids,
178                config.datasource_ids,
179                package.purl.as_deref(),
180            );
181
182            if is_debian_installed_package(package) {
183                merge_file_references(
184                    &mut file_references,
185                    collect_debian_installed_file_references(files, package),
186                );
187            }
188
189            let mut missing_refs = Vec::new();
190
191            for file_ref in &file_references {
192                let ref_path = file_ref.path.trim_start_matches('/');
193                let resolved_path = if root.is_empty() {
194                    ref_path.to_string()
195                } else {
196                    format!("{}{}", root, ref_path)
197                };
198
199                if let Some(&file_idx) = path_index.get(&resolved_path) {
200                    let package_uid = package.package_uid.clone();
201                    if !files[file_idx].for_packages.contains(&package_uid) {
202                        files[file_idx].for_packages.push(package_uid);
203                    }
204                } else {
205                    missing_refs.push(file_ref.path.clone());
206                }
207            }
208
209            if !missing_refs.is_empty() {
210                missing_refs.sort();
211                let missing_refs_json: Vec<serde_json::Value> = missing_refs
212                    .into_iter()
213                    .map(|path| serde_json::json!({"path": path}))
214                    .collect();
215
216                let extra_data = package.extra_data.get_or_insert_with(HashMap::new);
217                extra_data.insert(
218                    "missing_file_references".to_string(),
219                    serde_json::Value::Array(missing_refs_json),
220                );
221            }
222
223            if is_rpm_package(package)
224                && let Some(namespace) = resolve_rpm_namespace(files, &path_index, &root)
225            {
226                apply_rpm_namespace(files, package, dependencies, &namespace);
227            }
228            continue;
229        }
230
231        if let Some(python_resolution) = find_python_metadata_root(package) {
232            let datafile_path = match package
233                .datafile_paths
234                .iter()
235                .find(|path| is_python_metadata_layout(path))
236            {
237                Some(path) => path,
238                None => continue,
239            };
240
241            let file_references = collect_file_references(
242                files,
243                &path_index,
244                datafile_path,
245                &package.datasource_ids,
246                PYTHON_METADATA_DATASOURCE_IDS,
247                package.purl.as_deref(),
248            );
249
250            let mut missing_refs = Vec::new();
251            for file_ref in &file_references {
252                let Some(resolved_path) = normalize_relative_path(
253                    &python_resolution.base_path,
254                    &python_resolution.allowed_root,
255                    &file_ref.path,
256                ) else {
257                    missing_refs.push(file_ref.path.clone());
258                    continue;
259                };
260
261                if let Some(&file_idx) = path_index.get(&resolved_path) {
262                    let package_uid = package.package_uid.clone();
263                    if !files[file_idx].for_packages.contains(&package_uid) {
264                        files[file_idx].for_packages.push(package_uid);
265                    }
266                } else {
267                    missing_refs.push(file_ref.path.clone());
268                }
269            }
270
271            if !missing_refs.is_empty() {
272                missing_refs.sort();
273                let missing_refs_json: Vec<serde_json::Value> = missing_refs
274                    .into_iter()
275                    .map(|path| serde_json::json!({"path": path}))
276                    .collect();
277
278                let extra_data = package.extra_data.get_or_insert_with(HashMap::new);
279                extra_data.insert(
280                    "missing_file_references".to_string(),
281                    serde_json::Value::Array(missing_refs_json),
282                );
283            }
284        }
285    }
286}
287
288fn is_python_metadata_layout(path: &str) -> bool {
289    path.ends_with("/METADATA") || path.ends_with("/PKG-INFO")
290}
291
292fn find_python_metadata_root(package: &Package) -> Option<PythonMetadataResolution> {
293    let datafile_path = package
294        .datafile_paths
295        .iter()
296        .find(|path| is_python_metadata_layout(path))?;
297
298    if !package
299        .datasource_ids
300        .iter()
301        .any(|datasource_id| PYTHON_METADATA_DATASOURCE_IDS.contains(datasource_id))
302    {
303        return None;
304    }
305
306    for segment in PYTHON_SITE_PACKAGES_SEGMENTS {
307        if let Some(idx) = datafile_path.rfind(segment) {
308            if datafile_path.ends_with("/METADATA") {
309                let root_end = idx + segment.len();
310                let root = datafile_path[..root_end].to_string();
311                return Some(PythonMetadataResolution {
312                    base_path: root.clone(),
313                    allowed_root: root,
314                });
315            }
316
317            if datafile_path.ends_with("/PKG-INFO") {
318                let parent = Path::new(datafile_path).parent()?;
319                let allowed_root = datafile_path[..idx + segment.len()].to_string();
320                return Some(PythonMetadataResolution {
321                    base_path: parent.to_string_lossy().to_string(),
322                    allowed_root,
323                });
324            }
325        }
326    }
327
328    if datafile_path.ends_with(".egg-info/PKG-INFO") {
329        let metadata_parent = Path::new(datafile_path).parent()?;
330        let project_root = metadata_parent.parent()?;
331        let project_root = project_root.to_string_lossy().to_string();
332        return Some(PythonMetadataResolution {
333            base_path: project_root.clone(),
334            allowed_root: project_root,
335        });
336    }
337
338    None
339}
340
341fn normalize_relative_path(base: &str, allowed_root: &str, relative: &str) -> Option<String> {
342    let joined = Path::new(base).join(relative.trim_start_matches('/'));
343    let mut normalized = Path::new("").to_path_buf();
344
345    for component in joined.components() {
346        match component {
347            std::path::Component::CurDir => {}
348            std::path::Component::ParentDir => {
349                normalized.pop();
350            }
351            _ => normalized.push(component.as_os_str()),
352        }
353    }
354
355    let normalized_str = normalized.to_string_lossy().to_string();
356    if Path::new(&normalized_str).starts_with(Path::new(allowed_root)) {
357        Some(normalized_str)
358    } else {
359        None
360    }
361}
362
363fn is_conda_meta_package(package: &Package) -> bool {
364    package
365        .datasource_ids
366        .contains(&DatasourceId::CondaMetaJson)
367}
368
369fn compute_conda_root(datafile_path: Option<&str>) -> Option<String> {
370    let path = datafile_path?;
371    let idx = path.rfind(CONDA_META_PATH_SEGMENT)?;
372    Some(path[..idx].to_string())
373}
374
375pub fn merge_rpm_yumdb_metadata(files: &mut [FileInfo], packages: &mut Vec<Package>) {
376    let yumdb_indices: Vec<usize> = packages
377        .iter()
378        .enumerate()
379        .filter_map(|(idx, package)| {
380            package
381                .datasource_ids
382                .contains(&DatasourceId::RpmYumdb)
383                .then_some(idx)
384        })
385        .collect();
386    let mut removal_indices = Vec::new();
387
388    for yumdb_idx in yumdb_indices {
389        let yumdb_package = packages[yumdb_idx].clone();
390        let Some(yumdb_path) = yumdb_package.datafile_paths.first() else {
391            continue;
392        };
393        let yumdb_root = compute_root(yumdb_path, RPM_YUMDB_PATH_SUFFIX);
394        let yumdb_arch = yumdb_package
395            .qualifiers
396            .as_ref()
397            .and_then(|qualifiers| qualifiers.get("arch"));
398
399        let Some(target_idx) = packages.iter().enumerate().find_map(|(idx, package)| {
400            if idx == yumdb_idx || !is_rpm_package(package) {
401                return None;
402            }
403
404            let config = find_db_config(package)?;
405            let datafile_path = package.datafile_paths.first()?;
406            let target_root = compute_root(datafile_path, config.path_suffix);
407            let target_arch = package
408                .qualifiers
409                .as_ref()
410                .and_then(|qualifiers| qualifiers.get("arch"));
411
412            (target_root == yumdb_root
413                && package.name == yumdb_package.name
414                && package.version == yumdb_package.version
415                && target_arch == yumdb_arch)
416                .then_some(idx)
417        }) else {
418            continue;
419        };
420
421        let target_package_uid = packages[target_idx].package_uid.clone();
422        {
423            let target = &mut packages[target_idx];
424            target
425                .datafile_paths
426                .extend(yumdb_package.datafile_paths.clone());
427            target
428                .datasource_ids
429                .extend(yumdb_package.datasource_ids.clone());
430
431            if let Some(yumdb_extra) = yumdb_package.extra_data.clone()
432                && !yumdb_extra.is_empty()
433            {
434                let extra_data = target.extra_data.get_or_insert_with(HashMap::new);
435                let mut merged_yumdb = extra_data
436                    .get("yumdb")
437                    .and_then(|value| value.as_object().cloned())
438                    .unwrap_or_default();
439                for (key, value) in yumdb_extra {
440                    merged_yumdb.insert(key, value);
441                }
442                extra_data.insert("yumdb".to_string(), serde_json::Value::Object(merged_yumdb));
443            }
444        }
445
446        for file in files.iter_mut() {
447            for package_uid in &mut file.for_packages {
448                if *package_uid == yumdb_package.package_uid {
449                    *package_uid = target_package_uid.clone();
450                }
451            }
452        }
453
454        removal_indices.push(yumdb_idx);
455    }
456
457    removal_indices.sort_unstable();
458    removal_indices.dedup();
459    for idx in removal_indices.into_iter().rev() {
460        packages.remove(idx);
461    }
462}
463
464fn build_path_index(files: &[FileInfo]) -> HashMap<String, usize> {
465    files
466        .iter()
467        .enumerate()
468        .map(|(idx, file)| (file.path.clone(), idx))
469        .collect()
470}
471
472fn find_db_config(package: &Package) -> Option<&'static DbPathConfig> {
473    for config in DB_PATH_CONFIGS {
474        for &config_dsid in config.datasource_ids {
475            for &pkg_dsid in &package.datasource_ids {
476                if config_dsid == pkg_dsid {
477                    return Some(config);
478                }
479            }
480        }
481    }
482    None
483}
484
485fn compute_root(datafile_path: &str, suffix: &str) -> String {
486    if let Some(pos) = datafile_path.rfind(suffix) {
487        let root = &datafile_path[..pos];
488        if root.is_empty() {
489            String::new()
490        } else {
491            root.to_string()
492        }
493    } else {
494        String::new()
495    }
496}
497
498fn collect_file_references(
499    files: &[FileInfo],
500    path_index: &HashMap<String, usize>,
501    datafile_path: &str,
502    package_datasource_ids: &[DatasourceId],
503    config_datasource_ids: &[DatasourceId],
504    package_purl: Option<&str>,
505) -> Vec<crate::models::FileReference> {
506    let file_idx = match path_index.get(datafile_path) {
507        Some(&idx) => idx,
508        None => return Vec::new(),
509    };
510
511    let file = &files[file_idx];
512    let mut refs = Vec::new();
513
514    for pkg_data in &file.package_data {
515        let dsid_matches = pkg_data.datasource_id.is_some_and(|dsid| {
516            package_datasource_ids.contains(&dsid) || config_datasource_ids.contains(&dsid)
517        });
518
519        if !dsid_matches {
520            continue;
521        }
522
523        let purl_matches = match (package_purl, pkg_data.purl.as_deref()) {
524            (Some(pkg_purl), Some(data_purl)) => pkg_purl == data_purl,
525            _ => true,
526        };
527
528        if purl_matches {
529            refs.extend(pkg_data.file_references.clone());
530        }
531    }
532
533    refs
534}
535
536fn is_rpm_package(package: &Package) -> bool {
537    for &dsid in &package.datasource_ids {
538        for &rpm_dsid in RPM_DATASOURCE_IDS {
539            if rpm_dsid == dsid {
540                return true;
541            }
542        }
543    }
544    false
545}
546
547fn is_debian_installed_package(package: &Package) -> bool {
548    package
549        .datasource_ids
550        .contains(&DatasourceId::DebianInstalledStatusDb)
551        || package
552            .datasource_ids
553            .contains(&DatasourceId::DebianDistrolessInstalledDb)
554}
555
556fn collect_debian_installed_file_references(
557    files: &[FileInfo],
558    package: &Package,
559) -> Vec<crate::models::FileReference> {
560    let mut refs = Vec::new();
561
562    for file in files {
563        for pkg_data in &file.package_data {
564            let Some(dsid) = pkg_data.datasource_id else {
565                continue;
566            };
567            if !DEBIAN_INSTALLED_SUPPLEMENTAL_DATASOURCE_IDS.contains(&dsid) {
568                continue;
569            }
570
571            if pkg_data.name != package.name {
572                continue;
573            }
574            if !debian_installed_namespace_matches(&pkg_data.namespace, &package.namespace) {
575                continue;
576            }
577            if !debian_installed_arch_matches(&pkg_data.qualifiers, &package.qualifiers) {
578                continue;
579            }
580
581            merge_file_references(&mut refs, pkg_data.file_references.clone());
582        }
583    }
584
585    refs
586}
587
588fn debian_installed_namespace_matches(
589    supplemental_namespace: &Option<String>,
590    package_namespace: &Option<String>,
591) -> bool {
592    match (
593        supplemental_namespace.as_deref(),
594        package_namespace.as_deref(),
595    ) {
596        (None, _) => true,
597        (Some("debian"), Some("ubuntu")) => true,
598        (Some(left), Some(right)) => left == right,
599        (Some(_), None) => true,
600    }
601}
602
603fn debian_installed_arch_matches(
604    supplemental_qualifiers: &Option<HashMap<String, String>>,
605    package_qualifiers: &Option<HashMap<String, String>>,
606) -> bool {
607    let supplemental_arch = supplemental_qualifiers
608        .as_ref()
609        .and_then(|qualifiers| qualifiers.get("arch"));
610    let package_arch = package_qualifiers
611        .as_ref()
612        .and_then(|qualifiers| qualifiers.get("arch"));
613
614    match (supplemental_arch, package_arch) {
615        (Some(left), Some(right)) => left == right,
616        (Some(_), None) => false,
617        _ => true,
618    }
619}
620
621fn merge_file_references(
622    target: &mut Vec<crate::models::FileReference>,
623    incoming: Vec<crate::models::FileReference>,
624) {
625    for file_ref in incoming {
626        if let Some(existing) = target
627            .iter_mut()
628            .find(|existing| existing.path == file_ref.path)
629        {
630            if existing.size.is_none() {
631                existing.size = file_ref.size;
632            }
633            if existing.sha1.is_none() {
634                existing.sha1 = file_ref.sha1.clone();
635            }
636            if existing.md5.is_none() {
637                existing.md5 = file_ref.md5.clone();
638            }
639            if existing.sha256.is_none() {
640                existing.sha256 = file_ref.sha256.clone();
641            }
642            if existing.sha512.is_none() {
643                existing.sha512 = file_ref.sha512.clone();
644            }
645            if existing.extra_data.is_none() {
646                existing.extra_data = file_ref.extra_data.clone();
647            }
648        } else {
649            target.push(file_ref);
650        }
651    }
652}
653
654fn resolve_rpm_namespace(
655    files: &[FileInfo],
656    path_index: &HashMap<String, usize>,
657    root: &str,
658) -> Option<String> {
659    let os_release_paths = [
660        format!("{}etc/os-release", root),
661        format!("{}usr/lib/os-release", root),
662    ];
663
664    for os_release_path in &os_release_paths {
665        if let Some(&file_idx) = path_index.get(os_release_path) {
666            let file = &files[file_idx];
667            for pkg_data in &file.package_data {
668                if pkg_data.datasource_id == Some(DatasourceId::EtcOsRelease)
669                    && let Some(namespace) = &pkg_data.namespace
670                {
671                    return Some(namespace.clone());
672                }
673            }
674        }
675    }
676
677    None
678}
679
680fn replace_uid_base(old_uid: &str, new_purl: &str) -> String {
681    if let Some((_, suffix)) = old_uid.split_once("?uuid=") {
682        return format!("{}?uuid={}", new_purl, suffix);
683    }
684
685    if let Some((_, suffix)) = old_uid.split_once("&uuid=") {
686        let separator = if new_purl.contains('?') { '&' } else { '?' };
687        return format!("{}{separator}uuid={suffix}", new_purl);
688    }
689
690    old_uid.to_string()
691}
692
693fn rewrite_purl_namespace(existing_purl: &str, namespace: &str) -> Option<String> {
694    let parsed = PackageUrl::from_str(existing_purl).ok()?;
695    let mut updated = PackageUrl::new(parsed.ty(), parsed.name()).ok()?;
696
697    updated.with_namespace(namespace).ok()?;
698
699    if let Some(version) = parsed.version() {
700        updated.with_version(version).ok()?;
701    }
702
703    if let Some(subpath) = parsed.subpath() {
704        updated.with_subpath(subpath).ok()?;
705    }
706
707    for (key, value) in parsed.qualifiers() {
708        updated
709            .add_qualifier(key.to_string(), value.to_string())
710            .ok()?;
711    }
712
713    Some(updated.to_string())
714}
715
716fn apply_rpm_namespace(
717    files: &mut [FileInfo],
718    package: &mut Package,
719    dependencies: &mut [TopLevelDependency],
720    namespace: &str,
721) {
722    let old_package_uid = package.package_uid.clone();
723
724    package.namespace = Some(namespace.to_string());
725
726    if let Some(current_purl) = package.purl.as_deref()
727        && let Some(updated_purl) = rewrite_purl_namespace(current_purl, namespace)
728    {
729        package.purl = Some(updated_purl.clone());
730        package.package_uid = replace_uid_base(&old_package_uid, &updated_purl);
731    }
732
733    for file in files.iter_mut() {
734        for package_uid in &mut file.for_packages {
735            if *package_uid == old_package_uid {
736                *package_uid = package.package_uid.clone();
737            }
738        }
739    }
740
741    for dep in dependencies.iter_mut() {
742        if dep.for_package_uid.as_deref() == Some(old_package_uid.as_str()) {
743            dep.for_package_uid = Some(package.package_uid.clone());
744        }
745
746        if dep.for_package_uid.as_deref() == Some(package.package_uid.as_str()) {
747            dep.namespace = Some(namespace.to_string());
748
749            if let Some(current_purl) = dep.purl.as_deref()
750                && let Some(updated_purl) = rewrite_purl_namespace(current_purl, namespace)
751            {
752                dep.purl = Some(updated_purl.clone());
753                dep.dependency_uid = replace_uid_base(&dep.dependency_uid, &updated_purl);
754            }
755        }
756    }
757}
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762    use crate::models::{FileReference, FileType, PackageData, PackageType};
763
764    #[test]
765    fn test_find_root_from_path() {
766        assert_eq!(
767            compute_root("rootfs/lib/apk/db/installed", "lib/apk/db/installed"),
768            "rootfs/"
769        );
770        assert_eq!(
771            compute_root("lib/apk/db/installed", "lib/apk/db/installed"),
772            ""
773        );
774        assert_eq!(
775            compute_root("container/var/lib/rpm/Packages", "var/lib/rpm/Packages"),
776            "container/"
777        );
778        assert_eq!(
779            compute_root("var/lib/rpm/Packages", "var/lib/rpm/Packages"),
780            ""
781        );
782    }
783
784    #[test]
785    fn test_resolve_basic_alpine() {
786        let mut files = vec![
787            FileInfo {
788                name: "installed".to_string(),
789                base_name: "installed".to_string(),
790                extension: String::new(),
791                path: "lib/apk/db/installed".to_string(),
792                file_type: FileType::File,
793                mime_type: None,
794                size: 100,
795                date: None,
796                sha1: None,
797                md5: None,
798                sha256: None,
799                programming_language: None,
800                package_data: vec![PackageData {
801                    datasource_id: Some(DatasourceId::AlpineInstalledDb),
802                    purl: Some("pkg:alpine/musl@1.2.3".to_string()),
803                    name: Some("musl".to_string()),
804                    file_references: vec![
805                        FileReference {
806                            path: "lib/libc.so".to_string(),
807                            size: None,
808                            sha1: None,
809                            md5: None,
810                            sha256: None,
811                            sha512: None,
812                            extra_data: None,
813                        },
814                        FileReference {
815                            path: "usr/bin/ldconfig".to_string(),
816                            size: None,
817                            sha1: None,
818                            md5: None,
819                            sha256: None,
820                            sha512: None,
821                            extra_data: None,
822                        },
823                    ],
824                    ..Default::default()
825                }],
826                license_expression: None,
827                license_detections: vec![],
828                copyrights: vec![],
829                holders: vec![],
830                authors: vec![],
831                emails: vec![],
832                urls: vec![],
833                for_packages: vec![],
834                scan_errors: vec![],
835                is_source: None,
836                source_count: None,
837                is_legal: false,
838                is_manifest: false,
839                is_readme: false,
840                is_top_level: false,
841                is_key_file: false,
842            },
843            FileInfo {
844                name: "libc.so".to_string(),
845                base_name: "libc".to_string(),
846                extension: "so".to_string(),
847                path: "lib/libc.so".to_string(),
848                file_type: FileType::File,
849                mime_type: None,
850                size: 200,
851                date: None,
852                sha1: None,
853                md5: None,
854                sha256: None,
855                programming_language: None,
856                package_data: vec![],
857                license_expression: None,
858                license_detections: vec![],
859                copyrights: vec![],
860                holders: vec![],
861                authors: vec![],
862                emails: vec![],
863                urls: vec![],
864                for_packages: vec![],
865                scan_errors: vec![],
866                is_source: None,
867                source_count: None,
868                is_legal: false,
869                is_manifest: false,
870                is_readme: false,
871                is_top_level: false,
872                is_key_file: false,
873            },
874            FileInfo {
875                name: "ldconfig".to_string(),
876                base_name: "ldconfig".to_string(),
877                extension: String::new(),
878                path: "usr/bin/ldconfig".to_string(),
879                file_type: FileType::File,
880                mime_type: None,
881                size: 300,
882                date: None,
883                sha1: None,
884                md5: None,
885                sha256: None,
886                programming_language: None,
887                package_data: vec![],
888                license_expression: None,
889                license_detections: vec![],
890                copyrights: vec![],
891                holders: vec![],
892                authors: vec![],
893                emails: vec![],
894                urls: vec![],
895                for_packages: vec![],
896                scan_errors: vec![],
897                is_source: None,
898                source_count: None,
899                is_legal: false,
900                is_manifest: false,
901                is_readme: false,
902                is_top_level: false,
903                is_key_file: false,
904            },
905        ];
906
907        let mut packages = vec![Package {
908            package_type: Some(PackageType::Alpine),
909            namespace: None,
910            name: Some("musl".to_string()),
911            version: Some("1.2.3".to_string()),
912            qualifiers: None,
913            subpath: None,
914            primary_language: None,
915            description: None,
916            release_date: None,
917            parties: vec![],
918            keywords: vec![],
919            homepage_url: None,
920            download_url: None,
921            size: None,
922            sha1: None,
923            md5: None,
924            sha256: None,
925            sha512: None,
926            bug_tracking_url: None,
927            code_view_url: None,
928            vcs_url: None,
929            copyright: None,
930            holder: None,
931            declared_license_expression: None,
932            declared_license_expression_spdx: None,
933            license_detections: vec![],
934            other_license_expression: None,
935            other_license_expression_spdx: None,
936            other_license_detections: vec![],
937            extracted_license_statement: None,
938            notice_text: None,
939            source_packages: vec![],
940            is_private: false,
941            is_virtual: false,
942            extra_data: None,
943            repository_homepage_url: None,
944            repository_download_url: None,
945            api_data_url: None,
946            purl: Some("pkg:alpine/musl@1.2.3".to_string()),
947            package_uid: "pkg:alpine/musl@1.2.3?uuid=test-uuid".to_string(),
948            datafile_paths: vec!["lib/apk/db/installed".to_string()],
949            datasource_ids: vec![DatasourceId::AlpineInstalledDb],
950        }];
951
952        let mut dependencies = vec![];
953
954        resolve_file_references(&mut files, &mut packages, &mut dependencies);
955
956        assert_eq!(files[1].for_packages.len(), 1);
957        assert_eq!(
958            files[1].for_packages[0],
959            "pkg:alpine/musl@1.2.3?uuid=test-uuid"
960        );
961        assert_eq!(files[2].for_packages.len(), 1);
962        assert_eq!(
963            files[2].for_packages[0],
964            "pkg:alpine/musl@1.2.3?uuid=test-uuid"
965        );
966    }
967
968    #[test]
969    fn test_resolve_missing_refs() {
970        let mut files = vec![FileInfo {
971            name: "installed".to_string(),
972            base_name: "installed".to_string(),
973            extension: String::new(),
974            path: "lib/apk/db/installed".to_string(),
975            file_type: FileType::File,
976            mime_type: None,
977            size: 100,
978            date: None,
979            sha1: None,
980            md5: None,
981            sha256: None,
982            programming_language: None,
983            package_data: vec![PackageData {
984                datasource_id: Some(DatasourceId::AlpineInstalledDb),
985                purl: Some("pkg:alpine/test@1.0".to_string()),
986                name: Some("test".to_string()),
987                file_references: vec![
988                    FileReference {
989                        path: "missing/file1.txt".to_string(),
990                        size: None,
991                        sha1: None,
992                        md5: None,
993                        sha256: None,
994                        sha512: None,
995                        extra_data: None,
996                    },
997                    FileReference {
998                        path: "another/missing.so".to_string(),
999                        size: None,
1000                        sha1: None,
1001                        md5: None,
1002                        sha256: None,
1003                        sha512: None,
1004                        extra_data: None,
1005                    },
1006                ],
1007                ..Default::default()
1008            }],
1009            license_expression: None,
1010            license_detections: vec![],
1011            copyrights: vec![],
1012            holders: vec![],
1013            authors: vec![],
1014            emails: vec![],
1015            urls: vec![],
1016            for_packages: vec![],
1017            scan_errors: vec![],
1018            is_source: None,
1019            source_count: None,
1020            is_legal: false,
1021            is_manifest: false,
1022            is_readme: false,
1023            is_top_level: false,
1024            is_key_file: false,
1025        }];
1026
1027        let mut packages = vec![Package {
1028            package_type: Some(PackageType::Alpine),
1029            namespace: None,
1030            name: Some("test".to_string()),
1031            version: Some("1.0".to_string()),
1032            qualifiers: None,
1033            subpath: None,
1034            primary_language: None,
1035            description: None,
1036            release_date: None,
1037            parties: vec![],
1038            keywords: vec![],
1039            homepage_url: None,
1040            download_url: None,
1041            size: None,
1042            sha1: None,
1043            md5: None,
1044            sha256: None,
1045            sha512: None,
1046            bug_tracking_url: None,
1047            code_view_url: None,
1048            vcs_url: None,
1049            copyright: None,
1050            holder: None,
1051            declared_license_expression: None,
1052            declared_license_expression_spdx: None,
1053            license_detections: vec![],
1054            other_license_expression: None,
1055            other_license_expression_spdx: None,
1056            other_license_detections: vec![],
1057            extracted_license_statement: None,
1058            notice_text: None,
1059            source_packages: vec![],
1060            is_private: false,
1061            is_virtual: false,
1062            extra_data: None,
1063            repository_homepage_url: None,
1064            repository_download_url: None,
1065            api_data_url: None,
1066            purl: Some("pkg:alpine/test@1.0".to_string()),
1067            package_uid: "pkg:alpine/test@1.0?uuid=test-uuid".to_string(),
1068            datafile_paths: vec!["lib/apk/db/installed".to_string()],
1069            datasource_ids: vec![DatasourceId::AlpineInstalledDb],
1070        }];
1071
1072        let mut dependencies = vec![];
1073
1074        resolve_file_references(&mut files, &mut packages, &mut dependencies);
1075
1076        assert!(packages[0].extra_data.is_some());
1077        let extra_data = packages[0].extra_data.as_ref().unwrap();
1078        assert!(extra_data.contains_key("missing_file_references"));
1079
1080        let missing = extra_data.get("missing_file_references").unwrap();
1081        assert!(missing.is_array());
1082        let missing_array = missing.as_array().unwrap();
1083        assert_eq!(missing_array.len(), 2);
1084        assert_eq!(missing_array[0]["path"], "another/missing.so");
1085        assert_eq!(missing_array[1]["path"], "missing/file1.txt");
1086    }
1087
1088    #[test]
1089    fn test_resolve_rpm_namespace() {
1090        let mut files = vec![
1091            FileInfo {
1092                name: "Packages".to_string(),
1093                base_name: "Packages".to_string(),
1094                extension: String::new(),
1095                path: "rootfs/var/lib/rpm/Packages".to_string(),
1096                file_type: FileType::File,
1097                mime_type: None,
1098                size: 100,
1099                date: None,
1100                sha1: None,
1101                md5: None,
1102                sha256: None,
1103                programming_language: None,
1104                package_data: vec![PackageData {
1105                    datasource_id: Some(DatasourceId::RpmInstalledDatabaseBdb),
1106                    purl: Some("pkg:rpm/bash@5.0".to_string()),
1107                    name: Some("bash".to_string()),
1108                    file_references: vec![],
1109                    ..Default::default()
1110                }],
1111                license_expression: None,
1112                license_detections: vec![],
1113                copyrights: vec![],
1114                holders: vec![],
1115                authors: vec![],
1116                emails: vec![],
1117                urls: vec![],
1118                for_packages: vec![],
1119                scan_errors: vec![],
1120                is_source: None,
1121                source_count: None,
1122                is_legal: false,
1123                is_manifest: false,
1124                is_readme: false,
1125                is_top_level: false,
1126                is_key_file: false,
1127            },
1128            FileInfo {
1129                name: "os-release".to_string(),
1130                base_name: "os-release".to_string(),
1131                extension: String::new(),
1132                path: "rootfs/etc/os-release".to_string(),
1133                file_type: FileType::File,
1134                mime_type: None,
1135                size: 50,
1136                date: None,
1137                sha1: None,
1138                md5: None,
1139                sha256: None,
1140                programming_language: None,
1141                package_data: vec![PackageData {
1142                    datasource_id: Some(DatasourceId::EtcOsRelease),
1143                    namespace: Some("fedora".to_string()),
1144                    name: Some("fedora".to_string()),
1145                    ..Default::default()
1146                }],
1147                license_expression: None,
1148                license_detections: vec![],
1149                copyrights: vec![],
1150                holders: vec![],
1151                authors: vec![],
1152                emails: vec![],
1153                urls: vec![],
1154                for_packages: vec![],
1155                scan_errors: vec![],
1156                is_source: None,
1157                source_count: None,
1158                is_legal: false,
1159                is_manifest: false,
1160                is_readme: false,
1161                is_top_level: false,
1162                is_key_file: false,
1163            },
1164        ];
1165
1166        let mut packages = vec![Package {
1167            package_type: Some(PackageType::Rpm),
1168            namespace: None,
1169            name: Some("bash".to_string()),
1170            version: Some("5.0".to_string()),
1171            qualifiers: None,
1172            subpath: None,
1173            primary_language: None,
1174            description: None,
1175            release_date: None,
1176            parties: vec![],
1177            keywords: vec![],
1178            homepage_url: None,
1179            download_url: None,
1180            size: None,
1181            sha1: None,
1182            md5: None,
1183            sha256: None,
1184            sha512: None,
1185            bug_tracking_url: None,
1186            code_view_url: None,
1187            vcs_url: None,
1188            copyright: None,
1189            holder: None,
1190            declared_license_expression: None,
1191            declared_license_expression_spdx: None,
1192            license_detections: vec![],
1193            other_license_expression: None,
1194            other_license_expression_spdx: None,
1195            other_license_detections: vec![],
1196            extracted_license_statement: None,
1197            notice_text: None,
1198            source_packages: vec![],
1199            is_private: false,
1200            is_virtual: false,
1201            extra_data: None,
1202            repository_homepage_url: None,
1203            repository_download_url: None,
1204            api_data_url: None,
1205            purl: Some("pkg:rpm/bash@5.0".to_string()),
1206            package_uid: "pkg:rpm/bash@5.0?uuid=test-uuid".to_string(),
1207            datafile_paths: vec!["rootfs/var/lib/rpm/Packages".to_string()],
1208            datasource_ids: vec![DatasourceId::RpmInstalledDatabaseBdb],
1209        }];
1210
1211        let mut dependencies = vec![TopLevelDependency {
1212            purl: Some("pkg:rpm/readline@8.0".to_string()),
1213            extracted_requirement: None,
1214            scope: None,
1215            is_runtime: Some(true),
1216            is_optional: None,
1217            is_pinned: None,
1218            is_direct: None,
1219            resolved_package: None,
1220            extra_data: None,
1221            dependency_uid: "pkg:rpm/readline@8.0?uuid=dep-uuid".to_string(),
1222            for_package_uid: Some("pkg:rpm/bash@5.0?uuid=test-uuid".to_string()),
1223            datafile_path: "rootfs/var/lib/rpm/Packages".to_string(),
1224            datasource_id: DatasourceId::RpmInstalledDatabaseBdb,
1225            namespace: None,
1226        }];
1227
1228        resolve_file_references(&mut files, &mut packages, &mut dependencies);
1229
1230        assert_eq!(packages[0].namespace, Some("fedora".to_string()));
1231        assert_eq!(packages[0].purl.as_deref(), Some("pkg:rpm/fedora/bash@5.0"));
1232        assert!(
1233            packages[0]
1234                .package_uid
1235                .starts_with("pkg:rpm/fedora/bash@5.0?uuid=")
1236        );
1237        assert_eq!(dependencies[0].namespace, Some("fedora".to_string()));
1238        assert_eq!(
1239            dependencies[0].purl.as_deref(),
1240            Some("pkg:rpm/fedora/readline@8.0")
1241        );
1242        assert_eq!(
1243            dependencies[0].for_package_uid.as_deref(),
1244            Some(packages[0].package_uid.as_str())
1245        );
1246    }
1247
1248    #[test]
1249    fn test_merge_rpm_yumdb_metadata() {
1250        let mut files = vec![
1251            FileInfo {
1252                name: "Packages".to_string(),
1253                base_name: "Packages".to_string(),
1254                extension: String::new(),
1255                path: "rootfs/var/lib/rpm/Packages".to_string(),
1256                file_type: FileType::File,
1257                mime_type: None,
1258                size: 1,
1259                date: None,
1260                sha1: None,
1261                md5: None,
1262                sha256: None,
1263                programming_language: None,
1264                package_data: vec![],
1265                license_expression: None,
1266                license_detections: vec![],
1267                copyrights: vec![],
1268                holders: vec![],
1269                authors: vec![],
1270                emails: vec![],
1271                urls: vec![],
1272                for_packages: vec!["pkg:rpm/bash@5.0-1.el8?uuid=rpm-uuid".to_string()],
1273                scan_errors: vec![],
1274                is_source: None,
1275                source_count: None,
1276                is_legal: false,
1277                is_manifest: false,
1278                is_readme: false,
1279                is_top_level: false,
1280                is_key_file: false,
1281            },
1282            FileInfo {
1283                name: "from_repo".to_string(),
1284                base_name: "from_repo".to_string(),
1285                extension: String::new(),
1286                path: "rootfs/var/lib/yum/yumdb/p/abc123-bash-5.0-1.el8.x86_64/from_repo"
1287                    .to_string(),
1288                file_type: FileType::File,
1289                mime_type: None,
1290                size: 1,
1291                date: None,
1292                sha1: None,
1293                md5: None,
1294                sha256: None,
1295                programming_language: None,
1296                package_data: vec![],
1297                license_expression: None,
1298                license_detections: vec![],
1299                copyrights: vec![],
1300                holders: vec![],
1301                authors: vec![],
1302                emails: vec![],
1303                urls: vec![],
1304                for_packages: vec!["pkg:rpm/bash@5.0-1.el8?uuid=yumdb-uuid".to_string()],
1305                scan_errors: vec![],
1306                is_source: None,
1307                source_count: None,
1308                is_legal: false,
1309                is_manifest: false,
1310                is_readme: false,
1311                is_top_level: false,
1312                is_key_file: false,
1313            },
1314        ];
1315
1316        let mut packages = vec![
1317            Package {
1318                package_type: Some(PackageType::Rpm),
1319                namespace: None,
1320                name: Some("bash".to_string()),
1321                version: Some("5.0-1.el8".to_string()),
1322                qualifiers: Some(
1323                    std::iter::once(("arch".to_string(), "x86_64".to_string())).collect(),
1324                ),
1325                subpath: None,
1326                primary_language: None,
1327                description: None,
1328                release_date: None,
1329                parties: vec![],
1330                keywords: vec![],
1331                homepage_url: None,
1332                download_url: None,
1333                size: None,
1334                sha1: None,
1335                md5: None,
1336                sha256: None,
1337                sha512: None,
1338                bug_tracking_url: None,
1339                code_view_url: None,
1340                vcs_url: None,
1341                copyright: None,
1342                holder: None,
1343                declared_license_expression: None,
1344                declared_license_expression_spdx: None,
1345                license_detections: vec![],
1346                other_license_expression: None,
1347                other_license_expression_spdx: None,
1348                other_license_detections: vec![],
1349                extracted_license_statement: None,
1350                notice_text: None,
1351                source_packages: vec![],
1352                is_private: false,
1353                is_virtual: false,
1354                extra_data: None,
1355                repository_homepage_url: None,
1356                repository_download_url: None,
1357                api_data_url: None,
1358                purl: Some("pkg:rpm/bash@5.0-1.el8?arch=x86_64".to_string()),
1359                package_uid: "pkg:rpm/bash@5.0-1.el8?uuid=rpm-uuid".to_string(),
1360                datafile_paths: vec!["rootfs/var/lib/rpm/Packages".to_string()],
1361                datasource_ids: vec![DatasourceId::RpmInstalledDatabaseBdb],
1362            },
1363            Package {
1364                package_type: Some(PackageType::Rpm),
1365                namespace: None,
1366                name: Some("bash".to_string()),
1367                version: Some("5.0-1.el8".to_string()),
1368                qualifiers: Some(
1369                    std::iter::once(("arch".to_string(), "x86_64".to_string())).collect(),
1370                ),
1371                subpath: None,
1372                primary_language: None,
1373                description: None,
1374                release_date: None,
1375                parties: vec![],
1376                keywords: vec![],
1377                homepage_url: None,
1378                download_url: None,
1379                size: None,
1380                sha1: None,
1381                md5: None,
1382                sha256: None,
1383                sha512: None,
1384                bug_tracking_url: None,
1385                code_view_url: None,
1386                vcs_url: None,
1387                copyright: None,
1388                holder: None,
1389                declared_license_expression: None,
1390                declared_license_expression_spdx: None,
1391                license_detections: vec![],
1392                other_license_expression: None,
1393                other_license_expression_spdx: None,
1394                other_license_detections: vec![],
1395                extracted_license_statement: None,
1396                notice_text: None,
1397                source_packages: vec![],
1398                is_private: false,
1399                is_virtual: true,
1400                extra_data: Some(
1401                    [
1402                        (
1403                            "from_repo".to_string(),
1404                            serde_json::Value::String("baseos".to_string()),
1405                        ),
1406                        (
1407                            "releasever".to_string(),
1408                            serde_json::Value::String("8".to_string()),
1409                        ),
1410                    ]
1411                    .into_iter()
1412                    .collect(),
1413                ),
1414                repository_homepage_url: None,
1415                repository_download_url: None,
1416                api_data_url: None,
1417                purl: Some("pkg:rpm/bash@5.0-1.el8?arch=x86_64".to_string()),
1418                package_uid: "pkg:rpm/bash@5.0-1.el8?uuid=yumdb-uuid".to_string(),
1419                datafile_paths: vec![
1420                    "rootfs/var/lib/yum/yumdb/p/abc123-bash-5.0-1.el8.x86_64/from_repo".to_string(),
1421                ],
1422                datasource_ids: vec![DatasourceId::RpmYumdb],
1423            },
1424        ];
1425
1426        merge_rpm_yumdb_metadata(&mut files, &mut packages);
1427
1428        assert_eq!(packages.len(), 1);
1429        assert!(packages[0].datasource_ids.contains(&DatasourceId::RpmYumdb));
1430        assert!(
1431            packages[0]
1432                .datafile_paths
1433                .iter()
1434                .any(|path| path.contains("var/lib/yum/yumdb"))
1435        );
1436        let yumdb = packages[0]
1437            .extra_data
1438            .as_ref()
1439            .and_then(|extra| extra.get("yumdb"))
1440            .and_then(|value| value.as_object())
1441            .unwrap();
1442        assert_eq!(yumdb["from_repo"], "baseos");
1443        assert_eq!(yumdb["releasever"], "8");
1444        assert_eq!(
1445            files[1].for_packages,
1446            vec!["pkg:rpm/bash@5.0-1.el8?uuid=rpm-uuid".to_string()]
1447        );
1448    }
1449
1450    #[test]
1451    fn test_strip_leading_slash() {
1452        let mut files = vec![
1453            FileInfo {
1454                name: "installed".to_string(),
1455                base_name: "installed".to_string(),
1456                extension: String::new(),
1457                path: "lib/apk/db/installed".to_string(),
1458                file_type: FileType::File,
1459                mime_type: None,
1460                size: 100,
1461                date: None,
1462                sha1: None,
1463                md5: None,
1464                sha256: None,
1465                programming_language: None,
1466                package_data: vec![PackageData {
1467                    datasource_id: Some(DatasourceId::AlpineInstalledDb),
1468                    purl: Some("pkg:alpine/test@1.0".to_string()),
1469                    name: Some("test".to_string()),
1470                    file_references: vec![FileReference {
1471                        path: "/lib/test.so".to_string(),
1472                        size: None,
1473                        sha1: None,
1474                        md5: None,
1475                        sha256: None,
1476                        sha512: None,
1477                        extra_data: None,
1478                    }],
1479                    ..Default::default()
1480                }],
1481                license_expression: None,
1482                license_detections: vec![],
1483                copyrights: vec![],
1484                holders: vec![],
1485                authors: vec![],
1486                emails: vec![],
1487                urls: vec![],
1488                for_packages: vec![],
1489                scan_errors: vec![],
1490                is_source: None,
1491                source_count: None,
1492                is_legal: false,
1493                is_manifest: false,
1494                is_readme: false,
1495                is_top_level: false,
1496                is_key_file: false,
1497            },
1498            FileInfo {
1499                name: "test.so".to_string(),
1500                base_name: "test".to_string(),
1501                extension: "so".to_string(),
1502                path: "lib/test.so".to_string(),
1503                file_type: FileType::File,
1504                mime_type: None,
1505                size: 200,
1506                date: None,
1507                sha1: None,
1508                md5: None,
1509                sha256: None,
1510                programming_language: None,
1511                package_data: vec![],
1512                license_expression: None,
1513                license_detections: vec![],
1514                copyrights: vec![],
1515                holders: vec![],
1516                authors: vec![],
1517                emails: vec![],
1518                urls: vec![],
1519                for_packages: vec![],
1520                scan_errors: vec![],
1521                is_source: None,
1522                source_count: None,
1523                is_legal: false,
1524                is_manifest: false,
1525                is_readme: false,
1526                is_top_level: false,
1527                is_key_file: false,
1528            },
1529        ];
1530
1531        let mut packages = vec![Package {
1532            package_type: Some(PackageType::Alpine),
1533            namespace: None,
1534            name: Some("test".to_string()),
1535            version: Some("1.0".to_string()),
1536            qualifiers: None,
1537            subpath: None,
1538            primary_language: None,
1539            description: None,
1540            release_date: None,
1541            parties: vec![],
1542            keywords: vec![],
1543            homepage_url: None,
1544            download_url: None,
1545            size: None,
1546            sha1: None,
1547            md5: None,
1548            sha256: None,
1549            sha512: None,
1550            bug_tracking_url: None,
1551            code_view_url: None,
1552            vcs_url: None,
1553            copyright: None,
1554            holder: None,
1555            declared_license_expression: None,
1556            declared_license_expression_spdx: None,
1557            license_detections: vec![],
1558            other_license_expression: None,
1559            other_license_expression_spdx: None,
1560            other_license_detections: vec![],
1561            extracted_license_statement: None,
1562            notice_text: None,
1563            source_packages: vec![],
1564            is_private: false,
1565            is_virtual: false,
1566            extra_data: None,
1567            repository_homepage_url: None,
1568            repository_download_url: None,
1569            api_data_url: None,
1570            purl: Some("pkg:alpine/test@1.0".to_string()),
1571            package_uid: "pkg:alpine/test@1.0?uuid=test-uuid".to_string(),
1572            datafile_paths: vec!["lib/apk/db/installed".to_string()],
1573            datasource_ids: vec![DatasourceId::AlpineInstalledDb],
1574        }];
1575
1576        let mut dependencies = vec![];
1577
1578        resolve_file_references(&mut files, &mut packages, &mut dependencies);
1579
1580        assert_eq!(files[1].for_packages.len(), 1);
1581        assert_eq!(
1582            files[1].for_packages[0],
1583            "pkg:alpine/test@1.0?uuid=test-uuid"
1584        );
1585    }
1586
1587    #[test]
1588    fn test_resolve_python_metadata_file_references() {
1589        let mut files = vec![
1590            FileInfo {
1591                name: "METADATA".to_string(),
1592                base_name: "METADATA".to_string(),
1593                extension: String::new(),
1594                path: "venv/lib/python3.11/site-packages/click-8.0.4.dist-info/METADATA"
1595                    .to_string(),
1596                file_type: FileType::File,
1597                mime_type: None,
1598                size: 100,
1599                date: None,
1600                sha1: None,
1601                md5: None,
1602                sha256: None,
1603                programming_language: None,
1604                package_data: vec![PackageData {
1605                    datasource_id: Some(DatasourceId::PypiWheelMetadata),
1606                    purl: Some("pkg:pypi/click@8.0.4".to_string()),
1607                    name: Some("click".to_string()),
1608                    version: Some("8.0.4".to_string()),
1609                    file_references: vec![
1610                        FileReference {
1611                            path: "click/__init__.py".to_string(),
1612                            size: None,
1613                            sha1: None,
1614                            md5: None,
1615                            sha256: None,
1616                            sha512: None,
1617                            extra_data: None,
1618                        },
1619                        FileReference {
1620                            path: "click/core.py".to_string(),
1621                            size: None,
1622                            sha1: None,
1623                            md5: None,
1624                            sha256: None,
1625                            sha512: None,
1626                            extra_data: None,
1627                        },
1628                        FileReference {
1629                            path: "click-8.0.4.dist-info/LICENSE.rst".to_string(),
1630                            size: None,
1631                            sha1: None,
1632                            md5: None,
1633                            sha256: None,
1634                            sha512: None,
1635                            extra_data: None,
1636                        },
1637                    ],
1638                    ..Default::default()
1639                }],
1640                license_expression: None,
1641                license_detections: vec![],
1642                copyrights: vec![],
1643                holders: vec![],
1644                authors: vec![],
1645                emails: vec![],
1646                urls: vec![],
1647                for_packages: vec![],
1648                scan_errors: vec![],
1649                is_source: None,
1650                source_count: None,
1651                is_legal: false,
1652                is_manifest: false,
1653                is_readme: false,
1654                is_top_level: false,
1655                is_key_file: false,
1656            },
1657            FileInfo {
1658                name: "__init__.py".to_string(),
1659                base_name: "__init__".to_string(),
1660                extension: "py".to_string(),
1661                path: "venv/lib/python3.11/site-packages/click/__init__.py".to_string(),
1662                file_type: FileType::File,
1663                mime_type: None,
1664                size: 5,
1665                date: None,
1666                sha1: None,
1667                md5: None,
1668                sha256: None,
1669                programming_language: None,
1670                package_data: vec![],
1671                license_expression: None,
1672                license_detections: vec![],
1673                copyrights: vec![],
1674                holders: vec![],
1675                authors: vec![],
1676                emails: vec![],
1677                urls: vec![],
1678                for_packages: vec![],
1679                scan_errors: vec![],
1680                is_source: None,
1681                source_count: None,
1682                is_legal: false,
1683                is_manifest: false,
1684                is_readme: false,
1685                is_top_level: false,
1686                is_key_file: false,
1687            },
1688            FileInfo {
1689                name: "core.py".to_string(),
1690                base_name: "core".to_string(),
1691                extension: "py".to_string(),
1692                path: "venv/lib/python3.11/site-packages/click/core.py".to_string(),
1693                file_type: FileType::File,
1694                mime_type: None,
1695                size: 10,
1696                date: None,
1697                sha1: None,
1698                md5: None,
1699                sha256: None,
1700                programming_language: None,
1701                package_data: vec![],
1702                license_expression: None,
1703                license_detections: vec![],
1704                copyrights: vec![],
1705                holders: vec![],
1706                authors: vec![],
1707                emails: vec![],
1708                urls: vec![],
1709                for_packages: vec![],
1710                scan_errors: vec![],
1711                is_source: None,
1712                source_count: None,
1713                is_legal: false,
1714                is_manifest: false,
1715                is_readme: false,
1716                is_top_level: false,
1717                is_key_file: false,
1718            },
1719            FileInfo {
1720                name: "LICENSE.rst".to_string(),
1721                base_name: "LICENSE".to_string(),
1722                extension: "rst".to_string(),
1723                path: "venv/lib/python3.11/site-packages/click-8.0.4.dist-info/LICENSE.rst"
1724                    .to_string(),
1725                file_type: FileType::File,
1726                mime_type: None,
1727                size: 20,
1728                date: None,
1729                sha1: None,
1730                md5: None,
1731                sha256: None,
1732                programming_language: None,
1733                package_data: vec![],
1734                license_expression: None,
1735                license_detections: vec![],
1736                copyrights: vec![],
1737                holders: vec![],
1738                authors: vec![],
1739                emails: vec![],
1740                urls: vec![],
1741                for_packages: vec![],
1742                scan_errors: vec![],
1743                is_source: None,
1744                source_count: None,
1745                is_legal: false,
1746                is_manifest: false,
1747                is_readme: false,
1748                is_top_level: false,
1749                is_key_file: false,
1750            },
1751        ];
1752
1753        let mut packages = vec![Package {
1754            package_type: Some(PackageType::Pypi),
1755            namespace: None,
1756            name: Some("click".to_string()),
1757            version: Some("8.0.4".to_string()),
1758            qualifiers: None,
1759            subpath: None,
1760            primary_language: None,
1761            description: None,
1762            release_date: None,
1763            parties: vec![],
1764            keywords: vec![],
1765            homepage_url: None,
1766            download_url: None,
1767            size: None,
1768            sha1: None,
1769            md5: None,
1770            sha256: None,
1771            sha512: None,
1772            bug_tracking_url: None,
1773            code_view_url: None,
1774            vcs_url: None,
1775            copyright: None,
1776            holder: None,
1777            declared_license_expression: None,
1778            declared_license_expression_spdx: None,
1779            license_detections: vec![],
1780            other_license_expression: None,
1781            other_license_expression_spdx: None,
1782            other_license_detections: vec![],
1783            extracted_license_statement: None,
1784            notice_text: None,
1785            source_packages: vec![],
1786            is_private: false,
1787            is_virtual: false,
1788            extra_data: None,
1789            repository_homepage_url: None,
1790            repository_download_url: None,
1791            api_data_url: None,
1792            purl: Some("pkg:pypi/click@8.0.4".to_string()),
1793            package_uid: "pkg:pypi/click@8.0.4?uuid=test-uuid".to_string(),
1794            datafile_paths: vec![
1795                "venv/lib/python3.11/site-packages/click-8.0.4.dist-info/METADATA".to_string(),
1796            ],
1797            datasource_ids: vec![DatasourceId::PypiWheelMetadata],
1798        }];
1799
1800        let mut dependencies = vec![];
1801
1802        resolve_file_references(&mut files, &mut packages, &mut dependencies);
1803
1804        assert_eq!(files[1].for_packages.len(), 1);
1805        assert_eq!(files[2].for_packages.len(), 1);
1806        assert_eq!(files[3].for_packages.len(), 1);
1807        assert_eq!(
1808            files[2].for_packages[0],
1809            "pkg:pypi/click@8.0.4?uuid=test-uuid"
1810        );
1811    }
1812
1813    #[test]
1814    fn test_resolve_python_pkg_info_installed_files_references() {
1815        let mut files = vec![
1816            FileInfo {
1817                name: "PKG-INFO".to_string(),
1818                base_name: "PKG-INFO".to_string(),
1819                extension: String::new(),
1820                path: "venv/lib/python3.11/site-packages/examplepkg.egg-info/PKG-INFO".to_string(),
1821                file_type: FileType::File,
1822                mime_type: None,
1823                size: 100,
1824                date: None,
1825                sha1: None,
1826                md5: None,
1827                sha256: None,
1828                programming_language: None,
1829                package_data: vec![PackageData {
1830                    datasource_id: Some(DatasourceId::PypiSdistPkginfo),
1831                    purl: Some("pkg:pypi/examplepkg@1.0.0".to_string()),
1832                    name: Some("examplepkg".to_string()),
1833                    version: Some("1.0.0".to_string()),
1834                    file_references: vec![FileReference {
1835                        path: "../examplepkg/core.py".to_string(),
1836                        size: None,
1837                        sha1: None,
1838                        md5: None,
1839                        sha256: None,
1840                        sha512: None,
1841                        extra_data: None,
1842                    }],
1843                    ..Default::default()
1844                }],
1845                license_expression: None,
1846                license_detections: vec![],
1847                copyrights: vec![],
1848                holders: vec![],
1849                authors: vec![],
1850                emails: vec![],
1851                urls: vec![],
1852                for_packages: vec![],
1853                scan_errors: vec![],
1854                is_source: None,
1855                source_count: None,
1856                is_legal: false,
1857                is_manifest: false,
1858                is_readme: false,
1859                is_top_level: false,
1860                is_key_file: false,
1861            },
1862            FileInfo {
1863                name: "core.py".to_string(),
1864                base_name: "core".to_string(),
1865                extension: "py".to_string(),
1866                path: "venv/lib/python3.11/site-packages/examplepkg/core.py".to_string(),
1867                file_type: FileType::File,
1868                mime_type: None,
1869                size: 10,
1870                date: None,
1871                sha1: None,
1872                md5: None,
1873                sha256: None,
1874                programming_language: None,
1875                package_data: vec![],
1876                license_expression: None,
1877                license_detections: vec![],
1878                copyrights: vec![],
1879                holders: vec![],
1880                authors: vec![],
1881                emails: vec![],
1882                urls: vec![],
1883                for_packages: vec![],
1884                scan_errors: vec![],
1885                is_source: None,
1886                source_count: None,
1887                is_legal: false,
1888                is_manifest: false,
1889                is_readme: false,
1890                is_top_level: false,
1891                is_key_file: false,
1892            },
1893        ];
1894
1895        let mut packages = vec![Package {
1896            package_type: Some(PackageType::Pypi),
1897            namespace: None,
1898            name: Some("examplepkg".to_string()),
1899            version: Some("1.0.0".to_string()),
1900            qualifiers: None,
1901            subpath: None,
1902            primary_language: None,
1903            description: None,
1904            release_date: None,
1905            parties: vec![],
1906            keywords: vec![],
1907            homepage_url: None,
1908            download_url: None,
1909            size: None,
1910            sha1: None,
1911            md5: None,
1912            sha256: None,
1913            sha512: None,
1914            bug_tracking_url: None,
1915            code_view_url: None,
1916            vcs_url: None,
1917            copyright: None,
1918            holder: None,
1919            declared_license_expression: None,
1920            declared_license_expression_spdx: None,
1921            license_detections: vec![],
1922            other_license_expression: None,
1923            other_license_expression_spdx: None,
1924            other_license_detections: vec![],
1925            extracted_license_statement: None,
1926            notice_text: None,
1927            source_packages: vec![],
1928            is_private: false,
1929            is_virtual: false,
1930            extra_data: None,
1931            repository_homepage_url: None,
1932            repository_download_url: None,
1933            api_data_url: None,
1934            purl: Some("pkg:pypi/examplepkg@1.0.0".to_string()),
1935            package_uid: "pkg:pypi/examplepkg@1.0.0?uuid=test-uuid".to_string(),
1936            datafile_paths: vec![
1937                "venv/lib/python3.11/site-packages/examplepkg.egg-info/PKG-INFO".to_string(),
1938            ],
1939            datasource_ids: vec![DatasourceId::PypiSdistPkginfo],
1940        }];
1941
1942        let mut dependencies = vec![];
1943
1944        resolve_file_references(&mut files, &mut packages, &mut dependencies);
1945
1946        assert_eq!(
1947            files[1].for_packages,
1948            vec!["pkg:pypi/examplepkg@1.0.0?uuid=test-uuid".to_string()]
1949        );
1950    }
1951
1952    #[test]
1953    fn test_resolve_python_metadata_file_references_in_dist_packages() {
1954        let mut files = vec![
1955            FileInfo {
1956                name: "METADATA".to_string(),
1957                base_name: "METADATA".to_string(),
1958                extension: String::new(),
1959                path: "usr/lib/python3/dist-packages/click-8.0.4.dist-info/METADATA".to_string(),
1960                file_type: FileType::File,
1961                mime_type: None,
1962                size: 100,
1963                date: None,
1964                sha1: None,
1965                md5: None,
1966                sha256: None,
1967                programming_language: None,
1968                package_data: vec![PackageData {
1969                    datasource_id: Some(DatasourceId::PypiWheelMetadata),
1970                    purl: Some("pkg:pypi/click@8.0.4".to_string()),
1971                    name: Some("click".to_string()),
1972                    version: Some("8.0.4".to_string()),
1973                    file_references: vec![FileReference {
1974                        path: "click/core.py".to_string(),
1975                        size: None,
1976                        sha1: None,
1977                        md5: None,
1978                        sha256: None,
1979                        sha512: None,
1980                        extra_data: None,
1981                    }],
1982                    ..Default::default()
1983                }],
1984                license_expression: None,
1985                license_detections: vec![],
1986                copyrights: vec![],
1987                holders: vec![],
1988                authors: vec![],
1989                emails: vec![],
1990                urls: vec![],
1991                for_packages: vec![],
1992                scan_errors: vec![],
1993                is_source: None,
1994                source_count: None,
1995                is_legal: false,
1996                is_manifest: false,
1997                is_readme: false,
1998                is_top_level: false,
1999                is_key_file: false,
2000            },
2001            FileInfo {
2002                name: "core.py".to_string(),
2003                base_name: "core".to_string(),
2004                extension: "py".to_string(),
2005                path: "usr/lib/python3/dist-packages/click/core.py".to_string(),
2006                file_type: FileType::File,
2007                mime_type: None,
2008                size: 10,
2009                date: None,
2010                sha1: None,
2011                md5: None,
2012                sha256: None,
2013                programming_language: None,
2014                package_data: vec![],
2015                license_expression: None,
2016                license_detections: vec![],
2017                copyrights: vec![],
2018                holders: vec![],
2019                authors: vec![],
2020                emails: vec![],
2021                urls: vec![],
2022                for_packages: vec![],
2023                scan_errors: vec![],
2024                is_source: None,
2025                source_count: None,
2026                is_legal: false,
2027                is_manifest: false,
2028                is_readme: false,
2029                is_top_level: false,
2030                is_key_file: false,
2031            },
2032        ];
2033
2034        let mut packages = vec![Package {
2035            package_type: Some(PackageType::Pypi),
2036            namespace: None,
2037            name: Some("click".to_string()),
2038            version: Some("8.0.4".to_string()),
2039            qualifiers: None,
2040            subpath: None,
2041            primary_language: None,
2042            description: None,
2043            release_date: None,
2044            parties: vec![],
2045            keywords: vec![],
2046            homepage_url: None,
2047            download_url: None,
2048            size: None,
2049            sha1: None,
2050            md5: None,
2051            sha256: None,
2052            sha512: None,
2053            bug_tracking_url: None,
2054            code_view_url: None,
2055            vcs_url: None,
2056            copyright: None,
2057            holder: None,
2058            declared_license_expression: None,
2059            declared_license_expression_spdx: None,
2060            license_detections: vec![],
2061            other_license_expression: None,
2062            other_license_expression_spdx: None,
2063            other_license_detections: vec![],
2064            extracted_license_statement: None,
2065            notice_text: None,
2066            source_packages: vec![],
2067            is_private: false,
2068            is_virtual: false,
2069            extra_data: None,
2070            repository_homepage_url: None,
2071            repository_download_url: None,
2072            api_data_url: None,
2073            purl: Some("pkg:pypi/click@8.0.4".to_string()),
2074            package_uid: "pkg:pypi/click@8.0.4?uuid=test-uuid".to_string(),
2075            datafile_paths: vec![
2076                "usr/lib/python3/dist-packages/click-8.0.4.dist-info/METADATA".to_string(),
2077            ],
2078            datasource_ids: vec![DatasourceId::PypiWheelMetadata],
2079        }];
2080
2081        let mut dependencies = vec![];
2082
2083        resolve_file_references(&mut files, &mut packages, &mut dependencies);
2084
2085        assert_eq!(
2086            files[1].for_packages,
2087            vec!["pkg:pypi/click@8.0.4?uuid=test-uuid".to_string()]
2088        );
2089    }
2090
2091    #[test]
2092    fn test_python_metadata_file_references_do_not_assign_outside_packages_dirs() {
2093        let mut files = vec![
2094            FileInfo {
2095                name: "METADATA".to_string(),
2096                base_name: "METADATA".to_string(),
2097                extension: String::new(),
2098                path: "project/metadata/METADATA".to_string(),
2099                file_type: FileType::File,
2100                mime_type: None,
2101                size: 100,
2102                date: None,
2103                sha1: None,
2104                md5: None,
2105                sha256: None,
2106                programming_language: None,
2107                package_data: vec![PackageData {
2108                    datasource_id: Some(DatasourceId::PypiWheelMetadata),
2109                    purl: Some("pkg:pypi/examplepkg@1.0.0".to_string()),
2110                    name: Some("examplepkg".to_string()),
2111                    version: Some("1.0.0".to_string()),
2112                    file_references: vec![FileReference {
2113                        path: "examplepkg/core.py".to_string(),
2114                        size: None,
2115                        sha1: None,
2116                        md5: None,
2117                        sha256: None,
2118                        sha512: None,
2119                        extra_data: None,
2120                    }],
2121                    ..Default::default()
2122                }],
2123                license_expression: None,
2124                license_detections: vec![],
2125                copyrights: vec![],
2126                holders: vec![],
2127                authors: vec![],
2128                emails: vec![],
2129                urls: vec![],
2130                for_packages: vec![],
2131                scan_errors: vec![],
2132                is_source: None,
2133                source_count: None,
2134                is_legal: false,
2135                is_manifest: false,
2136                is_readme: false,
2137                is_top_level: false,
2138                is_key_file: false,
2139            },
2140            FileInfo {
2141                name: "core.py".to_string(),
2142                base_name: "core".to_string(),
2143                extension: "py".to_string(),
2144                path: "project/examplepkg/core.py".to_string(),
2145                file_type: FileType::File,
2146                mime_type: None,
2147                size: 10,
2148                date: None,
2149                sha1: None,
2150                md5: None,
2151                sha256: None,
2152                programming_language: None,
2153                package_data: vec![],
2154                license_expression: None,
2155                license_detections: vec![],
2156                copyrights: vec![],
2157                holders: vec![],
2158                authors: vec![],
2159                emails: vec![],
2160                urls: vec![],
2161                for_packages: vec![],
2162                scan_errors: vec![],
2163                is_source: None,
2164                source_count: None,
2165                is_legal: false,
2166                is_manifest: false,
2167                is_readme: false,
2168                is_top_level: false,
2169                is_key_file: false,
2170            },
2171        ];
2172
2173        let mut packages = vec![Package {
2174            package_type: Some(PackageType::Pypi),
2175            namespace: None,
2176            name: Some("examplepkg".to_string()),
2177            version: Some("1.0.0".to_string()),
2178            qualifiers: None,
2179            subpath: None,
2180            primary_language: None,
2181            description: None,
2182            release_date: None,
2183            parties: vec![],
2184            keywords: vec![],
2185            homepage_url: None,
2186            download_url: None,
2187            size: None,
2188            sha1: None,
2189            md5: None,
2190            sha256: None,
2191            sha512: None,
2192            bug_tracking_url: None,
2193            code_view_url: None,
2194            vcs_url: None,
2195            copyright: None,
2196            holder: None,
2197            declared_license_expression: None,
2198            declared_license_expression_spdx: None,
2199            license_detections: vec![],
2200            other_license_expression: None,
2201            other_license_expression_spdx: None,
2202            other_license_detections: vec![],
2203            extracted_license_statement: None,
2204            notice_text: None,
2205            source_packages: vec![],
2206            is_private: false,
2207            is_virtual: false,
2208            extra_data: None,
2209            repository_homepage_url: None,
2210            repository_download_url: None,
2211            api_data_url: None,
2212            purl: Some("pkg:pypi/examplepkg@1.0.0".to_string()),
2213            package_uid: "pkg:pypi/examplepkg@1.0.0?uuid=test-uuid".to_string(),
2214            datafile_paths: vec!["project/metadata/METADATA".to_string()],
2215            datasource_ids: vec![DatasourceId::PypiWheelMetadata],
2216        }];
2217
2218        let mut dependencies = vec![];
2219
2220        resolve_file_references(&mut files, &mut packages, &mut dependencies);
2221
2222        assert!(files[1].for_packages.is_empty());
2223    }
2224
2225    #[test]
2226    fn test_python_sources_file_references_do_not_escape_project_root() {
2227        let mut files = vec![
2228            FileInfo {
2229                name: "PKG-INFO".to_string(),
2230                base_name: "PKG-INFO".to_string(),
2231                extension: String::new(),
2232                path: "project/PyJPString.egg-info/PKG-INFO".to_string(),
2233                file_type: FileType::File,
2234                mime_type: None,
2235                size: 100,
2236                date: None,
2237                sha1: None,
2238                md5: None,
2239                sha256: None,
2240                programming_language: None,
2241                package_data: vec![PackageData {
2242                    datasource_id: Some(DatasourceId::PypiSdistPkginfo),
2243                    purl: Some("pkg:pypi/PyJPString@0.0.3".to_string()),
2244                    name: Some("PyJPString".to_string()),
2245                    version: Some("0.0.3".to_string()),
2246                    file_references: vec![FileReference {
2247                        path: "../../outside.py".to_string(),
2248                        size: None,
2249                        sha1: None,
2250                        md5: None,
2251                        sha256: None,
2252                        sha512: None,
2253                        extra_data: None,
2254                    }],
2255                    ..Default::default()
2256                }],
2257                license_expression: None,
2258                license_detections: vec![],
2259                copyrights: vec![],
2260                holders: vec![],
2261                authors: vec![],
2262                emails: vec![],
2263                urls: vec![],
2264                for_packages: vec![],
2265                scan_errors: vec![],
2266                is_source: None,
2267                source_count: None,
2268                is_legal: false,
2269                is_manifest: false,
2270                is_readme: false,
2271                is_top_level: false,
2272                is_key_file: false,
2273            },
2274            FileInfo {
2275                name: "outside.py".to_string(),
2276                base_name: "outside".to_string(),
2277                extension: "py".to_string(),
2278                path: "outside.py".to_string(),
2279                file_type: FileType::File,
2280                mime_type: None,
2281                size: 10,
2282                date: None,
2283                sha1: None,
2284                md5: None,
2285                sha256: None,
2286                programming_language: None,
2287                package_data: vec![],
2288                license_expression: None,
2289                license_detections: vec![],
2290                copyrights: vec![],
2291                holders: vec![],
2292                authors: vec![],
2293                emails: vec![],
2294                urls: vec![],
2295                for_packages: vec![],
2296                scan_errors: vec![],
2297                is_source: None,
2298                source_count: None,
2299                is_legal: false,
2300                is_manifest: false,
2301                is_readme: false,
2302                is_top_level: false,
2303                is_key_file: false,
2304            },
2305        ];
2306
2307        let mut packages = vec![Package {
2308            package_type: Some(PackageType::Pypi),
2309            namespace: None,
2310            name: Some("PyJPString".to_string()),
2311            version: Some("0.0.3".to_string()),
2312            qualifiers: None,
2313            subpath: None,
2314            primary_language: None,
2315            description: None,
2316            release_date: None,
2317            parties: vec![],
2318            keywords: vec![],
2319            homepage_url: None,
2320            download_url: None,
2321            size: None,
2322            sha1: None,
2323            md5: None,
2324            sha256: None,
2325            sha512: None,
2326            bug_tracking_url: None,
2327            code_view_url: None,
2328            vcs_url: None,
2329            copyright: None,
2330            holder: None,
2331            declared_license_expression: None,
2332            declared_license_expression_spdx: None,
2333            license_detections: vec![],
2334            other_license_expression: None,
2335            other_license_expression_spdx: None,
2336            other_license_detections: vec![],
2337            extracted_license_statement: None,
2338            notice_text: None,
2339            source_packages: vec![],
2340            is_private: false,
2341            is_virtual: false,
2342            extra_data: None,
2343            repository_homepage_url: None,
2344            repository_download_url: None,
2345            api_data_url: None,
2346            purl: Some("pkg:pypi/PyJPString@0.0.3".to_string()),
2347            package_uid: "pkg:pypi/PyJPString@0.0.3?uuid=test-uuid".to_string(),
2348            datafile_paths: vec!["project/PyJPString.egg-info/PKG-INFO".to_string()],
2349            datasource_ids: vec![DatasourceId::PypiSdistPkginfo],
2350        }];
2351
2352        let mut dependencies = vec![];
2353
2354        resolve_file_references(&mut files, &mut packages, &mut dependencies);
2355
2356        assert!(files[1].for_packages.is_empty());
2357        let missing = packages[0]
2358            .extra_data
2359            .as_ref()
2360            .and_then(|extra| extra.get("missing_file_references"))
2361            .and_then(|value| value.as_array())
2362            .expect("missing_file_references should be recorded");
2363        assert_eq!(missing.len(), 1);
2364    }
2365
2366    #[test]
2367    fn test_resolve_debian_installed_file_references_from_status_db() {
2368        let mut files = vec![
2369            FileInfo {
2370                name: "status".to_string(),
2371                base_name: "status".to_string(),
2372                extension: String::new(),
2373                path: "rootfs/var/lib/dpkg/status".to_string(),
2374                file_type: FileType::File,
2375                mime_type: None,
2376                size: 100,
2377                date: None,
2378                sha1: None,
2379                md5: None,
2380                sha256: None,
2381                programming_language: None,
2382                package_data: vec![PackageData {
2383                    datasource_id: Some(DatasourceId::DebianInstalledStatusDb),
2384                    package_type: Some(PackageType::Deb),
2385                    namespace: Some("debian".to_string()),
2386                    name: Some("bash".to_string()),
2387                    version: Some("5.2-1".to_string()),
2388                    purl: Some("pkg:deb/debian/bash@5.2-1?arch=amd64".to_string()),
2389                    ..Default::default()
2390                }],
2391                license_expression: None,
2392                license_detections: vec![],
2393                copyrights: vec![],
2394                holders: vec![],
2395                authors: vec![],
2396                emails: vec![],
2397                urls: vec![],
2398                for_packages: vec![],
2399                scan_errors: vec![],
2400                is_source: None,
2401                source_count: None,
2402                is_legal: false,
2403                is_manifest: false,
2404                is_readme: false,
2405                is_top_level: false,
2406                is_key_file: false,
2407            },
2408            FileInfo {
2409                name: "bash.list".to_string(),
2410                base_name: "bash".to_string(),
2411                extension: "list".to_string(),
2412                path: "rootfs/var/lib/dpkg/info/bash.list".to_string(),
2413                file_type: FileType::File,
2414                mime_type: None,
2415                size: 40,
2416                date: None,
2417                sha1: None,
2418                md5: None,
2419                sha256: None,
2420                programming_language: None,
2421                package_data: vec![PackageData {
2422                    datasource_id: Some(DatasourceId::DebianInstalledFilesList),
2423                    package_type: Some(PackageType::Deb),
2424                    namespace: Some("debian".to_string()),
2425                    name: Some("bash".to_string()),
2426                    purl: Some("pkg:deb/debian/bash".to_string()),
2427                    file_references: vec![
2428                        FileReference {
2429                            path: "/bin/bash".to_string(),
2430                            size: None,
2431                            sha1: None,
2432                            md5: None,
2433                            sha256: None,
2434                            sha512: None,
2435                            extra_data: None,
2436                        },
2437                        FileReference {
2438                            path: "/usr/share/doc/bash/copyright".to_string(),
2439                            size: None,
2440                            sha1: None,
2441                            md5: None,
2442                            sha256: None,
2443                            sha512: None,
2444                            extra_data: None,
2445                        },
2446                    ],
2447                    ..Default::default()
2448                }],
2449                license_expression: None,
2450                license_detections: vec![],
2451                copyrights: vec![],
2452                holders: vec![],
2453                authors: vec![],
2454                emails: vec![],
2455                urls: vec![],
2456                for_packages: vec![],
2457                scan_errors: vec![],
2458                is_source: None,
2459                source_count: None,
2460                is_legal: false,
2461                is_manifest: false,
2462                is_readme: false,
2463                is_top_level: false,
2464                is_key_file: false,
2465            },
2466            FileInfo {
2467                name: "bash.md5sums".to_string(),
2468                base_name: "bash".to_string(),
2469                extension: "md5sums".to_string(),
2470                path: "rootfs/var/lib/dpkg/info/bash.md5sums".to_string(),
2471                file_type: FileType::File,
2472                mime_type: None,
2473                size: 40,
2474                date: None,
2475                sha1: None,
2476                md5: None,
2477                sha256: None,
2478                programming_language: None,
2479                package_data: vec![PackageData {
2480                    datasource_id: Some(DatasourceId::DebianInstalledMd5Sums),
2481                    package_type: Some(PackageType::Deb),
2482                    namespace: Some("debian".to_string()),
2483                    name: Some("bash".to_string()),
2484                    purl: Some("pkg:deb/debian/bash".to_string()),
2485                    file_references: vec![FileReference {
2486                        path: "bin/bash".to_string(),
2487                        size: None,
2488                        sha1: None,
2489                        md5: Some("77506afebd3b7e19e937a678a185b62e".to_string()),
2490                        sha256: None,
2491                        sha512: None,
2492                        extra_data: None,
2493                    }],
2494                    ..Default::default()
2495                }],
2496                license_expression: None,
2497                license_detections: vec![],
2498                copyrights: vec![],
2499                holders: vec![],
2500                authors: vec![],
2501                emails: vec![],
2502                urls: vec![],
2503                for_packages: vec![],
2504                scan_errors: vec![],
2505                is_source: None,
2506                source_count: None,
2507                is_legal: false,
2508                is_manifest: false,
2509                is_readme: false,
2510                is_top_level: false,
2511                is_key_file: false,
2512            },
2513            FileInfo {
2514                name: "bash".to_string(),
2515                base_name: "bash".to_string(),
2516                extension: String::new(),
2517                path: "rootfs/bin/bash".to_string(),
2518                file_type: FileType::File,
2519                mime_type: None,
2520                size: 20,
2521                date: None,
2522                sha1: None,
2523                md5: None,
2524                sha256: None,
2525                programming_language: None,
2526                package_data: vec![],
2527                license_expression: None,
2528                license_detections: vec![],
2529                copyrights: vec![],
2530                holders: vec![],
2531                authors: vec![],
2532                emails: vec![],
2533                urls: vec![],
2534                for_packages: vec![],
2535                scan_errors: vec![],
2536                is_source: None,
2537                source_count: None,
2538                is_legal: false,
2539                is_manifest: false,
2540                is_readme: false,
2541                is_top_level: false,
2542                is_key_file: false,
2543            },
2544            FileInfo {
2545                name: "copyright".to_string(),
2546                base_name: "copyright".to_string(),
2547                extension: String::new(),
2548                path: "rootfs/usr/share/doc/bash/copyright".to_string(),
2549                file_type: FileType::File,
2550                mime_type: None,
2551                size: 20,
2552                date: None,
2553                sha1: None,
2554                md5: None,
2555                sha256: None,
2556                programming_language: None,
2557                package_data: vec![],
2558                license_expression: None,
2559                license_detections: vec![],
2560                copyrights: vec![],
2561                holders: vec![],
2562                authors: vec![],
2563                emails: vec![],
2564                urls: vec![],
2565                for_packages: vec![],
2566                scan_errors: vec![],
2567                is_source: None,
2568                source_count: None,
2569                is_legal: false,
2570                is_manifest: false,
2571                is_readme: false,
2572                is_top_level: false,
2573                is_key_file: false,
2574            },
2575        ];
2576
2577        let mut packages = vec![Package {
2578            package_type: Some(PackageType::Deb),
2579            namespace: Some("debian".to_string()),
2580            name: Some("bash".to_string()),
2581            version: Some("5.2-1".to_string()),
2582            qualifiers: Some(HashMap::from([("arch".to_string(), "amd64".to_string())])),
2583            subpath: None,
2584            primary_language: None,
2585            description: None,
2586            release_date: None,
2587            parties: vec![],
2588            keywords: vec![],
2589            homepage_url: None,
2590            download_url: None,
2591            size: None,
2592            sha1: None,
2593            md5: None,
2594            sha256: None,
2595            sha512: None,
2596            bug_tracking_url: None,
2597            code_view_url: None,
2598            vcs_url: None,
2599            copyright: None,
2600            holder: None,
2601            declared_license_expression: None,
2602            declared_license_expression_spdx: None,
2603            license_detections: vec![],
2604            other_license_expression: None,
2605            other_license_expression_spdx: None,
2606            other_license_detections: vec![],
2607            extracted_license_statement: None,
2608            notice_text: None,
2609            source_packages: vec![],
2610            is_private: false,
2611            is_virtual: false,
2612            extra_data: None,
2613            repository_homepage_url: None,
2614            repository_download_url: None,
2615            api_data_url: None,
2616            purl: Some("pkg:deb/debian/bash@5.2-1?arch=amd64".to_string()),
2617            package_uid: "pkg:deb/debian/bash@5.2-1?arch=amd64&uuid=test-uuid".to_string(),
2618            datafile_paths: vec!["rootfs/var/lib/dpkg/status".to_string()],
2619            datasource_ids: vec![DatasourceId::DebianInstalledStatusDb],
2620        }];
2621
2622        let mut dependencies = vec![];
2623        resolve_file_references(&mut files, &mut packages, &mut dependencies);
2624
2625        assert_eq!(
2626            files[3].for_packages,
2627            vec!["pkg:deb/debian/bash@5.2-1?arch=amd64&uuid=test-uuid".to_string()]
2628        );
2629        assert_eq!(
2630            files[4].for_packages,
2631            vec!["pkg:deb/debian/bash@5.2-1?arch=amd64&uuid=test-uuid".to_string()]
2632        );
2633    }
2634
2635    #[test]
2636    fn test_resolve_debian_installed_file_references_matches_ubuntu_package_namespace() {
2637        let mut files = vec![
2638            FileInfo {
2639                name: "status".to_string(),
2640                base_name: "status".to_string(),
2641                extension: String::new(),
2642                path: "rootfs/var/lib/dpkg/status".to_string(),
2643                file_type: FileType::File,
2644                mime_type: None,
2645                size: 100,
2646                date: None,
2647                sha1: None,
2648                md5: None,
2649                sha256: None,
2650                programming_language: None,
2651                package_data: vec![PackageData {
2652                    datasource_id: Some(DatasourceId::DebianInstalledStatusDb),
2653                    package_type: Some(PackageType::Deb),
2654                    namespace: Some("ubuntu".to_string()),
2655                    name: Some("bash".to_string()),
2656                    version: Some("5.2-1ubuntu1".to_string()),
2657                    purl: Some("pkg:deb/ubuntu/bash@5.2-1ubuntu1?arch=amd64".to_string()),
2658                    ..Default::default()
2659                }],
2660                license_expression: None,
2661                license_detections: vec![],
2662                copyrights: vec![],
2663                holders: vec![],
2664                authors: vec![],
2665                emails: vec![],
2666                urls: vec![],
2667                for_packages: vec![],
2668                scan_errors: vec![],
2669                is_source: None,
2670                source_count: None,
2671                is_legal: false,
2672                is_manifest: false,
2673                is_readme: false,
2674                is_top_level: false,
2675                is_key_file: false,
2676            },
2677            FileInfo {
2678                name: "bash.list".to_string(),
2679                base_name: "bash".to_string(),
2680                extension: "list".to_string(),
2681                path: "rootfs/var/lib/dpkg/info/bash.list".to_string(),
2682                file_type: FileType::File,
2683                mime_type: None,
2684                size: 40,
2685                date: None,
2686                sha1: None,
2687                md5: None,
2688                sha256: None,
2689                programming_language: None,
2690                package_data: vec![PackageData {
2691                    datasource_id: Some(DatasourceId::DebianInstalledFilesList),
2692                    package_type: Some(PackageType::Deb),
2693                    namespace: Some("debian".to_string()),
2694                    name: Some("bash".to_string()),
2695                    purl: Some("pkg:deb/debian/bash".to_string()),
2696                    file_references: vec![FileReference {
2697                        path: "/bin/bash".to_string(),
2698                        size: None,
2699                        sha1: None,
2700                        md5: None,
2701                        sha256: None,
2702                        sha512: None,
2703                        extra_data: None,
2704                    }],
2705                    ..Default::default()
2706                }],
2707                license_expression: None,
2708                license_detections: vec![],
2709                copyrights: vec![],
2710                holders: vec![],
2711                authors: vec![],
2712                emails: vec![],
2713                urls: vec![],
2714                for_packages: vec![],
2715                scan_errors: vec![],
2716                is_source: None,
2717                source_count: None,
2718                is_legal: false,
2719                is_manifest: false,
2720                is_readme: false,
2721                is_top_level: false,
2722                is_key_file: false,
2723            },
2724            FileInfo {
2725                name: "bash".to_string(),
2726                base_name: "bash".to_string(),
2727                extension: String::new(),
2728                path: "rootfs/bin/bash".to_string(),
2729                file_type: FileType::File,
2730                mime_type: None,
2731                size: 20,
2732                date: None,
2733                sha1: None,
2734                md5: None,
2735                sha256: None,
2736                programming_language: None,
2737                package_data: vec![],
2738                license_expression: None,
2739                license_detections: vec![],
2740                copyrights: vec![],
2741                holders: vec![],
2742                authors: vec![],
2743                emails: vec![],
2744                urls: vec![],
2745                for_packages: vec![],
2746                scan_errors: vec![],
2747                is_source: None,
2748                source_count: None,
2749                is_legal: false,
2750                is_manifest: false,
2751                is_readme: false,
2752                is_top_level: false,
2753                is_key_file: false,
2754            },
2755        ];
2756
2757        let mut packages = vec![Package {
2758            package_type: Some(PackageType::Deb),
2759            namespace: Some("ubuntu".to_string()),
2760            name: Some("bash".to_string()),
2761            version: Some("5.2-1ubuntu1".to_string()),
2762            qualifiers: Some(HashMap::from([("arch".to_string(), "amd64".to_string())])),
2763            subpath: None,
2764            primary_language: None,
2765            description: None,
2766            release_date: None,
2767            parties: vec![],
2768            keywords: vec![],
2769            homepage_url: None,
2770            download_url: None,
2771            size: None,
2772            sha1: None,
2773            md5: None,
2774            sha256: None,
2775            sha512: None,
2776            bug_tracking_url: None,
2777            code_view_url: None,
2778            vcs_url: None,
2779            copyright: None,
2780            holder: None,
2781            declared_license_expression: None,
2782            declared_license_expression_spdx: None,
2783            license_detections: vec![],
2784            other_license_expression: None,
2785            other_license_expression_spdx: None,
2786            other_license_detections: vec![],
2787            extracted_license_statement: None,
2788            notice_text: None,
2789            source_packages: vec![],
2790            is_private: false,
2791            is_virtual: false,
2792            extra_data: None,
2793            repository_homepage_url: None,
2794            repository_download_url: None,
2795            api_data_url: None,
2796            purl: Some("pkg:deb/ubuntu/bash@5.2-1ubuntu1?arch=amd64".to_string()),
2797            package_uid: "pkg:deb/ubuntu/bash@5.2-1ubuntu1?arch=amd64&uuid=test-uuid".to_string(),
2798            datafile_paths: vec!["rootfs/var/lib/dpkg/status".to_string()],
2799            datasource_ids: vec![DatasourceId::DebianInstalledStatusDb],
2800        }];
2801
2802        let mut dependencies = vec![];
2803        resolve_file_references(&mut files, &mut packages, &mut dependencies);
2804
2805        assert_eq!(
2806            files[2].for_packages,
2807            vec!["pkg:deb/ubuntu/bash@5.2-1ubuntu1?arch=amd64&uuid=test-uuid".to_string()]
2808        );
2809    }
2810
2811    #[test]
2812    fn test_resolve_debian_installed_file_references_respects_arch_qualifier() {
2813        let mut files = vec![
2814            FileInfo {
2815                name: "status".to_string(),
2816                base_name: "status".to_string(),
2817                extension: String::new(),
2818                path: "rootfs/var/lib/dpkg/status".to_string(),
2819                file_type: FileType::File,
2820                mime_type: None,
2821                size: 100,
2822                date: None,
2823                sha1: None,
2824                md5: None,
2825                sha256: None,
2826                programming_language: None,
2827                package_data: vec![PackageData {
2828                    datasource_id: Some(DatasourceId::DebianInstalledStatusDb),
2829                    package_type: Some(PackageType::Deb),
2830                    namespace: Some("debian".to_string()),
2831                    name: Some("libc6".to_string()),
2832                    version: Some("2.36-1".to_string()),
2833                    purl: Some("pkg:deb/debian/libc6@2.36-1?arch=amd64".to_string()),
2834                    qualifiers: Some(HashMap::from([("arch".to_string(), "amd64".to_string())])),
2835                    ..Default::default()
2836                }],
2837                license_expression: None,
2838                license_detections: vec![],
2839                copyrights: vec![],
2840                holders: vec![],
2841                authors: vec![],
2842                emails: vec![],
2843                urls: vec![],
2844                for_packages: vec![],
2845                scan_errors: vec![],
2846                is_source: None,
2847                source_count: None,
2848                is_legal: false,
2849                is_manifest: false,
2850                is_readme: false,
2851                is_top_level: false,
2852                is_key_file: false,
2853            },
2854            FileInfo {
2855                name: "libc6:amd64.list".to_string(),
2856                base_name: "libc6:amd64".to_string(),
2857                extension: "list".to_string(),
2858                path: "rootfs/var/lib/dpkg/info/libc6:amd64.list".to_string(),
2859                file_type: FileType::File,
2860                mime_type: None,
2861                size: 20,
2862                date: None,
2863                sha1: None,
2864                md5: None,
2865                sha256: None,
2866                programming_language: None,
2867                package_data: vec![PackageData {
2868                    datasource_id: Some(DatasourceId::DebianInstalledFilesList),
2869                    package_type: Some(PackageType::Deb),
2870                    namespace: Some("debian".to_string()),
2871                    name: Some("libc6".to_string()),
2872                    qualifiers: Some(HashMap::from([("arch".to_string(), "amd64".to_string())])),
2873                    purl: Some("pkg:deb/debian/libc6?arch=amd64".to_string()),
2874                    file_references: vec![FileReference {
2875                        path: "/lib/x86_64-linux-gnu/libc.so.6".to_string(),
2876                        size: None,
2877                        sha1: None,
2878                        md5: None,
2879                        sha256: None,
2880                        sha512: None,
2881                        extra_data: None,
2882                    }],
2883                    ..Default::default()
2884                }],
2885                license_expression: None,
2886                license_detections: vec![],
2887                copyrights: vec![],
2888                holders: vec![],
2889                authors: vec![],
2890                emails: vec![],
2891                urls: vec![],
2892                for_packages: vec![],
2893                scan_errors: vec![],
2894                is_source: None,
2895                source_count: None,
2896                is_legal: false,
2897                is_manifest: false,
2898                is_readme: false,
2899                is_top_level: false,
2900                is_key_file: false,
2901            },
2902            FileInfo {
2903                name: "libc6:i386.list".to_string(),
2904                base_name: "libc6:i386".to_string(),
2905                extension: "list".to_string(),
2906                path: "rootfs/var/lib/dpkg/info/libc6:i386.list".to_string(),
2907                file_type: FileType::File,
2908                mime_type: None,
2909                size: 20,
2910                date: None,
2911                sha1: None,
2912                md5: None,
2913                sha256: None,
2914                programming_language: None,
2915                package_data: vec![PackageData {
2916                    datasource_id: Some(DatasourceId::DebianInstalledFilesList),
2917                    package_type: Some(PackageType::Deb),
2918                    namespace: Some("debian".to_string()),
2919                    name: Some("libc6".to_string()),
2920                    qualifiers: Some(HashMap::from([("arch".to_string(), "i386".to_string())])),
2921                    purl: Some("pkg:deb/debian/libc6?arch=i386".to_string()),
2922                    file_references: vec![FileReference {
2923                        path: "/lib/i386-linux-gnu/libc.so.6".to_string(),
2924                        size: None,
2925                        sha1: None,
2926                        md5: None,
2927                        sha256: None,
2928                        sha512: None,
2929                        extra_data: None,
2930                    }],
2931                    ..Default::default()
2932                }],
2933                license_expression: None,
2934                license_detections: vec![],
2935                copyrights: vec![],
2936                holders: vec![],
2937                authors: vec![],
2938                emails: vec![],
2939                urls: vec![],
2940                for_packages: vec![],
2941                scan_errors: vec![],
2942                is_source: None,
2943                source_count: None,
2944                is_legal: false,
2945                is_manifest: false,
2946                is_readme: false,
2947                is_top_level: false,
2948                is_key_file: false,
2949            },
2950            FileInfo {
2951                name: "libc.so.6".to_string(),
2952                base_name: "libc.so".to_string(),
2953                extension: "6".to_string(),
2954                path: "rootfs/lib/x86_64-linux-gnu/libc.so.6".to_string(),
2955                file_type: FileType::File,
2956                mime_type: None,
2957                size: 10,
2958                date: None,
2959                sha1: None,
2960                md5: None,
2961                sha256: None,
2962                programming_language: None,
2963                package_data: vec![],
2964                license_expression: None,
2965                license_detections: vec![],
2966                copyrights: vec![],
2967                holders: vec![],
2968                authors: vec![],
2969                emails: vec![],
2970                urls: vec![],
2971                for_packages: vec![],
2972                scan_errors: vec![],
2973                is_source: None,
2974                source_count: None,
2975                is_legal: false,
2976                is_manifest: false,
2977                is_readme: false,
2978                is_top_level: false,
2979                is_key_file: false,
2980            },
2981            FileInfo {
2982                name: "libc.so.6".to_string(),
2983                base_name: "libc.so".to_string(),
2984                extension: "6".to_string(),
2985                path: "rootfs/lib/i386-linux-gnu/libc.so.6".to_string(),
2986                file_type: FileType::File,
2987                mime_type: None,
2988                size: 10,
2989                date: None,
2990                sha1: None,
2991                md5: None,
2992                sha256: None,
2993                programming_language: None,
2994                package_data: vec![],
2995                license_expression: None,
2996                license_detections: vec![],
2997                copyrights: vec![],
2998                holders: vec![],
2999                authors: vec![],
3000                emails: vec![],
3001                urls: vec![],
3002                for_packages: vec![],
3003                scan_errors: vec![],
3004                is_source: None,
3005                source_count: None,
3006                is_legal: false,
3007                is_manifest: false,
3008                is_readme: false,
3009                is_top_level: false,
3010                is_key_file: false,
3011            },
3012        ];
3013
3014        let mut packages = vec![Package {
3015            package_type: Some(PackageType::Deb),
3016            namespace: Some("debian".to_string()),
3017            name: Some("libc6".to_string()),
3018            version: Some("2.36-1".to_string()),
3019            qualifiers: Some(HashMap::from([("arch".to_string(), "amd64".to_string())])),
3020            subpath: None,
3021            primary_language: None,
3022            description: None,
3023            release_date: None,
3024            parties: vec![],
3025            keywords: vec![],
3026            homepage_url: None,
3027            download_url: None,
3028            size: None,
3029            sha1: None,
3030            md5: None,
3031            sha256: None,
3032            sha512: None,
3033            bug_tracking_url: None,
3034            code_view_url: None,
3035            vcs_url: None,
3036            copyright: None,
3037            holder: None,
3038            declared_license_expression: None,
3039            declared_license_expression_spdx: None,
3040            license_detections: vec![],
3041            other_license_expression: None,
3042            other_license_expression_spdx: None,
3043            other_license_detections: vec![],
3044            extracted_license_statement: None,
3045            notice_text: None,
3046            source_packages: vec![],
3047            is_private: false,
3048            is_virtual: false,
3049            extra_data: None,
3050            repository_homepage_url: None,
3051            repository_download_url: None,
3052            api_data_url: None,
3053            purl: Some("pkg:deb/debian/libc6@2.36-1?arch=amd64".to_string()),
3054            package_uid: "pkg:deb/debian/libc6@2.36-1?arch=amd64&uuid=test-uuid".to_string(),
3055            datafile_paths: vec!["rootfs/var/lib/dpkg/status".to_string()],
3056            datasource_ids: vec![DatasourceId::DebianInstalledStatusDb],
3057        }];
3058
3059        let mut dependencies = vec![];
3060        resolve_file_references(&mut files, &mut packages, &mut dependencies);
3061
3062        assert_eq!(
3063            files[3].for_packages,
3064            vec!["pkg:deb/debian/libc6@2.36-1?arch=amd64&uuid=test-uuid".to_string()]
3065        );
3066        assert!(files[4].for_packages.is_empty());
3067    }
3068}