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_version_release(version: &str, release: &str) -> Option<String> {
307 if version.is_empty() {
308 return None;
309 }
310
311 let mut vr = String::from(version);
312
313 if !release.is_empty() {
314 vr.push('-');
315 vr.push_str(release);
316 }
317
318 Some(vr)
319}
320
321fn build_file_references(
322 base_names: &[Option<String>],
323 dir_indexes: &[u32],
324 dir_names: &[String],
325) -> Vec<FileReference> {
326 if base_names.is_empty() || dir_names.is_empty() {
327 return Vec::new();
328 }
329
330 base_names
331 .iter()
332 .zip(dir_indexes.iter())
333 .take(MAX_ITERATION_COUNT)
334 .filter_map(|(basename, &dir_idx)| {
335 let dirname = dir_names.get(dir_idx as usize)?;
336 let basename = basename.as_deref().unwrap_or_default();
337 let path = format!("{}{}", dirname, basename);
338 if path.is_empty() || path == "/" {
339 return None;
340 }
341 Some(FileReference {
342 path,
343 size: None,
344 sha1: None,
345 md5: None,
346 sha256: None,
347 sha512: None,
348 extra_data: None,
349 })
350 })
351 .collect()
352}
353
354fn build_file_references_from_paths(paths: &[Option<String>]) -> Vec<FileReference> {
355 paths
356 .iter()
357 .take(MAX_ITERATION_COUNT)
358 .filter_map(|path| {
359 let path = path.as_deref()?.trim();
360 if path.is_empty() || path == "/" {
361 return None;
362 }
363
364 Some(FileReference {
365 path: path.to_string(),
366 size: None,
367 sha1: None,
368 md5: None,
369 sha256: None,
370 sha512: None,
371 extra_data: None,
372 })
373 })
374 .collect()
375}
376
377fn build_package_data(pkg: RpmQueryPackage, datasource_id: DatasourceId) -> PackageData {
378 let name = normalize_optional_string(pkg.name).map(truncate_field);
379 let version_raw = normalize_optional_string(pkg.version).map(truncate_field);
380 let release = normalize_optional_string(pkg.release).map(truncate_field);
381 let version = build_version_release(
382 version_raw.as_deref().unwrap_or_default(),
383 release.as_deref().unwrap_or_default(),
384 );
385 let epoch_value = parse_epoch(pkg.epoch);
388 let epoch = (epoch_value > 0).then(|| epoch_value.to_string());
389
390 let vendor = normalize_optional_string(pkg.vendor)
391 .map(truncate_field)
392 .or_else(|| normalize_optional_string(pkg.distribution).map(truncate_field));
393 let source_rpm = normalize_optional_string(pkg.source_rpm).map(truncate_field);
394 let namespace =
395 infer_rpm_namespace(None, vendor.as_deref(), release.as_deref(), None).or_else(|| {
396 source_rpm
397 .as_deref()
398 .and_then(|source_rpm| infer_rpm_namespace_from_filename(Path::new(source_rpm)))
399 });
400
401 let architecture = normalize_optional_string(pkg.arch)
402 .map(truncate_field)
403 .or_else(|| infer_platform_architecture(pkg.platform.as_deref()));
404 let dependencies = pkg
405 .requires
406 .into_iter()
407 .take(MAX_ITERATION_COUNT)
408 .filter_map(|require| build_dependency(&require))
409 .collect();
410 let extracted_license_statement = normalize_optional_string(pkg.license).map(truncate_field);
411 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
412 extracted_license_statement
413 .as_deref()
414 .and_then(normalize_rpm_declared_license)
415 .map(|normalized| {
416 build_declared_license_data(
417 normalized,
418 DeclaredLicenseMatchMetadata::single_line(
419 extracted_license_statement.as_deref().unwrap_or_default(),
420 ),
421 )
422 })
423 .map(|(expr, spdx, detections)| {
424 (
425 expr.map(truncate_field),
426 spdx.map(truncate_field),
427 detections,
428 )
429 })
430 .unwrap_or_else(empty_declared_license_data);
431 let source_packages = source_rpm.clone().into_iter().collect();
432 let file_references = {
433 let from_dir_components =
434 build_file_references(&pkg.base_names, &pkg.dir_indexes, &pkg.dir_names);
435 if from_dir_components.is_empty() {
436 build_file_references_from_paths(&pkg.file_names)
437 } else {
438 from_dir_components
439 }
440 };
441 let purl = build_package_purl(
442 name.as_deref(),
443 namespace.as_deref(),
444 version.as_deref(),
445 architecture.as_deref(),
446 epoch.as_deref(),
447 );
448
449 PackageData {
450 datasource_id: Some(datasource_id),
451 package_type: Some(PACKAGE_TYPE),
452 namespace,
453 name,
454 version,
455 qualifiers: {
456 let mut q = std::collections::HashMap::new();
457 if let Some(arch) = &architecture {
458 q.insert("arch".to_string(), arch.clone());
459 }
460 if let Some(epoch) = &epoch {
461 q.insert("epoch".to_string(), epoch.clone());
462 }
463 (!q.is_empty()).then_some(q)
464 },
465 subpath: None,
466 primary_language: None,
467 description: None,
468 release_date: None,
469 parties: Vec::new(),
470 keywords: Vec::new(),
471 homepage_url: None,
472 download_url: None,
473 size: pkg.size.filter(|size| *size > 0),
474 sha1: None,
475 md5: None,
476 sha256: None,
477 sha512: None,
478 bug_tracking_url: None,
479 code_view_url: None,
480 vcs_url: None,
481 copyright: None,
482 holder: None,
483 declared_license_expression,
484 declared_license_expression_spdx,
485 license_detections,
486 other_license_expression: None,
487 other_license_expression_spdx: None,
488 other_license_detections: Vec::new(),
489 extracted_license_statement,
490 notice_text: None,
491 source_packages,
492 file_references,
493 is_private: false,
494 is_virtual: false,
495 extra_data: None,
496 dependencies,
497 repository_homepage_url: None,
498 repository_download_url: None,
499 api_data_url: None,
500 purl,
501 }
502}
503
504fn build_dependency(require: &str) -> Option<Dependency> {
505 let require = require.trim();
506 if require.is_empty() || require.starts_with("rpmlib(") || require.starts_with("config(") {
507 return None;
508 }
509
510 let purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), require)
511 .ok()
512 .map(|p| p.to_string());
513
514 Some(Dependency {
515 purl,
516 extracted_requirement: None,
517 scope: Some("requires".to_string()),
518 is_runtime: Some(true),
519 is_optional: Some(false),
520 is_pinned: Some(false),
521 is_direct: Some(true),
522 resolved_package: None,
523 extra_data: None,
524 })
525}
526
527fn build_package_purl(
528 name: Option<&str>,
529 namespace: Option<&str>,
530 version: Option<&str>,
531 arch: Option<&str>,
532 epoch: Option<&str>,
533) -> Option<String> {
534 let name = name?;
535 let mut purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), name).ok()?;
536
537 if let Some(namespace) = namespace {
538 purl.with_namespace(namespace).ok()?;
539 }
540
541 if let Some(version) = version {
542 purl.with_version(version).ok()?;
543 }
544
545 if let Some(arch) = arch {
546 purl.add_qualifier("arch", arch).ok()?;
547 }
548
549 if let Some(epoch) = epoch {
551 purl.add_qualifier("epoch", epoch).ok()?;
552 }
553
554 Some(purl.to_string())
555}
556
557fn normalize_optional_string(value: Option<String>) -> Option<String> {
558 value.and_then(|value| {
559 let trimmed = value.trim();
560 if trimmed.is_empty() || trimmed == "(none)" || trimmed == "[]" {
561 None
562 } else {
563 Some(trimmed.to_string())
564 }
565 })
566}
567
568fn parse_epoch(value: Option<String>) -> u32 {
569 normalize_optional_string(value)
570 .and_then(|value| value.parse::<u32>().ok())
571 .unwrap_or(0)
572}
573
574fn infer_platform_architecture(platform: Option<&str>) -> Option<String> {
575 let platform = platform?.trim();
576 if platform.is_empty() {
577 return None;
578 }
579
580 platform
581 .split_once('-')
582 .map(|(arch, _)| arch)
583 .filter(|arch| !arch.is_empty())
584 .map(|arch| arch.to_string())
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590
591 use crate::models::DatasourceId;
592 use std::path::PathBuf;
593
594 #[test]
595 fn test_bdb_parser_is_match() {
596 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
597 "/var/lib/rpm/Packages"
598 )));
599 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
600 "rootfs/var/lib/rpm/Packages"
601 )));
602 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
603 "/usr/lib/sysimage/rpm/Packages"
604 )));
605 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
606 "/var/lib/rpm/Packages.db"
607 )));
608 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
609 "lib/modules/datasource/deb/__fixtures__/Packages"
610 )));
611 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from("Packages")));
612 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
613 "testdata/rpm/var/lib/rpm/Packages.expected.json"
614 )));
615 }
616
617 #[test]
618 fn test_ndb_parser_is_match() {
619 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
620 "usr/lib/sysimage/rpm/Packages.db"
621 )));
622 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
623 "/rootfs/usr/lib/sysimage/rpm/Packages.db"
624 )));
625 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
626 "usr/lib/rpm/Packages"
627 )));
628 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
629 "var/lib/rpm/Packages.db"
630 )));
631 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
632 "testdata/rpm/usr/lib/sysimage/rpm/Packages.db.expected.json"
633 )));
634 }
635
636 #[cfg(feature = "rpm-sqlite")]
637 #[test]
638 fn test_sqlite_parser_is_match() {
639 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
640 "var/lib/rpm/rpmdb.sqlite"
641 )));
642 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
643 "/rootfs/var/lib/rpm/rpmdb.sqlite"
644 )));
645 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
646 "/rootfs/usr/lib/sysimage/rpm/rpmdb.sqlite"
647 )));
648 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
649 "/var/lib/rpm/Packages"
650 )));
651 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
652 "testdata/rpm/rpmdb.sqlite.expected.json"
653 )));
654 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
655 "testdata/rpm/rpmdb.sqlite-shm"
656 )));
657 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
658 "testdata/rpm/rpmdb.sqlite-wal"
659 )));
660 }
661
662 #[test]
663 fn test_build_version_release_full() {
664 assert_eq!(
666 build_version_release("1.0.0", "1.el7"),
667 Some("1.0.0-1.el7".to_string())
668 );
669 }
670
671 #[test]
672 fn test_build_version_release_no_release() {
673 assert_eq!(
674 build_version_release("1.0.0", ""),
675 Some("1.0.0".to_string())
676 );
677 }
678
679 #[test]
680 fn test_build_version_release_empty() {
681 assert_eq!(build_version_release("", ""), None);
682 }
683
684 #[cfg(feature = "rpm-sqlite")]
685 #[test]
686 fn test_parse_rpm_database_sqlite() {
687 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
688
689 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
690
691 assert_eq!(pkg.package_type, Some(PackageType::Rpm));
692 assert_eq!(
693 pkg.datasource_id,
694 Some(DatasourceId::RpmInstalledDatabaseSqlite)
695 );
696 assert!(pkg.name.is_some());
697 }
698
699 #[cfg(feature = "rpm-sqlite")]
700 #[test]
701 fn test_parse_rpm_database_sqlite_preserves_release_in_version() {
702 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
703
704 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
705
706 assert!(
707 pkg.version
708 .as_ref()
709 .is_some_and(|version| version.contains('-'))
710 );
711 }
712
713 #[test]
714 fn test_build_file_references_skips_invalid_entries() {
715 let file_refs = build_file_references(
716 &[
717 Some("valid".to_string()),
718 Some("".to_string()),
719 Some("ignored".to_string()),
720 ],
721 &[0, 0, u32::MAX],
722 &["/usr/bin/".to_string()],
723 );
724
725 assert_eq!(file_refs.len(), 2);
726 assert_eq!(file_refs[0].path, "/usr/bin/valid");
727 assert_eq!(file_refs[1].path, "/usr/bin/");
728 }
729
730 #[test]
731 fn test_build_package_data_falls_back_to_file_names() {
732 let package = build_package_data(
733 RpmQueryPackage {
734 name: Some("libgcc".to_string()),
735 epoch: None,
736 version: Some("13.1.1".to_string()),
737 release: Some("2.fc38".to_string()),
738 vendor: Some("Fedora Project".to_string()),
739 distribution: None,
740 arch: Some("x86_64".to_string()),
741 platform: None,
742 size: Some(235748),
743 license: Some("GPLv3+".to_string()),
744 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
745 requires: Vec::new(),
746 file_names: vec![
747 Some("/usr/share/licenses/libgcc/COPYING".to_string()),
748 Some("/usr/share/licenses/libgcc/COPYING.RUNTIME".to_string()),
749 ],
750 dir_indexes: Vec::new(),
751 base_names: Vec::new(),
752 dir_names: Vec::new(),
753 },
754 DatasourceId::RpmInstalledDatabaseSqlite,
755 );
756
757 assert_eq!(package.file_references.len(), 2);
758 assert_eq!(
759 package.file_references[0].path,
760 "/usr/share/licenses/libgcc/COPYING"
761 );
762 assert_eq!(
763 package.file_references[1].path,
764 "/usr/share/licenses/libgcc/COPYING.RUNTIME"
765 );
766 }
767
768 #[test]
769 fn test_build_package_data_uses_distribution_for_namespace() {
770 let package = build_package_data(
771 RpmQueryPackage {
772 name: Some("libgcc".to_string()),
773 epoch: None,
774 version: Some("13.1.1".to_string()),
775 release: Some("2.fc38".to_string()),
776 vendor: None,
777 distribution: Some("Fedora Project".to_string()),
778 arch: Some("x86_64".to_string()),
779 platform: None,
780 size: Some(235748),
781 license: Some("GPLv3+".to_string()),
782 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
783 requires: Vec::new(),
784 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
785 dir_indexes: Vec::new(),
786 base_names: Vec::new(),
787 dir_names: Vec::new(),
788 },
789 DatasourceId::RpmInstalledDatabaseSqlite,
790 );
791
792 assert_eq!(package.namespace.as_deref(), Some("fedora"));
793 }
794
795 #[test]
796 fn test_build_package_data_normalizes_declared_license_expression() {
797 let package = build_package_data(
798 RpmQueryPackage {
799 name: Some("libgcc".to_string()),
800 epoch: None,
801 version: Some("13.1.1".to_string()),
802 release: Some("2.fc38".to_string()),
803 vendor: Some("Fedora Project".to_string()),
804 distribution: None,
805 arch: Some("x86_64".to_string()),
806 platform: None,
807 size: Some(235748),
808 license: Some("LGPLv2".to_string()),
809 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
810 requires: Vec::new(),
811 file_names: Vec::new(),
812 dir_indexes: Vec::new(),
813 base_names: Vec::new(),
814 dir_names: Vec::new(),
815 },
816 DatasourceId::RpmInstalledDatabaseSqlite,
817 );
818
819 assert_eq!(
820 package.declared_license_expression.as_deref(),
821 Some("lgpl-2.0-only")
822 );
823 assert_eq!(
824 package.declared_license_expression_spdx.as_deref(),
825 Some("LGPL-2.0-only")
826 );
827 assert_eq!(package.license_detections.len(), 1);
828 }
829
830 #[test]
831 fn test_build_package_data_uses_source_rpm_for_namespace() {
832 let package = build_package_data(
833 RpmQueryPackage {
834 name: Some("libgcc".to_string()),
835 epoch: None,
836 version: Some("13.1.1".to_string()),
837 release: None,
838 vendor: None,
839 distribution: None,
840 arch: Some("x86_64".to_string()),
841 platform: None,
842 size: Some(235748),
843 license: Some("GPLv3+".to_string()),
844 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
845 requires: Vec::new(),
846 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
847 dir_indexes: Vec::new(),
848 base_names: Vec::new(),
849 dir_names: Vec::new(),
850 },
851 DatasourceId::RpmInstalledDatabaseSqlite,
852 );
853
854 assert_eq!(package.namespace.as_deref(), Some("fedora"));
855 }
856
857 #[test]
858 fn test_build_package_data_uses_platform_for_architecture() {
859 let package = build_package_data(
860 RpmQueryPackage {
861 name: Some("libgcc".to_string()),
862 epoch: None,
863 version: Some("13.1.1".to_string()),
864 release: None,
865 vendor: None,
866 distribution: None,
867 arch: None,
868 platform: Some("x86_64-redhat-linux".to_string()),
869 size: Some(235748),
870 license: Some("GPLv3+".to_string()),
871 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
872 requires: Vec::new(),
873 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
874 dir_indexes: Vec::new(),
875 base_names: Vec::new(),
876 dir_names: Vec::new(),
877 },
878 DatasourceId::RpmInstalledDatabaseSqlite,
879 );
880
881 assert_eq!(
882 package.qualifiers.as_ref().and_then(|q| q.get("arch")),
883 Some(&"x86_64".to_string())
884 );
885 }
886}