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) -> InstalledRpmDbKind {
185    match datasource_id {
186        DatasourceId::RpmInstalledDatabaseBdb => InstalledRpmDbKind::Bdb,
187        DatasourceId::RpmInstalledDatabaseNdb => InstalledRpmDbKind::Ndb,
188        DatasourceId::RpmInstalledDatabaseSqlite => InstalledRpmDbKind::Sqlite,
189        other => panic!("unexpected datasource for installed RPM DB: {other:?}"),
190    }
191}
192
193fn native_package_to_query_package(package: InstalledRpmPackage) -> RpmQueryPackage {
194    RpmQueryPackage {
195        name: truncate_optional_string(Some(package.name)),
196        epoch: Some(package.epoch.to_string()),
197        version: truncate_optional_string(Some(package.version)),
198        release: truncate_optional_string(Some(package.release)),
199        vendor: truncate_optional_string(Some(package.vendor)),
200        distribution: truncate_optional_string(Some(package.distribution)),
201        arch: truncate_optional_string(Some(package.arch)),
202        platform: truncate_optional_string(Some(package.platform)),
203        size: (package.size > 0).then_some(u64::from(package.size)),
204        license: truncate_optional_string(Some(package.license)),
205        source_rpm: truncate_optional_string(Some(package.source_rpm)),
206        requires: package
207            .requires
208            .into_iter()
209            .take(MAX_ITERATION_COUNT)
210            .map(truncate_field)
211            .collect(),
212        file_names: package
213            .file_names
214            .into_iter()
215            .take(MAX_ITERATION_COUNT)
216            .map(|s| Some(truncate_field(s)))
217            .collect(),
218        dir_indexes: package.dir_indexes,
219        base_names: package
220            .base_names
221            .into_iter()
222            .take(MAX_ITERATION_COUNT)
223            .map(|s| Some(truncate_field(s)))
224            .collect(),
225        dir_names: package
226            .dir_names
227            .into_iter()
228            .take(MAX_ITERATION_COUNT)
229            .map(truncate_field)
230            .collect(),
231    }
232}
233
234fn truncate_optional_string(value: Option<String>) -> Option<String> {
235    value
236        .map(truncate_field)
237        .and_then(|v| normalize_optional_string(Some(v)))
238}
239
240fn build_evr_version(epoch: u32, version: &str, release: &str) -> Option<String> {
241    if version.is_empty() {
242        return None;
243    }
244
245    let mut evr = String::new();
246
247    if epoch > 0 {
248        evr.push_str(&format!("{}:", epoch));
249    }
250
251    evr.push_str(version);
252
253    if !release.is_empty() {
254        evr.push('-');
255        evr.push_str(release);
256    }
257
258    Some(evr)
259}
260
261fn build_file_references(
262    base_names: &[Option<String>],
263    dir_indexes: &[u32],
264    dir_names: &[String],
265) -> Vec<FileReference> {
266    if base_names.is_empty() || dir_names.is_empty() {
267        return Vec::new();
268    }
269
270    base_names
271        .iter()
272        .zip(dir_indexes.iter())
273        .take(MAX_ITERATION_COUNT)
274        .filter_map(|(basename, &dir_idx)| {
275            let dirname = dir_names.get(dir_idx as usize)?;
276            let basename = basename.as_deref().unwrap_or_default();
277            let path = format!("{}{}", dirname, basename);
278            if path.is_empty() || path == "/" {
279                return None;
280            }
281            Some(FileReference {
282                path,
283                size: None,
284                sha1: None,
285                md5: None,
286                sha256: None,
287                sha512: None,
288                extra_data: None,
289            })
290        })
291        .collect()
292}
293
294fn build_file_references_from_paths(paths: &[Option<String>]) -> Vec<FileReference> {
295    paths
296        .iter()
297        .take(MAX_ITERATION_COUNT)
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 build_package_data(pkg: RpmQueryPackage, datasource_id: DatasourceId) -> PackageData {
318    let name = normalize_optional_string(pkg.name).map(truncate_field);
319    let version_raw = normalize_optional_string(pkg.version).map(truncate_field);
320    let release = normalize_optional_string(pkg.release).map(truncate_field);
321    let version = build_evr_version(
322        parse_epoch(pkg.epoch),
323        version_raw.as_deref().unwrap_or_default(),
324        release.as_deref().unwrap_or_default(),
325    );
326
327    let vendor = normalize_optional_string(pkg.vendor)
328        .map(truncate_field)
329        .or_else(|| normalize_optional_string(pkg.distribution).map(truncate_field));
330    let source_rpm = normalize_optional_string(pkg.source_rpm).map(truncate_field);
331    let namespace =
332        infer_rpm_namespace(None, vendor.as_deref(), release.as_deref(), None).or_else(|| {
333            source_rpm
334                .as_deref()
335                .and_then(|source_rpm| infer_rpm_namespace_from_filename(Path::new(source_rpm)))
336        });
337
338    let architecture = normalize_optional_string(pkg.arch)
339        .map(truncate_field)
340        .or_else(|| infer_platform_architecture(pkg.platform.as_deref()));
341    let dependencies = pkg
342        .requires
343        .into_iter()
344        .take(MAX_ITERATION_COUNT)
345        .filter_map(|require| build_dependency(&require))
346        .collect();
347    let extracted_license_statement = normalize_optional_string(pkg.license).map(truncate_field);
348    let source_packages = source_rpm.clone().into_iter().collect();
349    let file_references = {
350        let from_dir_components =
351            build_file_references(&pkg.base_names, &pkg.dir_indexes, &pkg.dir_names);
352        if from_dir_components.is_empty() {
353            build_file_references_from_paths(&pkg.file_names)
354        } else {
355            from_dir_components
356        }
357    };
358    let purl = build_package_purl(
359        name.as_deref(),
360        namespace.as_deref(),
361        version.as_deref(),
362        architecture.as_deref(),
363    );
364
365    PackageData {
366        datasource_id: Some(datasource_id),
367        package_type: Some(PACKAGE_TYPE),
368        namespace,
369        name,
370        version,
371        qualifiers: architecture.as_ref().map(|arch| {
372            let mut q = std::collections::HashMap::new();
373            q.insert("arch".to_string(), arch.clone());
374            q
375        }),
376        subpath: None,
377        primary_language: None,
378        description: None,
379        release_date: None,
380        parties: Vec::new(),
381        keywords: Vec::new(),
382        homepage_url: None,
383        download_url: None,
384        size: pkg.size.filter(|size| *size > 0),
385        sha1: None,
386        md5: None,
387        sha256: None,
388        sha512: None,
389        bug_tracking_url: None,
390        code_view_url: None,
391        vcs_url: None,
392        copyright: None,
393        holder: None,
394        declared_license_expression: None,
395        declared_license_expression_spdx: None,
396        license_detections: Vec::new(),
397        other_license_expression: None,
398        other_license_expression_spdx: None,
399        other_license_detections: Vec::new(),
400        extracted_license_statement,
401        notice_text: None,
402        source_packages,
403        file_references,
404        is_private: false,
405        is_virtual: false,
406        extra_data: None,
407        dependencies,
408        repository_homepage_url: None,
409        repository_download_url: None,
410        api_data_url: None,
411        purl,
412    }
413}
414
415fn build_dependency(require: &str) -> Option<Dependency> {
416    let require = require.trim();
417    if require.is_empty() || require.starts_with("rpmlib(") || require.starts_with("config(") {
418        return None;
419    }
420
421    let purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), require)
422        .ok()
423        .map(|p| p.to_string());
424
425    Some(Dependency {
426        purl,
427        extracted_requirement: None,
428        scope: Some("requires".to_string()),
429        is_runtime: Some(true),
430        is_optional: Some(false),
431        is_pinned: Some(false),
432        is_direct: Some(true),
433        resolved_package: None,
434        extra_data: None,
435    })
436}
437
438fn build_package_purl(
439    name: Option<&str>,
440    namespace: Option<&str>,
441    version: Option<&str>,
442    arch: Option<&str>,
443) -> Option<String> {
444    let name = name?;
445    let mut purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), name).ok()?;
446
447    if let Some(namespace) = namespace {
448        purl.with_namespace(namespace).ok()?;
449    }
450
451    if let Some(version) = version {
452        purl.with_version(version).ok()?;
453    }
454
455    if let Some(arch) = arch {
456        purl.add_qualifier("arch", arch).ok()?;
457    }
458
459    Some(purl.to_string())
460}
461
462fn normalize_optional_string(value: Option<String>) -> Option<String> {
463    value.and_then(|value| {
464        let trimmed = value.trim();
465        if trimmed.is_empty() || trimmed == "(none)" || trimmed == "[]" {
466            None
467        } else {
468            Some(trimmed.to_string())
469        }
470    })
471}
472
473fn parse_epoch(value: Option<String>) -> u32 {
474    normalize_optional_string(value)
475        .and_then(|value| value.parse::<u32>().ok())
476        .unwrap_or(0)
477}
478
479fn infer_platform_architecture(platform: Option<&str>) -> Option<String> {
480    let platform = platform?.trim();
481    if platform.is_empty() {
482        return None;
483    }
484
485    platform
486        .split_once('-')
487        .map(|(arch, _)| arch)
488        .filter(|arch| !arch.is_empty())
489        .map(|arch| arch.to_string())
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    use crate::models::DatasourceId;
497    use std::path::PathBuf;
498
499    #[test]
500    fn test_bdb_parser_is_match() {
501        assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
502            "/var/lib/rpm/Packages"
503        )));
504        assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
505            "rootfs/var/lib/rpm/Packages"
506        )));
507        assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
508            "/usr/lib/sysimage/rpm/Packages"
509        )));
510        assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
511            "/var/lib/rpm/Packages.db"
512        )));
513        assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
514            "lib/modules/datasource/deb/__fixtures__/Packages"
515        )));
516        assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from("Packages")));
517        assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
518            "testdata/rpm/var/lib/rpm/Packages.expected.json"
519        )));
520    }
521
522    #[test]
523    fn test_ndb_parser_is_match() {
524        assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
525            "usr/lib/sysimage/rpm/Packages.db"
526        )));
527        assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
528            "/rootfs/usr/lib/sysimage/rpm/Packages.db"
529        )));
530        assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
531            "usr/lib/rpm/Packages"
532        )));
533        assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
534            "var/lib/rpm/Packages.db"
535        )));
536        assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
537            "testdata/rpm/usr/lib/sysimage/rpm/Packages.db.expected.json"
538        )));
539    }
540
541    #[cfg(feature = "rpm-sqlite")]
542    #[test]
543    fn test_sqlite_parser_is_match() {
544        assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
545            "var/lib/rpm/rpmdb.sqlite"
546        )));
547        assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
548            "/rootfs/var/lib/rpm/rpmdb.sqlite"
549        )));
550        assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
551            "/rootfs/usr/lib/sysimage/rpm/rpmdb.sqlite"
552        )));
553        assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
554            "/var/lib/rpm/Packages"
555        )));
556        assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
557            "testdata/rpm/rpmdb.sqlite.expected.json"
558        )));
559        assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
560            "testdata/rpm/rpmdb.sqlite-shm"
561        )));
562        assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
563            "testdata/rpm/rpmdb.sqlite-wal"
564        )));
565    }
566
567    #[test]
568    fn test_build_evr_version_full() {
569        assert_eq!(
570            build_evr_version(2, "1.0.0", "1.el7"),
571            Some("2:1.0.0-1.el7".to_string())
572        );
573    }
574
575    #[test]
576    fn test_build_evr_version_no_epoch() {
577        assert_eq!(
578            build_evr_version(0, "1.0.0", "1.el7"),
579            Some("1.0.0-1.el7".to_string())
580        );
581    }
582
583    #[test]
584    fn test_build_evr_version_no_release() {
585        assert_eq!(build_evr_version(0, "1.0.0", ""), Some("1.0.0".to_string()));
586    }
587
588    #[test]
589    fn test_build_evr_version_empty() {
590        assert_eq!(build_evr_version(0, "", ""), None);
591    }
592
593    #[cfg(feature = "rpm-sqlite")]
594    #[test]
595    fn test_parse_rpm_database_sqlite() {
596        let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
597
598        let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
599
600        assert_eq!(pkg.package_type, Some(PackageType::Rpm));
601        assert_eq!(
602            pkg.datasource_id,
603            Some(DatasourceId::RpmInstalledDatabaseSqlite)
604        );
605        assert!(pkg.name.is_some());
606    }
607
608    #[cfg(feature = "rpm-sqlite")]
609    #[test]
610    fn test_parse_rpm_database_sqlite_preserves_release_in_version() {
611        let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
612
613        let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
614
615        assert!(
616            pkg.version
617                .as_ref()
618                .is_some_and(|version| version.contains('-'))
619        );
620    }
621
622    #[test]
623    fn test_build_file_references_skips_invalid_entries() {
624        let file_refs = build_file_references(
625            &[
626                Some("valid".to_string()),
627                Some("".to_string()),
628                Some("ignored".to_string()),
629            ],
630            &[0, 0, u32::MAX],
631            &["/usr/bin/".to_string()],
632        );
633
634        assert_eq!(file_refs.len(), 2);
635        assert_eq!(file_refs[0].path, "/usr/bin/valid");
636        assert_eq!(file_refs[1].path, "/usr/bin/");
637    }
638
639    #[test]
640    fn test_build_package_data_falls_back_to_file_names() {
641        let package = build_package_data(
642            RpmQueryPackage {
643                name: Some("libgcc".to_string()),
644                epoch: None,
645                version: Some("13.1.1".to_string()),
646                release: Some("2.fc38".to_string()),
647                vendor: Some("Fedora Project".to_string()),
648                distribution: None,
649                arch: Some("x86_64".to_string()),
650                platform: None,
651                size: Some(235748),
652                license: Some("GPLv3+".to_string()),
653                source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
654                requires: Vec::new(),
655                file_names: vec![
656                    Some("/usr/share/licenses/libgcc/COPYING".to_string()),
657                    Some("/usr/share/licenses/libgcc/COPYING.RUNTIME".to_string()),
658                ],
659                dir_indexes: Vec::new(),
660                base_names: Vec::new(),
661                dir_names: Vec::new(),
662            },
663            DatasourceId::RpmInstalledDatabaseSqlite,
664        );
665
666        assert_eq!(package.file_references.len(), 2);
667        assert_eq!(
668            package.file_references[0].path,
669            "/usr/share/licenses/libgcc/COPYING"
670        );
671        assert_eq!(
672            package.file_references[1].path,
673            "/usr/share/licenses/libgcc/COPYING.RUNTIME"
674        );
675    }
676
677    #[test]
678    fn test_build_package_data_uses_distribution_for_namespace() {
679        let package = build_package_data(
680            RpmQueryPackage {
681                name: Some("libgcc".to_string()),
682                epoch: None,
683                version: Some("13.1.1".to_string()),
684                release: Some("2.fc38".to_string()),
685                vendor: None,
686                distribution: Some("Fedora Project".to_string()),
687                arch: Some("x86_64".to_string()),
688                platform: None,
689                size: Some(235748),
690                license: Some("GPLv3+".to_string()),
691                source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
692                requires: Vec::new(),
693                file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
694                dir_indexes: Vec::new(),
695                base_names: Vec::new(),
696                dir_names: Vec::new(),
697            },
698            DatasourceId::RpmInstalledDatabaseSqlite,
699        );
700
701        assert_eq!(package.namespace.as_deref(), Some("fedora"));
702    }
703
704    #[test]
705    fn test_build_package_data_uses_source_rpm_for_namespace() {
706        let package = build_package_data(
707            RpmQueryPackage {
708                name: Some("libgcc".to_string()),
709                epoch: None,
710                version: Some("13.1.1".to_string()),
711                release: None,
712                vendor: None,
713                distribution: None,
714                arch: Some("x86_64".to_string()),
715                platform: None,
716                size: Some(235748),
717                license: Some("GPLv3+".to_string()),
718                source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
719                requires: Vec::new(),
720                file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
721                dir_indexes: Vec::new(),
722                base_names: Vec::new(),
723                dir_names: Vec::new(),
724            },
725            DatasourceId::RpmInstalledDatabaseSqlite,
726        );
727
728        assert_eq!(package.namespace.as_deref(), Some("fedora"));
729    }
730
731    #[test]
732    fn test_build_package_data_uses_platform_for_architecture() {
733        let package = build_package_data(
734            RpmQueryPackage {
735                name: Some("libgcc".to_string()),
736                epoch: None,
737                version: Some("13.1.1".to_string()),
738                release: None,
739                vendor: None,
740                distribution: None,
741                arch: None,
742                platform: Some("x86_64-redhat-linux".to_string()),
743                size: Some(235748),
744                license: Some("GPLv3+".to_string()),
745                source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
746                requires: Vec::new(),
747                file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
748                dir_indexes: Vec::new(),
749                base_names: Vec::new(),
750                dir_names: Vec::new(),
751            },
752            DatasourceId::RpmInstalledDatabaseSqlite,
753        );
754
755        assert_eq!(
756            package.qualifiers.as_ref().and_then(|q| q.get("arch")),
757            Some(&"x86_64".to_string())
758        );
759    }
760}
761
762#[cfg(feature = "rpm-sqlite")]
763crate::register_parser!(
764    "RPM installed package database",
765    &[
766        "**/var/lib/rpm/Packages",
767        "**/usr/lib/sysimage/rpm/Packages",
768        "**/var/lib/rpm/Packages.db",
769        "**/usr/lib/sysimage/rpm/Packages.db",
770        "**/var/lib/rpm/rpmdb.sqlite",
771        "**/usr/lib/sysimage/rpm/rpmdb.sqlite"
772    ],
773    "rpm",
774    "",
775    Some("https://rpm.org/"),
776);
777
778#[cfg(not(feature = "rpm-sqlite"))]
779crate::register_parser!(
780    "RPM installed package database",
781    &[
782        "**/var/lib/rpm/Packages",
783        "**/usr/lib/sysimage/rpm/Packages",
784        "**/var/lib/rpm/Packages.db",
785        "**/usr/lib/sysimage/rpm/Packages.db"
786    ],
787    "rpm",
788    "",
789    Some("https://rpm.org/"),
790);