apt_release_file/
entry.rs

1use super::ImageSize;
2use deb_architectures::Architecture;
3use std::str::FromStr;
4
5/// The hash, size, and path of a file that this release file points to.
6#[derive(Debug, Default, Clone, Hash, PartialEq)]
7pub struct ReleaseEntry {
8    pub sum: String,
9    pub size: u64,
10    pub path: String,
11}
12
13impl ReleaseEntry {
14    /// If required, the precise variant of an apt entry can be determined here.
15    ///
16    /// Malformed / unsupported apt entries will return `None`.
17    pub fn variant(&self) -> Option<EntryVariant> {
18        entry_variant(&self.path)
19    }
20}
21
22impl FromStr for ReleaseEntry {
23    type Err = &'static str;
24
25    fn from_str(input: &str) -> Result<Self, Self::Err> {
26        let mut iterator = input.split_whitespace();
27
28        let output = Self {
29            sum: iterator.next().ok_or("missing sum field")?.to_owned(),
30            size: iterator
31                .next()
32                .ok_or("missing size field")?
33                .parse::<u64>()
34                .map_err(|_| "size field is not a number")?,
35            path: iterator.next().ok_or("missing path field")?.to_owned(),
36        };
37
38        Ok(output)
39    }
40}
41
42/// Defines the kind of file that this apt entry is.
43#[derive(Debug, Clone, Hash, PartialEq)]
44pub enum EntryVariant {
45    Binary(BinaryEntry, Architecture),
46    Contents(Architecture, Option<String>),
47    Dep11(Dep11Entry),
48    Source(SourceEntry),
49    I18n(I18nEntry),
50}
51
52/// Dep11 entries contain appstream metadata and their required icons.
53#[derive(Debug, Clone, Hash, PartialEq)]
54pub enum Dep11Entry {
55    Components(Architecture, Option<String>),
56    Icons(ImageSize, Option<String>),
57}
58
59/// I18n entries contain translations for a given locale.
60#[derive(Debug, Clone, Hash, PartialEq)]
61pub enum I18nEntry {
62    Index,
63    Translations(String, Option<String>),
64}
65
66/// Binary entries contain the Packages lists, which dpkg and apt use for dependency resolution.
67#[derive(Debug, Clone, Hash, PartialEq)]
68pub enum BinaryEntry {
69    Packages(Option<String>),
70    Release,
71}
72
73/// Similar to binary entries, but for source packages.
74#[derive(Debug, Clone, Hash, PartialEq)]
75pub enum SourceEntry {
76    Sources(Option<String>),
77    Release,
78}
79
80// If the apt entry is not a base length, it has an extension.
81fn extension_from(input: &str, len: usize) -> Option<String> {
82    if input.len() < len + 1 {
83        None
84    } else {
85        Some(input[len + 1..].to_owned())
86    }
87}
88
89// Apt entries tend to name a variant with a possible extension (compression).
90fn type_with_extension<T: FromStr>(input: &str) -> Option<(T, Option<String>)> {
91    let (kind, ext) = match input.find('.') {
92        Some(pos) => (&input[..pos], Some(input[pos + 1..].to_owned())),
93        None => (input, None),
94    };
95
96    kind.parse::<T>().ok().map(|kind| (kind, ext))
97}
98
99fn entry_variant(original_path: &str) -> Option<EntryVariant> {
100    let mut path = original_path;
101    let mut found = false;
102    while let Some(pos) = path.find('/') {
103        found = true;
104        let base = &path[..pos];
105
106        match base {
107            _ if base.starts_with("binary-") => {
108                let binary = &path[7..];
109
110                return binary.find('/').and_then(|pos| {
111                    binary[..pos].parse::<Architecture>().ok().and_then(|arch| {
112                        let filename = &binary[pos + 1..];
113                        if filename.starts_with("Packages") {
114                            let ext = extension_from(filename, 8);
115                            Some(EntryVariant::Binary(BinaryEntry::Packages(ext), arch))
116                        } else if filename.starts_with("Release") {
117                            Some(EntryVariant::Binary(BinaryEntry::Release, arch))
118                        } else {
119                            None
120                        }
121                    })
122                });
123            }
124            "debian-installer" => {
125                return None;
126                // TODO
127            }
128            "dep11" => {
129                let path = &path[6..];
130                return if path.starts_with("icons-") {
131                    type_with_extension::<ImageSize>(&path[6..])
132                        .map(|(res, ext)| EntryVariant::Dep11(Dep11Entry::Icons(res, ext)))
133                } else if path.starts_with("Components-") {
134                    type_with_extension::<Architecture>(&path[11..])
135                        .map(|(arch, ext)| EntryVariant::Dep11(Dep11Entry::Components(arch, ext)))
136                } else {
137                    None
138                };
139            }
140            "i18n" => {
141                let path = &path[5..];
142                return if path.starts_with("Translation") {
143                    type_with_extension::<String>(&path[12..])
144                        .map(|(loc, ext)| EntryVariant::I18n(I18nEntry::Translations(loc, ext)))
145                } else if path == "Index" {
146                    Some(EntryVariant::I18n(I18nEntry::Index))
147                } else {
148                    None
149                };
150            }
151            "source" => {
152                let path = &path[7..];
153                return if path.starts_with("Sources") {
154                    let ext = extension_from(path, 7);
155                    Some(EntryVariant::Source(SourceEntry::Sources(ext)))
156                } else if path == "Release" {
157                    Some(EntryVariant::Source(SourceEntry::Release))
158                } else {
159                    None
160                };
161            }
162            _ => path = &path[pos + 1..],
163        }
164    }
165
166    if !found && original_path.starts_with("Contents-") {
167        return type_with_extension::<Architecture>(&original_path[9..])
168            .map(|(arch, ext)| EntryVariant::Contents(arch, ext));
169    }
170
171    None
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn entry_parsing() {
180        assert_eq!(
181            entry_variant("binary-amd64/Packages.xz").expect("bad entry result"),
182            EntryVariant::Binary(
183                BinaryEntry::Packages(Some("xz".into())),
184                Architecture::Amd64
185            )
186        )
187    }
188}