apt-mirror-check 1.0.0

Check errors for apt mirror
Documentation
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
use crate::file_attr::RelativeFileAttr;
use regex::Regex;
use relative_path::RelativePathBuf;
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::{BufRead, BufReader, Lines};
use std::path::Path;

enum Section {
    Md5,
    Sha256,
    Sha512,
}

#[derive(Debug, thiserror::Error)]
pub enum ReleaseError {
    #[error("{0}")]
    Io(#[from] io::Error),

    #[error("unexpected line format: {0}")]
    Format(String),

    #[error("size error for line: {0}")]
    Size(String),
}

fn parse_release_line_stream(
    stream: impl Iterator<Item = io::Result<String>>,
) -> Result<Vec<RelativeFileAttr>, ReleaseError> {
    let algorithm_re = Regex::new(r"^(\w+):$").unwrap();
    let split_re = Regex::new(r"\s+").unwrap();

    let mut files = HashMap::new();
    let mut current_section = None;

    for line_result in stream {
        let origin_line = line_result?;
        if !origin_line.starts_with(' ') {
            current_section = if let Some(m) = algorithm_re.captures(&origin_line) {
                match m.get(1).unwrap().as_str() {
                    "MD5Sum" => Some(Section::Md5),
                    "SHA256" => Some(Section::Sha256),
                    "SHA512" => Some(Section::Sha512),
                    _ => None, // ignore unsupported algorithm and the total section
                }
            } else {
                None // ignore unused fields
            };
            
            continue;
        }
        
        let line = origin_line.trim();
        if line.is_empty() {
            continue;
        }
        
        if let Some(section) = current_section.as_ref() {
            let mut parts = split_re.splitn(line, 3);

            let hash = parts
                .next()
                .ok_or_else(|| ReleaseError::Format(line.to_string()))?;

            let size = parts
                .next()
                .ok_or_else(|| ReleaseError::Format(line.to_string()))?
                .parse()
                .map_err(|_| ReleaseError::Size(line.to_string()))?;

            let path = parts
                .next()
                .map(|s| RelativePathBuf::from(s))
                .ok_or_else(|| ReleaseError::Format(line.to_string()))?;

            let attr = files
                .entry(path.clone())
                .or_insert_with(|| RelativeFileAttr {
                    path,
                    size: Some(size),
                    md5sum: None,
                    sha256sum: None,
                    sha512sum: None,
                });

            match section {
                Section::Md5 => {
                    attr.md5sum = Some(hash.to_string());
                }
                Section::Sha256 => {
                    attr.sha256sum = Some(hash.to_string());
                }
                Section::Sha512 => {
                    attr.sha512sum = Some(hash.to_string());
                }
            }
        }
    }

    Ok(files.into_values().collect())
}

/// Parse Debian/Ubuntu release file (Release file)
///
/// This function reads and parses the Release file at the specified path,
/// extracting file attribute information contained within.
/// Release files typically contain hash values and size information of files in the repository.
///
/// # Parameters
/// * `path` - Path parameter implementing AsRef<Path> trait, pointing to the Release file to parse
///
/// # Return Value
/// * `Ok(Vec<RelativeFileAttr>)` - Parsing successful, returns a vector containing file attributes
/// * `Err(ReleaseError)` - Parsing failed, returns corresponding error information
///
/// # Errors
/// Returns ReleaseError when the file cannot be opened or the parsing format is incorrect
pub fn parse_release_file<P>(path: P) -> Result<Vec<RelativeFileAttr>, ReleaseError>
where
    P: AsRef<Path>,
{
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    parse_release_line_stream(reader.lines())
}

enum SignedMessageParserState {
    ExpectStart,
    ExpectHash,
    ExpectBlank,
    WaitEnd,
    Finished,
}

struct SignedMessageLines<R: BufRead> {
    lines: Lines<R>,
    state: SignedMessageParserState,
}

impl<R: BufRead> SignedMessageLines<R> {
    pub fn new(reader: R) -> Self {
        Self {
            lines: reader.lines(),
            state: SignedMessageParserState::ExpectStart,
        }
    }
}

impl<R: BufRead> Iterator for SignedMessageLines<R> {
    type Item = io::Result<String>;

    fn next(&mut self) -> Option<Self::Item> {
        if matches!(self.state, SignedMessageParserState::Finished) {
            return None;
        }

        while let Some(line_res) = self.lines.next() {
            match line_res {
                Ok(line) => match self.state {
                    SignedMessageParserState::ExpectStart => {
                        if line.starts_with("-----BEGIN PGP SIGNED MESSAGE-----") {
                            self.state = SignedMessageParserState::ExpectHash;
                        }
                    }
                    SignedMessageParserState::ExpectHash => {
                        if line.starts_with("Hash:") {
                            self.state = SignedMessageParserState::ExpectBlank;
                        }
                    }
                    SignedMessageParserState::ExpectBlank => {
                        if line.trim().is_empty() {
                            self.state = SignedMessageParserState::WaitEnd;
                        }
                    }
                    SignedMessageParserState::WaitEnd => {
                        return if line.starts_with("-----BEGIN PGP SIGNATURE-----") {
                            self.state = SignedMessageParserState::Finished;
                            None
                        } else {
                            Some(Ok(line))
                        };
                    }
                    SignedMessageParserState::Finished => {
                        panic!("unreachable");
                    }
                },
                Err(e) => return Some(Err(e)),
            }
        }

        None
    }
}

/// Parse Debian/Ubuntu InRelease file
///
/// This function reads and parses the InRelease file at the specified path,
/// extracting file attribute information contained within.
/// InRelease files are Release files with PGP signatures, this function skips
/// the signature part and only parses the actual content.
///
/// # Parameters
/// * `path` - Path parameter implementing AsRef<Path> trait, pointing to the InRelease file to parse
///
/// # Return Value
/// * `Ok(Vec<RelativeFileAttr>)` - Parsing successful, returns a vector containing file attributes
/// * `Err(ReleaseError)` - Parsing failed, returns corresponding error information
///
/// # Errors
/// Returns ReleaseError when the file cannot be opened or the parsing format is incorrect
pub fn parse_inrelease_file<P>(path: P) -> Result<Vec<RelativeFileAttr>, ReleaseError>
where
    P: AsRef<Path>,
{
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    parse_release_line_stream(SignedMessageLines::new(reader))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_parse_release_file() {
        let release_content = r#"Origin: Debian
Label: Debian
Suite: oldstable
Version: 12.12
MD5Sum:
 0ed6d4c8891eb86358b94bb35d9e4da4  1484322 contrib/Contents-all
 d0a0325a97c42fd5f66a8c3e29bcea64    98581 contrib/Contents-all.gz
SHA256:
 d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79  1484322 contrib/Contents-all
 c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0    98581 contrib/Contents-all.gz
"#;

        let mut test_file = NamedTempFile::new().unwrap();
        test_file.write_all(release_content.as_bytes()).unwrap();
        test_file.flush().unwrap();

        let result = parse_release_file(test_file.path()).unwrap();

        assert_eq!(result.len(), 2);

        let mut found_contents_all = false;
        let mut found_contents_all_gz = false;

        for file in &result {
            if file.path.as_str() == "contrib/Contents-all" {
                assert_eq!(file.size, Some(1484322));
                assert_eq!(
                    file.md5sum,
                    Some("0ed6d4c8891eb86358b94bb35d9e4da4".to_string())
                );
                assert_eq!(
                    file.sha256sum,
                    Some(
                        "d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79"
                            .to_string()
                    )
                );
                found_contents_all = true;
            } else if file.path.as_str() == "contrib/Contents-all.gz" {
                assert_eq!(file.size, Some(98581));
                assert_eq!(
                    file.md5sum,
                    Some("d0a0325a97c42fd5f66a8c3e29bcea64".to_string())
                );
                assert_eq!(
                    file.sha256sum,
                    Some(
                        "c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0"
                            .to_string()
                    )
                );
                found_contents_all_gz = true;
            }
        }

        assert!(found_contents_all, "Should find contrib/Contents-all");
        assert!(found_contents_all_gz, "Should find contrib/Contents-all.gz");
    }

    #[test]
    fn test_parse_inrelease_file() {
        let inrelease_content = r#"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Origin: Debian
Label: Debian
Suite: oldstable
Version: 12.12
MD5Sum:
 0ed6d4c8891eb86358b94bb35d9e4da4  1484322 contrib/Contents-all
 d0a0325a97c42fd5f66a8c3e29bcea64    98581 contrib/Contents-all.gz
SHA256:
 d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79  1484322 contrib/Contents-all
 c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0    98581 contrib/Contents-all.gz
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEETLUBkCB7R1ij9zp5btDnuCZD4TEFAmi8FQAACgkQbtDnuCZD
4TFrlw/7B94IEcTIVbakR7nZ9/ThA5HsdOo3UmUSh75owDRbs/dCs9IZOauo+qov
cg9DtMOwpab77HYFDcIWFPyA50VaHzaMc7CnzKjq1lX6KchDH6cNrvzTeTzJdVsj
rvvYcMAMQGiJgOU2MPwka/VoD2p4HINNSAsxvrnrAybS+7Uerop0GeSDpXoJ3ynr
9OZaBS1f8bavmQgfDiSpsLJZ+LulynrawM4bcgCCTrvndX+0aRtcE4qkmQ9jq2a4
IldfWVo3vUGvTWhuzXBtvl8hWCktdzj/2OXHwMSSIGcgusZgG18LzRoJeem/JUKu
PQUmJFVdbhUt3zIo0FRCG22T6Q/wKZMj0cJuvdGSkPV/jgUZGplPSF7t/aMCN8p7
zxIt79SKXPeHFI5dZQvAT9fazg3JX2R0NJOL2hH6rZjtWE3OThSPXDacsneDQ4T+
F7kVlutS0SfEdF8nnd4tboNZS/GHYFM98e9TGeWgxvvs4KMXO6hdA1dmEl8gO9x6
AxobWe00TL1vpeNZse9CxUPdGD28ZNjn4lUO4mMQL6JwMsNeK+DusQZNNUcbCRdu
tBCfF6j+YygiHITBKBj2T6H02utYD9JuOINkm4I5xXvOO0ZZyUetYlyd3+AWpFr6
7S5MsrsFJSvQ2Oo6Kt0D1mOJJqBVL85eHMVhGZJ9VvBBH/KqJpOJAjMEAQEIAB0W
IQS45fExdtKnp1IgAoB426O8R+8iZQUCaLwVAQAKCRB426O8R+8iZTNLD/0WDudI
H3jrsKTZ/CmKeidU5xLvt5V5XdSpQD9gDDRiU16+3iXlTyr21+uT6t1ln85xp6f/
a7r545PfgRvfP+ujSHfQnfybgCv1Z4irBv+Yn0wfIYhyvTS65FBr1ek5U41d+ZKF
PwRywZ9ZgdwtSXos7uN63v7UZ4UZIFaTVRIWAXYgxfz9JOS3sr/j2C64mRj/z8q/
rCS9lAleC/cl3genM2sJA1NvAnXXFsCm2Xuii+nTeLQdLkvYQDaI/miWZ56HfbD8
/T8HPjbRdV8kzCrhT5RZ9Y3EE8SdbeoaFi92Oe/0j6ERP9Hct33mXbJfMBfl2DZI
b9apdTiF5NWkCtIta8fAI5JUn2iwoha65q1wHGDCVruyFy3DLbABtpYGLzBr0oaM
5x+483i+QbZ8mFSS/9Z+pRFHjZJGnyZ2gUaIAf/FLZbugeNDBTTBY76l07/z7RKH
QQXXK6EVEV9VZw4rdzEQ9HMRIWQYQMsii4cZc+AJwLXLTnhEkag4OnsSJbnOsa5R
lvtAfkExNdOJAMhbElgYUfrD6jDHZn1e1J0omq2SYV7mrCDbwrfJaEywTI4AbB7V
cpQKe/FJuY3rjHEhvwxN5tW3x+z3jJ4BkV+xBLAZgEU/CQP7b2cLNwkIEr2b8Evt
osLESOMHayZxXFgj+h329+QHP9bSxv6s9FXBDoh1BAEWCAAdFiEETWT+wRnCApBn
1ueR+NJYW4eD1IEFAmi8FYQACgkQ+NJYW4eD1IEcCwEAxNzCORTxjfZvSd7n7SVH
hNmCeDqJgGNMgkwYcwjwqpgBAPvdB2qYLunIpWcGu6VEQFOkC3A9iHsp+WFAXkMP
4NYJ
-----END PGP SIGNATURE-----
"#;

        let mut test_file = NamedTempFile::new().unwrap();
        test_file.write_all(inrelease_content.as_bytes()).unwrap();
        test_file.flush().unwrap();

        let result = parse_inrelease_file(test_file.path()).unwrap();

        assert_eq!(result.len(), 2);

        let mut found_contents_all = false;
        let mut found_contents_all_gz = false;

        for file in &result {
            if file.path.as_str() == "contrib/Contents-all" {
                assert_eq!(file.size, Some(1484322));
                assert_eq!(
                    file.md5sum,
                    Some("0ed6d4c8891eb86358b94bb35d9e4da4".to_string())
                );
                assert_eq!(
                    file.sha256sum,
                    Some(
                        "d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79"
                            .to_string()
                    )
                );
                found_contents_all = true;
            } else if file.path.as_str() == "contrib/Contents-all.gz" {
                assert_eq!(file.size, Some(98581));
                assert_eq!(
                    file.md5sum,
                    Some("d0a0325a97c42fd5f66a8c3e29bcea64".to_string())
                );
                assert_eq!(
                    file.sha256sum,
                    Some(
                        "c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0"
                            .to_string()
                    )
                );
                found_contents_all_gz = true;
            }
        }

        assert!(found_contents_all, "Should find contrib/Contents-all");
        assert!(found_contents_all_gz, "Should find contrib/Contents-all.gz");
    }

    #[test]
    fn test_parse_inrelease_file_no_signature() {
        let inrelease_content = r#"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Origin: Debian
Label: Debian
Suite: oldstable
Version: 12.12
MD5Sum:
 0ed6d4c8891eb86358b94bb35d9e4da4  1484322 contrib/Contents-all
 d0a0325a97c42fd5f66a8c3e29bcea64    98581 contrib/Contents-all.gz
SHA256:
 d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79  1484322 contrib/Contents-all
 c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0    98581 contrib/Contents-all.gz
"#;

        let mut test_file = NamedTempFile::new().unwrap();
        test_file.write_all(inrelease_content.as_bytes()).unwrap();
        test_file.flush().unwrap();

        let result = parse_inrelease_file(test_file.path()).unwrap();

        assert_eq!(result.len(), 2);
    }

    #[test]
    fn test_parse_inrelease_file_empty() {
        let inrelease_content = "";

        let mut test_file = NamedTempFile::new().unwrap();
        test_file.write_all(inrelease_content.as_bytes()).unwrap();
        test_file.flush().unwrap();

        let result = parse_inrelease_file(test_file.path()).unwrap();

        assert_eq!(result.len(), 0);
    }
}