provenant/parsers/
rpm_yumdb.rs1use 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);