Skip to main content

provenant/parsers/
rpm_yumdb.rs

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