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