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