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