Skip to main content

provenant/parsers/
rpm_yumdb.rs

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