1use std::path::Path;
26
27use crate::parser_warn as warn;
28
29use crate::models::{DatasourceId, PackageData, PackageType};
30use crate::models::{Dependency, FileReference};
31use crate::parsers::utils::{MAX_ITERATION_COUNT, MAX_MANIFEST_SIZE, truncate_field};
32
33use super::PackageParser;
34use super::license_normalization::{
35 DeclaredLicenseMatchMetadata, build_declared_license_data, empty_declared_license_data,
36};
37use super::rpm_db_native::{InstalledRpmDbKind, InstalledRpmPackage, read_installed_rpm_packages};
38use super::rpm_parser::infer_rpm_namespace;
39use super::rpm_parser::infer_rpm_namespace_from_filename;
40use super::rpm_parser::normalize_rpm_declared_license;
41
42const PACKAGE_TYPE: PackageType = PackageType::Rpm;
43const RPM_BDB_PATH_SUFFIXES: &[&str] = &["var/lib/rpm/Packages", "usr/lib/sysimage/rpm/Packages"];
44const RPM_NDB_PATH_SUFFIXES: &[&str] = &[
45 "var/lib/rpm/Packages.db",
46 "usr/lib/sysimage/rpm/Packages.db",
47];
48#[cfg(feature = "rpm-sqlite")]
49const RPM_SQLITE_PATH_SUFFIXES: &[&str] = &[
50 "var/lib/rpm/rpmdb.sqlite",
51 "usr/lib/sysimage/rpm/rpmdb.sqlite",
52];
53
54#[derive(Debug)]
55struct RpmQueryPackage {
56 name: Option<String>,
57 epoch: Option<String>,
58 version: Option<String>,
59 release: Option<String>,
60 vendor: Option<String>,
61 distribution: Option<String>,
62 arch: Option<String>,
63 platform: Option<String>,
64 size: Option<u64>,
65 license: Option<String>,
66 source_rpm: Option<String>,
67 requires: Vec<String>,
68 file_names: Vec<Option<String>>,
69 dir_indexes: Vec<u32>,
70 base_names: Vec<Option<String>>,
71 dir_names: Vec<String>,
72}
73
74fn default_package_data(datasource_id: DatasourceId) -> PackageData {
75 PackageData {
76 package_type: Some(PACKAGE_TYPE),
77 datasource_id: Some(datasource_id),
78 ..Default::default()
79 }
80}
81
82pub struct RpmBdbDatabaseParser;
83
84#[cfg(feature = "rpm-sqlite")]
88impl PackageParser for RpmBdbDatabaseParser {
89 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
90
91 fn is_match(path: &Path) -> bool {
92 path_matches_any_suffix(path, RPM_BDB_PATH_SUFFIXES)
93 }
94
95 fn extract_packages(path: &Path) -> Vec<PackageData> {
96 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseBdb) {
97 Ok(pkgs) if !pkgs.is_empty() => pkgs,
98 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)],
99 Err(e) => {
100 warn!("Failed to parse RPM BDB database {:?}: {}", path, e);
101 vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)]
102 }
103 }
104 }
105
106 fn metadata() -> Vec<super::metadata::ParserMetadata> {
107 vec![super::metadata::ParserMetadata {
108 description: "RPM installed package database",
109 file_patterns: &[
110 "**/var/lib/rpm/Packages",
111 "**/usr/lib/sysimage/rpm/Packages",
112 "**/var/lib/rpm/Packages.db",
113 "**/usr/lib/sysimage/rpm/Packages.db",
114 "**/var/lib/rpm/rpmdb.sqlite",
115 "**/usr/lib/sysimage/rpm/rpmdb.sqlite",
116 ],
117 package_type: "rpm",
118 primary_language: "",
119 documentation_url: Some("https://rpm.org/"),
120 }]
121 }
122}
123
124#[cfg(not(feature = "rpm-sqlite"))]
125impl PackageParser for RpmBdbDatabaseParser {
126 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
127
128 fn is_match(path: &Path) -> bool {
129 path_matches_any_suffix(path, RPM_BDB_PATH_SUFFIXES)
130 }
131
132 fn extract_packages(path: &Path) -> Vec<PackageData> {
133 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseBdb) {
134 Ok(pkgs) if !pkgs.is_empty() => pkgs,
135 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)],
136 Err(e) => {
137 warn!("Failed to parse RPM BDB database {:?}: {}", path, e);
138 vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)]
139 }
140 }
141 }
142
143 fn metadata() -> Vec<super::metadata::ParserMetadata> {
144 vec![super::metadata::ParserMetadata {
145 description: "RPM installed package database",
146 file_patterns: &[
147 "**/var/lib/rpm/Packages",
148 "**/usr/lib/sysimage/rpm/Packages",
149 "**/var/lib/rpm/Packages.db",
150 "**/usr/lib/sysimage/rpm/Packages.db",
151 ],
152 package_type: "rpm",
153 primary_language: "",
154 documentation_url: Some("https://rpm.org/"),
155 }]
156 }
157}
158
159pub struct RpmNdbDatabaseParser;
160
161impl PackageParser for RpmNdbDatabaseParser {
162 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
163
164 fn is_match(path: &Path) -> bool {
165 path_matches_any_suffix(path, RPM_NDB_PATH_SUFFIXES)
166 }
167
168 fn extract_packages(path: &Path) -> Vec<PackageData> {
169 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseNdb) {
170 Ok(pkgs) if !pkgs.is_empty() => pkgs,
171 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseNdb)],
172 Err(e) => {
173 warn!("Failed to parse RPM NDB database {:?}: {}", path, e);
174 vec![default_package_data(DatasourceId::RpmInstalledDatabaseNdb)]
175 }
176 }
177 }
178}
179
180#[cfg(feature = "rpm-sqlite")]
181pub struct RpmSqliteDatabaseParser;
182
183#[cfg(feature = "rpm-sqlite")]
184impl PackageParser for RpmSqliteDatabaseParser {
185 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
186
187 fn is_match(path: &Path) -> bool {
188 path_matches_any_suffix(path, RPM_SQLITE_PATH_SUFFIXES)
189 }
190
191 fn extract_packages(path: &Path) -> Vec<PackageData> {
192 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseSqlite) {
193 Ok(pkgs) if !pkgs.is_empty() => pkgs,
194 Ok(_) => vec![default_package_data(
195 DatasourceId::RpmInstalledDatabaseSqlite,
196 )],
197 Err(e) => {
198 warn!("Failed to parse RPM SQLite database {:?}: {}", path, e);
199 vec![default_package_data(
200 DatasourceId::RpmInstalledDatabaseSqlite,
201 )]
202 }
203 }
204 }
205}
206
207fn parse_rpm_database(
208 path: &Path,
209 datasource_id: DatasourceId,
210) -> Result<Vec<PackageData>, String> {
211 let metadata = std::fs::metadata(path)
212 .map_err(|e| format!("Cannot stat RPM database file {:?}: {}", path, e))?;
213
214 if metadata.len() > MAX_MANIFEST_SIZE {
215 return Err(format!(
216 "RPM database file {:?} is {} bytes, exceeding the {} byte limit",
217 path,
218 metadata.len(),
219 MAX_MANIFEST_SIZE
220 ));
221 }
222
223 let native_kind = native_kind_for_datasource(datasource_id)?;
224 match read_installed_rpm_packages(path, native_kind) {
225 Ok(packages) => Ok(packages
226 .into_iter()
227 .take(MAX_ITERATION_COUNT)
228 .map(native_package_to_query_package)
229 .map(|pkg| build_package_data(pkg, datasource_id))
230 .collect()),
231 Err(native_error) => Err(format!(
232 "native installed RPM reader failed for {:?}: {}",
233 path, native_error
234 )),
235 }
236}
237
238fn path_matches_suffix(path: &Path, suffix: &str) -> bool {
239 path.to_string_lossy().replace('\\', "/").ends_with(suffix)
240}
241
242fn path_matches_any_suffix(path: &Path, suffixes: &[&str]) -> bool {
243 suffixes
244 .iter()
245 .any(|suffix| path_matches_suffix(path, suffix))
246}
247
248fn native_kind_for_datasource(datasource_id: DatasourceId) -> Result<InstalledRpmDbKind, String> {
249 match datasource_id {
250 DatasourceId::RpmInstalledDatabaseBdb => Ok(InstalledRpmDbKind::Bdb),
251 DatasourceId::RpmInstalledDatabaseNdb => Ok(InstalledRpmDbKind::Ndb),
252 DatasourceId::RpmInstalledDatabaseSqlite => Ok(InstalledRpmDbKind::Sqlite),
253 other => Err(format!(
254 "unexpected datasource for installed RPM DB: {other:?}"
255 )),
256 }
257}
258
259fn native_package_to_query_package(package: InstalledRpmPackage) -> RpmQueryPackage {
260 RpmQueryPackage {
261 name: truncate_optional_string(Some(package.name)),
262 epoch: Some(package.epoch.to_string()),
263 version: truncate_optional_string(Some(package.version)),
264 release: truncate_optional_string(Some(package.release)),
265 vendor: truncate_optional_string(Some(package.vendor)),
266 distribution: truncate_optional_string(Some(package.distribution)),
267 arch: truncate_optional_string(Some(package.arch)),
268 platform: truncate_optional_string(Some(package.platform)),
269 size: (package.size > 0).then_some(u64::from(package.size)),
270 license: truncate_optional_string(Some(package.license)),
271 source_rpm: truncate_optional_string(Some(package.source_rpm)),
272 requires: package
273 .requires
274 .into_iter()
275 .take(MAX_ITERATION_COUNT)
276 .map(truncate_field)
277 .collect(),
278 file_names: package
279 .file_names
280 .into_iter()
281 .take(MAX_ITERATION_COUNT)
282 .map(|s| Some(truncate_field(s)))
283 .collect(),
284 dir_indexes: package.dir_indexes,
285 base_names: package
286 .base_names
287 .into_iter()
288 .take(MAX_ITERATION_COUNT)
289 .map(|s| Some(truncate_field(s)))
290 .collect(),
291 dir_names: package
292 .dir_names
293 .into_iter()
294 .take(MAX_ITERATION_COUNT)
295 .map(truncate_field)
296 .collect(),
297 }
298}
299
300fn truncate_optional_string(value: Option<String>) -> Option<String> {
301 value
302 .map(truncate_field)
303 .and_then(|v| normalize_optional_string(Some(v)))
304}
305
306fn build_evr_version(epoch: u32, version: &str, release: &str) -> Option<String> {
307 if version.is_empty() {
308 return None;
309 }
310
311 let mut evr = String::new();
312
313 if epoch > 0 {
314 evr.push_str(&format!("{}:", epoch));
315 }
316
317 evr.push_str(version);
318
319 if !release.is_empty() {
320 evr.push('-');
321 evr.push_str(release);
322 }
323
324 Some(evr)
325}
326
327fn build_file_references(
328 base_names: &[Option<String>],
329 dir_indexes: &[u32],
330 dir_names: &[String],
331) -> Vec<FileReference> {
332 if base_names.is_empty() || dir_names.is_empty() {
333 return Vec::new();
334 }
335
336 base_names
337 .iter()
338 .zip(dir_indexes.iter())
339 .take(MAX_ITERATION_COUNT)
340 .filter_map(|(basename, &dir_idx)| {
341 let dirname = dir_names.get(dir_idx as usize)?;
342 let basename = basename.as_deref().unwrap_or_default();
343 let path = format!("{}{}", dirname, basename);
344 if path.is_empty() || path == "/" {
345 return None;
346 }
347 Some(FileReference {
348 path,
349 size: None,
350 sha1: None,
351 md5: None,
352 sha256: None,
353 sha512: None,
354 extra_data: None,
355 })
356 })
357 .collect()
358}
359
360fn build_file_references_from_paths(paths: &[Option<String>]) -> Vec<FileReference> {
361 paths
362 .iter()
363 .take(MAX_ITERATION_COUNT)
364 .filter_map(|path| {
365 let path = path.as_deref()?.trim();
366 if path.is_empty() || path == "/" {
367 return None;
368 }
369
370 Some(FileReference {
371 path: path.to_string(),
372 size: None,
373 sha1: None,
374 md5: None,
375 sha256: None,
376 sha512: None,
377 extra_data: None,
378 })
379 })
380 .collect()
381}
382
383fn build_package_data(pkg: RpmQueryPackage, datasource_id: DatasourceId) -> PackageData {
384 let name = normalize_optional_string(pkg.name).map(truncate_field);
385 let version_raw = normalize_optional_string(pkg.version).map(truncate_field);
386 let release = normalize_optional_string(pkg.release).map(truncate_field);
387 let version = build_evr_version(
388 parse_epoch(pkg.epoch),
389 version_raw.as_deref().unwrap_or_default(),
390 release.as_deref().unwrap_or_default(),
391 );
392
393 let vendor = normalize_optional_string(pkg.vendor)
394 .map(truncate_field)
395 .or_else(|| normalize_optional_string(pkg.distribution).map(truncate_field));
396 let source_rpm = normalize_optional_string(pkg.source_rpm).map(truncate_field);
397 let namespace =
398 infer_rpm_namespace(None, vendor.as_deref(), release.as_deref(), None).or_else(|| {
399 source_rpm
400 .as_deref()
401 .and_then(|source_rpm| infer_rpm_namespace_from_filename(Path::new(source_rpm)))
402 });
403
404 let architecture = normalize_optional_string(pkg.arch)
405 .map(truncate_field)
406 .or_else(|| infer_platform_architecture(pkg.platform.as_deref()));
407 let dependencies = pkg
408 .requires
409 .into_iter()
410 .take(MAX_ITERATION_COUNT)
411 .filter_map(|require| build_dependency(&require))
412 .collect();
413 let extracted_license_statement = normalize_optional_string(pkg.license).map(truncate_field);
414 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
415 extracted_license_statement
416 .as_deref()
417 .and_then(normalize_rpm_declared_license)
418 .map(|normalized| {
419 build_declared_license_data(
420 normalized,
421 DeclaredLicenseMatchMetadata::single_line(
422 extracted_license_statement.as_deref().unwrap_or_default(),
423 ),
424 )
425 })
426 .map(|(expr, spdx, detections)| {
427 (
428 expr.map(truncate_field),
429 spdx.map(truncate_field),
430 detections,
431 )
432 })
433 .unwrap_or_else(empty_declared_license_data);
434 let source_packages = source_rpm.clone().into_iter().collect();
435 let file_references = {
436 let from_dir_components =
437 build_file_references(&pkg.base_names, &pkg.dir_indexes, &pkg.dir_names);
438 if from_dir_components.is_empty() {
439 build_file_references_from_paths(&pkg.file_names)
440 } else {
441 from_dir_components
442 }
443 };
444 let purl = build_package_purl(
445 name.as_deref(),
446 namespace.as_deref(),
447 version.as_deref(),
448 architecture.as_deref(),
449 );
450
451 PackageData {
452 datasource_id: Some(datasource_id),
453 package_type: Some(PACKAGE_TYPE),
454 namespace,
455 name,
456 version,
457 qualifiers: architecture.as_ref().map(|arch| {
458 let mut q = std::collections::HashMap::new();
459 q.insert("arch".to_string(), arch.clone());
460 q
461 }),
462 subpath: None,
463 primary_language: None,
464 description: None,
465 release_date: None,
466 parties: Vec::new(),
467 keywords: Vec::new(),
468 homepage_url: None,
469 download_url: None,
470 size: pkg.size.filter(|size| *size > 0),
471 sha1: None,
472 md5: None,
473 sha256: None,
474 sha512: None,
475 bug_tracking_url: None,
476 code_view_url: None,
477 vcs_url: None,
478 copyright: None,
479 holder: None,
480 declared_license_expression,
481 declared_license_expression_spdx,
482 license_detections,
483 other_license_expression: None,
484 other_license_expression_spdx: None,
485 other_license_detections: Vec::new(),
486 extracted_license_statement,
487 notice_text: None,
488 source_packages,
489 file_references,
490 is_private: false,
491 is_virtual: false,
492 extra_data: None,
493 dependencies,
494 repository_homepage_url: None,
495 repository_download_url: None,
496 api_data_url: None,
497 purl,
498 }
499}
500
501fn build_dependency(require: &str) -> Option<Dependency> {
502 let require = require.trim();
503 if require.is_empty() || require.starts_with("rpmlib(") || require.starts_with("config(") {
504 return None;
505 }
506
507 let purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), require)
508 .ok()
509 .map(|p| p.to_string());
510
511 Some(Dependency {
512 purl,
513 extracted_requirement: None,
514 scope: Some("requires".to_string()),
515 is_runtime: Some(true),
516 is_optional: Some(false),
517 is_pinned: Some(false),
518 is_direct: Some(true),
519 resolved_package: None,
520 extra_data: None,
521 })
522}
523
524fn build_package_purl(
525 name: Option<&str>,
526 namespace: Option<&str>,
527 version: Option<&str>,
528 arch: Option<&str>,
529) -> Option<String> {
530 let name = name?;
531 let mut purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), name).ok()?;
532
533 if let Some(namespace) = namespace {
534 purl.with_namespace(namespace).ok()?;
535 }
536
537 if let Some(version) = version {
538 purl.with_version(version).ok()?;
539 }
540
541 if let Some(arch) = arch {
542 purl.add_qualifier("arch", arch).ok()?;
543 }
544
545 Some(purl.to_string())
546}
547
548fn normalize_optional_string(value: Option<String>) -> Option<String> {
549 value.and_then(|value| {
550 let trimmed = value.trim();
551 if trimmed.is_empty() || trimmed == "(none)" || trimmed == "[]" {
552 None
553 } else {
554 Some(trimmed.to_string())
555 }
556 })
557}
558
559fn parse_epoch(value: Option<String>) -> u32 {
560 normalize_optional_string(value)
561 .and_then(|value| value.parse::<u32>().ok())
562 .unwrap_or(0)
563}
564
565fn infer_platform_architecture(platform: Option<&str>) -> Option<String> {
566 let platform = platform?.trim();
567 if platform.is_empty() {
568 return None;
569 }
570
571 platform
572 .split_once('-')
573 .map(|(arch, _)| arch)
574 .filter(|arch| !arch.is_empty())
575 .map(|arch| arch.to_string())
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 use crate::models::DatasourceId;
583 use std::path::PathBuf;
584
585 #[test]
586 fn test_bdb_parser_is_match() {
587 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
588 "/var/lib/rpm/Packages"
589 )));
590 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
591 "rootfs/var/lib/rpm/Packages"
592 )));
593 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
594 "/usr/lib/sysimage/rpm/Packages"
595 )));
596 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
597 "/var/lib/rpm/Packages.db"
598 )));
599 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
600 "lib/modules/datasource/deb/__fixtures__/Packages"
601 )));
602 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from("Packages")));
603 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
604 "testdata/rpm/var/lib/rpm/Packages.expected.json"
605 )));
606 }
607
608 #[test]
609 fn test_ndb_parser_is_match() {
610 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
611 "usr/lib/sysimage/rpm/Packages.db"
612 )));
613 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
614 "/rootfs/usr/lib/sysimage/rpm/Packages.db"
615 )));
616 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
617 "usr/lib/rpm/Packages"
618 )));
619 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
620 "var/lib/rpm/Packages.db"
621 )));
622 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
623 "testdata/rpm/usr/lib/sysimage/rpm/Packages.db.expected.json"
624 )));
625 }
626
627 #[cfg(feature = "rpm-sqlite")]
628 #[test]
629 fn test_sqlite_parser_is_match() {
630 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
631 "var/lib/rpm/rpmdb.sqlite"
632 )));
633 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
634 "/rootfs/var/lib/rpm/rpmdb.sqlite"
635 )));
636 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
637 "/rootfs/usr/lib/sysimage/rpm/rpmdb.sqlite"
638 )));
639 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
640 "/var/lib/rpm/Packages"
641 )));
642 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
643 "testdata/rpm/rpmdb.sqlite.expected.json"
644 )));
645 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
646 "testdata/rpm/rpmdb.sqlite-shm"
647 )));
648 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
649 "testdata/rpm/rpmdb.sqlite-wal"
650 )));
651 }
652
653 #[test]
654 fn test_build_evr_version_full() {
655 assert_eq!(
656 build_evr_version(2, "1.0.0", "1.el7"),
657 Some("2:1.0.0-1.el7".to_string())
658 );
659 }
660
661 #[test]
662 fn test_build_evr_version_no_epoch() {
663 assert_eq!(
664 build_evr_version(0, "1.0.0", "1.el7"),
665 Some("1.0.0-1.el7".to_string())
666 );
667 }
668
669 #[test]
670 fn test_build_evr_version_no_release() {
671 assert_eq!(build_evr_version(0, "1.0.0", ""), Some("1.0.0".to_string()));
672 }
673
674 #[test]
675 fn test_build_evr_version_empty() {
676 assert_eq!(build_evr_version(0, "", ""), None);
677 }
678
679 #[cfg(feature = "rpm-sqlite")]
680 #[test]
681 fn test_parse_rpm_database_sqlite() {
682 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
683
684 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
685
686 assert_eq!(pkg.package_type, Some(PackageType::Rpm));
687 assert_eq!(
688 pkg.datasource_id,
689 Some(DatasourceId::RpmInstalledDatabaseSqlite)
690 );
691 assert!(pkg.name.is_some());
692 }
693
694 #[cfg(feature = "rpm-sqlite")]
695 #[test]
696 fn test_parse_rpm_database_sqlite_preserves_release_in_version() {
697 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
698
699 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
700
701 assert!(
702 pkg.version
703 .as_ref()
704 .is_some_and(|version| version.contains('-'))
705 );
706 }
707
708 #[test]
709 fn test_build_file_references_skips_invalid_entries() {
710 let file_refs = build_file_references(
711 &[
712 Some("valid".to_string()),
713 Some("".to_string()),
714 Some("ignored".to_string()),
715 ],
716 &[0, 0, u32::MAX],
717 &["/usr/bin/".to_string()],
718 );
719
720 assert_eq!(file_refs.len(), 2);
721 assert_eq!(file_refs[0].path, "/usr/bin/valid");
722 assert_eq!(file_refs[1].path, "/usr/bin/");
723 }
724
725 #[test]
726 fn test_build_package_data_falls_back_to_file_names() {
727 let package = build_package_data(
728 RpmQueryPackage {
729 name: Some("libgcc".to_string()),
730 epoch: None,
731 version: Some("13.1.1".to_string()),
732 release: Some("2.fc38".to_string()),
733 vendor: Some("Fedora Project".to_string()),
734 distribution: None,
735 arch: Some("x86_64".to_string()),
736 platform: None,
737 size: Some(235748),
738 license: Some("GPLv3+".to_string()),
739 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
740 requires: Vec::new(),
741 file_names: vec![
742 Some("/usr/share/licenses/libgcc/COPYING".to_string()),
743 Some("/usr/share/licenses/libgcc/COPYING.RUNTIME".to_string()),
744 ],
745 dir_indexes: Vec::new(),
746 base_names: Vec::new(),
747 dir_names: Vec::new(),
748 },
749 DatasourceId::RpmInstalledDatabaseSqlite,
750 );
751
752 assert_eq!(package.file_references.len(), 2);
753 assert_eq!(
754 package.file_references[0].path,
755 "/usr/share/licenses/libgcc/COPYING"
756 );
757 assert_eq!(
758 package.file_references[1].path,
759 "/usr/share/licenses/libgcc/COPYING.RUNTIME"
760 );
761 }
762
763 #[test]
764 fn test_build_package_data_uses_distribution_for_namespace() {
765 let package = build_package_data(
766 RpmQueryPackage {
767 name: Some("libgcc".to_string()),
768 epoch: None,
769 version: Some("13.1.1".to_string()),
770 release: Some("2.fc38".to_string()),
771 vendor: None,
772 distribution: Some("Fedora Project".to_string()),
773 arch: Some("x86_64".to_string()),
774 platform: None,
775 size: Some(235748),
776 license: Some("GPLv3+".to_string()),
777 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
778 requires: Vec::new(),
779 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
780 dir_indexes: Vec::new(),
781 base_names: Vec::new(),
782 dir_names: Vec::new(),
783 },
784 DatasourceId::RpmInstalledDatabaseSqlite,
785 );
786
787 assert_eq!(package.namespace.as_deref(), Some("fedora"));
788 }
789
790 #[test]
791 fn test_build_package_data_normalizes_declared_license_expression() {
792 let package = build_package_data(
793 RpmQueryPackage {
794 name: Some("libgcc".to_string()),
795 epoch: None,
796 version: Some("13.1.1".to_string()),
797 release: Some("2.fc38".to_string()),
798 vendor: Some("Fedora Project".to_string()),
799 distribution: None,
800 arch: Some("x86_64".to_string()),
801 platform: None,
802 size: Some(235748),
803 license: Some("LGPLv2".to_string()),
804 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
805 requires: Vec::new(),
806 file_names: Vec::new(),
807 dir_indexes: Vec::new(),
808 base_names: Vec::new(),
809 dir_names: Vec::new(),
810 },
811 DatasourceId::RpmInstalledDatabaseSqlite,
812 );
813
814 assert_eq!(
815 package.declared_license_expression.as_deref(),
816 Some("lgpl-2.0-only")
817 );
818 assert_eq!(
819 package.declared_license_expression_spdx.as_deref(),
820 Some("LGPL-2.0-only")
821 );
822 assert_eq!(package.license_detections.len(), 1);
823 }
824
825 #[test]
826 fn test_build_package_data_uses_source_rpm_for_namespace() {
827 let package = build_package_data(
828 RpmQueryPackage {
829 name: Some("libgcc".to_string()),
830 epoch: None,
831 version: Some("13.1.1".to_string()),
832 release: None,
833 vendor: None,
834 distribution: None,
835 arch: Some("x86_64".to_string()),
836 platform: None,
837 size: Some(235748),
838 license: Some("GPLv3+".to_string()),
839 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
840 requires: Vec::new(),
841 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
842 dir_indexes: Vec::new(),
843 base_names: Vec::new(),
844 dir_names: Vec::new(),
845 },
846 DatasourceId::RpmInstalledDatabaseSqlite,
847 );
848
849 assert_eq!(package.namespace.as_deref(), Some("fedora"));
850 }
851
852 #[test]
853 fn test_build_package_data_uses_platform_for_architecture() {
854 let package = build_package_data(
855 RpmQueryPackage {
856 name: Some("libgcc".to_string()),
857 epoch: None,
858 version: Some("13.1.1".to_string()),
859 release: None,
860 vendor: None,
861 distribution: None,
862 arch: None,
863 platform: Some("x86_64-redhat-linux".to_string()),
864 size: Some(235748),
865 license: Some("GPLv3+".to_string()),
866 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
867 requires: Vec::new(),
868 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
869 dir_indexes: Vec::new(),
870 base_names: Vec::new(),
871 dir_names: Vec::new(),
872 },
873 DatasourceId::RpmInstalledDatabaseSqlite,
874 );
875
876 assert_eq!(
877 package.qualifiers.as_ref().and_then(|q| q.get("arch")),
878 Some(&"x86_64".to_string())
879 );
880 }
881}