assorted_debian_utils/
release.rs

1// Copyright 2024 Sebastian Ramacher
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! # Helper to handle `Release` files
5
6use std::{
7    collections::HashMap,
8    fmt::Formatter,
9    io::{BufRead, Cursor},
10};
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Deserializer, de};
14
15use crate::{
16    architectures::Architecture,
17    archive::{Codename, Component, Suite},
18    utils::{DateTimeVisitor, WhitespaceListVisitor},
19};
20
21/// Deserialize a datetime string into a `DateTime<Utc>`
22fn deserialize_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
23where
24    D: Deserializer<'de>,
25{
26    deserializer.deserialize_str(DateTimeVisitor("%a, %d %b %Y %H:%M:%S %Z"))
27}
28
29/// Deserialize a datetime string into a `Option<DateTime<Utc>>`
30fn deserialize_datetime_option<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
31where
32    D: Deserializer<'de>,
33{
34    deserialize_datetime(deserializer).map(Some)
35}
36
37/// Deserialize a list of architectures into a `Vec<Architecture>`
38fn deserialize_architectures<'de, D>(deserializer: D) -> Result<Vec<Architecture>, D::Error>
39where
40    D: Deserializer<'de>,
41{
42    deserializer.deserialize_str(WhitespaceListVisitor::<Architecture>::new())
43}
44
45/// Deserialize a list of components into a `Vec<Component>`
46fn deserialize_components<'de, D>(deserializer: D) -> Result<Vec<Component>, D::Error>
47where
48    D: Deserializer<'de>,
49{
50    deserializer.deserialize_str(WhitespaceListVisitor::<Component>::new())
51}
52
53struct SHA256Visitor;
54
55impl de::Visitor<'_> for SHA256Visitor {
56    type Value = HashMap<String, FileInfo>;
57
58    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
59        write!(formatter, "a list of files")
60    }
61
62    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
63    where
64        E: de::Error,
65    {
66        let cursor = Cursor::new(s);
67        let mut ret: HashMap<String, FileInfo> = HashMap::default();
68        for line in cursor.lines() {
69            let Ok(line) = line else {
70                break;
71            };
72
73            let fields: Vec<_> = line.split_ascii_whitespace().collect();
74            if fields.len() != 3 {
75                return Err(E::invalid_value(de::Unexpected::Str(&line), &self));
76            }
77
78            let file = fields[2];
79            let file_size = fields[1].parse().map_err(E::custom)?;
80            let hash = hex::decode(fields[0]).map_err(E::custom)?;
81
82            ret.insert(
83                file.to_string(),
84                FileInfo {
85                    file_size,
86                    hash: hash
87                        .try_into()
88                        .map_err(|_| E::invalid_value(de::Unexpected::Str(fields[0]), &self))?,
89                },
90            );
91        }
92        Ok(ret)
93    }
94}
95
96/// Deserialize files listed as SHA256
97fn deserialize_sha256<'de, D>(deserializer: D) -> Result<HashMap<String, FileInfo>, D::Error>
98where
99    D: Deserializer<'de>,
100{
101    deserializer.deserialize_str(SHA256Visitor)
102}
103
104/// Representation of reference `Package` files in a `Release` file
105#[derive(Debug, Deserialize, PartialEq, Eq)]
106pub struct FileInfo {
107    file_size: u64,
108    hash: [u8; 32],
109}
110
111/// Possible values for `Acquire-By-Hash`
112#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
113#[serde(rename_all = "lowercase")]
114pub enum AcquireByHash {
115    /// Acquire by hash
116    Yes,
117    /// Do not acquire by hash
118    No,
119}
120
121/// Representation of a `Release` file
122#[derive(Debug, Deserialize, PartialEq, Eq)]
123#[serde(rename_all = "PascalCase")]
124pub struct Release {
125    /// Origin of the release
126    pub origin: String,
127    /// Label of the release
128    pub label: String,
129    /// Suite of the release
130    pub suite: Suite,
131    /// Suite of the release
132    pub codename: Codename,
133    /// Version of the release
134    pub version: Option<String>,
135    /// Date of the release
136    #[serde(deserialize_with = "deserialize_datetime")]
137    pub date: DateTime<Utc>,
138    #[serde(
139        default,
140        deserialize_with = "deserialize_datetime_option",
141        rename = "Valid-Until"
142    )]
143    /// Validity of the release
144    pub valid_until: Option<DateTime<Utc>>,
145    #[serde(rename = "Acquire-By-Hash")]
146    /// Whether files should be acquired by hash
147    pub acquire_by_hash: Option<AcquireByHash>,
148    /// Supported architectures of the release
149    #[serde(deserialize_with = "deserialize_architectures")]
150    pub architectures: Vec<Architecture>,
151    /// Components of the release
152    #[serde(deserialize_with = "deserialize_components")]
153    pub components: Vec<Component>,
154    /// Release description
155    pub description: String,
156    /// Referenced `Package` files and others from the release
157    #[serde(rename = "SHA256", deserialize_with = "deserialize_sha256")]
158    pub files: HashMap<String, FileInfo>,
159}
160
161impl Release {
162    /// Lookup path for a specific file honoring `Acquire-By-Hash`
163    pub fn lookup_url(&self, file: &str) -> Option<String> {
164        let info = self.files.get(file)?;
165
166        if self
167            .acquire_by_hash
168            .is_some_and(|by_hash| by_hash == AcquireByHash::Yes)
169        {
170            file.rsplit_once('/').map(|(component, _)| {
171                format!("{}/by-hash/SHA256/{}", component, hex::encode(info.hash))
172            })
173        } else {
174            Some(file.to_string())
175        }
176    }
177}
178
179/// Read release from a reader
180pub fn from_reader(reader: impl BufRead) -> Result<Release, rfc822_like::de::Error> {
181    rfc822_like::from_reader(reader)
182}
183
184/// Read release from a string
185pub fn from_str(data: &str) -> Result<Release, rfc822_like::de::Error> {
186    rfc822_like::from_str(data)
187}
188
189#[cfg(test)]
190mod test {
191    use super::*;
192
193    #[test]
194    fn archive() {
195        let data = r"Origin: Debian-ramacher.at
196Label: Debian-ramacher.at
197Suite: unstable
198Codename: sid
199Version: 13.0
200Date: Sun, 17 Dec 2023 18:43:37 UTC
201Architectures: i386 amd64
202Components: main
203Description: Experimental and unfinished Debian packages (for unstable)
204MD5Sum:
205 628a4efab35e598c7b6debdb0ac85314 26187 main/binary-i386/Packages
206 6c849211e65839aac2682c461c82dbb3 7777 main/binary-i386/Packages.gz
207 05ee2bfa660c3acc3559928769c29730 191 main/binary-i386/Release
208 d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-i386/Packages
209 7029066c27ac6f5ef18d660d5741979a 20 main/debian-installer/binary-i386/Packages.gz
210 296265926c83b0d9d9d43fcc6c43496d 30187 main/binary-amd64/Packages
211 8dad6d33daa175a4a54b9d328e9bb491 8821 main/binary-amd64/Packages.gz
212 c0f8f3dd5202483a2b57bb348a3741a6 192 main/binary-amd64/Release
213 d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-amd64/Packages
214 7029066c27ac6f5ef18d660d5741979a 20 main/debian-installer/binary-amd64/Packages.gz
215 4b35b2727e9c1d87c775e35fd8d00cf4 15130 main/source/Sources
216 689c40d665e43a8f9a94d6e2b1dd47a4 4582 main/source/Sources.gz
217 3ce12e6e384a34e6e1850bcc192edf8c 193 main/source/Release
218SHA1:
219 da7a5b4f20e79cab9bacca996d83419d5224a709 26187 main/binary-i386/Packages
220 a0b5ae4166358c741f1c27bf457c3b31bcdb495a 7777 main/binary-i386/Packages.gz
221 046a2ee510a7ea14c8b718dd153077b0359b3509 191 main/binary-i386/Release
222 da39a3ee5e6b4b0d3255bfef95601890afd80709 0 main/debian-installer/binary-i386/Packages
223 46c6643f07aa7f6bfe7118de926b86defc5087c4 20 main/debian-installer/binary-i386/Packages.gz
224 d7fc79844dbc2702ca889a985f716374f7c8b9a5 30187 main/binary-amd64/Packages
225 21374a60ce3d47b87bac11b3b3a96795020a0d41 8821 main/binary-amd64/Packages.gz
226 01f970b6eae435dd8b6b1f8f61727db854212ce4 192 main/binary-amd64/Release
227 da39a3ee5e6b4b0d3255bfef95601890afd80709 0 main/debian-installer/binary-amd64/Packages
228 46c6643f07aa7f6bfe7118de926b86defc5087c4 20 main/debian-installer/binary-amd64/Packages.gz
229 12b46a55c05518bfcfb267908185f041a1b984ae 15130 main/source/Sources
230 5e2bfa609cbc328e07336f8e17707683fda37011 4582 main/source/Sources.gz
231 96d0688be60481ba7eb71007b609bdf1f8323725 193 main/source/Release
232SHA256:
233 efe2dafdf6a50f376af1dfc574d6bd3360558fde917555671b13832c89604d9f 26187 main/binary-i386/Packages
234 ba66d22607be572323b72ca152d6e635fab075d92a2265bbfe319337c35ccd13 7777 main/binary-i386/Packages.gz
235 e6be53e3210056ed6854cf2a362cb953eaa962ea811cfbe34cdad2807be61101 191 main/binary-i386/Release
236 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-i386/Packages
237 59869db34853933b239f1e2219cf7d431da006aa919635478511fabbfc8849d2 20 main/debian-installer/binary-i386/Packages.gz
238 baf930986b322ef7ff8cc04fa57762c68e7f9d8b67a0423bd5441686cbf3e751 30187 main/binary-amd64/Packages
239 0ad7ab0202ece24b57051f16010c72479b97e905c659f975eac5d69284c562f3 8821 main/binary-amd64/Packages.gz
240 97e06eefea86617e4abc8a647d0faebd0eaca7c87031423a4ae1d38e8f1c97bb 192 main/binary-amd64/Release
241 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-amd64/Packages
242 59869db34853933b239f1e2219cf7d431da006aa919635478511fabbfc8849d2 20 main/debian-installer/binary-amd64/Packages.gz
243 b0a524d1ba90e253c937859e3ce30bc49a291e33dbb8124706424cf5c06100a8 15130 main/source/Sources
244 2bc04b364bfc30657836faf8d1de7f6044652bcca6af6503ef404a086897267a 4582 main/source/Sources.gz
245 3637559f78ac17d0e55bce465d510ef912d539e4b810a66b32431dd76f5929d8 193 main/source/Release";
246        let release = from_str(data).unwrap();
247
248        assert_eq!(
249            release.architectures,
250            vec![Architecture::I386, Architecture::Amd64]
251        );
252        assert_eq!(release.components, vec![Component::Main]);
253        assert_eq!(release.suite, Suite::Unstable);
254        assert_eq!(release.codename, Codename::Sid);
255        assert!(release.files.contains_key("main/source/Release"));
256        assert_eq!(
257            release.files["main/source/Release"],
258            FileInfo {
259                file_size: 193,
260                hash: [
261                    0x36, 0x37, 0x55, 0x9f, 0x78, 0xac, 0x17, 0xd0, 0xe5, 0x5b, 0xce, 0x46, 0x5d,
262                    0x51, 0x0e, 0xf9, 0x12, 0xd5, 0x39, 0xe4, 0xb8, 0x10, 0xa6, 0x6b, 0x32, 0x43,
263                    0x1d, 0xd7, 0x6f, 0x59, 0x29, 0xd8
264                ]
265            }
266        );
267    }
268}