Skip to main content

isr_dl_linux/ubuntu/
parse.rs

1//! Parser for Debian `Packages` files (the per-dist package metadata).
2
3/// One package stanza parsed from a Debian `Packages` file.
4#[derive(Debug, Default, Clone, PartialEq, Eq)]
5pub struct UbuntuRepositoryEntry {
6    /// `Package:` field.
7    pub package: Option<String>,
8
9    /// `Version:` field.
10    pub version: Option<String>,
11
12    /// `Filename:` field, relative to the repository host.
13    pub filename: Option<String>,
14
15    /// `Size:` field, the compressed `.deb` size in bytes.
16    pub size: Option<usize>,
17
18    /// `Installed-Size:` field, the installed size in KiB.
19    pub installed_size: Option<usize>,
20
21    /// `Depends:` field.
22    pub depends: Option<String>,
23
24    /// `Section:` field.
25    pub section: Option<String>,
26
27    /// `Source:` field.
28    pub source: Option<String>,
29
30    /// `MD5sum:` field.
31    pub md5sum: Option<String>,
32
33    /// `SHA1:` field.
34    pub sha1: Option<String>,
35
36    /// `SHA256:` field.
37    pub sha256: Option<String>,
38
39    /// `SHA512:` field.
40    pub sha512: Option<String>,
41}
42
43/// Parses a decompressed Debian `Packages` file body into a list of entries.
44pub fn parse_packages(text: &str) -> Vec<UbuntuRepositoryEntry> {
45    let mut result = Vec::new();
46    let mut entry = UbuntuRepositoryEntry::default();
47
48    for line in text.lines() {
49        if line.is_empty() {
50            result.push(entry);
51            entry = UbuntuRepositoryEntry::default();
52            continue;
53        }
54
55        if line.starts_with(' ') {
56            continue;
57        }
58
59        let (key, value) = match line.split_once(": ") {
60            Some(kv) => kv,
61            None => continue,
62        };
63
64        match key {
65            "Package" => entry.package = Some(value.into()),
66            "Version" => entry.version = Some(value.into()),
67            "Filename" => entry.filename = Some(value.into()),
68            "Size" => entry.size = value.parse().ok(),
69            "Installed-Size" => entry.installed_size = value.parse().ok(),
70            "Depends" => entry.depends = Some(value.into()),
71            "Section" => entry.section = Some(value.into()),
72            "Source" => entry.source = Some(value.into()),
73            "MD5sum" => entry.md5sum = Some(value.into()),
74            "SHA1" => entry.sha1 = Some(value.into()),
75            "SHA256" => entry.sha256 = Some(value.into()),
76            "SHA512" => entry.sha512 = Some(value.into()),
77            _ => (),
78        }
79    }
80
81    if entry.package.is_some() || entry.filename.is_some() {
82        result.push(entry);
83    }
84
85    result
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn parses_two_entries_with_blank_separator() {
94        let input = "\
95Package: linux-image-6.8.0-40-generic
96Version: 6.8.0-40.40
97Filename: pool/main/l/linux/linux-image-6.8.0-40-generic_6.8.0-40.40_amd64.deb
98Size: 12345
99
100Package: linux-modules-6.8.0-40-generic
101Version: 6.8.0-40.40
102Filename: pool/main/l/linux/linux-modules-6.8.0-40-generic_6.8.0-40.40_amd64.deb
103Size: 67890
104";
105        let entries = parse_packages(input);
106        assert_eq!(entries.len(), 2);
107        assert_eq!(
108            entries[0].package.as_deref(),
109            Some("linux-image-6.8.0-40-generic")
110        );
111        assert_eq!(entries[0].size, Some(12345));
112        assert_eq!(
113            entries[1].package.as_deref(),
114            Some("linux-modules-6.8.0-40-generic")
115        );
116    }
117
118    #[test]
119    fn ignores_continuation_lines() {
120        let input = "\
121Package: foo
122Description: a package
123 with a wrapped description
124 across multiple lines
125Version: 1.0
126";
127        let entries = parse_packages(input);
128        assert_eq!(entries.len(), 1);
129        assert_eq!(entries[0].version.as_deref(), Some("1.0"));
130    }
131
132    #[test]
133    fn skips_lines_without_separator() {
134        let input = "Package: foo\nNotAField\nVersion: 1.0\n";
135        let entries = parse_packages(input);
136        assert_eq!(entries.len(), 1);
137        assert_eq!(entries[0].version.as_deref(), Some("1.0"));
138    }
139
140    #[test]
141    fn empty_input_produces_no_entries() {
142        assert_eq!(parse_packages("").len(), 0);
143    }
144}