Skip to main content

provenant/parsers/
rpm_yumdb.rs

1use std::fs;
2use std::path::Path;
3
4use crate::parser_warn as warn;
5use packageurl::PackageUrl;
6
7use crate::models::{DatasourceId, PackageData, PackageType};
8use crate::parsers::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
9
10use super::PackageParser;
11
12const PACKAGE_TYPE: PackageType = PackageType::Rpm;
13
14fn default_package_data() -> PackageData {
15    PackageData {
16        package_type: Some(PACKAGE_TYPE),
17        datasource_id: Some(DatasourceId::RpmYumdb),
18        ..Default::default()
19    }
20}
21
22fn parse_yumdb_dir_name(dir_name: &str) -> Option<(String, String, String)> {
23    let (_, package_part) = dir_name.split_once('-')?;
24    let (name_version_release, arch) = package_part.rsplit_once('.')?;
25
26    let mut parts = name_version_release.rsplitn(3, '-');
27    let release = parts.next()?;
28    let version = parts.next()?;
29    let name = parts.next()?;
30
31    Some((
32        truncate_field(name.to_string()),
33        truncate_field(format!("{}-{}", version, release)),
34        truncate_field(arch.to_string()),
35    ))
36}
37
38fn build_yumdb_purl(name: &str, version: &str, arch: &str) -> Option<String> {
39    let mut purl = PackageUrl::new(PACKAGE_TYPE.as_str(), name).ok()?;
40    purl.with_version(version).ok()?;
41    purl.add_qualifier("arch", arch).ok()?;
42    Some(truncate_field(purl.to_string()))
43}
44
45pub struct RpmYumdbParser;
46
47impl PackageParser for RpmYumdbParser {
48    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
49
50    fn is_match(path: &Path) -> bool {
51        path.file_name().and_then(|name| name.to_str()) == Some("from_repo")
52            && path.to_string_lossy().contains("/var/lib/yum/yumdb/")
53    }
54
55    fn extract_packages(path: &Path) -> Vec<PackageData> {
56        let Some(package_dir) = path.parent() else {
57            return vec![default_package_data()];
58        };
59
60        let Some(dir_name) = package_dir.file_name().and_then(|name| name.to_str()) else {
61            return vec![default_package_data()];
62        };
63
64        let Some((name, version, arch)) = parse_yumdb_dir_name(dir_name) else {
65            warn!(
66                "Failed to parse yumdb package directory name {:?}",
67                package_dir
68            );
69            return vec![default_package_data()];
70        };
71
72        let mut extra_data = std::collections::HashMap::new();
73        let entries = match fs::read_dir(package_dir) {
74            Ok(entries) => entries,
75            Err(e) => {
76                warn!(
77                    "Failed to read yumdb package directory {:?}: {}",
78                    package_dir, e
79                );
80                return vec![default_package_data()];
81            }
82        };
83
84        let entries: Vec<_> = entries.flatten().take(MAX_ITERATION_COUNT).collect();
85
86        for entry in entries {
87            let key_path = entry.path();
88            if !key_path.is_file() {
89                continue;
90            }
91
92            let Some(key) = key_path.file_name().and_then(|name| name.to_str()) else {
93                continue;
94            };
95
96            match read_file_to_string(&key_path, None) {
97                Ok(value) => {
98                    let value = value.trim();
99                    if !value.is_empty() {
100                        extra_data.insert(
101                            key.to_string(),
102                            serde_json::Value::String(truncate_field(value.to_string())),
103                        );
104                    }
105                }
106                Err(e) => warn!("Failed to read yumdb key {:?}: {}", key_path, e),
107            }
108        }
109
110        let qualifiers = std::iter::once(("arch".to_string(), arch.clone())).collect();
111
112        vec![PackageData {
113            datasource_id: Some(DatasourceId::RpmYumdb),
114            package_type: Some(PACKAGE_TYPE),
115            name: Some(name.clone()),
116            version: Some(version.clone()),
117            qualifiers: Some(qualifiers),
118            purl: build_yumdb_purl(&name, &version, &arch),
119            extra_data: (!extra_data.is_empty()).then_some(extra_data),
120            is_virtual: true,
121            ..Default::default()
122        }]
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::fs;
130    use tempfile::tempdir;
131
132    #[test]
133    fn test_parse_yumdb_dir_name() {
134        let parsed = parse_yumdb_dir_name("p/bash-5.0-1.el8.x86_64");
135        assert!(parsed.is_none());
136
137        let parsed = parse_yumdb_dir_name("abc123-bash-5.0-1.el8.x86_64").unwrap();
138        assert_eq!(parsed.0, "bash");
139        assert_eq!(parsed.1, "5.0-1.el8");
140        assert_eq!(parsed.2, "x86_64");
141    }
142
143    #[test]
144    fn test_is_match() {
145        assert!(RpmYumdbParser::is_match(Path::new(
146            "/rootfs/var/lib/yum/yumdb/p/abc123-bash-5.0-1.el8.x86_64/from_repo"
147        )));
148        assert!(!RpmYumdbParser::is_match(Path::new(
149            "/rootfs/var/lib/yum/yumdb/p/abc123-bash-5.0-1.el8.x86_64/reason"
150        )));
151    }
152
153    #[test]
154    fn test_extract_packages_reads_sibling_metadata() {
155        let tempdir = tempdir().unwrap();
156        let package_dir = tempdir
157            .path()
158            .join("rootfs/var/lib/yum/yumdb/p/abc123-bash-5.0-1.el8.x86_64");
159        fs::create_dir_all(&package_dir).unwrap();
160        fs::write(package_dir.join("from_repo"), "baseos\n").unwrap();
161        fs::write(package_dir.join("reason"), "dep\n").unwrap();
162        fs::write(package_dir.join("releasever"), "8\n").unwrap();
163
164        let packages = RpmYumdbParser::extract_packages(&package_dir.join("from_repo"));
165        let pkg = &packages[0];
166
167        assert_eq!(pkg.datasource_id, Some(DatasourceId::RpmYumdb));
168        assert_eq!(pkg.name.as_deref(), Some("bash"));
169        assert_eq!(pkg.version.as_deref(), Some("5.0-1.el8"));
170        assert_eq!(
171            pkg.qualifiers.as_ref().and_then(|q| q.get("arch")),
172            Some(&"x86_64".to_string())
173        );
174        let extra = pkg.extra_data.as_ref().unwrap();
175        assert_eq!(extra["from_repo"], "baseos");
176        assert_eq!(extra["reason"], "dep");
177        assert_eq!(extra["releasever"], "8");
178    }
179}
180
181crate::register_parser!(
182    "RPM yumdb metadata",
183    &["**/var/lib/yum/yumdb/*/*/from_repo"],
184    "rpm",
185    "",
186    Some("http://yum.baseurl.org/wiki/YumDB.html"),
187);