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