Skip to main content

provenant/parsers/debian/
file_list.rs

1// SPDX-FileCopyrightText: nexB Inc. and others
2// SPDX-FileCopyrightText: Provenant contributors
3// SPDX-License-Identifier: Apache-2.0
4// Derived from ScanCode Toolkit (Apache-2.0); modified. See NOTICE.
5
6use 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
17/// Parser for Debian installed file lists (*.list)
18pub 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
59/// Parser for Debian installed MD5 checksum files (*.md5sums)
60pub 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}