Skip to main content

provenant/parsers/
rpm_db.rs

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