provenant/parsers/debian/
file_list.rs1use std::path::Path;
7
8use crate::models::{DatasourceId, FileReference, Md5Digest, PackageData, PackageType};
9use crate::parser_warn as warn;
10use crate::parsers::utils::{MAX_ITERATION_COUNT, truncate_field};
11
12use super::super::metadata::ParserMetadata;
13use super::utils::build_debian_purl;
14use super::{IGNORED_ROOT_DIRS, PACKAGE_TYPE, default_package_data, read_or_default};
15use crate::parsers::PackageParser;
16
17pub struct DebianInstalledListParser;
19
20impl PackageParser for DebianInstalledListParser {
21 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
22
23 fn metadata() -> Vec<ParserMetadata> {
24 vec![ParserMetadata {
25 description: "Debian installed files list",
26 file_patterns: &["**/var/lib/dpkg/info/*.list"],
27 package_type: "deb",
28 primary_language: "",
29 documentation_url: Some("https://www.debian.org/doc/debian-policy/ch-files.html"),
30 }]
31 }
32
33 fn is_match(path: &Path) -> bool {
34 path.extension().and_then(|e| e.to_str()) == Some("list")
35 && path
36 .to_str()
37 .map(|p| p.contains("/var/lib/dpkg/info/"))
38 .unwrap_or(false)
39 }
40
41 fn extract_packages(path: &Path) -> Vec<PackageData> {
42 let filename = match path.file_stem().and_then(|s| s.to_str()) {
43 Some(f) => f,
44 None => {
45 return vec![default_package_data(DatasourceId::DebianInstalledFilesList)];
46 }
47 };
48
49 let content = read_or_default!(path, ".list file", DatasourceId::DebianInstalledFilesList);
50
51 vec![parse_debian_file_list(
52 &content,
53 filename,
54 DatasourceId::DebianInstalledFilesList,
55 )]
56 }
57}
58
59pub struct DebianInstalledMd5sumsParser;
61
62impl PackageParser for DebianInstalledMd5sumsParser {
63 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
64
65 fn metadata() -> Vec<ParserMetadata> {
66 vec![ParserMetadata {
67 description: "Debian installed package md5sums",
68 file_patterns: &["**/var/lib/dpkg/info/*.md5sums"],
69 package_type: "deb",
70 primary_language: "",
71 documentation_url: Some("https://www.debian.org/doc/debian-policy/ch-files.html"),
72 }]
73 }
74
75 fn is_match(path: &Path) -> bool {
76 path.extension().and_then(|e| e.to_str()) == Some("md5sums")
77 && path
78 .to_str()
79 .map(|p| p.contains("/var/lib/dpkg/info/"))
80 .unwrap_or(false)
81 }
82
83 fn extract_packages(path: &Path) -> Vec<PackageData> {
84 let filename = match path.file_stem().and_then(|s| s.to_str()) {
85 Some(f) => f,
86 None => {
87 return vec![default_package_data(DatasourceId::DebianInstalledMd5Sums)];
88 }
89 };
90
91 let content = read_or_default!(path, ".md5sums file", DatasourceId::DebianInstalledMd5Sums);
92
93 vec![parse_debian_file_list(
94 &content,
95 filename,
96 DatasourceId::DebianInstalledMd5Sums,
97 )]
98 }
99}
100
101pub(crate) fn parse_file_entries(content: &str, log_label: &str) -> Vec<FileReference> {
102 let mut file_references = Vec::new();
103 let mut count = 0usize;
104
105 for line in content.lines() {
106 count += 1;
107 if count > MAX_ITERATION_COUNT {
108 warn!("{log_label}: exceeded MAX_ITERATION_COUNT lines, stopping");
109 break;
110 }
111 let line = line.trim();
112 if line.is_empty() || line.starts_with('#') {
113 continue;
114 }
115
116 let (md5sum, path): (Option<Md5Digest>, &str) = if let Some(idx) = line.find(" ") {
117 (
118 Md5Digest::from_hex(line[..idx].trim()).ok(),
119 line[idx + 2..].trim(),
120 )
121 } else if let Some((hash, p)) = line.split_once(' ') {
122 (Md5Digest::from_hex(hash.trim()).ok(), p.trim())
123 } else {
124 (None, line)
125 };
126
127 if IGNORED_ROOT_DIRS.contains(&path) {
128 continue;
129 }
130
131 file_references.push(FileReference {
132 path: path.to_string(),
133 size: None,
134 sha1: None,
135 md5: md5sum,
136 sha256: None,
137 sha512: None,
138 extra_data: None,
139 });
140 }
141
142 file_references
143}
144
145fn parse_debian_file_list(
146 content: &str,
147 filename: &str,
148 datasource_id: DatasourceId,
149) -> PackageData {
150 let (name, arch_qualifier) = if let Some((pkg, arch)) = filename.split_once(':') {
151 (
152 Some(truncate_field(pkg.to_string())),
153 Some(arch.to_string()),
154 )
155 } else if filename == "md5sums" {
156 (None, None)
157 } else {
158 (Some(truncate_field(filename.to_string())), None)
159 };
160
161 let file_references = parse_file_entries(content, "parse_debian_file_list");
162
163 if file_references.is_empty() {
164 return default_package_data(datasource_id);
165 }
166
167 let namespace = Some("debian".to_string());
168 let mut package = PackageData {
169 datasource_id: Some(datasource_id),
170 package_type: Some(PACKAGE_TYPE),
171 namespace: namespace.clone(),
172 name: name.clone(),
173 file_references,
174 ..Default::default()
175 };
176
177 if let Some(n) = &name {
178 package.purl = build_debian_purl(n, None, namespace.as_deref(), arch_qualifier.as_deref());
179 }
180
181 package
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::models::DatasourceId;
188 use std::path::PathBuf;
189
190 #[test]
191 fn test_list_parser_is_match() {
192 assert!(DebianInstalledListParser::is_match(&PathBuf::from(
193 "/var/lib/dpkg/info/bash.list"
194 )));
195 assert!(DebianInstalledListParser::is_match(&PathBuf::from(
196 "/var/lib/dpkg/info/package:amd64.list"
197 )));
198 assert!(!DebianInstalledListParser::is_match(&PathBuf::from(
199 "bash.list"
200 )));
201 assert!(!DebianInstalledListParser::is_match(&PathBuf::from(
202 "/var/lib/dpkg/info/bash.md5sums"
203 )));
204 }
205
206 #[test]
207 fn test_md5sums_parser_is_match() {
208 assert!(DebianInstalledMd5sumsParser::is_match(&PathBuf::from(
209 "/var/lib/dpkg/info/bash.md5sums"
210 )));
211 assert!(DebianInstalledMd5sumsParser::is_match(&PathBuf::from(
212 "/var/lib/dpkg/info/package:amd64.md5sums"
213 )));
214 assert!(!DebianInstalledMd5sumsParser::is_match(&PathBuf::from(
215 "bash.md5sums"
216 )));
217 assert!(!DebianInstalledMd5sumsParser::is_match(&PathBuf::from(
218 "/var/lib/dpkg/info/bash.list"
219 )));
220 }
221
222 #[test]
223 fn test_parse_debian_file_list_plain_list() {
224 let content = "/.
225/bin
226/bin/bash
227/usr/bin/bashbug
228/usr/share/doc/bash/README
229";
230 let pkg = parse_debian_file_list(content, "bash", DatasourceId::DebianInstalledFilesList);
231 assert_eq!(pkg.name, Some("bash".to_string()));
232 assert_eq!(pkg.file_references.len(), 3);
233 assert_eq!(pkg.file_references[0].path, "/bin/bash");
234 assert_eq!(pkg.file_references[0].md5, None);
235 assert_eq!(pkg.file_references[1].path, "/usr/bin/bashbug");
236 assert_eq!(pkg.file_references[2].path, "/usr/share/doc/bash/README");
237 }
238
239 #[test]
240 fn test_parse_debian_file_list_md5sums() {
241 let content = "77506afebd3b7e19e937a678a185b62e bin/bash
2421c77d2031971b4e4c512ac952102cd85 usr/bin/bashbug
243f55e3a16959b0bb8915cb5f219521c80 usr/share/doc/bash/COMPAT.gz
244";
245 let pkg = parse_debian_file_list(content, "bash", DatasourceId::DebianInstalledFilesList);
246 assert_eq!(pkg.name, Some("bash".to_string()));
247 assert_eq!(pkg.file_references.len(), 3);
248 assert_eq!(pkg.file_references[0].path, "bin/bash");
249 assert_eq!(
250 pkg.file_references[0].md5,
251 Some(Md5Digest::from_hex("77506afebd3b7e19e937a678a185b62e").unwrap())
252 );
253 assert_eq!(pkg.file_references[1].path, "usr/bin/bashbug");
254 assert_eq!(
255 pkg.file_references[1].md5,
256 Some(Md5Digest::from_hex("1c77d2031971b4e4c512ac952102cd85").unwrap())
257 );
258 }
259
260 #[test]
261 fn test_parse_debian_file_list_with_arch() {
262 let content = "/usr/bin/foo
263/usr/lib/x86_64-linux-gnu/libfoo.so
264";
265 let pkg = parse_debian_file_list(
266 content,
267 "libfoo:amd64",
268 DatasourceId::DebianInstalledFilesList,
269 );
270 assert_eq!(pkg.name, Some("libfoo".to_string()));
271 assert!(pkg.purl.is_some());
272 assert!(pkg.purl.as_ref().unwrap().contains("arch=amd64"));
273 assert_eq!(pkg.file_references.len(), 2);
274 }
275
276 #[test]
277 fn test_parse_debian_file_list_skips_comments_and_empty() {
278 let content = "# This is a comment
279/bin/bash
280
281/usr/bin/bashbug
282
283";
284 let pkg = parse_debian_file_list(content, "bash", DatasourceId::DebianInstalledFilesList);
285 assert_eq!(pkg.file_references.len(), 2);
286 }
287
288 #[test]
289 fn test_parse_debian_file_list_md5sums_only() {
290 let content = "abc123 usr/bin/tool
291";
292 let pkg =
293 parse_debian_file_list(content, "md5sums", DatasourceId::DebianInstalledFilesList);
294 assert_eq!(pkg.name, None);
295 assert_eq!(pkg.file_references.len(), 1);
296 }
297
298 #[test]
299 fn test_parse_debian_file_list_ignores_root_dirs() {
300 let content = "/.
301/bin
302/bin/bash
303/etc
304/usr
305/var
306";
307 let pkg = parse_debian_file_list(content, "bash", DatasourceId::DebianInstalledFilesList);
308 assert_eq!(pkg.file_references.len(), 1);
309 assert_eq!(pkg.file_references[0].path, "/bin/bash");
310 }
311}