Skip to main content

provenant/parsers/debian/
file_list.rs

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