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