assorted_debian_utils/
release.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Copyright 2024 Sebastian Ramacher
// SPDX-License-Identifier: LGPL-3.0-or-later

//! # Helper to handle `Release` files

use std::collections::HashMap;
use std::fmt::Formatter;
use std::io::{BufRead, Cursor};

use chrono::{DateTime, Utc};
use serde::{de, Deserialize, Deserializer};

use crate::architectures::Architecture;
use crate::archive::{Codename, Component, Suite};
use crate::utils::{DateTimeVisitor, WhitespaceListVisitor};

/// Deserialize a datetime string into a `DateTime<Utc>`
fn deserialize_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_str(DateTimeVisitor("%a, %d %b %Y %H:%M:%S %Z"))
}

/// Deserialize a datetime string into a `Option<DateTime<Utc>>`
fn deserialize_datetime_option<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
    D: Deserializer<'de>,
{
    deserialize_datetime(deserializer).map(Some)
}

/// Deserialize a list of architectures into a `Vec<Architecture>`
fn deserialize_architectures<'de, D>(deserializer: D) -> Result<Vec<Architecture>, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_str(WhitespaceListVisitor::<Architecture>::new())
}

/// Deserialize a list of components into a `Vec<Component>`
fn deserialize_components<'de, D>(deserializer: D) -> Result<Vec<Component>, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_str(WhitespaceListVisitor::<Component>::new())
}

struct SHA256Visitor;

impl de::Visitor<'_> for SHA256Visitor {
    type Value = HashMap<String, FileInfo>;

    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
        write!(formatter, "a list of files")
    }

    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        let cursor = Cursor::new(s);
        let mut ret: HashMap<String, FileInfo> = HashMap::default();
        for line in cursor.lines() {
            let Ok(line) = line else {
                break;
            };

            let fields: Vec<_> = line.split_ascii_whitespace().collect();
            if fields.len() != 3 {
                return Err(E::invalid_value(de::Unexpected::Str(&line), &self));
            }

            let file = fields[2];
            let file_size = fields[1].parse().map_err(E::custom)?;
            let hash = hex::decode(fields[0]).map_err(E::custom)?;

            ret.insert(
                file.to_string(),
                FileInfo {
                    file_size,
                    hash: hash
                        .try_into()
                        .map_err(|_| E::invalid_value(de::Unexpected::Str(fields[0]), &self))?,
                },
            );
        }
        Ok(ret)
    }
}

/// Deserialize files listed as SHA256
fn deserialize_sha256<'de, D>(deserializer: D) -> Result<HashMap<String, FileInfo>, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_str(SHA256Visitor)
}

/// Representation of reference `Package` files in a `Release` file
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct FileInfo {
    file_size: u64,
    hash: [u8; 32],
}

/// Possible values for `Acquire-By-Hash`
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum AcquireByHash {
    /// Acquire by hash
    Yes,
    /// Do not acquire by hash
    No,
}

/// Representation of a `Release` file
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct Release {
    /// Origin of the release
    pub origin: String,
    /// Label of the release
    pub label: String,
    /// Suite of the release
    pub suite: Suite,
    /// Suite of the release
    pub codename: Codename,
    /// Version of the release
    pub version: Option<String>,
    /// Date of the release
    #[serde(deserialize_with = "deserialize_datetime")]
    pub date: DateTime<Utc>,
    #[serde(
        default,
        deserialize_with = "deserialize_datetime_option",
        rename = "Valid-Until"
    )]
    /// Validity of the release
    pub valid_until: Option<DateTime<Utc>>,
    #[serde(rename = "Acquire-By-Hash")]
    /// Whether files should be acquired by hash
    pub acquire_by_hash: Option<AcquireByHash>,
    /// Supported architectures of the release
    #[serde(deserialize_with = "deserialize_architectures")]
    pub architectures: Vec<Architecture>,
    /// Components of the release
    #[serde(deserialize_with = "deserialize_components")]
    pub components: Vec<Component>,
    /// Release description
    pub description: String,
    /// Referenced `Package` files and others from the release
    #[serde(rename = "SHA256", deserialize_with = "deserialize_sha256")]
    pub files: HashMap<String, FileInfo>,
}

impl Release {
    /// Lookup path for a specific file honoring `Acquire-By-Hash`
    pub fn lookup_url(&self, file: &str) -> Option<String> {
        let info = self.files.get(file)?;

        if self
            .acquire_by_hash
            .is_some_and(|by_hash| by_hash == AcquireByHash::Yes)
        {
            file.rsplit_once('/').map(|(component, _)| {
                format!("{}/by-hash/SHA256/{}", component, hex::encode(info.hash))
            })
        } else {
            Some(file.to_string())
        }
    }
}

/// Read release from a reader
pub fn from_reader(reader: impl BufRead) -> Result<Release, rfc822_like::de::Error> {
    rfc822_like::from_reader(reader)
}

/// Read release from a string
pub fn from_str(data: &str) -> Result<Release, rfc822_like::de::Error> {
    rfc822_like::from_str(data)
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn archive() {
        let data = r"Origin: Debian-ramacher.at
Label: Debian-ramacher.at
Suite: unstable
Codename: sid
Version: 13.0
Date: Sun, 17 Dec 2023 18:43:37 UTC
Architectures: i386 amd64
Components: main
Description: Experimental and unfinished Debian packages (for unstable)
MD5Sum:
 628a4efab35e598c7b6debdb0ac85314 26187 main/binary-i386/Packages
 6c849211e65839aac2682c461c82dbb3 7777 main/binary-i386/Packages.gz
 05ee2bfa660c3acc3559928769c29730 191 main/binary-i386/Release
 d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-i386/Packages
 7029066c27ac6f5ef18d660d5741979a 20 main/debian-installer/binary-i386/Packages.gz
 296265926c83b0d9d9d43fcc6c43496d 30187 main/binary-amd64/Packages
 8dad6d33daa175a4a54b9d328e9bb491 8821 main/binary-amd64/Packages.gz
 c0f8f3dd5202483a2b57bb348a3741a6 192 main/binary-amd64/Release
 d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-amd64/Packages
 7029066c27ac6f5ef18d660d5741979a 20 main/debian-installer/binary-amd64/Packages.gz
 4b35b2727e9c1d87c775e35fd8d00cf4 15130 main/source/Sources
 689c40d665e43a8f9a94d6e2b1dd47a4 4582 main/source/Sources.gz
 3ce12e6e384a34e6e1850bcc192edf8c 193 main/source/Release
SHA1:
 da7a5b4f20e79cab9bacca996d83419d5224a709 26187 main/binary-i386/Packages
 a0b5ae4166358c741f1c27bf457c3b31bcdb495a 7777 main/binary-i386/Packages.gz
 046a2ee510a7ea14c8b718dd153077b0359b3509 191 main/binary-i386/Release
 da39a3ee5e6b4b0d3255bfef95601890afd80709 0 main/debian-installer/binary-i386/Packages
 46c6643f07aa7f6bfe7118de926b86defc5087c4 20 main/debian-installer/binary-i386/Packages.gz
 d7fc79844dbc2702ca889a985f716374f7c8b9a5 30187 main/binary-amd64/Packages
 21374a60ce3d47b87bac11b3b3a96795020a0d41 8821 main/binary-amd64/Packages.gz
 01f970b6eae435dd8b6b1f8f61727db854212ce4 192 main/binary-amd64/Release
 da39a3ee5e6b4b0d3255bfef95601890afd80709 0 main/debian-installer/binary-amd64/Packages
 46c6643f07aa7f6bfe7118de926b86defc5087c4 20 main/debian-installer/binary-amd64/Packages.gz
 12b46a55c05518bfcfb267908185f041a1b984ae 15130 main/source/Sources
 5e2bfa609cbc328e07336f8e17707683fda37011 4582 main/source/Sources.gz
 96d0688be60481ba7eb71007b609bdf1f8323725 193 main/source/Release
SHA256:
 efe2dafdf6a50f376af1dfc574d6bd3360558fde917555671b13832c89604d9f 26187 main/binary-i386/Packages
 ba66d22607be572323b72ca152d6e635fab075d92a2265bbfe319337c35ccd13 7777 main/binary-i386/Packages.gz
 e6be53e3210056ed6854cf2a362cb953eaa962ea811cfbe34cdad2807be61101 191 main/binary-i386/Release
 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-i386/Packages
 59869db34853933b239f1e2219cf7d431da006aa919635478511fabbfc8849d2 20 main/debian-installer/binary-i386/Packages.gz
 baf930986b322ef7ff8cc04fa57762c68e7f9d8b67a0423bd5441686cbf3e751 30187 main/binary-amd64/Packages
 0ad7ab0202ece24b57051f16010c72479b97e905c659f975eac5d69284c562f3 8821 main/binary-amd64/Packages.gz
 97e06eefea86617e4abc8a647d0faebd0eaca7c87031423a4ae1d38e8f1c97bb 192 main/binary-amd64/Release
 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-amd64/Packages
 59869db34853933b239f1e2219cf7d431da006aa919635478511fabbfc8849d2 20 main/debian-installer/binary-amd64/Packages.gz
 b0a524d1ba90e253c937859e3ce30bc49a291e33dbb8124706424cf5c06100a8 15130 main/source/Sources
 2bc04b364bfc30657836faf8d1de7f6044652bcca6af6503ef404a086897267a 4582 main/source/Sources.gz
 3637559f78ac17d0e55bce465d510ef912d539e4b810a66b32431dd76f5929d8 193 main/source/Release";
        let release = from_str(data).unwrap();

        assert_eq!(
            release.architectures,
            vec![Architecture::I386, Architecture::Amd64]
        );
        assert_eq!(release.components, vec![Component::Main]);
        assert_eq!(release.suite, Suite::Unstable);
        assert_eq!(release.codename, Codename::Sid);
        assert!(release.files.contains_key("main/source/Release"));
        assert_eq!(
            release.files["main/source/Release"],
            FileInfo {
                file_size: 193,
                hash: [
                    0x36, 0x37, 0x55, 0x9f, 0x78, 0xac, 0x17, 0xd0, 0xe5, 0x5b, 0xce, 0x46, 0x5d,
                    0x51, 0x0e, 0xf9, 0x12, 0xd5, 0x39, 0xe4, 0xb8, 0x10, 0xa6, 0x6b, 0x32, 0x43,
                    0x1d, 0xd7, 0x6f, 0x59, 0x29, 0xd8
                ]
            }
        );
    }
}