ambient-ci 0.14.0

A continuous integration engine
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
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
//! Actions related to Debian packages.

#![allow(clippy::result_large_err)]

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

use serde::{Deserialize, Serialize};
use url::Url;

use crate::{
    action::{ActionError, Context},
    action_impl::{spawn, ActionImpl},
    qemu,
    util::{http_get_to_file, mkdir, UtilError},
};

const DEFAULT_SUITE: &str = "stable";
const SUBDIR: &str = "debian";

/// Download `deb` packages and their dependencies.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebGet {
    packages: Vec<String>,
    suite: Option<String>,
    cert: Option<String>,
}

impl DebGet {
    /// Create a new `DebGet` action.
    pub fn new(suite: &Option<String>, cert: &Option<String>, packages: &[String]) -> Self {
        Self {
            packages: packages.to_vec(),
            suite: suite.clone(),
            cert: cert.clone(),
        }
    }

    fn suite(&self) -> &str {
        self.suite.as_deref().unwrap_or(DEFAULT_SUITE)
    }

    fn packages_needed(
        &self,
        url: &str,
        suite: &str,
        arch: repo::Arch,
        deps_dir: &Path,
    ) -> Result<Vec<repo::Needed>, ActionError> {
        let debian = deps_dir.join(SUBDIR);
        mkdir(&debian).map_err(|err| DebError::Mkdir2(debian, err))?;

        let cached_in_release_file = deps_dir.join(SUBDIR).join("InRelease");
        let url = Url::parse(url).map_err(|err| DebError::UrlParse(url.to_string(), err))?;
        let mut certs = repo::Certs::empty();
        if let Some(cert) = &self.cert {
            certs.push(cert);
        } else {
            certs.push_for_suite(suite).map_err(DebError::Apt)?;
        }
        let debian_suite = repo::DebianSuite::new(&cached_in_release_file, url, suite, &certs)
            .map_err(DebError::Apt)?;

        let cached_packages_gz_file = deps_dir.join(SUBDIR).join("Packages.gz");
        let packages_file = debian_suite
            .packages_file(&cached_packages_gz_file, suite, arch)
            .map_err(DebError::Apt)?;
        let packages: HashMap<String, repo::Package> =
            HashMap::from_iter(packages_file.split("\n\n").filter(|s| !s.is_empty()).map(
                |stanza| match repo::Package::new(stanza) {
                    Ok(p) => (p.package.clone(), p),
                    Err(err) => panic!("can't parse package: {err}\nProblem stanza: {stanza:?}"),
                },
            ));

        Ok(debian_suite.needed(&self.packages, &packages))
    }

    fn download_needed(&self, deps_dir: &Path, needed: &[repo::Needed]) -> Result<(), ActionError> {
        for n in needed {
            let url =
                Url::parse(n.url()).map_err(|err| DebError::UrlParse(n.url().to_string(), err))?;
            let filename = deps_dir
                .join("debian")
                .join(format!("{}.deb", n.package_name()));
            http_get_to_file(url.as_str(), &filename)
                .map_err(|err| DebError::HttpGet(url, filename.clone(), err))?;
        }
        Ok(())
    }
}

impl ActionImpl for DebGet {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        use repo::*;

        const URL: &str = "http://deb.debian.org/debian";
        const ARCH: Arch = Arch::Amd64;

        let needed = self.packages_needed(URL, self.suite(), ARCH, context.deps_dir())?;
        self.download_needed(context.deps_dir(), &needed)?;
        context
            .runlog()
            .deb_get(crate::runlog::RunLogSource::PrePlan, &needed);
        Ok(())
    }
}

/// Install downloaded `deb` packages.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebInstall {}

impl ActionImpl for DebInstall {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        let shell = "apt-get install -y /ci/deps/debian/*.deb";
        spawn(context, &["/bin/bash", "-c", shell])?;
        Ok(())
    }
}

/// Build a `deb` package.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deb {
    packages: Option<PathBuf>,
}

impl Deb {
    /// Create a new `Deb` action.
    pub fn new<P: AsRef<Path>>(packages: P) -> Self {
        Self {
            packages: Some(packages.as_ref().to_path_buf()),
        }
    }
}

impl ActionImpl for Deb {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        let packages = Path::new(qemu::ARTIFACTS_DIR)
            .join(self.packages.clone().unwrap_or(PathBuf::from(".")));
        if !packages.exists() {
            std::fs::create_dir(&packages).map_err(|err| DebError::mkdir(&packages, err))?;
        }
        let shell = format!(
            r#"#!/usr/bin/env bash
set -xeuo pipefail

echo "PATH at start: $PATH"
export PATH="/root/.cargo/bin:$PATH"
export CARGO_HOME=/ci/deps
export DEBEMAIL=liw@liw.fi
export DEBFULLNAME="Lars Wirzenius"
/bin/env

command -v cargo
command -v rustc

cargo --version
rustc --version

# Get name and version of source package.
name="$(dpkg-parsechangelog -SSource)"
version="$(dpkg-parsechangelog -SVersion)"

# Get upstream version: everything before the last dash.
uv="$(echo "$version" | sed 's/-[^-]*$//')"

# Files that will be created.
arch="$(dpkg --print-architecture)"
orig="../${{name}}_${{uv}}.orig.tar.xz"
deb="../${{name}}_${{version}}_${{arch}}.deb"
changes="../${{name}}_${{version}}_${{arch}}.changes"

# Create "upstream tarball".
git archive HEAD | xz >"$orig"

# Build package.
dpkg-buildpackage -us -uc

# Dump some information to make it easier to visually verify
# everything looks OK. Also, test the package with the lintian tool.

ls -l ..
for x in ../*.deb; do dpkg -c "$x"; done
# FIXME: disabled while this prevents radicle-native-ci deb from being built.
# lintian -i --allow-root --fail-on warning ../*.changes

# Move files to artifacts directory.
mv ../*_* {}
        "#,
            packages.display()
        );

        spawn(context, &["/bin/bash", "-c", &shell])?;

        Ok(())
    }
}

/// Errors from the `deb` action.
#[derive(Debug, thiserror::Error)]
pub enum DebError {
    /// Failed to create the artifacts directory for `deb` packages.
    #[error("could not create artifacts directory {0}")]
    Mkdir(PathBuf, #[source] std::io::Error),

    /// Failed to create the artifacts directory for `deb` packages.
    #[error("could not create artifacts directory {0}")]
    Mkdir2(PathBuf, #[source] crate::util::UtilError),

    /// APT repository error.
    #[error(transparent)]
    Apt(#[from] repo::AptRepoError),

    /// Parse URL.
    #[error("failed to parse URL {0:?}")]
    UrlParse(String, #[source] url::ParseError),

    /// Download file
    #[error("failed to download {0} to {1}")]
    HttpGet(Url, PathBuf, #[source] UtilError),
}

impl DebError {
    fn mkdir<P: Into<PathBuf>>(dirname: P, err: std::io::Error) -> Self {
        Self::Mkdir(dirname.into(), err)
    }
}

impl From<DebError> for ActionError {
    fn from(value: DebError) -> Self {
        Self::Deb(value)
    }
}

/// Represent a Debian package archive ("APT repository").
pub mod repo {
    use std::{
        collections::{HashMap, HashSet},
        io::Read,
        path::{Path, PathBuf},
        process::Command,
    };

    use rfc822_like::Deserializer;
    use serde::{Deserialize, Serialize};

    use clingwrap::runner::{CommandError, CommandRunner};
    use flate2::read::GzDecoder;
    use reqwest::StatusCode;
    use url::Url;

    use crate::util::{http_get_to_file, UtilError};

    const DEBIAN_TRIXIE_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEZ+Gq1RYJKwYBBAHaRw8BAQdARlh1OX84KPJRedAP/M7WxPFEthWypAp8nved
FhqaX0q0R0RlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEzL3RyaXhpZSkgPGRl
Ymlhbi1yZWxlYXNlQGxpc3RzLmRlYmlhbi5vcmc+iJYEExYIAD4WIQRBWH99uMd0
vM8TFBZ2L2egssOd5AUCZ+Gq1QIbAwUJDwmcAAULCQgHAgYVCgkICwIEFgIDAQIe
AQIXgAAKCRB2L2egssOd5DjVAP49S/e9VAtn9ip2DXj5zx87MpUXnLHAhT/LsJ7J
odnKoQEA8murEqdcC0pPsAA6Gmev/lz0MWUyfe5Y6DMB6t16FQuIdQQTFgoAHRYh
BMphnWWnKnut/JbSgBlkGKrrdMihBQJn4atmAAoJEBlkGKrrdMihxnAA/3i7WOUU
Dyea6tABVGfFKfHj6yAbfcKVr5GL0WSOQiXNAQC2YTKoTfCY/WOZnCwEHebpM3Mr
6+A8lenj8pFzm8mFAokCMgQQAQoAHRYhBHIDYw4sjnJyUWhP68XOXcLFQs1ZBQJn
4v59AAoJEMXOXcLFQs1ZfdkP90fbXOydWNb1iXwu/vaUjxSx5Nk0Zwkjj7Pi74PZ
Ifd9c0Luf/j7dEHuJOzkOKvkrpTYeQN8Ms9ITVTMeNSOoQn8tnYsxhHqHUcI9ym/
vJ6liIsE2K95gwH2mOQ24ot59pMyQX7sXE4DuQqlrEW1JRemKs7WJB2E/4I7smqG
3+/mqXAoHKkpFBMm1v9tNVx1Tp3ER6C4VFszONlmX0NLB5If+wSits1LnsVgBK1D
Hd3Nxh78YQL0W2xBCOfsHryaKkLW6tp1efTpMHfe4lDEgTFvEWaO3NlPEeD67uxA
KpEG1b/CzBqx+Og1RT+0iZkW9oXYoKJI0Vx0HlYeMrhuE+Q0Fqy/nZYcWhq3y6Tu
GC9J+hyB7D3Z7ne48zNGaAU4x7+6pvkmlBWnNwPH/IlGbexa/F8o+rU0AWrvYCVR
cbbcSsgkq1Dl/Xjbvlx9gwkKnhacbQ5VUOA2Zkp6oBn/pFD1W8xE0X4dNHcM3J5k
CAeZuszjPIZXvwuvAjaOE7ZxKvGDlQTWK8zR5h9tvcwMfVND+EqK0qfDGDMtJqu/
W0lSJj3G/FMO3aGn0Ks6b7cmjbCGjmn7WBGJT2PvH4A4ml1qopWy7Ont6zt1+Cll
y/0Uf75AamJSrAAYFFIJlcpWH/7NRW2L+n1FoIOv5xj5h675uHh/WFlPTNQqrRaP
p1o=
=k1cs
-----END PGP PUBLIC KEY BLOCK-----
"#;

    const DEBIAN_BOOKWORM_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: B8B8 0B5B 623E AB6A D877  5C45 B7C5 D7D6 3509 47F8
Comment: Debian Archive Automatic Signing Key (12/bookworm) <ftp

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
NlB2T6bKGcYGqL7L
=AKf0
-----END PGP PUBLIC KEY BLOCK-----
"#;

    pub(super) struct Certs {
        certs: Vec<String>,
    }

    impl Certs {
        pub fn empty() -> Self {
            Self { certs: vec![] }
        }

        pub fn push(&mut self, cert: impl Into<String>) {
            self.certs.push(cert.into());
        }

        pub fn push_for_suite(&mut self, suite: &str) -> Result<(), AptRepoError> {
            let cert = match suite {
                "trixie" => DEBIAN_TRIXIE_CERT,
                "bookworm" => DEBIAN_BOOKWORM_CERT,
                _ => return Err(AptRepoError::UnknownRelease(suite.into())),
            };
            self.push(cert);
            Ok(())
        }
    }

    impl Default for Certs {
        fn default() -> Self {
            Self {
                certs: vec![DEBIAN_TRIXIE_CERT.to_string()],
            }
        }
    }

    pub(super) struct DebianSuite {
        base_url: Url,
        in_release: InRelease,
    }

    impl DebianSuite {
        pub fn new(
            cached_in_release: &Path,
            base_url: Url,
            suite: &str,
            allowed_certs: &Certs,
        ) -> Result<Self, AptRepoError> {
            let in_release_url = Url::parse(&format!("{}/dists/{suite}/InRelease", base_url))?;

            let signed = http_get_to_file(in_release_url.as_str(), cached_in_release)?;
            let in_release = inline_verify(&signed, allowed_certs)?;

            Ok(Self {
                base_url,
                in_release: InRelease::new(&in_release)?,
            })
        }

        #[allow(clippy::unwrap_used)]
        pub fn packages_file(
            &self,
            cached_file: &Path,
            suite: &str,
            arch: Arch,
        ) -> Result<String, AptRepoError> {
            for line in self
                .in_release
                .sha256
                .lines()
                .filter(|line| !line.is_empty())
            {
                let mut words = line.split_ascii_whitespace();
                let checksum = words.next().unwrap();
                let _length = words.next().unwrap();
                let filename = words.next().unwrap();
                let wanted = format!("main/binary-{arch}/Packages.gz");
                if filename == wanted {
                    let url = format!("{}/dists/{suite}/{filename}", self.base_url);
                    let data = http_get_to_file(&url, cached_file)?;
                    let actual = sha256::digest(&data);

                    if actual == checksum {
                        let mut gz = GzDecoder::new(&*data);
                        let mut packages = vec![];
                        gz.read_to_end(&mut packages)
                            .map_err(|source| AptRepoError::Inflate { source })?;
                        let data = String::from_utf8_lossy(&packages).to_string();
                        return Ok(data);
                    } else {
                        panic!("bad checksum");
                    }
                }
            }
            Err(AptRepoError::NoPackagesGz)
        }

        pub fn needed(
            &self,
            package_names: &[String],
            packages: &HashMap<String, Package>,
        ) -> Vec<Needed> {
            let mut wanted = HashMap::new();
            let mut todo: HashSet<String> =
                HashSet::from_iter(package_names.iter().map(|s| s.to_string()));
            while !todo.is_empty() {
                let mut new = vec![];
                for name in todo.drain() {
                    if !wanted.contains_key(&name) {
                        if let Some(p) = packages.get(&name) {
                            wanted.insert(name.to_string(), p.clone());
                            for need in p.needs() {
                                new.push(need);
                            }
                        }
                    }
                }
                for name in new {
                    todo.insert(name);
                }
            }

            wanted
                .values()
                .map(|p| {
                    let url = format!("{}/{}", self.base_url, p.filename);
                    Needed::new(p.package.clone(), url)
                })
                .collect()
        }
    }

    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "PascalCase")]
    struct InRelease {
        #[serde(rename = "SHA256")]
        sha256: String,
    }

    impl InRelease {
        fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
            Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
        }
    }

    #[derive(Debug, Clone, Deserialize)]
    #[serde(rename_all = "PascalCase")]
    pub(super) struct Package {
        pub package: String,
        pre_depends: Option<String>,
        depends: Option<String>,
        filename: String,
    }

    impl Package {
        pub fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
            Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
        }

        pub fn needs(&self) -> Vec<String> {
            let mut needs = vec![];
            if let Some(x) = &self.pre_depends {
                self.parse_needs(&mut needs, x);
            }
            if let Some(x) = &self.depends {
                self.parse_needs(&mut needs, x);
            }
            needs
        }

        #[allow(clippy::unwrap_used)]
        fn parse_needs(&self, needs: &mut Vec<String>, x: &str) {
            for item in x.split(',') {
                let item = item.split_ascii_whitespace().next().unwrap();
                needs.push(item.to_string());
            }
        }
    }

    /// A deb dependency that needs to be downloaded.
    #[derive(Debug, Clone, Deserialize, Serialize)]
    pub struct Needed {
        package_name: String,
        url: String,
    }

    impl Needed {
        fn new(package_name: impl Into<String>, url: impl Into<String>) -> Self {
            Self {
                package_name: package_name.into(),
                url: url.into(),
            }
        }

        /// Name of package.
        pub fn package_name(&self) -> &str {
            &self.package_name
        }

        /// URL from where to download package.
        pub fn url(&self) -> &str {
            &self.url
        }
    }

    #[derive(Debug, Copy, Clone)]
    pub(super) enum Arch {
        Amd64,
    }

    impl std::fmt::Display for Arch {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let arch = match self {
                Self::Amd64 => "amd64",
            };
            write!(f, "{arch}")
        }
    }

    fn inline_verify(data: &[u8], allowed_certs: &Certs) -> Result<String, AptRepoError> {
        let mut cmd = Command::new("rsop");
        cmd.arg("inline-verify");

        let tmp = tempfile::tempdir().map_err(|source| AptRepoError::TempDir { source })?;
        for (i, cert) in allowed_certs.certs.iter().enumerate() {
            let filename = tmp.path().join(format!("cert-{i}"));
            std::fs::write(&filename, cert.as_bytes())
                .map_err(|source| AptRepoError::CertFile { source })?;
            cmd.arg(filename);
        }

        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(data);
        runner.capture_stdout();
        let output = runner
            .execute()
            .map_err(|source| AptRepoError::InlineVerify { source })?;
        String::from_utf8(output.stdout).map_err(|source| AptRepoError::InReleaseUtf8 { source })
    }

    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum AptRepoError {
        #[error("failed to fetch InRelease file for {suite} from {url}")]
        FetchInRelease {
            suite: String,
            url: Url,
            source: reqwest::Error,
        },

        #[error("failed to retrieve InRelease text from HTTP response")]
        InReleaseText { source: reqwest::Error },

        #[error(transparent)]
        Join(#[from] url::ParseError),

        #[error("failed to verify InRelease inline signature")]
        InlineVerify { source: CommandError },

        #[error("InRelease file is not UTF8")]
        InReleaseUtf8 { source: std::string::FromUtf8Error },

        #[error("failed to create temporary directory for verifying InRelease signature")]
        TempDir { source: std::io::Error },

        #[error("failed to write temparary file for verifying InRelease signature")]
        CertFile { source: std::io::Error },

        #[error("InRelease file doesn't have a single stanza")]
        NoStanzas,

        #[error("InRelease does not have a SHA256 field")]
        NoSha256Field,

        #[error("InRelease SHA256 field line lacks {0}")]
        Sha256Field(&'static str),

        #[error("failed to download Packages.gz file")]
        FetchPackagesGz { source: reqwest::Error },

        #[error("failed to get Packages.gz file from HTTP request")]
        PackagesGz { source: reqwest::Error },

        #[error("failed to decompress Packages.gz file")]
        Inflate { source: std::io::Error },

        #[error("Packages file is not UTF8")]
        PackagesUtf8 { source: std::string::FromUtf8Error },

        #[error("Packages.gz has the wrong checksum")]
        PackagesChecksum,

        #[error("failed to find desired Packages.gz in InRelease file")]
        NoPackagesGz,

        #[error(transparent)]
        HttpGet(#[from] crate::action_impl::HttpGetError),

        #[error("failed to format timestamp")]
        TimeFormat { source: time::error::Format },

        #[error("failed to create HTTP client")]
        ClientBuild(#[source] reqwest::Error),

        #[error("failed to build a reqwest client")]
        Client(#[source] reqwest::Error),

        #[error("failed to build a reqwest request")]
        BuildRequest(#[source] reqwest::Error),

        #[error("failed to GET URL {0:?}")]
        Get(String, reqwest::Error),

        #[error("failed to get body of response from {0:?}")]
        GetBody(String, reqwest::Error),

        #[error("failure getting file with HTTP GET: status code {0}")]
        UnwantedStatus(StatusCode),

        #[error("failed to write file {filename}")]
        Write {
            filename: PathBuf,
            source: std::io::Error,
        },

        #[error("failed to read file {filename}")]
        Read {
            filename: PathBuf,
            source: std::io::Error,
        },

        #[error("entry for {package} in Packages file lacks Filename field")]
        NoFilename { package: String },

        #[error(transparent)]
        Util(#[from] UtilError),

        #[error("failed to parse URL {0:?}")]
        UrlParse(String, #[source] url::ParseError),

        #[error(transparent)]
        Rfc822Like(#[from] rfc822_like::de::Error),

        #[error("do not know archive signing key for release {0}")]
        UnknownRelease(String),
    }
}