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