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