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