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