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