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