Skip to main content

ferroday_cage/provision/gentoo/
mod.rs

1//! The Gentoo userland provisioner: bootstrap a rootfs from a signed stage3,
2//! and install prebuilt packages into it.
3//!
4//! A run is two waves, and a run that asks for no package is the first alone,
5//! byte for byte:
6//!
7//! 1. Resolve a stage3 variant to the build the archive currently publishes,
8//!    verify the signed documents that vouch for it, and extract the tarball.
9//! 2. Read the extracted root's own package database, resolve the caller's
10//!    atoms against it and the binary-package index, and fetch, verify and
11//!    merge what is missing.
12//!
13//! # Resolution starts from a populated root
14//!
15//! That is the shape neither of the other two userlands has, and it decides the
16//! resolver's inputs rather than being a detail of them. Debian and Alpine
17//! bootstrap from nothing, so their resolvers start with an empty root and the
18//! archive is the only source of truth. A stage3 arrives with 296 packages
19//! already installed and a database describing them, so resolution here asks
20//! what is *missing* — and it is measurably load-bearing, because one of those
21//! 296 has no binary package in the archive at all.
22//!
23//! # What binds the archive together
24//!
25//! One key, vouching for cleartext-signed documents that vouch for the bytes:
26//!
27//! ```text
28//! vendored service keyring
29//!   -> latest-stage3.txt      (cleartext-signed)  the variants, their sizes
30//!   -> <tarball>.DIGESTS      (cleartext-signed)  the tarball's SHA-512
31//!   -> <tarball>              (bytes)             verified, then extracted
32//!
33//! vendored service keyring
34//!   -> <package>/Manifest     (cleartext-signed)  each member's SHA-512
35//!   -> the members            (bytes)             verified, then read
36//! ```
37//!
38//! The subkey that signs a package's `Manifest` is the one that signs the
39//! pointer and the digest documents, so the binary-package half adds no trust
40//! anchor: the crate's OpenPGP verification is called on one more document.
41//!
42//! That is the same shape Debian's release chain has, and it is verified with
43//! the same machinery: the crate's OpenPGP verification establishes each
44//! document's authenticity, and the digest it carries is what the fetched bytes
45//! are held to. A `.sha256` sidecar sits beside every tarball and is never read
46//! — nothing signs it, and a chain resting on an unauthenticated file is not a
47//! chain.
48//!
49//! What no signature covers is the binary-package index, in any form, and the
50//! two consequences are answered in different places. That a mirror can decide
51//! *which* package a resolution asks for is closed after verification, by
52//! comparing the metadata inside a container against the record the resolution
53//! chose -- a `Manifest` carries no field naming the package, so a genuine,
54//! correctly signed *other* package would otherwise pass every check. That
55//! nothing vouches for the index's own freshness is not closed at all, and the
56//! rollback the stage3's signed timestamp shuts is open here — which is a fact
57//! about the archive rather than about this layer.
58
59mod atom;
60mod digests;
61mod gpkg;
62mod index;
63mod installed;
64mod plan;
65mod pointer;
66mod resolve;
67mod vdb;
68mod version;
69
70use std::fmt;
71use std::io;
72use std::io::Read as _;
73use std::path::{Path, PathBuf};
74use std::time::Duration;
75
76pub use index::Catalogue;
77pub use installed::{Installed, InstalledPackage, installed};
78pub use plan::{Plan, PlannedPackage};
79
80use pointer::Pointer;
81
82use super::compress;
83use super::coordinate::{self, Nesting};
84use super::digest::{self, Algorithm};
85use super::layer::BuildLayer;
86use super::openpgp::{Keyring, OpenPgpError};
87use super::rooted::Rooted;
88use super::{
89    Delegate, Failover, Fetch, FetchError, FetchJob, FetchRequest, HttpFetch, LimitedWriter,
90    PackageCache, ProvisionError, ProvisionEvent, ProvisionRequest, Provisioner, Tarball,
91    mirror_url, walk_mirrors,
92};
93use crate::IdentityMap;
94use crate::failure::path_io_error;
95
96/// The archive the layer reads unless the caller names another.
97const DEFAULT_MIRROR: &str = "http://distfiles.gentoo.org";
98
99/// How old a pointer may be before the layer refuses to act on it, unless the
100/// caller says otherwise.
101///
102/// About four regeneration cycles. The document is rebuilt for every
103/// architecture on a weekly schedule whether or not new builds landed in it, so
104/// a live archive never approaches this; what it bounds is a mirror replaying a
105/// correctly signed document long after it stopped being current.
106const DEFAULT_MAX_POINTER_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
107
108/// The ceiling on a pointer document, which declares no size of its own.
109const MAX_POINTER_BYTES: u64 = 1024 * 1024;
110
111/// The ceiling on a digest document, which declares no size of its own.
112const MAX_DIGESTS_BYTES: u64 = 64 * 1024;
113
114/// The ceiling on a binary-package index, which declares no size of its own.
115///
116/// Sized against today's 19.1 MB with room for the archive to grow. It applies
117/// to the plain and the compressed form alike -- for the compressed one, to the
118/// decompressed stream, since that is what a decompression bomb inflates.
119const MAX_INDEX_BYTES: u64 = 256 * 1024 * 1024;
120
121/// The ceiling on a binary package's metadata archive, which the container
122/// declares no length for until its Manifest has been read.
123///
124/// The largest a real one runs to is a few tens of kilobytes; this is far above
125/// that and far below a bound that means nothing. It applies to the compressed
126/// member and again to what it inflates to, since a decompression bomb spends
127/// its bytes in the second.
128const MAX_METADATA_BYTES: u64 = 16 * 1024 * 1024;
129
130/// The most paths one binary package may ship.
131///
132/// Every one becomes a line of the database entry's `CONTENTS`, held in memory
133/// until the entry is written. The largest package Gentoo publishes ships tens
134/// of thousands; this is an order above that, and it bounds what a container
135/// whose length passed the index's `SIZE` can still make the merge spend.
136const MAX_CONTENTS_ENTRIES: usize = 1_000_000;
137
138/// The suffix of the cache binary packages are downloaded into when the caller
139/// named no directory of their own.
140const PACKAGE_CACHE: &str = ".fcage-binpkgs";
141
142/// Where portage keeps its database inside a root.
143///
144/// Here rather than in either half that names it, because the pair is what must
145/// agree: [`vdb`] writes an entry under this path and [`installed`] reads one
146/// back, and a spelling that moved in one alone would register packages where
147/// the layer will not find them — resolution would report a populated root as
148/// empty and install every dependency it already had.
149pub(super) const DB_DIR: &str = "var/db/pkg";
150
151/// The ceiling on a stage3 tarball where nothing declares its size.
152///
153/// A fetch is normally bounded by the exact size the signed enumeration
154/// recorded. One path has no such number: a build id pin, which skips the
155/// enumeration -- and, with it, a [`Stage3`] carried over from a pinned
156/// resolution, which is that same answer again. The largest stage3 published
157/// today is 796 MB, so this is far above anything real and far below a bound
158/// that means nothing.
159///
160/// It is this layer's own ceiling rather than the effective one. A transport
161/// applies whatever bound it has of its own, and the bundled client's is half
162/// this, so a default build stops a pinned download at 2 GiB. Both are far above
163/// any published stage3; where they differ, the smaller wins, as a ceiling
164/// should.
165const MAX_STAGE3_BYTES: u64 = 4 * 1024 * 1024 * 1024;
166
167/// The ceiling on one binary package, over and above the size the index
168/// published for it.
169///
170/// The index's own `SIZE` bounds the download, and for an understated one that
171/// is enough: the fetch stops short and fails closed. An *overstated* one only
172/// relaxes the bound, because a transport caps at the smaller of the declared
173/// size and its own ceiling — so an index declaring `SIZE:
174/// 18446744073709551615` for each of a hundred packages buys whatever the
175/// transport's ceiling is, a hundred times over, before anything is verified.
176///
177/// Nothing signs the index, ever, which puts the binary-package path
178/// permanently where a build-id pin puts the stage3 path: holding a number no
179/// signature stands behind. [`MAX_STAGE3_BYTES`] is that path's answer and this
180/// is this one's.
181///
182/// The value is this layer's judgement rather than a measurement of the
183/// archive. What it has to be is smaller than the transport's own ceiling —
184/// the bundled client's is 2 GiB, and a bound above it would never bind — and
185/// larger than any package the archive publishes. A stage3 is a whole base
186/// system at 796 MB, and this is above that, so a single package reaching it
187/// would be a bound to raise rather than a package to refuse.
188const MAX_PACKAGE_BYTES: u64 = 1024 * 1024 * 1024;
189
190/// The suffix of the cache a run downloads into when the caller named no
191/// directory of their own.
192const STAGE3_CACHE: &str = ".fcage-stage3";
193
194/// The Gentoo service keyring shipped with the crate, mirroring what Gentoo
195/// publishes at `qa-reports.gentoo.org/output/service-keys.gpg`.
196///
197/// Vendored rather than fetched, and that is a security property rather than a
198/// saved round trip: a trust anchor fetched over the same channel as the
199/// artifact it authenticates authenticates nothing, because whoever can serve a
200/// forged stage3 can serve the key that signs it.
201///
202/// Mirrored mechanically, certificates this layer never meets included. An
203/// unusable key in the anchor weakens nothing — nothing rests on a key until it
204/// produces a signature that passes the gates — while a hand-picked subset would
205/// be a judgement a later reader has to reconstruct instead of a rule they can
206/// check.
207const EMBEDDED_KEYRING: &[u8] = include_bytes!("keyring/service-keys.gpg");
208
209/// A failure provisioning a Gentoo rootfs.
210#[derive(Debug)]
211#[non_exhaustive]
212pub enum GentooError {
213    /// The configuration cannot be provisioned from as it stands.
214    #[non_exhaustive]
215    Config {
216        /// What is wrong with it, and what to do instead.
217        reason: String,
218    },
219    /// A signed document was not accepted: it did not verify against the
220    /// keyring, or the keyring itself could not be read.
221    #[non_exhaustive]
222    Signature {
223        /// What was being verified: the URL a document was served from, or the
224        /// keyring.
225        subject: String,
226        /// Why it was not accepted.
227        reason: String,
228    },
229    /// A signed document verified but does not say what the layer needs.
230    ///
231    /// The signature held; what it covers is not a document this crate can act
232    /// on — a pointer naming no addressable stage3, or a digest document
233    /// recording no usable SHA-512 for the tarball beside it.
234    #[non_exhaustive]
235    Document {
236        /// The URL the document was served from, mirror included, so a walk
237        /// over several of them names the one that answered.
238        subject: String,
239        /// What was wrong with it.
240        reason: String,
241    },
242    /// The pointer verified but is too old to act on.
243    ///
244    /// Gentoo publishes no `Valid-Until` and no signature expiration, so the
245    /// regeneration timestamp inside the signed body is the only bound on how
246    /// stale a correctly signed document may be. Without it, a mirror could
247    /// replay a months-old pointer indefinitely and pass every other check.
248    /// Clear the bound with
249    /// [`max_pointer_age`](GentooBuilder::max_pointer_age) to provision from a
250    /// deliberately archived mirror.
251    #[non_exhaustive]
252    Stale {
253        /// The epoch the pointer states it was regenerated at, or `None` where
254        /// it states none — which is refused rather than read as permission,
255        /// since deleting a line would otherwise disable the check.
256        published: Option<i64>,
257        /// How long ago that was, where it said.
258        age: Option<Duration>,
259        /// The bound it exceeded.
260        limit: Duration,
261    },
262    /// The binary-package index was fetched but does not say what the layer
263    /// needs.
264    ///
265    /// Nothing signs the index, so this is not a signature failure and never
266    /// can be: no `Packages.gpg`, `.asc`, `.sig`, `Manifest` or `.DIGESTS` is
267    /// published beside it in any form. What it reports is a document that
268    /// arrived and could not be read as one — an empty index, or one whose
269    /// stanzas this layer could not act on.
270    #[non_exhaustive]
271    Index {
272        /// The URL the index was served from, mirror included.
273        subject: String,
274        /// What was wrong with it.
275        reason: String,
276    },
277    /// A plan document cannot be read, or cannot be written.
278    ///
279    /// Reading: the document is not one this library reads, or it states
280    /// something an index would never have published — a name that is not a
281    /// package, a version this format does not define, a path that would not
282    /// stay inside the archive. Writing: the plan holds a value the format
283    /// cannot carry back unchanged, which is refused rather than written as
284    /// something that reads back differently.
285    #[non_exhaustive]
286    PlanDocument {
287        /// What was wrong with it.
288        reason: String,
289    },
290    /// The closure the binhost offers is not one that can be installed.
291    ///
292    /// An atom nothing published answers, or a blocker that matches something
293    /// the root has or the resolution chose. Both sides of every refusal are
294    /// named: which atom, who asked for it, and what stopped it.
295    #[non_exhaustive]
296    Resolve {
297        /// What could not be resolved, and what the binhost offers instead.
298        reason: String,
299    },
300    /// A binary package's container is not one this layer can read.
301    ///
302    /// Not a signature failure and not a digest one: those have variants of
303    /// their own. This is a container whose *shape* is wrong — no format
304    /// marker, a member its Manifest does not list or one it lists that is
305    /// absent, no image or metadata archive — or one whose metadata names a
306    /// different package than the index did.
307    ///
308    /// That last is the one worth naming. A Manifest carries no field saying
309    /// which package it is, and GLEP 78 forbids trusting the container's
310    /// directory prefix for it, so a hostile mirror answering a request for one
311    /// package with a genuine, correctly signed *other* package passes every
312    /// signature check there is. The identity check inside the container is
313    /// what closes that, and this is what it reports.
314    #[non_exhaustive]
315    Container {
316        /// The container concerned, in the cache.
317        subject: String,
318        /// What was wrong with it.
319        reason: String,
320    },
321    /// A root's own package database holds something that is not a package
322    /// entry.
323    ///
324    /// The database is read to learn what a root already has, so an entry that
325    /// cannot be read is a gap in that answer rather than a detail to pass
326    /// over: resolving against a set that is missing a package asks the archive
327    /// for something the root already satisfies, or -- where the archive does
328    /// not publish it -- refuses an install that would have worked.
329    #[non_exhaustive]
330    Database {
331        /// The root whose database was being read.
332        root: PathBuf,
333        /// What was wrong with it.
334        reason: String,
335    },
336    /// The tarball does not have the digest the signed document recorded for
337    /// it.
338    ///
339    /// The chain held as far as the document; what a mirror served is not what
340    /// the document vouched for. The bytes are not extracted, and the cached
341    /// file is removed so a later run downloads afresh rather than meeting the
342    /// same refusal from its own cache.
343    #[non_exhaustive]
344    Digest {
345        /// The tarball the digest was taken over.
346        subject: String,
347        /// The SHA-512 the signed document recorded, as it spells it.
348        expected: String,
349        /// The SHA-512 the bytes actually have.
350        actual: String,
351    },
352    /// The run's observer asked the bootstrap to stop.
353    ///
354    /// Surfaces to a caller as [`super::ProvisionError::Cancelled`].
355    Cancelled,
356    /// Fetching from the archive failed.
357    Fetch(FetchError),
358    /// A host I/O operation failed.
359    #[non_exhaustive]
360    Io {
361        /// What the operation was doing.
362        op: &'static str,
363        /// The path concerned.
364        path: PathBuf,
365        /// The underlying error.
366        source: io::Error,
367    },
368}
369
370impl GentooError {
371    /// A configuration that cannot be provisioned from, naming what to do
372    /// instead.
373    pub(crate) fn config(reason: impl Into<String>) -> GentooError {
374        GentooError::Config {
375            reason: reason.into(),
376        }
377    }
378
379    /// A refused signature, naming what was being verified and why it was not
380    /// accepted.
381    ///
382    /// Arguments run subject, then cause, which is the order every constructor
383    /// in the crate takes them in.
384    pub(crate) fn signature(subject: impl Into<String>, err: OpenPgpError) -> GentooError {
385        GentooError::Signature {
386            subject: subject.into(),
387            reason: err.to_string(),
388        }
389    }
390
391    /// A verified document that does not say what the layer needs, naming what
392    /// was being read and what was wrong.
393    pub(crate) fn document(subject: impl Into<String>, reason: impl Into<String>) -> GentooError {
394        GentooError::Document {
395            subject: subject.into(),
396            reason: reason.into(),
397        }
398    }
399
400    /// A package database holding something that is not an entry, naming the
401    /// root and what was wrong.
402    pub(crate) fn database(root: impl Into<PathBuf>, reason: impl Into<String>) -> GentooError {
403        GentooError::Database {
404            root: root.into(),
405            reason: reason.into(),
406        }
407    }
408
409    /// An index that arrived and could not be read, naming where it came from
410    /// and what was wrong.
411    pub(crate) fn index(subject: impl Into<String>, reason: impl Into<String>) -> GentooError {
412        GentooError::Index {
413            subject: subject.into(),
414            reason: reason.into(),
415        }
416    }
417}
418
419path_io_error!(GentooError);
420
421impl Failover for GentooError {
422    /// Reaches through the wrapper this layer carries a transport failure in.
423    /// A failure that is not a `Fetch` one at all — a signature or digest
424    /// mismatch over bytes that did arrive — never advances a walk.
425    fn is_failover(&self) -> bool {
426        matches!(self, GentooError::Fetch(fetch) if fetch.is_failover())
427    }
428}
429
430impl From<FetchError> for GentooError {
431    fn from(err: FetchError) -> GentooError {
432        GentooError::Fetch(err)
433    }
434}
435
436impl fmt::Display for GentooError {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        match self {
439            GentooError::Config { reason } => {
440                write!(f, "the Gentoo provisioner is misconfigured: {reason}")
441            }
442            GentooError::Signature { subject, reason } => {
443                write!(f, "{subject} was not accepted: {reason}")
444            }
445            GentooError::Document { subject, reason } => {
446                write!(f, "{subject} verified but {reason}")
447            }
448            GentooError::Index { subject, reason } => {
449                write!(f, "the index at {subject} {reason}")
450            }
451            GentooError::PlanDocument { reason } => {
452                write!(f, "the plan document cannot be used: {reason}")
453            }
454            GentooError::Resolve { reason } => {
455                write!(f, "the binhost cannot satisfy what was asked for: {reason}")
456            }
457            GentooError::Container { subject, reason } => {
458                write!(f, "the package at {subject} {reason}")
459            }
460            GentooError::Database { root, reason } => write!(
461                f,
462                "the package database of the root at {} cannot be read: {reason}",
463                root.display(),
464            ),
465            GentooError::Stale {
466                published,
467                age,
468                limit,
469            } => match (published, age) {
470                (Some(published), Some(age)) => write!(
471                    f,
472                    "the pointer was regenerated at {published}, which is {} days ago, past the \
473                     {} days this provisioner accepts",
474                    age.as_secs() / 86_400,
475                    limit.as_secs() / 86_400,
476                ),
477                _ => write!(
478                    f,
479                    "the pointer states no regeneration timestamp, which is the only bound on how \
480                     old it may be; clear the bound with max_pointer_age to accept one anyway",
481                ),
482            },
483            GentooError::Digest {
484                subject,
485                expected,
486                actual,
487            } => write!(
488                f,
489                "{subject} does not have the digest the archive recorded for it: the signed \
490                 document states {expected}, the bytes are {actual}",
491            ),
492            GentooError::Cancelled => f.write_str("the bootstrap was cancelled"),
493            GentooError::Fetch(err) => write!(f, "{err}"),
494            GentooError::Io { op, path, source } => {
495                write!(
496                    f,
497                    "provisioning failed while {op} {}: {source}",
498                    path.display()
499                )
500            }
501        }
502    }
503}
504
505impl std::error::Error for GentooError {
506    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
507        match self {
508            GentooError::Fetch(err) => Some(err),
509            GentooError::Io { source, .. } => Some(source),
510            _ => None,
511        }
512    }
513}
514
515impl From<GentooError> for super::ProvisionError {
516    /// A cancellation is the run's own outcome rather than a layer failure, so
517    /// it surfaces as the shared variant a caller matches on whichever
518    /// provisioner they drove; everything else is this layer's.
519    fn from(err: GentooError) -> super::ProvisionError {
520        match err {
521            GentooError::Cancelled => super::ProvisionError::Cancelled,
522            other => super::ProvisionError::other(other),
523        }
524    }
525}
526
527/// What one certificate of the keyring can still be trusted to do, and until
528/// when.
529///
530/// The vendored keyring is a trust anchor with a shelf life, and Gentoo's
531/// certificates do not share one date: two of the eleven lapse well before the
532/// rest. Reporting per certificate rather than as a single earliest expiry is
533/// what keeps a keyring whose signing key is good for years from reading as
534/// nearly expired because a certificate it never meets is. Reducing the list to
535/// one number is the caller's judgement, and it needs [`signs`](Self::signs) to
536/// make it well.
537#[derive(Debug, Clone, PartialEq, Eq)]
538#[non_exhaustive]
539pub struct CertificateHorizon {
540    /// The primary key's fingerprint, uppercase hex and undelimited, as the
541    /// OpenPGP tools present one.
542    pub fingerprint: String,
543    /// The certificate's primary user id, as it spells itself, or `None` where
544    /// it carries none that is UTF-8.
545    pub user_id: Option<String>,
546    /// The epoch at which the certificate's authority lapses, or `None` when
547    /// nothing bounds it.
548    ///
549    /// Where the certificate can sign, this is the last moment at which a
550    /// signature from it would still be accepted — which is not simply the
551    /// primary's own expiry, since a subkey's binding may lapse first and a
552    /// certificate whose primary may not sign is worth only what its subkeys
553    /// are. Where it cannot sign, this is the primary's expiry, which is when
554    /// the certificate lapses whether or not that ever mattered.
555    pub expires: Option<i64>,
556    /// Whether any key under the certificate could make a signature this crate
557    /// would accept.
558    ///
559    /// `false` for a certificate that is in the keyring but can vouch for
560    /// nothing: its primary rests on an algorithm this crate refuses and it
561    /// delegates to no signing subkey that does. Gentoo's keyring holds one such
562    /// certificate, a DSA-1024 release key from 2004. It is harmless — nothing
563    /// rests on a key until it produces a signature that passes the gates — but
564    /// its expiry says nothing about how long the keyring remains usable, so a
565    /// caller reducing the list to one number reads this first.
566    pub signs: bool,
567}
568
569/// The stage3 a resolution chose: which build, and the digest its bytes are
570/// held to.
571///
572/// This layer's answer to the other userlands' plan, and deliberately a record
573/// rather than a document format. A single tarball does not need a versioned
574/// serialization, and one would outlive what it describes: the archive keeps
575/// about five weeks of builds and publishes no snapshot service, so a document
576/// naming a build id would routinely name a build that is gone. A caller who
577/// wants to provision the same root again records the
578/// [`build_id`](Self::build_id) and [`variant`](Self::variant) however they
579/// already record configuration, and
580/// [`GentooBuilder::build_id`] takes them back; within one process,
581/// [`GentooBuilder::stage3`] carries the whole answer over.
582#[derive(Debug, Clone, PartialEq, Eq)]
583pub struct Stage3 {
584    build_id: String,
585    variant: String,
586    path: String,
587    size: u64,
588    sha512: String,
589    certificate: String,
590}
591
592impl Stage3 {
593    /// The build directory the tarball sits in, such as `20260810T204554Z`.
594    pub fn build_id(&self) -> &str {
595        &self.build_id
596    }
597
598    /// The variant, as the pointer document spells it.
599    pub fn variant(&self) -> &str {
600        &self.variant
601    }
602
603    /// The tarball's path under the architecture's `autobuilds/` tree.
604    pub fn path(&self) -> &str {
605        &self.path
606    }
607
608    /// The most bytes the tarball may weigh, and what the download is bounded
609    /// by.
610    ///
611    /// A mirror that answers with more than this is not serving the resource
612    /// that was asked for, and the digest that would catch the substitution runs
613    /// only after the bytes have been spent.
614    ///
615    /// Where the resolution read the signed enumeration, this is the exact
616    /// length it recorded. Where it acted on a
617    /// [`build_id`](GentooBuilder::build_id) pin it is a fixed ceiling instead,
618    /// several times the largest stage3 Gentoo publishes: a pin names the build
619    /// directly, and nothing Gentoo signs states the length of a tarball inside
620    /// one. So this is a bound in both cases and a size in one, and a caller
621    /// showing a download's size or preallocating for it wants the resolution
622    /// they got it from to be an unpinned one.
623    pub fn size(&self) -> u64 {
624        self.size
625    }
626
627    /// The SHA-512 the signed digest document records for the tarball,
628    /// lowercase hex.
629    pub fn sha512(&self) -> &str {
630        &self.sha512
631    }
632
633    /// The tarball's file name, which is what the digest document names it by
634    /// and what the cache holds it under.
635    fn file_name(&self) -> &str {
636        file_name_of(&self.path)
637    }
638
639    /// The fingerprint of the certificate that vouched for the digest document,
640    /// uppercase hex.
641    ///
642    /// The stable identity of the key the chain rests on: it names a keyring
643    /// entry, it is what a policy pinning the archive would pin, and it does not
644    /// move when the certificate rotates its signing subkey.
645    pub fn certificate(&self) -> &str {
646        &self.certificate
647    }
648}
649
650/// The file name at the end of an archive-relative path.
651///
652/// The tarball is named by its file name in two places that have to agree: the
653/// digest document records it that way, and the cache holds it under it.
654fn file_name_of(path: &str) -> &str {
655    path.rsplit('/').next().unwrap_or(path)
656}
657
658/// The stage3 variants an architecture publishes.
659///
660/// Produced by [`Gentoo::available`], and read out of the signed pointer
661/// document, so this is what Gentoo publishes now rather than what this crate
662/// was compiled knowing. It is a snapshot: the archive regenerates the document
663/// on a schedule, so a build id reported here is the one that was current when
664/// it was read.
665pub struct Available {
666    published: Option<i64>,
667    entries: Vec<pointer::Entry>,
668}
669
670impl fmt::Debug for Available {
671    /// Renders how many variants were read and when the document was
672    /// regenerated, rather than the whole enumeration: an architecture can
673    /// publish forty of them, and the counts are what a reader of a rendering
674    /// can act on.
675    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
676        f.debug_struct("Available")
677            .field("variants", &self.entries.len())
678            .field("published", &self.published)
679            .finish()
680    }
681}
682
683impl Available {
684    /// Every variant the architecture publishes, as the document spells them,
685    /// in the order it lists them.
686    pub fn variants(&self) -> impl Iterator<Item = &str> {
687        self.entries.iter().map(|entry| entry.variant.as_str())
688    }
689
690    /// Whether `variant` is one of them, matched exactly.
691    pub fn contains(&self, variant: &str) -> bool {
692        self.entries.iter().any(|entry| entry.variant == variant)
693    }
694
695    /// The build `variant` currently points at, such as `20260810T204554Z`.
696    ///
697    /// Not one build id for the architecture: a sub-architecture is built on its
698    /// own cadence, so one document routinely points different variants at
699    /// different builds.
700    pub fn build_id(&self, variant: &str) -> Option<&str> {
701        self.entry(variant).map(|entry| entry.build_id.as_str())
702    }
703
704    /// The size in bytes of the tarball `variant` currently points at.
705    pub fn size(&self, variant: &str) -> Option<u64> {
706        self.entry(variant).map(|entry| entry.size)
707    }
708
709    /// The epoch the pointer states it was regenerated at, or `None` where it
710    /// states none.
711    ///
712    /// Gentoo's only published measure of how current the enumeration is. It
713    /// measures the document rather than the builds it names, being rebuilt on a
714    /// schedule whether or not anything new landed.
715    pub fn published(&self) -> Option<i64> {
716        self.published
717    }
718
719    /// The entry for `variant`.
720    fn entry(&self, variant: &str) -> Option<&pointer::Entry> {
721        self.entries.iter().find(|entry| entry.variant == variant)
722    }
723}
724
725/// A Gentoo stage3 bootstrap, configured then run.
726///
727/// The architecture is the tree the archive publishes under — `amd64`, `arm64`,
728/// `x86` — and names a directory in every URL the layer fetches.
729///
730/// # Example
731///
732/// ```
733/// use ferroday_cage::provision::gentoo::Gentoo;
734///
735/// # fn main() -> Result<(), ferroday_cage::provision::gentoo::GentooError> {
736/// let gentoo = Gentoo::builder("amd64").build()?;
737/// // Every certificate the crate would verify a document against, with the
738/// // date its authority lapses.
739/// assert!(gentoo.keyring_horizon().iter().any(|held| held.signs));
740/// # Ok(())
741/// # }
742/// ```
743pub struct Gentoo {
744    /// The architecture every document is read for, and a directory in each
745    /// URL the layer fetches.
746    architecture: String,
747    /// The variant to resolve, where the caller named one.
748    variant: Option<String>,
749    /// The binhost sub-architecture to install binary packages from, where the
750    /// caller named one.
751    binhost: Option<String>,
752    /// The atoms to install from the binhost, parsed at build time so a
753    /// misspelled one is refused before anything is fetched.
754    install: Vec<atom::Atom>,
755    /// The USE flags the caller would rather a build carried, in the order they
756    /// were named.
757    prefer_use: Vec<resolve::Preference>,
758    /// A resolution to install instead of performing one, where the caller
759    /// supplied a plan.
760    plan: Option<Plan>,
761    /// The root a layered build stages its increment over, where the caller
762    /// named one.
763    base_layer: Option<PathBuf>,
764    /// The build directory to resolve within instead of the one the pointer
765    /// currently names, where the caller pinned one.
766    build_id: Option<String>,
767    /// A resolution to act on instead of performing one, where the caller
768    /// carried one over from an earlier call.
769    stage3: Option<Stage3>,
770    /// How old a pointer may be, or `None` where the caller cleared the bound.
771    max_pointer_age: Option<Duration>,
772    /// The archives to read, the primary first.
773    mirrors: Vec<String>,
774    /// Where the tarball is kept across runs, where the caller named a place.
775    cache_dir: Option<PathBuf>,
776    /// The certificates every signed document is verified against.
777    keyring: Keyring,
778    /// The transport every fetch goes through.
779    fetcher: Box<dyn Fetch>,
780}
781
782impl fmt::Debug for Gentoo {
783    /// Renders the configuration. The keyring is not rendered: it is a fixed
784    /// trust anchor, and what a reader can act on about it is
785    /// [`keyring_horizon`](Gentoo::keyring_horizon) rather than its bytes. The
786    /// transport is named by neither its type nor its state, a `Fetch` being a
787    /// caller's value this crate knows nothing renderable about.
788    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
789        f.debug_struct("Gentoo")
790            .field("architecture", &self.architecture)
791            .field("variant", &self.variant)
792            .field("binhost", &self.binhost)
793            .field("install", &self.install)
794            .field("prefer_use", &self.prefer_use)
795            .field("plan", &self.plan)
796            .field("base_layer", &self.base_layer)
797            .field("build_id", &self.build_id)
798            .field("stage3", &self.stage3)
799            .field("max_pointer_age", &self.max_pointer_age)
800            .field("mirrors", &self.mirrors)
801            .field("cache_dir", &self.cache_dir)
802            .field("fetcher", &Delegate("dyn Fetch"))
803            .finish_non_exhaustive()
804    }
805}
806
807impl Gentoo {
808    /// Returns a builder for a bootstrap of `architecture` — the tree the
809    /// archive publishes under, such as `amd64`, `arm64` or `x86`.
810    pub fn builder(architecture: impl Into<String>) -> GentooBuilder {
811        GentooBuilder {
812            architecture: architecture.into(),
813            variant: None,
814            binhost: None,
815            install: Vec::new(),
816            prefer_use: Vec::new(),
817            plan: None,
818            base_layer: None,
819            build_id: None,
820            stage3: None,
821            max_pointer_age: Some(DEFAULT_MAX_POINTER_AGE),
822            mirror: None,
823            fallbacks: Vec::new(),
824            cache_dir: None,
825            fetcher: None,
826        }
827    }
828
829    /// The architecture the layer reads the archive for.
830    pub fn architecture(&self) -> &str {
831        &self.architecture
832    }
833
834    /// What each certificate of the keyring can still be trusted to do, and
835    /// until when, in the order the keyring holds them.
836    ///
837    /// Gentoo publishes no deadline on the keyring itself, and its certificates
838    /// expire on several different dates, so this is how a caller learns the
839    /// anchor is ageing before an archive fetch tells them. The judgement is the
840    /// verification's own rather than a second reading of the same certificates,
841    /// so what is reported here is what a signature would actually be held to.
842    ///
843    /// Reducing the list to one number is the caller's: the earliest expiry
844    /// among the certificates that [can sign](CertificateHorizon::signs) is the
845    /// one worth acting on.
846    pub fn keyring_horizon(&self) -> Vec<CertificateHorizon> {
847        self.keyring
848            .horizon()
849            .into_iter()
850            .map(|held| CertificateHorizon {
851                fingerprint: held.fingerprint,
852                user_id: held.user_id,
853                expires: held.expires,
854                signs: held.signs,
855            })
856            .collect()
857    }
858
859    /// The stage3 variants the architecture publishes, from the signed
860    /// enumeration.
861    ///
862    /// The read half of a resolution: it fetches the pointer document, verifies
863    /// it against the keyring, and reports what it names. Nothing is downloaded.
864    ///
865    /// # Errors
866    ///
867    /// Returns [`GentooError::Fetch`] when no mirror serves the pointer,
868    /// [`GentooError::Signature`] when what one served does not verify,
869    /// [`GentooError::Stale`] when the pointer is older than
870    /// [`max_pointer_age`](GentooBuilder::max_pointer_age), and
871    /// [`GentooError::Document`] when it names no stage3 this crate can address.
872    ///
873    /// # Example
874    ///
875    /// ```no_run
876    /// use ferroday_cage::provision::gentoo::Gentoo;
877    ///
878    /// # fn main() -> Result<(), ferroday_cage::provision::gentoo::GentooError> {
879    /// let mut gentoo = Gentoo::builder("amd64").build()?;
880    /// let available = gentoo.available()?;
881    /// assert!(available.contains("amd64-openrc"));
882    /// # Ok(())
883    /// # }
884    /// ```
885    pub fn available(&mut self) -> Result<Available, GentooError> {
886        self.observe(&mut Silent).available()
887    }
888
889    /// What the configured binhost publishes.
890    ///
891    /// The read half of an install: it fetches the binary-package index,
892    /// parses it, and reports the names and versions it names. Nothing is
893    /// downloaded, so this serves an architecture the host could never run.
894    ///
895    /// Nothing signs the index, and no amount of asking would change that —
896    /// Gentoo publishes no signature over it in any form. Every package it
897    /// names carries a cleartext-signed `Manifest` of its own, which is what an
898    /// install verifies; the index decides which package is asked for, and a
899    /// hostile mirror can therefore decide that. What the identity check inside
900    /// each container closes is the substitution that would otherwise follow.
901    ///
902    /// # Errors
903    ///
904    /// Returns [`GentooError::Config`] when no binhost sub-architecture was
905    /// named, [`GentooError::Fetch`] when no mirror serves the index, and
906    /// [`GentooError::Index`] when what one served could not be read as one.
907    ///
908    /// # Example
909    ///
910    /// ```no_run
911    /// use ferroday_cage::provision::gentoo::Gentoo;
912    ///
913    /// # fn main() -> Result<(), ferroday_cage::provision::gentoo::GentooError> {
914    /// let mut gentoo = Gentoo::builder("amd64").binhost("x86-64").build()?;
915    /// let catalogue = gentoo.packages()?;
916    /// println!("{} packages in {} builds", catalogue.len(), catalogue.builds());
917    /// # Ok(())
918    /// # }
919    /// ```
920    pub fn packages(&mut self) -> Result<Catalogue, GentooError> {
921        self.observe(&mut Silent).packages()
922    }
923
924    /// What installing the configured atoms into the root at `root` would
925    /// install.
926    ///
927    /// The read half of an install wave: it fetches the index, reads what
928    /// `root` already has, resolves the two against each other and reports the
929    /// [`Plan`]. Nothing is downloaded and nothing is merged.
930    ///
931    /// # Why this takes a root
932    ///
933    /// Because on this archive the answer has no meaning without one, and that
934    /// is the shape neither of the other userlands has. A stage3 arrives with
935    /// 296 packages already installed, and one of them — `app-crypt/pinentry` —
936    /// the binhost does not publish in any version. It is in `dev-vcs/git`'s
937    /// runtime closure, so resolving git against an empty root does not merely
938    /// produce a larger plan: it fails, on an atom the real root answers.
939    ///
940    /// So `root` is the root the packages would be installed into: the one a
941    /// bootstrap has already published, or the base of a layered build. A
942    /// resolution against a root that does not exist yet is a resolution
943    /// against nothing, which this archive does not support and which a
944    /// signature that defaulted the root would quietly produce.
945    ///
946    /// # Errors
947    ///
948    /// Returns [`GentooError::Config`] when no binhost or no atom was named,
949    /// [`GentooError::Fetch`] or [`GentooError::Index`] for an index that
950    /// cannot be read, [`GentooError::Database`] for a root whose own database
951    /// cannot be, and [`GentooError::Resolve`] for a request the binhost cannot
952    /// satisfy — naming every atom it could not answer and every conflict it
953    /// met, rather than only the first.
954    ///
955    /// # Example
956    ///
957    /// ```no_run
958    /// use ferroday_cage::provision::gentoo::Gentoo;
959    ///
960    /// # fn main() -> Result<(), ferroday_cage::provision::gentoo::GentooError> {
961    /// let mut gentoo = Gentoo::builder("amd64")
962    ///     .binhost("x86-64")
963    ///     .install(["dev-vcs/git"])
964    ///     .build()?;
965    /// let plan = gentoo.resolve_packages("/var/lib/machines/gentoo")?;
966    /// for package in &plan.packages {
967    ///     println!("{}-{} build {}", package.name, package.version, package.build_id);
968    /// }
969    /// # Ok(())
970    /// # }
971    /// ```
972    ///
973    /// A resolution reports which atom blocked which, which packages form a
974    /// cycle, and which slot two builds contended for. Those are events rather
975    /// than part of the plan, so a caller that wants them binds a sink with
976    /// [`observe`](Self::observe) and calls
977    /// [`Observed::resolve_packages`] instead.
978    pub fn resolve_packages(&mut self, root: impl AsRef<Path>) -> Result<Plan, GentooError> {
979        self.observe(&mut Silent).resolve_packages(root)
980    }
981
982    /// The resolution, with somewhere to report to.
983    ///
984    /// Shared by [`resolve_packages`](Self::resolve_packages) and the install
985    /// wave, so the answer a caller previews and the answer a run installs come
986    /// from one code path -- and, since both reach it with an observer, say the
987    /// same things about how they got there. A preview taken through
988    /// [`Gentoo::observe`] sees every [`GentooEvent::Conflict`],
989    /// [`GentooEvent::DependencyCycle`] and [`GentooEvent::Occupied`] the
990    /// resolution met, which is the half a preview is for.
991    fn resolve_install(
992        &mut self,
993        installed: &Installed,
994        to: Reporting<'_>,
995    ) -> Result<Plan, GentooError> {
996        // A plan is a caller saying what to install, so there is nothing to
997        // resolve and no index to fetch.
998        if let Some(plan) = &self.plan {
999            return Ok(plan.clone());
1000        }
1001        let Some(binhost) = self.binhost.clone() else {
1002            return Err(GentooError::config(
1003                "no binhost sub-architecture was named; call GentooBuilder::binhost with one of \
1004                 the trees the architecture publishes, such as \"x86-64\"",
1005            ));
1006        };
1007        if self.install.is_empty() {
1008            return Err(GentooError::config(
1009                "no package was asked for; call GentooBuilder::install with an atom such as \
1010                 \"dev-vcs/git\"",
1011            ));
1012        }
1013        let index = self.read_index(to)?;
1014        to.progress(GentooEvent::Index {
1015            builds: index.records().len(),
1016            skipped: index.skipped(),
1017            generated: index.preamble().generated,
1018        });
1019        let (resolved, refusals) =
1020            resolve::resolve(&index, installed, &self.install, &self.prefer_use);
1021        for cycle in &resolved.cycles {
1022            to.progress(GentooEvent::DependencyCycle { packages: cycle });
1023        }
1024        if !refusals.is_empty() {
1025            for unmet in &refusals.unsatisfied {
1026                to.progress(GentooEvent::Unsatisfiable {
1027                    atom: &unmet.atom,
1028                    wanted_by: &unmet.wanted_by,
1029                    reason: &unmet.reason,
1030                });
1031            }
1032            for conflict in &refusals.conflicts {
1033                to.progress(GentooEvent::Conflict {
1034                    atom: &conflict.atom,
1035                    stated_by: &conflict.stated_by,
1036                    blocks: &conflict.blocks,
1037                    installed: conflict.installed,
1038                });
1039            }
1040            for taken in &refusals.occupied {
1041                to.progress(GentooEvent::Occupied {
1042                    package: &taken.name,
1043                    slot: &taken.slot,
1044                    held: &taken.held,
1045                    second: &taken.second,
1046                    installed: taken.installed,
1047                    atom: &taken.atom,
1048                    wanted_by: &taken.wanted_by,
1049                });
1050            }
1051            return Err(GentooError::Resolve {
1052                reason: refusals.describe(),
1053            });
1054        }
1055        Ok(Plan::project(
1056            &self.architecture,
1057            &binhost,
1058            index.preamble().generated,
1059            &resolved.packages,
1060        ))
1061    }
1062
1063    /// Fetches and parses the binary-package index.
1064    fn read_index(&mut self, to: Reporting<'_>) -> Result<index::Index, GentooError> {
1065        let Some(binhost) = self.binhost.clone() else {
1066            return Err(GentooError::config(
1067                "no binhost sub-architecture was named; call GentooBuilder::binhost with one of \
1068                 the trees the architecture publishes, such as \"x86-64\"",
1069            ));
1070        };
1071        let directory = index::directory(&self.architecture, &binhost);
1072        // The compressed form first and the plain one behind it, each walked
1073        // across every mirror: a mirror that publishes only the plain form is a
1074        // mirror to fall back to rather than one to give up on. This is the
1075        // walk rule one level up from the mirrors, which is why it is the same
1076        // call.
1077        walk_mirrors(
1078            &index::NAMES,
1079            |name| {
1080                let suffix = format!("{directory}/{name}");
1081                let served = self.read_bytes(&suffix, MAX_INDEX_BYTES, to)?;
1082                read_index_body(&served)
1083            },
1084            || GentooError::Fetch(no_mirror(&directory)),
1085        )
1086    }
1087
1088    /// Fetches one unsigned document, walking the mirrors as
1089    /// [`read_signed`](Self::read_signed) does but stopping short of a
1090    /// verification there is nothing to perform.
1091    ///
1092    /// Kept apart from that walk rather than folded into it, because the two
1093    /// differ in what a failure means and not only in a step: a document that
1094    /// does not verify is fatal there and there is no such outcome here, so a
1095    /// shared body would carry a flag that decides whether the layer is
1096    /// checking a signature. The index has none to check.
1097    fn read_bytes(
1098        &mut self,
1099        suffix: &str,
1100        limit: u64,
1101        to: Reporting<'_>,
1102    ) -> Result<Fetched, GentooError> {
1103        let fetcher = &mut self.fetcher;
1104        walk_mirrors(
1105            &self.mirrors,
1106            |mirror| {
1107                stop_if_cancelled(to)?;
1108                let url = mirror_url(mirror, suffix);
1109                to.progress(GentooEvent::Fetching { url: &url });
1110                let mut bytes = Vec::new();
1111                fetcher.fetch(
1112                    &FetchRequest::new(&url).sized(limit),
1113                    &mut LimitedWriter::new(&mut bytes, limit),
1114                )?;
1115                Ok(Fetched { url, body: bytes })
1116            },
1117            || GentooError::Fetch(no_mirror(suffix)),
1118        )
1119    }
1120
1121    /// Resolves the configured variant to a build, and that build to the digest
1122    /// its bytes will be held to.
1123    ///
1124    /// The whole read half of a bootstrap: it fetches and verifies the pointer,
1125    /// matches the variant against what that names, then fetches and verifies
1126    /// the digest document beside the chosen build and reads the SHA-512 out of
1127    /// it. Nothing is downloaded, so this serves an architecture the host could
1128    /// never run.
1129    ///
1130    /// A [`build_id`](GentooBuilder::build_id) resolves within that build
1131    /// directory instead of the one the pointer names, and skips the pointer
1132    /// entirely: a pin is a caller saying which build they want, and asking the
1133    /// archive which is current would not change the answer.
1134    ///
1135    /// # Errors
1136    ///
1137    /// As [`available`](Self::available), plus [`GentooError::Config`] when no
1138    /// variant was named or the named one is not in the enumeration — which
1139    /// names what the architecture does offer.
1140    pub fn resolve(&mut self) -> Result<Stage3, GentooError> {
1141        self.observe(&mut Silent).resolve()
1142    }
1143
1144    /// The resolution, with somewhere to report to.
1145    ///
1146    /// Shared by [`resolve`](Self::resolve) and the bootstrap, so the answer a
1147    /// caller previews and the answer a bootstrap installs come from one code
1148    /// path and cannot drift apart.
1149    fn resolve_reporting(&mut self, to: Reporting<'_>) -> Result<Stage3, GentooError> {
1150        if let Some(stage3) = &self.stage3 {
1151            return Ok(stage3.clone());
1152        }
1153        let Some(variant) = self.variant.clone() else {
1154            return Err(GentooError::config(
1155                "no variant was named; call GentooBuilder::variant with one of the names \
1156                 Gentoo::available reports, such as \"amd64-openrc\"",
1157            ));
1158        };
1159        // A pinned build id is the caller saying which build they want, so the
1160        // enumeration -- which only ever reports the current one -- has no say.
1161        // The tarball's name is composed rather than read from a document, and
1162        // it is the one shape the archive publishes; it is checked on the way
1163        // out for the same reason every other segment is.
1164        let (build_id, path, size) = match &self.build_id {
1165            Some(build_id) => {
1166                let file = format!("stage3-{variant}-{build_id}.tar.xz");
1167                let path = format!("{build_id}/{file}");
1168                coordinate::check("the pinned stage3 path", &path, Nesting::Nested)
1169                    .map_err(GentooError::config)?;
1170                (build_id.clone(), path, MAX_STAGE3_BYTES)
1171            }
1172            None => {
1173                let pointer = self.read_pointer(to)?;
1174                let Some(entry) = pointer.entry(&variant) else {
1175                    return Err(GentooError::config(format!(
1176                        "the architecture {} publishes no stage3 variant {variant:?}; it offers {}",
1177                        self.architecture,
1178                        pointer.variants().join(", "),
1179                    )));
1180                };
1181                (entry.build_id.clone(), entry.path.clone(), entry.size)
1182            }
1183        };
1184
1185        let suffix = self.url(&format!("{path}.DIGESTS"));
1186        let digests = self.read_signed(&suffix, MAX_DIGESTS_BYTES, to)?;
1187        let sha512 = digests::sha512_of(&digests.url, &digests.body, file_name_of(&path))?;
1188        Ok(Stage3 {
1189            build_id,
1190            variant,
1191            path,
1192            size,
1193            sha512,
1194            certificate: digests.certificate,
1195        })
1196    }
1197
1198    /// Fetches the pointer document, verifies it, and holds it to the age bound.
1199    fn read_pointer(&mut self, to: Reporting<'_>) -> Result<Pointer, GentooError> {
1200        let suffix = self.url("latest-stage3.txt");
1201        let served = self.read_signed(&suffix, MAX_POINTER_BYTES, to)?;
1202        let pointer = Pointer::parse(&served.url, &served.body)?;
1203        self.check_fresh(&pointer)?;
1204        Ok(pointer)
1205    }
1206
1207    /// Refuses a pointer older than the configured bound.
1208    ///
1209    /// A pointer stating no timestamp is refused on the same terms rather than
1210    /// waved through: it is the only bound the layer has, so reading its absence
1211    /// as permission would let whoever serves the document disable the check by
1212    /// deleting a line. That is the reasoning the signature checks apply to a
1213    /// missing creation time, arrived at from the same direction.
1214    ///
1215    /// When the clock cannot be read the bound is not applied, since a deadline
1216    /// evaluated against an unreadable clock decides nothing.
1217    fn check_fresh(&self, pointer: &Pointer) -> Result<(), GentooError> {
1218        let Some(limit) = self.max_pointer_age else {
1219            return Ok(());
1220        };
1221        let stale = |age| GentooError::Stale {
1222            published: pointer.published(),
1223            age,
1224            limit,
1225        };
1226        let Some(published) = pointer.published() else {
1227            return Err(stale(None));
1228        };
1229        let Some(now) = super::now_epoch() else {
1230            return Ok(());
1231        };
1232        let age = Duration::from_secs(now.saturating_sub(published).max(0) as u64);
1233        if age > limit {
1234            return Err(stale(Some(age)));
1235        }
1236        Ok(())
1237    }
1238
1239    /// Fetches a cleartext-signed document and returns it as the mirror that
1240    /// answered served it.
1241    ///
1242    /// The mirror list is walked in order, advancing past a mirror that could
1243    /// not serve the document. One that serves something that does not verify is
1244    /// fatal rather than a reason to try the next: what it served was answered
1245    /// for by the URL that was asked for, and the answer to a refused signature
1246    /// is never to ask somewhere else. A URL the transport will not accept is
1247    /// fatal for the same reason -- it is the configuration rather than the
1248    /// mirror, and walking past it would report the failure against a mirror
1249    /// that was spelled fine.
1250    fn read_signed(
1251        &mut self,
1252        suffix: &str,
1253        limit: u64,
1254        to: Reporting<'_>,
1255    ) -> Result<Served, GentooError> {
1256        let fetcher = &mut self.fetcher;
1257        let keyring = &self.keyring;
1258        walk_mirrors(
1259            &self.mirrors,
1260            |mirror| {
1261                // Before each mirror rather than before the walk: an
1262                // unreachable mirror costs a connect timeout, so a list of them
1263                // is a step long enough to want stopping in the middle of.
1264                stop_if_cancelled(to)?;
1265                let url = mirror_url(mirror, suffix);
1266                // Reported per mirror rather than per document: this walk spends
1267                // one mirror timeout per unreachable mirror, and an observer
1268                // that saw only the resolution would watch a bootstrap that
1269                // looks stopped.
1270                to.progress(GentooEvent::Fetching { url: &url });
1271                let mut bytes = Vec::new();
1272                fetcher.fetch(
1273                    &FetchRequest::new(&url).sized(limit),
1274                    &mut LimitedWriter::new(&mut bytes, limit),
1275                )?;
1276                let verified = keyring
1277                    .verify(&bytes, true)
1278                    .map_err(|err| GentooError::signature(&url, err))?;
1279                Ok(Served {
1280                    url,
1281                    body: verified.body,
1282                    certificate: verified.certificate,
1283                })
1284            },
1285            || GentooError::Fetch(no_mirror(suffix)),
1286        )
1287    }
1288
1289    /// The mirror-relative path of something in this architecture's autobuilds
1290    /// tree.
1291    fn url(&self, suffix: &str) -> String {
1292        format!("releases/{}/autobuilds/{suffix}", self.architecture)
1293    }
1294}
1295
1296/// An unsigned document as it arrived, and the URL that served it.
1297///
1298/// The URL is the whole one, as [`Served`]'s is and for the same reason: a walk
1299/// over several mirrors is exactly when a refusal has to name which one
1300/// answered.
1301struct Fetched {
1302    /// The URL the document arrived from.
1303    url: String,
1304    /// The bytes, in whatever compression the archive served them.
1305    body: Vec<u8>,
1306}
1307
1308/// Reads a fetched index body into a parsed index.
1309///
1310/// The compression is selected from the bytes rather than from the file name,
1311/// so the plain form and the compressed one go through one path — and a mirror
1312/// serving one under the other's name is read for what it is rather than for
1313/// what it is called.
1314///
1315/// The decompressed stream is bounded as well as the fetch: the fetch's ceiling
1316/// bounds what arrived, and this one bounds what that inflates to, which is the
1317/// only place a decompression bomb could spend anything.
1318fn read_index_body(served: &Fetched) -> Result<index::Index, GentooError> {
1319    let mut body = served.body.as_slice();
1320    let head = compress::sniff_head(&mut body)
1321        .map_err(|err| GentooError::index(&served.url, format!("could not be read: {err}")))?;
1322    let mut reader = compress::decompress(&head, body)
1323        .map_err(|err| GentooError::index(&served.url, format!("could not be read: {err}")))?;
1324    let mut bytes = Vec::new();
1325    io::copy(&mut reader.by_ref().take(MAX_INDEX_BYTES), &mut bytes)
1326        .map_err(|err| GentooError::index(&served.url, format!("could not be read: {err}")))?;
1327    // Lossy rather than strict: the index is fifteen thousand stanzas of which
1328    // this layer reads a dozen fields, and one package's description holding a
1329    // byte sequence that is not UTF-8 is not a reason to refuse the archive.
1330    // Every field this layer acts on is held to its own grammar afterwards.
1331    let parsed = index::Index::parse(&String::from_utf8_lossy(&bytes));
1332    if parsed.records().is_empty() {
1333        return Err(GentooError::index(
1334            &served.url,
1335            "names no package this layer can act on",
1336        ));
1337    }
1338    // The index's own count against what arrived. Not a security property --
1339    // the count is in the same unsigned document as the stanzas, so a mirror
1340    // composing one composes both -- but a plain-text index cut short otherwise
1341    // parses perfectly and resolves against a silently smaller archive, and
1342    // this is the only thing that would notice.
1343    let read = (parsed.records().len() + parsed.skipped()) as u64;
1344    if let Some(declared) = parsed.preamble().declared
1345        && read < declared
1346    {
1347        return Err(GentooError::index(
1348            &served.url,
1349            format!("states that it holds {declared} packages and carries {read}"),
1350        ));
1351    }
1352    Ok(parsed)
1353}
1354
1355/// A verified document, and the URL that served it.
1356///
1357/// The URL is the whole one, mirror included, rather than the archive-relative
1358/// path the walk was asked for: it is what tells a caller reading a refusal
1359/// which mirror answered, and a walk over several of them is exactly when that
1360/// matters.
1361struct Served {
1362    /// The URL the document arrived from.
1363    url: String,
1364    /// The cleartext the signature covers.
1365    body: String,
1366    /// The verifying certificate's primary-key fingerprint, uppercase hex.
1367    certificate: String,
1368}
1369
1370/// The failure a mirror walk reports when it had nowhere to walk.
1371///
1372/// Unreachable as the layer is configured — [`GentooBuilder::build`] composes at
1373/// least the default archive, so every walk has a mirror and either returns or
1374/// records that mirror's failure. It is a configuration failure rather than a
1375/// missing resource, which is also what keeps it out of the failover class:
1376/// there is nothing to advance to.
1377fn no_mirror(path: &str) -> FetchError {
1378    FetchError::url(path, "no mirror is configured to fetch it from")
1379}
1380
1381impl Provisioner for Gentoo {
1382    /// Provisions the configured variant into the request's staging directory.
1383    ///
1384    /// Reached through [`provision::ensure`](super::ensure), which supplies the
1385    /// staging directory, serializes concurrent callers, publishes the finished
1386    /// tree atomically and removes it on any failure — exactly as it does for
1387    /// every other provisioner.
1388    ///
1389    /// A run that has not resolved resolves as its first step, so a caller who
1390    /// only wants a root never has to.
1391    ///
1392    /// [`ProvisionRequest::cancelled`] is consulted at every step boundary and
1393    /// while the tarball is being written, which is where the time goes: one
1394    /// file of several hundred megabytes, with no package boundary to break it
1395    /// up.
1396    fn provision(&mut self, request: &ProvisionRequest<'_>) -> Result<(), ProvisionError> {
1397        self.provision_reporting(request, &mut RunObserver { request })
1398    }
1399}
1400
1401impl Gentoo {
1402    /// Binds a [`GentooObserver`] for one call, returning a view that reports
1403    /// to it.
1404    ///
1405    /// A closure is an observer, so a caller that only wants events writes one
1406    /// and never names the trait; a caller that also wants to *stop* the work
1407    /// implements [`GentooObserver`] and answers its
1408    /// [`cancelled`](GentooObserver::cancelled).
1409    ///
1410    /// The sink is borrowed for the returned value's lifetime, not the
1411    /// provisioner's, so a `Gentoo` outlives every observed call and whatever
1412    /// the sink borrows is free again as soon as the call ends:
1413    ///
1414    /// ```no_run
1415    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1416    /// use ferroday_cage::provision::gentoo::{Gentoo, GentooEvent};
1417    ///
1418    /// let mut gentoo = Gentoo::builder("amd64")
1419    ///     .binhost("x86-64")
1420    ///     .install(["dev-vcs/git"])
1421    ///     .build()?;
1422    /// let mut refused: Vec<String> = Vec::new();
1423    ///
1424    /// let mut sink = |event: GentooEvent<'_>| {
1425    ///     if let GentooEvent::Conflict { atom, blocks, .. } = event {
1426    ///         refused.push(format!("{atom} blocks {blocks}"));
1427    ///     }
1428    /// };
1429    /// let plan = gentoo.observe(&mut sink).resolve_packages("/var/lib/machines/gentoo");
1430    /// drop(sink);
1431    ///
1432    /// // `refused` is free here, and `gentoo` is still usable.
1433    /// println!("{} conflicts on the way to {:?}", refused.len(), plan.is_ok());
1434    /// # Ok(())
1435    /// # }
1436    /// ```
1437    pub fn observe<'o>(&'o mut self, sink: &'o mut dyn GentooObserver) -> Observed<'o> {
1438        Observed { gentoo: self, sink }
1439    }
1440
1441    /// The body of [`Provisioner::provision`], reporting to `to`.
1442    fn provision_reporting(
1443        &mut self,
1444        request: &ProvisionRequest<'_>,
1445        to: Reporting<'_>,
1446    ) -> Result<(), ProvisionError> {
1447        let stage3 = self.resolve_reporting(to)?;
1448        to.progress(GentooEvent::Resolved { stage3: &stage3 });
1449        // The resolution is two small documents and the download is not, so a
1450        // caller who cancelled during it should not be committed to the
1451        // download by having asked what would be downloaded.
1452        stop_if_cancelled(to)?;
1453
1454        // The cache is the caller's if they named one, and otherwise a sibling
1455        // of the tree being built that goes with this frame however it ends: a
1456        // half-gigabyte download beside a root that was never published is not
1457        // something a later run would find and clear.
1458        let cache =
1459            PackageCache::beside(request.staging(), self.cache_dir.as_deref(), STAGE3_CACHE);
1460        std::fs::create_dir_all(cache.path())
1461            .map_err(GentooError::at("creating the download cache", cache.path()))?;
1462        let file = stage3.file_name();
1463        let tarball = cache.path().join(file);
1464
1465        // A cached tarball is bytes on a disk this crate does not own, so it is
1466        // verified on the read rather than trusted for having been verified
1467        // once. That makes the cache-hit path and the fresh-download path one
1468        // code path, whichever produced the file.
1469        if !tarball.is_file() {
1470            self.download(&stage3, &tarball, to)?;
1471        }
1472        // A digest pass over the whole tarball is the second long step, and
1473        // what precedes it is a complete file in a cache that outlives the run,
1474        // so stopping here costs the download nothing.
1475        stop_if_cancelled(to)?;
1476        to.progress(GentooEvent::Verifying {
1477            path: tarball.as_path(),
1478        });
1479        verify(&tarball, stage3.sha512())?;
1480
1481        to.progress(GentooEvent::Extracting {
1482            path: tarball.as_path(),
1483        });
1484        // The bytes are verified before the extractor sees them, rather than
1485        // streamed through it as they arrive: an archive that turns out not to
1486        // be the one the signed document named would otherwise already have
1487        // written most of a root filesystem.
1488        Tarball::new(&tarball).provision(request)?;
1489
1490        // The second wave. A run that asked for no package stops here and has
1491        // done exactly what a stage3 bootstrap does, byte for byte.
1492        if self.install.is_empty() && self.plan.is_none() {
1493            return Ok(());
1494        }
1495        self.install_wave(request.staging(), None, to)?;
1496        Ok(())
1497    }
1498
1499    /// Stages the configured install as a disposable overlay upper over the
1500    /// [`base_layer`](GentooBuilder::base_layer).
1501    ///
1502    /// A layered build provisions one base and stages each increment over it:
1503    /// the base is read-only, the increment lives in `upper`, and the
1504    /// [`BuildLayer`] returned removes the upper when it is dropped. Root a
1505    /// cage on
1506    /// [`overlay_rootfs(base, layer.path())`](crate::CageBuilder::overlay_rootfs)
1507    /// to build against the merged view.
1508    ///
1509    /// Resolution reads the merged view — the base's own database and whatever
1510    /// the upper already holds — so an increment installs what the base is
1511    /// missing and no more. The entries it writes land in the upper, and
1512    /// portage reading the merged root sees the base's and the increment's
1513    /// together.
1514    ///
1515    /// # Errors
1516    ///
1517    /// Returns [`ProvisionError`] wrapping [`GentooError::Config`] when no base
1518    /// layer is set, and the same failures an install wave reports otherwise.
1519    ///
1520    /// # Example
1521    ///
1522    /// ```no_run
1523    /// use ferroday_cage::provision::gentoo::Gentoo;
1524    ///
1525    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1526    /// let mut increment = Gentoo::builder("amd64")
1527    ///     .binhost("x86-64")
1528    ///     .install(["dev-vcs/git"])
1529    ///     .base_layer("/var/lib/machines/gentoo")
1530    ///     .build()?;
1531    /// let layer = increment.stage_layer("/var/tmp/build-upper")?;
1532    /// // Root a cage on the merged view; drop `layer` to revert the increment.
1533    /// # let _ = layer;
1534    /// # Ok(())
1535    /// # }
1536    /// ```
1537    pub fn stage_layer(&mut self, upper: impl AsRef<Path>) -> Result<BuildLayer, ProvisionError> {
1538        self.stage_layer_reporting(upper.as_ref(), &mut Silent)
1539    }
1540
1541    /// The body of [`stage_layer`](Self::stage_layer), reporting to `to`.
1542    fn stage_layer_reporting(
1543        &mut self,
1544        upper: &Path,
1545        to: Reporting<'_>,
1546    ) -> Result<BuildLayer, ProvisionError> {
1547        let Some(base) = self.base_layer.clone() else {
1548            return Err(GentooError::config(
1549                "no base layer was named; call GentooBuilder::base_layer with the Gentoo root the \
1550                 increment is staged over",
1551            )
1552            .into());
1553        };
1554        std::fs::create_dir_all(upper)
1555            .map_err(|err| ProvisionError::io("creating the overlay upper", upper, err))?;
1556        // The handle that owns the upper is taken as soon as the upper exists,
1557        // not on the way out, so a failure below disposes of a partly-installed
1558        // increment instead of orphaning it: there is no handle in an `Err` for
1559        // the caller to drop.
1560        let layer = BuildLayer::new(upper, IdentityMap::Single);
1561        self.install_wave(upper, Some(&base), to)?;
1562        Ok(layer)
1563    }
1564
1565    /// Installs the configured atoms into `root`, merging each package and
1566    /// registering its database entry.
1567    ///
1568    /// `over` is the base an increment is staged over, where this is a layered
1569    /// build: resolution then describes the merged view rather than the upper
1570    /// alone, because that is what a package manager inside the finished
1571    /// sandbox sees.
1572    ///
1573    /// Cancellation is consulted between packages and while each one is being
1574    /// downloaded. A wave installing a hundred packages is the longest thing
1575    /// this layer does, and a check that ran only around it would answer a
1576    /// caller who asked to stop once all of it had arrived.
1577    fn install_wave(
1578        &mut self,
1579        root: &Path,
1580        over: Option<&Path>,
1581        to: Reporting<'_>,
1582    ) -> Result<(), ProvisionError> {
1583        let installed = match over {
1584            Some(base) => installed::merged(base, root)?,
1585            None => installed::installed(root)?,
1586        };
1587        let plan = self.resolve_install(&installed, to)?;
1588        to.progress(GentooEvent::Planned {
1589            packages: plan.packages.len(),
1590            bytes: planned_bytes(&plan.packages),
1591        });
1592        // The resolution is one document and the downloads are not, so a caller
1593        // who cancelled during it is not committed to them by having asked what
1594        // would be installed.
1595        stop_if_cancelled(to)?;
1596        if plan.packages.is_empty() {
1597            return Ok(());
1598        }
1599
1600        let cache = PackageCache::beside(root, self.cache_dir.as_deref(), PACKAGE_CACHE);
1601        let directory = index::directory(&self.architecture, &plan.binhost);
1602        let rooted = Rooted::open(root).map_err(GentooError::at("opening the root", root))?;
1603        // The merge ordinal continues the root's own sequence, and for a
1604        // layered build that sequence is the base's: an increment that started
1605        // again at one would give two packages the same ordinal, and the
1606        // database could no longer say which was merged first.
1607        let mut counter = vdb::counter_of(root, &rooted)?;
1608        if let Some(base) = over {
1609            let below = Rooted::open(base).map_err(GentooError::at("opening the base", base))?;
1610            counter = counter.max(vdb::counter_of(base, &below)?);
1611        }
1612        // The cache pass, before anything is fetched: the paths a merge will
1613        // read, and which of them are not there yet.
1614        let mut cached: Vec<PathBuf> = Vec::with_capacity(plan.packages.len());
1615        for planned in &plan.packages {
1616            // The path came out of an index or a plan document and passed
1617            // `coordinate::check` either way, so it names something under the
1618            // cache and under the binhost directory and nothing else.
1619            let path = cache.path().join(&planned.path);
1620            if let Some(parent) = path.parent() {
1621                std::fs::create_dir_all(parent)
1622                    .map_err(GentooError::at("creating the download cache", parent))?;
1623            }
1624            cached.push(path);
1625        }
1626        let missing: Vec<(&PlannedPackage, &Path)> = plan
1627            .packages
1628            .iter()
1629            .zip(&cached)
1630            .filter(|(_, path)| !path.is_file())
1631            .map(|(planned, path)| (planned, path.as_path()))
1632            .collect();
1633        self.prefetch_packages(&directory, &missing, to)?;
1634
1635        for (planned, cached) in plan.packages.iter().zip(&cached) {
1636            stop_if_cancelled(to)?;
1637            // A cached container is bytes on a disk this crate does not own, so
1638            // it is verified on the read rather than trusted for having been
1639            // verified once -- which makes the cache-hit path and the
1640            // fresh-download path one code path.
1641            if !cached.is_file() {
1642                self.download_package(&directory, planned, cached, to)?;
1643            }
1644            to.progress(GentooEvent::Merging {
1645                package: &planned.name,
1646                version: &planned.version,
1647            });
1648            let package = match gpkg::Package::open(cached, planned, &self.keyring) {
1649                Ok(package) => package,
1650                Err(err) => {
1651                    // A container that is not what it should be is not the
1652                    // archive's file whatever it is, and leaving it would make
1653                    // every later run fail identically out of its own cache --
1654                    // which reads as a permanent failure rather than as a
1655                    // mirror that served the wrong bytes once. The same rule
1656                    // the stage3's digest check applies.
1657                    let _ = std::fs::remove_file(cached);
1658                    return Err(err.into());
1659                }
1660            };
1661            counter += 1;
1662            vdb::merge(root, &rooted, cached, &package, planned, counter)?;
1663        }
1664        Ok(())
1665    }
1666
1667    /// How many packages one batch asks for.
1668    ///
1669    /// Each job holds an open staging file for as long as the batch runs, so
1670    /// this is a bound on descriptors rather than on the transport, which
1671    /// decides for itself how many of a batch to have in flight. It is also the
1672    /// granularity a cancelled wave stops at, which is the reason it is not
1673    /// larger.
1674    const BATCH: usize = 32;
1675
1676    /// Fills the cache with the packages a wave is about to merge, several at a
1677    /// time, through [`Fetch::fetch_all`].
1678    ///
1679    /// Every job asks the first mirror, which is the one the walk would ask
1680    /// first too. A job that does not arrive leaves the cache without that
1681    /// package and the walk that follows fetches it with every mirror available
1682    /// to it, so this is an optimization and nothing more. What the rename
1683    /// publishes is a complete container rather than a verified one, exactly as
1684    /// [`download_package`](Self::download_package) does: the verification is
1685    /// the read that follows, which is the same read a cache hit goes through.
1686    fn prefetch_packages(
1687        &mut self,
1688        directory: &str,
1689        missing: &[(&PlannedPackage, &Path)],
1690        to: Reporting<'_>,
1691    ) -> Result<(), GentooError> {
1692        let Some(mirror) = self.mirrors.first().cloned() else {
1693            return Ok(());
1694        };
1695        for batch in missing.chunks(Self::BATCH) {
1696            stop_if_cancelled(to)?;
1697            let mut staged = Vec::with_capacity(batch.len());
1698            for (planned, dest) in batch {
1699                let url = mirror_url(&mirror, &format!("{directory}/{}", planned.path));
1700                to.progress(GentooEvent::Fetching { url: &url });
1701                let path = super::staging_path(dest);
1702                let Ok(file) = std::fs::OpenOptions::new()
1703                    .write(true)
1704                    .create_new(true)
1705                    .open(&path)
1706                else {
1707                    continue;
1708                };
1709                // Capped as the per-package download is, at the smaller of the
1710                // index's number and this layer's own ceiling: nothing signs
1711                // the index, so its number can be wrong in both directions,
1712                // and a transport is free to ignore the size on the request.
1713                let sink = io::BufWriter::new(file);
1714                let cap = planned.size.min(MAX_PACKAGE_BYTES);
1715                staged.push((*planned, *dest, url, path, LimitedWriter::new(sink, cap)));
1716            }
1717
1718            let outcomes = {
1719                let mut jobs: Vec<FetchJob<'_>> = staged
1720                    .iter_mut()
1721                    .map(|(planned, _dest, url, _path, sink)| {
1722                        let bound = planned.size.min(MAX_PACKAGE_BYTES);
1723                        FetchJob::new(FetchRequest::new(url).sized(bound), sink)
1724                    })
1725                    .collect();
1726                self.fetcher.fetch_all(&mut jobs)
1727            };
1728
1729            for ((_planned, dest, _url, path, sink), outcome) in staged.into_iter().zip(outcomes) {
1730                let published = outcome.is_ok()
1731                    && sink.into_inner().into_inner().is_ok()
1732                    && std::fs::rename(&path, dest).is_ok();
1733                if !published {
1734                    let _ = std::fs::remove_file(&path);
1735                }
1736            }
1737        }
1738        Ok(())
1739    }
1740
1741    /// Downloads one binary package into the cache, atomically.
1742    ///
1743    /// Bounded by the length the index published for it *and* by
1744    /// [`MAX_PACKAGE_BYTES`], because the index's number can be wrong in both
1745    /// directions and only one of them fails closed. Understated, it stops the
1746    /// download short and the container fails its digest. Overstated, it merely
1747    /// relaxes the bound — a transport caps at the smaller of the declared size
1748    /// and its own ceiling — so a hostile or damaged index buys whatever that
1749    /// ceiling is, once per package. Nothing signs the index, so this layer
1750    /// keeps a ceiling of its own and the smaller of the two wins.
1751    fn download_package(
1752        &mut self,
1753        directory: &str,
1754        planned: &PlannedPackage,
1755        dest: &Path,
1756        to: Reporting<'_>,
1757    ) -> Result<(), GentooError> {
1758        let fetcher = &mut self.fetcher;
1759        walk_mirrors(
1760            &self.mirrors,
1761            |mirror| {
1762                stop_if_cancelled(to)?;
1763                let url = mirror_url(mirror, &format!("{directory}/{}", planned.path));
1764                to.progress(GentooEvent::Fetching { url: &url });
1765                let bound = planned.size.min(MAX_PACKAGE_BYTES);
1766                fetch_to(fetcher.as_mut(), &url, bound, dest, BINARY_PACKAGE, to)
1767            },
1768            || GentooError::Fetch(no_mirror(&planned.path)),
1769        )
1770    }
1771
1772    /// Downloads a stage3 into the cache, atomically.
1773    ///
1774    /// The body is written to a staging file beside the destination and renamed
1775    /// onto it, so a partial download is never visible at the cache path and a
1776    /// concurrent download of the same build publishes its own file rather than
1777    /// consuming this one. What the rename publishes is a complete body rather
1778    /// than a verified one; the verification is the read that follows.
1779    ///
1780    /// The size the signed enumeration recorded bounds what the download may
1781    /// spend. It is a length a verified source declared, so it travels as
1782    /// [`FetchRequest::sized`] as well as capping the writer.
1783    fn download(
1784        &mut self,
1785        stage3: &Stage3,
1786        dest: &Path,
1787        to: Reporting<'_>,
1788    ) -> Result<(), GentooError> {
1789        let fetcher = &mut self.fetcher;
1790        let architecture = &self.architecture;
1791        walk_mirrors(
1792            &self.mirrors,
1793            |mirror| {
1794                stop_if_cancelled(to)?;
1795                let url = mirror_url(
1796                    mirror,
1797                    &format!("releases/{architecture}/autobuilds/{}", stage3.path),
1798                );
1799                to.progress(GentooEvent::Fetching { url: &url });
1800                fetch_to(fetcher.as_mut(), &url, stage3.size, dest, STAGE3, to)
1801            },
1802            || GentooError::Fetch(no_mirror(&stage3.path)),
1803        )
1804    }
1805}
1806
1807/// What a download is of, so the three operations [`fetch_to`] can fail at name
1808/// their subject.
1809///
1810/// Spelled out per caller rather than composed from one noun, because an error's
1811/// operation is a `&'static str` throughout this crate: naming what failed is
1812/// part of the convention, and formatting the subject in would make each
1813/// operation a `String` that outlives nothing.
1814#[derive(Clone, Copy)]
1815struct Downloading {
1816    /// Creating the staging file beside the destination.
1817    staging: &'static str,
1818    /// Writing the body into it.
1819    writing: &'static str,
1820    /// Renaming it onto the destination.
1821    publishing: &'static str,
1822}
1823
1824/// A stage3 tarball.
1825const STAGE3: Downloading = Downloading {
1826    staging: "staging a stage3",
1827    writing: "writing a stage3",
1828    publishing: "publishing a stage3",
1829};
1830
1831/// A binary package's container.
1832const BINARY_PACKAGE: Downloading = Downloading {
1833    staging: "staging a binary package",
1834    writing: "writing a binary package",
1835    publishing: "publishing a binary package",
1836};
1837
1838/// Writes one URL's body to `dest` through a staging file beside it.
1839///
1840/// A cancelled run stops mid-body rather than at the end of it: the write is
1841/// where a bootstrap spends its time, and a check that only ran between mirrors
1842/// would leave a caller who asked to stop waiting for the whole tarball.
1843fn fetch_to(
1844    fetcher: &mut dyn Fetch,
1845    url: &str,
1846    size: u64,
1847    dest: &Path,
1848    what: Downloading,
1849    to: Reporting<'_>,
1850) -> Result<(), GentooError> {
1851    let staged = super::staging_path(dest);
1852    let file = std::fs::OpenOptions::new()
1853        .write(true)
1854        .create_new(true)
1855        .open(&staged)
1856        .map_err(GentooError::at(what.staging, &staged))?;
1857
1858    let mut sink = io::BufWriter::new(file);
1859    let (fetched, cancelled) = {
1860        let mut body = CancellableWriter {
1861            inner: LimitedWriter::new(&mut sink, size),
1862            to,
1863            cancelled: false,
1864        };
1865        let fetched = fetcher.fetch(&FetchRequest::new(url).sized(size), &mut body);
1866        (fetched, body.cancelled)
1867    };
1868    // The last buffer is written here rather than by `BufWriter`'s own `Drop`,
1869    // which has nowhere to report a failure and so swallows it. A body whose
1870    // tail never reached the disk would otherwise be renamed into the cache and
1871    // reported by the read that follows as a digest failure -- an authenticity
1872    // answer to a local write failure, which is the wrong thing to tell a caller
1873    // and the wrong thing for them to act on.
1874    let flushed = sink
1875        .into_inner()
1876        .map(|_| ())
1877        .map_err(|err| GentooError::at(what.writing, &staged)(err.into_error()));
1878
1879    // A stopped write reaches the transport as a write failure, which is what
1880    // the sink has to answer it with; what it means is the run's own outcome,
1881    // and reporting it as a transport failure would send a mirror walk to the
1882    // next mirror to do the same thing again.
1883    let published = if cancelled {
1884        Err(GentooError::Cancelled)
1885    } else {
1886        fetched
1887            .map_err(GentooError::Fetch)
1888            .and(flushed)
1889            .and_then(|()| {
1890                std::fs::rename(&staged, dest).map_err(GentooError::at(what.publishing, dest))
1891            })
1892    };
1893    if published.is_err() {
1894        let _ = std::fs::remove_file(&staged);
1895    }
1896    published
1897}
1898
1899/// A sink that stops accepting bytes once the run has been cancelled.
1900///
1901/// The other userlands need nothing like it: they download packages, so a check
1902/// between two of them stops a bootstrap within one package's worth of bytes. A
1903/// stage3 is a single file of several hundred megabytes, and a check that ran
1904/// only at the step boundaries around it would answer a caller who asked to stop
1905/// once the whole thing had arrived.
1906///
1907/// The binary packages go through it too, since they share [`fetch_to`]. There
1908/// the argument is weaker per file and stronger per run: a package is a few
1909/// megabytes, and an install wave is a hundred of them.
1910///
1911/// The stop is recorded here rather than read back out of the [`io::Error`] the
1912/// transport returns: a `Fetch` implementation is a caller's own and may report
1913/// a failed write however it likes, and this is the one thing about it this
1914/// crate knows first-hand.
1915struct CancellableWriter<'a, W> {
1916    inner: W,
1917    to: Reporting<'a>,
1918    cancelled: bool,
1919}
1920
1921impl<W: io::Write> io::Write for CancellableWriter<'_, W> {
1922    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1923        if self.to.cancelled() {
1924            self.cancelled = true;
1925            // Not `Interrupted`, which `io::copy` and `write_all` retry: a
1926            // cancellation is the one write failure that must not be retried.
1927            return Err(io::Error::other("the bootstrap was cancelled"));
1928        }
1929        self.inner.write(buf)
1930    }
1931
1932    fn flush(&mut self) -> io::Result<()> {
1933        self.inner.flush()
1934    }
1935}
1936
1937/// Holds a downloaded tarball to the SHA-512 the signed document recorded.
1938///
1939/// A tarball that does not match is removed rather than left in the cache. It
1940/// is not the archive's file whatever it is, and leaving it would make every
1941/// later run of the same build fail identically out of its own cache, which
1942/// reads as a permanent failure rather than as a mirror that served the wrong
1943/// bytes once.
1944fn verify(tarball: &Path, expected: &str) -> Result<(), GentooError> {
1945    let mut file = std::fs::File::open(tarball).map_err(GentooError::at("opening", tarball))?;
1946    let (actual, _) = digest::stream(Algorithm::Sha512, &mut file)
1947        .map_err(GentooError::at("digesting", tarball))?;
1948    if actual != expected {
1949        let _ = std::fs::remove_file(tarball);
1950        return Err(GentooError::Digest {
1951            subject: tarball.display().to_string(),
1952            expected: expected.to_string(),
1953            actual,
1954        });
1955    }
1956    Ok(())
1957}
1958
1959/// Something the Gentoo provisioner is doing, reported to a [`GentooObserver`]
1960/// or to a run's observer.
1961///
1962/// Nested in [`ProvisionEvent::Gentoo`], so one observer attached with
1963/// [`Provision::observe`](super::Provision::observe) covers every provisioner;
1964/// [`Gentoo::observe`] binds a sink that receives these directly, including on
1965/// the read-half calls no run drives.
1966#[derive(Debug)]
1967#[non_exhaustive]
1968pub enum GentooEvent<'a> {
1969    /// A URL is being fetched: one of the two signed documents, or the tarball.
1970    #[non_exhaustive]
1971    Fetching {
1972        /// The URL.
1973        url: &'a str,
1974    },
1975    /// The variant has been resolved to a build, carrying what a bootstrap will
1976    /// install.
1977    ///
1978    /// Emitted mid-bootstrap, once the resolution has produced an answer and
1979    /// before the tarball is downloaded, so a consumer sees exactly what
1980    /// [`provision::ensure`](super::ensure) chose without a separate
1981    /// [`Gentoo::resolve`] pass. Where the caller carried a [`Stage3`] over with
1982    /// [`GentooBuilder::stage3`] that answer is the one they supplied, and
1983    /// nothing was fetched or verified to arrive at it.
1984    #[non_exhaustive]
1985    Resolved {
1986        /// The stage3 the bootstrap will install.
1987        stage3: &'a Stage3,
1988    },
1989    /// A tarball is being digested and compared against the signed document.
1990    ///
1991    /// Reported for a fresh download and a cache hit alike, both going through
1992    /// the same check: a file in the cache is bytes on a disk this crate does
1993    /// not own, so it is verified on the read rather than trusted for having
1994    /// been verified once.
1995    #[non_exhaustive]
1996    Verifying {
1997        /// The tarball, in the cache.
1998        path: &'a Path,
1999    },
2000    /// A verified tarball is being extracted into the root.
2001    #[non_exhaustive]
2002    Extracting {
2003        /// The tarball, in the cache.
2004        path: &'a Path,
2005    },
2006    /// The binary-package index has been read.
2007    #[non_exhaustive]
2008    Index {
2009        /// How many published builds it names.
2010        builds: usize,
2011        /// How many stanzas were read and not kept: a record whose `CPV` this
2012        /// layer could not split, whose path would not stay inside the archive,
2013        /// or whose dependency classes it could not read. A live archive
2014        /// reports zero.
2015        skipped: usize,
2016        /// The epoch the index states it was generated at, where it states one.
2017        ///
2018        /// Nothing signs the index, so this is a mirror's claim rather than a
2019        /// verified fact.
2020        generated: Option<i64>,
2021    },
2022    /// A runtime-dependency cycle was broken to produce an install order.
2023    ///
2024    /// Gentoo's runtime graph has them, and portage breaks them too. The rule
2025    /// is stable — the first package still waiting, in name order — so a cycle
2026    /// broken in one run is broken the same way in the next.
2027    #[non_exhaustive]
2028    DependencyCycle {
2029        /// The packages still waiting on each other, as `category/name-version`.
2030        packages: &'a [String],
2031    },
2032    /// An atom nothing published can satisfy.
2033    ///
2034    /// Reported for every such atom before the run fails, so a caller fixing an
2035    /// install list sees the whole list rather than the first entry of it.
2036    #[non_exhaustive]
2037    Unsatisfiable {
2038        /// The atom, as it was written.
2039        atom: &'a str,
2040        /// Who asked for it: the install list, or the package whose dependency
2041        /// it is.
2042        wanted_by: &'a str,
2043        /// What the wall was, in the sentence "no build ...": the name, the
2044        /// version, the slot, or a USE flag.
2045        reason: &'a str,
2046    },
2047    /// The install set has been resolved, and the wave is about to download it.
2048    #[non_exhaustive]
2049    Planned {
2050        /// How many packages it holds, which is what the root is missing rather
2051        /// than what the request names: a stage3 already answers most of a
2052        /// closure.
2053        packages: usize,
2054        /// What they weigh on the wire in total, from the sizes the index
2055        /// published. Saturating: the figures are a mirror's, and nothing
2056        /// bounds their sum.
2057        bytes: u64,
2058    },
2059    /// A verified package is being merged into the root and registered.
2060    #[non_exhaustive]
2061    Merging {
2062        /// The package, as `category/name`.
2063        package: &'a str,
2064        /// The version being merged.
2065        version: &'a str,
2066    },
2067    /// A blocker matched something the root has or the resolution chose.
2068    #[non_exhaustive]
2069    Conflict {
2070        /// The blocker atom, as it was written.
2071        atom: &'a str,
2072        /// The package that states it.
2073        stated_by: &'a str,
2074        /// What it matched, as `category/name-version`.
2075        blocks: &'a str,
2076        /// Whether what it matched is already in the root, rather than
2077        /// something this same resolution would install.
2078        installed: bool,
2079    },
2080    /// A build the resolution would have chosen occupies a slot of a package
2081    /// something already holds.
2082    ///
2083    /// Portage keeps one instance per `(package, slot)` and reports a root
2084    /// holding two as a conflict, so this is refused rather than merged. It is
2085    /// reached whenever the closure constrains one package from two directions
2086    /// at once — an atom holding it below a version and another holding it at or
2087    /// above one — and whenever an atom is answered only by a version later
2088    /// than the one the root already has in that slot, since merging installs
2089    /// beside what is there rather than replacing it.
2090    #[non_exhaustive]
2091    Occupied {
2092        /// The package, as `category/name`.
2093        package: &'a str,
2094        /// The slot both would occupy, without a sub-slot.
2095        slot: &'a str,
2096        /// What holds the slot, as `category/name-version` — and `build N` as
2097        /// well, where it is a build this same resolution chose.
2098        held: &'a str,
2099        /// The build that would have joined it, as `category/name-version
2100        /// build N`.
2101        second: &'a str,
2102        /// Whether what holds the slot is already in the root or would be
2103        /// installed by this same resolution.
2104        installed: bool,
2105        /// The atom that asked for the second, as it was written.
2106        atom: &'a str,
2107        /// Who asked for it: the caller, or the package whose dependency it is.
2108        wanted_by: &'a str,
2109    },
2110}
2111
2112/// What a plan's packages weigh on the wire in total.
2113///
2114/// Saturating, because the figures are a mirror's own: `SIZE` is read as a
2115/// `u64` and nothing bounds a sum of them, so an index publishing two packages
2116/// at the largest one overflows the total — a panic in a debug build and a
2117/// wrapped figure in a release one, from a number that only ever reaches a
2118/// progress event. Reported as the largest total there is, which says exactly
2119/// as much as the arithmetic could.
2120fn planned_bytes(packages: &[PlannedPackage]) -> u64 {
2121    packages
2122        .iter()
2123        .map(|package| package.size)
2124        .fold(0, u64::saturating_add)
2125}
2126
2127/// Receives progress from a Gentoo resolution or bootstrap, and decides whether
2128/// it should stop.
2129///
2130/// Bound for one call with [`Gentoo::observe`]. A closure is an observer, so a
2131/// caller that only wants events writes one and never names the trait; a caller
2132/// that also wants to *stop* the work implements this and answers its
2133/// [`cancelled`](GentooObserver::cancelled).
2134///
2135/// Every method has a default body, so a later release adding one does not
2136/// break an implementation.
2137pub trait GentooObserver {
2138    /// Receives one progress event.
2139    fn progress(&mut self, event: GentooEvent<'_>) {
2140        let _ = event;
2141    }
2142
2143    /// Whether the work should stop.
2144    ///
2145    /// Consulted at the boundaries where stopping is clean — a mirror in a
2146    /// walk, a package boundary, either side of a long download. Returning
2147    /// `true` aborts with [`GentooError::Cancelled`], and a run driven through
2148    /// [`provision::ensure`](super::ensure) then removes the staging tree, so a
2149    /// cancelled bootstrap leaves no destination behind.
2150    ///
2151    /// The default is `false`: work that is never cancelled.
2152    fn cancelled(&mut self) -> bool {
2153        false
2154    }
2155}
2156
2157/// A closure is an observer that reports and never cancels.
2158impl<F: FnMut(GentooEvent<'_>)> GentooObserver for F {
2159    fn progress(&mut self, event: GentooEvent<'_>) {
2160        self(event);
2161    }
2162}
2163
2164/// The observer an unobserved call reports to: nowhere, and never cancelled.
2165struct Silent;
2166
2167impl GentooObserver for Silent {}
2168
2169/// An observer that reports nowhere of its own and takes both answers from the
2170/// provisioning run: what an unobserved [`Gentoo`] uses as a [`Provisioner`].
2171///
2172/// The events still reach the run's own observer, wrapped in the shared
2173/// vocabulary — that is the one channel every provisioner reports through.
2174struct RunObserver<'a, 'r> {
2175    request: &'a ProvisionRequest<'r>,
2176}
2177
2178impl GentooObserver for RunObserver<'_, '_> {
2179    fn progress(&mut self, event: GentooEvent<'_>) {
2180        self.request.report(ProvisionEvent::Gentoo(&event));
2181    }
2182
2183    fn cancelled(&mut self) -> bool {
2184        self.request.cancelled()
2185    }
2186}
2187
2188/// An observer that reports to a sink the caller bound with [`Gentoo::observe`]
2189/// and takes its cancellation from the provisioning run.
2190///
2191/// The caller chose the richer, Gentoo-specific channel for events; a
2192/// [`Provision::observe`](crate::provision::Provision::observe) observer can
2193/// still stop a bootstrap it is not reporting on.
2194struct ObservedRun<'a, 'r, 'o> {
2195    sink: &'o mut dyn GentooObserver,
2196    request: &'a ProvisionRequest<'r>,
2197}
2198
2199impl GentooObserver for ObservedRun<'_, '_, '_> {
2200    fn progress(&mut self, event: GentooEvent<'_>) {
2201        self.sink.progress(event);
2202    }
2203
2204    fn cancelled(&mut self) -> bool {
2205        self.sink.cancelled() || self.request.cancelled()
2206    }
2207}
2208
2209/// A [`Gentoo`] with a progress sink bound for one call.
2210///
2211/// Returned by [`Gentoo::observe`]. It implements [`Provisioner`], so it is
2212/// what a caller hands to [`provision::ensure`](super::ensure) when it wants
2213/// progress; and it mirrors the provisioner's own entry points so a resolution
2214/// or a staged layer reports too.
2215pub struct Observed<'o> {
2216    gentoo: &'o mut Gentoo,
2217    sink: &'o mut dyn GentooObserver,
2218}
2219
2220impl fmt::Debug for Observed<'_> {
2221    /// Renders the provisioner being observed. The sink is a caller's closure,
2222    /// so it renders as its presence rather than its contents.
2223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2224        f.debug_struct("Observed")
2225            .field("gentoo", &self.gentoo)
2226            .field("sink", &Delegate("dyn GentooObserver"))
2227            .finish()
2228    }
2229}
2230
2231impl Observed<'_> {
2232    /// Reports what the architecture publishes. See [`Gentoo::available`].
2233    pub fn available(&mut self) -> Result<Available, GentooError> {
2234        let pointer = self.gentoo.read_pointer(self.sink)?;
2235        Ok(Available {
2236            published: pointer.published(),
2237            entries: pointer.entries().to_vec(),
2238        })
2239    }
2240
2241    /// Reports what the binhost publishes. See [`Gentoo::packages`].
2242    pub fn packages(&mut self) -> Result<Catalogue, GentooError> {
2243        Ok(self.gentoo.read_index(self.sink)?.into_catalogue())
2244    }
2245
2246    /// Resolves the install closure against `root`, reporting every refusal it
2247    /// met on the way. See [`Gentoo::resolve_packages`].
2248    pub fn resolve_packages(&mut self, root: impl AsRef<Path>) -> Result<Plan, GentooError> {
2249        let installed = installed::installed(root)?;
2250        self.gentoo.resolve_install(&installed, self.sink)
2251    }
2252
2253    /// Resolves the configured variant to a build. See [`Gentoo::resolve`].
2254    pub fn resolve(&mut self) -> Result<Stage3, GentooError> {
2255        self.gentoo.resolve_reporting(self.sink)
2256    }
2257
2258    /// Stages the configured install over the base layer. See
2259    /// [`Gentoo::stage_layer`].
2260    pub fn stage_layer(&mut self, upper: impl AsRef<Path>) -> Result<BuildLayer, ProvisionError> {
2261        self.gentoo.stage_layer_reporting(upper.as_ref(), self.sink)
2262    }
2263}
2264
2265impl Provisioner for Observed<'_> {
2266    /// Reports to the sink bound here rather than to the run's observer: the
2267    /// caller chose the richer, Gentoo-specific channel by using
2268    /// [`Gentoo::observe`]. Cancellation comes from either, so a
2269    /// [`Provision::observe`](crate::provision::Provision::observe) observer
2270    /// can stop a bootstrap it is not reporting on.
2271    fn provision(&mut self, request: &ProvisionRequest<'_>) -> Result<(), ProvisionError> {
2272        let mut observer = ObservedRun {
2273            sink: self.sink,
2274            request,
2275        };
2276        self.gentoo.provision_reporting(request, &mut observer)
2277    }
2278}
2279
2280/// Where a step reports to.
2281///
2282/// A step never asks whether anything is listening: [`Silent`] is what an
2283/// unobserved call passes, [`RunObserver`] is what a run through
2284/// [`provision::ensure`](super::ensure) passes, and a caller's own sink is what
2285/// [`Gentoo::observe`] passes.
2286type Reporting<'a> = &'a mut dyn GentooObserver;
2287
2288/// Stops where the observer has cancelled the work.
2289///
2290/// Consulted at the boundaries where stopping is clean: nothing has been
2291/// published, and the staging tree [`provision::ensure`](super::ensure) removes
2292/// on any failure is what a partial bootstrap amounts to.
2293fn stop_if_cancelled(to: Reporting<'_>) -> Result<(), GentooError> {
2294    if to.cancelled() {
2295        return Err(GentooError::Cancelled);
2296    }
2297    Ok(())
2298}
2299
2300/// A [`Gentoo`] under construction.
2301pub struct GentooBuilder {
2302    architecture: String,
2303    variant: Option<String>,
2304    binhost: Option<String>,
2305    /// The atoms as the caller wrote them, parsed at
2306    /// [`build`](GentooBuilder::build) so a refusal names the whole
2307    /// configuration rather than the call that added a line to it.
2308    install: Vec<String>,
2309    prefer_use: Vec<String>,
2310    plan: Option<Plan>,
2311    base_layer: Option<PathBuf>,
2312    build_id: Option<String>,
2313    stage3: Option<Stage3>,
2314    max_pointer_age: Option<Duration>,
2315    /// The archive to read, or `None` for the default one.
2316    ///
2317    /// Kept apart from the backstops, as the other two userlands keep theirs,
2318    /// so that the two methods compose whichever order a caller writes them in.
2319    /// The walk list is assembled in [`build`](GentooBuilder::build).
2320    mirror: Option<String>,
2321    fallbacks: Vec<String>,
2322    cache_dir: Option<PathBuf>,
2323    fetcher: Option<Box<dyn Fetch>>,
2324}
2325
2326impl fmt::Debug for GentooBuilder {
2327    /// Renders the configuration, with the transport named by its presence
2328    /// rather than its contents.
2329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2330        f.debug_struct("GentooBuilder")
2331            .field("architecture", &self.architecture)
2332            .field("variant", &self.variant)
2333            .field("binhost", &self.binhost)
2334            .field("install", &self.install)
2335            .field("prefer_use", &self.prefer_use)
2336            .field("plan", &self.plan)
2337            .field("base_layer", &self.base_layer)
2338            .field("build_id", &self.build_id)
2339            .field("stage3", &self.stage3)
2340            .field("max_pointer_age", &self.max_pointer_age)
2341            .field("mirror", &self.mirror)
2342            .field("fallbacks", &self.fallbacks)
2343            .field("cache_dir", &self.cache_dir)
2344            .field(
2345                "fetcher",
2346                &self.fetcher.as_ref().map(|_| Delegate("dyn Fetch")),
2347            )
2348            .finish_non_exhaustive()
2349    }
2350}
2351
2352impl GentooBuilder {
2353    /// The variant to provision, as the pointer document spells it —
2354    /// `amd64-openrc`, `amd64-hardened-systemd`, `x32-openrc`.
2355    ///
2356    /// The spelling is the whole compound the archive publishes, not a variant
2357    /// relative to the architecture: amd64's own document lists `amd64-openrc`
2358    /// and `x32-openrc` as siblings, so a name relative to `amd64` could not
2359    /// reach the second at all. It is matched exactly against the signed
2360    /// enumeration, so an unknown one is refused before anything is fetched,
2361    /// naming what the architecture does offer. [`Gentoo::available`] lists them.
2362    pub fn variant(mut self, variant: impl Into<String>) -> GentooBuilder {
2363        self.variant = Some(variant.into());
2364        self
2365    }
2366
2367    /// Install binary packages from the binhost for this sub-architecture —
2368    /// `x86-64`, `x86-64-v3`, `x86-64_hardened`, `x32`.
2369    ///
2370    /// The binhost publishes one tree per sub-architecture under the
2371    /// architecture's own, and there is no default: the set moves — amd64
2372    /// publishes four today where it published eight — and the only thing that
2373    /// enumerates it is an unsigned directory listing. Reading that listing as
2374    /// discovery would put an unauthenticated document at the head of the trust
2375    /// chain, which is the reason the keyring is vendored, so the
2376    /// sub-architecture is the caller's and a wrong one is a 404 rather than a
2377    /// silently different userland.
2378    ///
2379    /// This selects the archive to read. What to install from it is
2380    /// [`install`](Self::install), and a builder that names a binhost and asks
2381    /// for no package bootstraps exactly as one that names neither.
2382    pub fn binhost(mut self, sub_architecture: impl Into<String>) -> GentooBuilder {
2383        self.binhost = Some(sub_architecture.into());
2384        self
2385    }
2386
2387    /// Install these atoms and their runtime dependencies from the binhost.
2388    ///
2389    /// An atom is the dependency grammar Gentoo itself writes:
2390    /// `dev-vcs/git`, `>=dev-lang/python-3.12`, `dev-libs/openssl:0`,
2391    /// `dev-vcs/git[keyring]`. Repeating this adds to the list.
2392    ///
2393    /// **A USE dependency in an atom is a constraint.** `dev-vcs/git[keyring]`
2394    /// removes every build that was not compiled with the flag, and if the
2395    /// binhost publishes none the atom is unsatisfiable and says so. That is
2396    /// the difference from [`prefer_use`](Self::prefer_use), which reorders the
2397    /// candidates and can never refuse: state a flag the install *must* have
2398    /// here, and a flag you would rather it had there.
2399    ///
2400    /// A blocker — an atom written `!` or `!!` — is refused. It says what must
2401    /// not be installed alongside a package rather than what to install, and
2402    /// this layer installs into a root it is building rather than removing from
2403    /// one.
2404    ///
2405    /// Resolution starts from what the root already has, so an atom the stage3
2406    /// already satisfies costs nothing and pulls nothing.
2407    pub fn install<I, S>(mut self, atoms: I) -> GentooBuilder
2408    where
2409        I: IntoIterator<Item = S>,
2410        S: Into<String>,
2411    {
2412        self.install.extend(atoms.into_iter().map(Into::into));
2413        self
2414    }
2415
2416    /// Prefer builds carrying these USE flags, and builds without the ones
2417    /// written `-flag`.
2418    ///
2419    /// A preference, never a constraint. The archive publishes a version more
2420    /// than once — up to twenty-four times — and the builds differ in the flags
2421    /// they were compiled with; this reorders those candidates and removes
2422    /// none, so it provably cannot turn a resolvable request into an
2423    /// unresolvable one. Measured over four hundred package names, the count of
2424    /// unsatisfiable atoms is the same under every preference as under none.
2425    ///
2426    /// What a broad preference does cost is a larger closure, and a larger
2427    /// closure meets more of the archive's own conflicts: preferring `X`,
2428    /// `gtk`, `qt6` and `systemd` together doubles them. A narrow one costs
2429    /// nothing measurable.
2430    ///
2431    /// State a flag an install must have in the atom instead, where
2432    /// [`install`](Self::install) explains why.
2433    pub fn prefer_use<I, S>(mut self, flags: I) -> GentooBuilder
2434    where
2435        I: IntoIterator<Item = S>,
2436        S: Into<String>,
2437    {
2438        self.prefer_use.extend(flags.into_iter().map(Into::into));
2439        self
2440    }
2441
2442    /// Stage the install as an increment over the Gentoo root at `base`,
2443    /// rather than into a root being bootstrapped.
2444    ///
2445    /// The base is never written to. [`Gentoo::stage_layer`] installs into an
2446    /// overlay upper over it, and a cage rooted on the merged view sees the
2447    /// base's own files and the increment's together.
2448    ///
2449    /// Resolution then describes that merged view rather than the upper alone,
2450    /// which is not a detail: an increment resolving against an empty upper
2451    /// would ask the archive for everything the base already has, and would
2452    /// fail on the packages the binhost does not publish at all.
2453    ///
2454    /// This is the only way to add packages to a root a caller already has,
2455    /// since [`provision::ensure`](super::ensure) reports a directory that
2456    /// exists as `Existing` and does not touch it.
2457    pub fn base_layer(mut self, base: impl AsRef<Path>) -> GentooBuilder {
2458        self.base_layer = Some(base.as_ref().to_path_buf());
2459        self
2460    }
2461
2462    /// Install exactly what this plan names, resolving nothing.
2463    ///
2464    /// A [`Plan`] carries the build id, path, size and digest of every package
2465    /// and the order they merge in, so a run given one fetches no index and
2466    /// makes no choice. That is the reproducibility story for a root built from
2467    /// binaries: the binhost keeps a published version far longer than the five
2468    /// weeks the stage3 autobuilds tree keeps a build, so a plan replays for as
2469    /// long as the archive carries what it names.
2470    ///
2471    /// It supersedes [`install`](Self::install) and
2472    /// [`prefer_use`](Self::prefer_use), which describe a resolution that no
2473    /// longer happens.
2474    pub fn plan(mut self, plan: Plan) -> GentooBuilder {
2475        self.plan = Some(plan);
2476        self
2477    }
2478
2479    /// Resolves within a named build directory instead of the one the pointer
2480    /// currently names — `20260810T204554Z`.
2481    ///
2482    /// What a caller records to provision the same root again later. It skips
2483    /// the enumeration entirely, so the digest document beside that build is
2484    /// what vouches for the tarball.
2485    ///
2486    /// The archive keeps about five weeks of builds and publishes no snapshot
2487    /// service, so a pin is reproducible against the archive for roughly a month
2488    /// and against a populated cache indefinitely. Past that the build id
2489    /// answers 404, which is a fact about Gentoo rather than about this layer.
2490    pub fn build_id(mut self, build_id: impl Into<String>) -> GentooBuilder {
2491        self.build_id = Some(build_id.into());
2492        self
2493    }
2494
2495    /// Provisions a resolution already in hand, performing none.
2496    ///
2497    /// A [`Stage3`] carries everything a provision needs, digest included, so a
2498    /// caller who has resolved once can provision several roots from that one
2499    /// answer without fetching the pointer again.
2500    pub fn stage3(mut self, stage3: Stage3) -> GentooBuilder {
2501        self.stage3 = Some(stage3);
2502        self
2503    }
2504
2505    /// How old the pointer document may be before it is refused, or `None` to
2506    /// accept one of any age.
2507    ///
2508    /// Defaults to thirty days, about four of the weekly regeneration cycles.
2509    /// Gentoo publishes no `Valid-Until` and no signature expiration, so the
2510    /// timestamp inside the signed pointer is the only thing that says when the
2511    /// document stopped being current; without a bound on it, a mirror can
2512    /// replay a correctly signed but months-old pointer forever and pass every
2513    /// other check the crate makes.
2514    ///
2515    /// Clearing the bound gives that up, and is the right answer for a
2516    /// deliberately archived mirror where a stale pointer is the point. It also
2517    /// accepts a pointer that states no timestamp at all, which is otherwise
2518    /// refused: reading its absence as permission would let whoever serves the
2519    /// document disable the check by deleting a line.
2520    pub fn max_pointer_age(mut self, age: impl Into<Option<Duration>>) -> GentooBuilder {
2521        self.max_pointer_age = age.into();
2522        self
2523    }
2524
2525    /// The archive to read, replacing the default `distfiles.gentoo.org`.
2526    ///
2527    /// The URL names the root the `releases/` tree sits under. Repeating this
2528    /// takes the last value; the backstops are
2529    /// [`mirror_fallback`](Self::mirror_fallback)'s and are unaffected.
2530    pub fn mirror(mut self, url: impl Into<String>) -> GentooBuilder {
2531        self.mirror = Some(url.into());
2532        self
2533    }
2534
2535    /// Adds a backstop mirror, tried after the primary and after the backstops
2536    /// already added.
2537    ///
2538    /// The list is walked in order, advancing past a mirror that could not serve
2539    /// a document. A mirror that serves one that does not verify is fatal rather
2540    /// than a reason to try the next: what it served was answered for by the URL
2541    /// that was asked for, and the answer to a refused signature is never to ask
2542    /// somewhere else.
2543    pub fn mirror_fallback(mut self, url: impl Into<String>) -> GentooBuilder {
2544        self.fallbacks.push(url.into());
2545        self
2546    }
2547
2548    /// Where the downloaded tarball is kept across runs.
2549    ///
2550    /// A directory the caller owns and that survives the run. Without one the
2551    /// tarball is downloaded beside the tree being built and removed with it,
2552    /// which costs a re-download on the next provision of the same build.
2553    pub fn cache_dir(mut self, dir: impl AsRef<std::path::Path>) -> GentooBuilder {
2554        self.cache_dir = Some(dir.as_ref().to_path_buf());
2555        self
2556    }
2557
2558    /// The transport every fetch goes through, replacing the bundled HTTP
2559    /// client.
2560    ///
2561    /// The archive serves plain HTTP with no redirect to HTTPS, so the bundled
2562    /// client suffices; a caller wanting TLS, a proxy, or a private mirror
2563    /// protocol supplies their own.
2564    pub fn fetcher(mut self, fetcher: Box<dyn Fetch>) -> GentooBuilder {
2565        self.fetcher = Some(fetcher);
2566        self
2567    }
2568
2569    /// Builds the provisioner.
2570    ///
2571    /// # Errors
2572    ///
2573    /// Returns [`GentooError::Config`] for an architecture or a build id a
2574    /// request line or a path cannot carry, and [`GentooError::Signature`] if
2575    /// the keyring the crate vendors does not parse — which is a defect in this
2576    /// crate rather than anything a caller did.
2577    pub fn build(self) -> Result<Gentoo, GentooError> {
2578        // Both become directory segments in every URL the layer fetches, so
2579        // both are checked before they can reach one. The build id is the one
2580        // worth naming: a pin skips the signed enumeration, so it is a caller's
2581        // string with nothing between it and a request line.
2582        coordinate::check(
2583            "the archive's architecture",
2584            &self.architecture,
2585            Nesting::Single,
2586        )
2587        .map_err(GentooError::config)?;
2588        if let Some(build_id) = &self.build_id {
2589            coordinate::check("the archive's build id", build_id, Nesting::Single)
2590                .map_err(GentooError::config)?;
2591        }
2592        if let Some(binhost) = &self.binhost {
2593            coordinate::check("the archive's binhost", binhost, Nesting::Single)
2594                .map_err(GentooError::config)?;
2595        }
2596        // The install list and the preferences are parsed here rather than
2597        // where they were set, so a misspelled atom is refused before anything
2598        // is fetched and the refusal names the whole configuration rather than
2599        // one call. It is also what makes an atom's USE dependency a real
2600        // constraint: it is part of the grammar the resolver matches with, so
2601        // parsing it is the only thing that has to happen for it to apply.
2602        let install = self
2603            .install
2604            .iter()
2605            .map(|text| atom::parse(text))
2606            .collect::<Result<Vec<_>, String>>()
2607            .map_err(GentooError::config)?;
2608        if let Some(blocked) = install.iter().find(|atom| atom.blocker().is_some()) {
2609            return Err(GentooError::config(format!(
2610                "the install list holds the blocker {blocked}; a blocker says what must not be \
2611                 installed alongside a package rather than what to install, and this layer \
2612                 installs into a root it is building rather than removing from one",
2613            )));
2614        }
2615        let prefer_use = self
2616            .prefer_use
2617            .iter()
2618            .map(|text| resolve::Preference::parse(text))
2619            .collect::<Result<Vec<_>, String>>()
2620            .map_err(GentooError::config)?;
2621        if let Some(plan) = &self.plan {
2622            if let Some(reason) = plan.unusable() {
2623                return Err(GentooError::config(reason));
2624            }
2625            // A plan names the architecture and the binhost it was resolved
2626            // for, and the install wave composes a URL from the builder's
2627            // architecture and the plan's binhost. Left unchecked, a plan
2628            // resolved for one architecture and replayed through a builder for
2629            // another fetches `releases/<builder>/binpackages/23.0/<plan>/...`
2630            // and 404s against a mirror that was spelled correctly -- and where
2631            // the two happen to publish a tree of the same name, it does not
2632            // fail at all. Refusing the pair here is the diagnostic that names
2633            // what is actually wrong.
2634            if plan.architecture != self.architecture {
2635                return Err(GentooError::config(format!(
2636                    "the plan was resolved for the architecture {:?} and this builder provisions \
2637                     {:?}; build the plan's own architecture, or resolve a plan for this one",
2638                    plan.architecture, self.architecture,
2639                )));
2640            }
2641            if let Some(binhost) = &self.binhost
2642                && *binhost != plan.binhost
2643            {
2644                return Err(GentooError::config(format!(
2645                    "the plan names the binhost {:?} and this builder names {:?}; a plan installs \
2646                     the containers it resolved, so the two cannot differ",
2647                    plan.binhost, binhost,
2648                )));
2649            }
2650        }
2651        let keyring = Keyring::parse(EMBEDDED_KEYRING)
2652            .map_err(|err| GentooError::signature("the vendored Gentoo keyring", err))?;
2653        // The walk list is the primary followed by its backstops, composed
2654        // here so that neither method can undo the other.
2655        let mut mirrors = Vec::with_capacity(1 + self.fallbacks.len());
2656        mirrors.push(self.mirror.unwrap_or_else(|| DEFAULT_MIRROR.to_string()));
2657        mirrors.extend(self.fallbacks);
2658        Ok(Gentoo {
2659            architecture: self.architecture,
2660            variant: self.variant,
2661            binhost: self.binhost,
2662            install,
2663            prefer_use,
2664            plan: self.plan,
2665            base_layer: self.base_layer,
2666            build_id: self.build_id,
2667            stage3: self.stage3,
2668            max_pointer_age: self.max_pointer_age,
2669            mirrors,
2670            cache_dir: self.cache_dir,
2671            keyring,
2672            fetcher: self
2673                .fetcher
2674                .unwrap_or_else(|| Box::new(HttpFetch::new()) as Box<dyn Fetch>),
2675        })
2676    }
2677}
2678
2679#[cfg(test)]
2680mod tests {
2681
2682    /// A planned package weighing `size`, otherwise minimal.
2683    fn weighing(size: u64) -> PlannedPackage {
2684        let stanza = format!(
2685            "BUILD_ID: 1\nCPV: sys-libs/zlib-1.3.1\n\
2686             MD5: d41d8cd98f00b204e9800998ecf8427e\n\
2687             PATH: sys-libs/zlib/zlib-1.3.1-1.gpkg.tar\nSIZE: {size}\n"
2688        );
2689        let index = index::Index::parse(&format!("TIMESTAMP: 1\n\n{stanza}"));
2690        Plan::project("amd64", "amd64", None, index.records())
2691            .packages
2692            .pop()
2693            .expect("the fixture publishes one build")
2694    }
2695
2696    #[test]
2697    fn a_planned_total_saturates_rather_than_overflowing() {
2698        // `SIZE` is a mirror's own number, read as a u64 and bounded by nothing
2699        // else. Two packages at the largest one overflow the sum, which panics
2700        // a debug build over a figure that only ever reaches a progress event.
2701        assert_eq!(planned_bytes(&[weighing(2), weighing(3)]), 5);
2702        assert_eq!(
2703            planned_bytes(&[weighing(u64::MAX), weighing(u64::MAX)]),
2704            u64::MAX,
2705        );
2706        assert_eq!(planned_bytes(&[]), 0);
2707    }
2708    use std::collections::HashMap;
2709    use std::io::Write;
2710
2711    use pgp::composed::SignedSecretKey;
2712    use pgp::types::KeyDetails as _;
2713
2714    use super::*;
2715    use crate::provision::openpgp::fixture;
2716    use crate::scratch::Scratch;
2717
2718    /// The architecture every test below reads.
2719    const ARCH: &str = "amd64";
2720
2721    /// The build the fixture pointer names.
2722    const BUILD: &str = "20260810T204554Z";
2723
2724    /// A transport serving a fixed set of URLs and nothing else.
2725    ///
2726    /// Each layer keeps its own rather than sharing one from the test kit: that
2727    /// crate is a dev-dependency whose features forward to this one's, so
2728    /// reaching for it from a unit test would unify those features into every
2729    /// `cargo test` of the library and take the meaning out of the featureless
2730    /// and powerset checks.
2731    struct Canned {
2732        served: HashMap<String, Vec<u8>>,
2733        /// The most bytes one write to the sink carries.
2734        ///
2735        /// A real transport delivers a body in read-sized pieces, and the
2736        /// download consults the run's cancellation on each of them, so a
2737        /// fixture that wrote every body in a single call could not exercise a
2738        /// stop part way through one.
2739        chunk: usize,
2740    }
2741
2742    impl Canned {
2743        /// A transport serving `served`, in pieces no real body would exceed.
2744        fn new(served: HashMap<String, Vec<u8>>) -> Canned {
2745            Canned {
2746                served,
2747                chunk: 64 * 1024,
2748            }
2749        }
2750    }
2751
2752    impl Fetch for Canned {
2753        fn fetch(
2754            &mut self,
2755            request: &FetchRequest<'_>,
2756            sink: &mut dyn Write,
2757        ) -> Result<(), FetchError> {
2758            match self.served.get(request.url()) {
2759                Some(body) => body
2760                    .chunks(self.chunk)
2761                    .try_for_each(|chunk| sink.write_all(chunk))
2762                    .map_err(FetchError::at("writing the body", request.url())),
2763                None => Err(FetchError::not_found(request.url())),
2764            }
2765        }
2766    }
2767
2768    /// A transport that refuses everything the way an unreachable mirror does.
2769    struct Unreachable;
2770
2771    impl Fetch for Unreachable {
2772        fn fetch(
2773            &mut self,
2774            request: &FetchRequest<'_>,
2775            _sink: &mut dyn Write,
2776        ) -> Result<(), FetchError> {
2777            Err(FetchError::status(request.url(), 503))
2778        }
2779    }
2780
2781    /// A one-file tar archive, as the layer will extract it.
2782    fn tarball_bytes() -> Vec<u8> {
2783        use crate::provision::tar;
2784        let body = b"a provisioned root\n";
2785        let mut out = Vec::new();
2786        for (name, mode, size, typeflag) in [
2787            ("etc/", 0o755, 0u64, b'5'),
2788            ("etc/marker", 0o644, body.len() as u64, b'0'),
2789        ] {
2790            out.extend_from_slice(
2791                &tar::ustar_block(name.as_bytes(), b"", mode, 0, 0, size, 0, typeflag, b"")
2792                    .expect("a ustar header"),
2793            );
2794            if size > 0 {
2795                out.extend_from_slice(body);
2796                tar::pad(&mut out, size).expect("padding a member");
2797            }
2798        }
2799        // Two zero blocks end the archive.
2800        out.extend_from_slice(&[0u8; 1024]);
2801        out
2802    }
2803
2804    /// The digest the fixture digest document records for the tarball.
2805    fn tarball_sha512() -> String {
2806        Algorithm::Sha512.hex_of(&tarball_bytes())
2807    }
2808
2809    /// A pointer body naming two variants of one build, stamped `published`.
2810    fn pointer_body(published: Option<i64>) -> String {
2811        let stamp = match published {
2812            Some(epoch) => format!("# ts={epoch}\n"),
2813            None => String::new(),
2814        };
2815        format!(
2816            "# Latest as of some time\n{stamp}\
2817             {BUILD}/stage3-amd64-openrc-{BUILD}.tar.xz 495865168\n\
2818             {BUILD}/stage3-amd64-openrc-splitusr-{BUILD}.tar.xz 494479304\n",
2819        )
2820    }
2821
2822    /// A digest document for a variant's tarball, recording the sidecar's
2823    /// SHA-512 first so a reader keying on the algorithm alone takes the wrong
2824    /// one.
2825    fn digests_body(variant: &str) -> String {
2826        format!(
2827            "# SHA512 HASH\n{sidecar}  stage3-{variant}-{BUILD}.tar.xz.CONTENTS.gz\n\
2828             # SHA512 HASH\n{tarball}  stage3-{variant}-{BUILD}.tar.xz\n\
2829             # BLAKE2B HASH\n{blake}  stage3-{variant}-{BUILD}.tar.xz\n",
2830            sidecar = "b".repeat(128),
2831            tarball = tarball_sha512(),
2832            blake = "c".repeat(128),
2833        )
2834    }
2835
2836    /// The archive as the fixtures publish it, signed by `key`.
2837    fn archive(key: &SignedSecretKey, published: Option<i64>) -> Canned {
2838        let at = |suffix: &str| format!("{DEFAULT_MIRROR}/releases/{ARCH}/autobuilds/{suffix}");
2839        let mut served = HashMap::from([(
2840            at("latest-stage3.txt"),
2841            fixture::clearsign(key, &pointer_body(published)).into_bytes(),
2842        )]);
2843        for variant in ["amd64-openrc", "amd64-openrc-splitusr"] {
2844            served.insert(
2845                at(&format!("{BUILD}/stage3-{variant}-{BUILD}.tar.xz.DIGESTS")),
2846                fixture::clearsign(key, &digests_body(variant)).into_bytes(),
2847            );
2848            served.insert(
2849                at(&format!("{BUILD}/stage3-{variant}-{BUILD}.tar.xz")),
2850                tarball_bytes(),
2851            );
2852        }
2853        Canned::new(served)
2854    }
2855
2856    /// A provisioner trusting `key` alone and reading `fetcher`.
2857    ///
2858    /// Built as the value rather than through the builder, because what the
2859    /// tests need is a keyring holding a key generated in the test, and the
2860    /// public surface deliberately offers no way to replace the vendored one.
2861    fn provisioner(key: &SignedSecretKey, fetcher: Box<dyn Fetch>) -> Gentoo {
2862        Gentoo {
2863            architecture: ARCH.to_string(),
2864            variant: Some("amd64-openrc".to_string()),
2865            binhost: None,
2866            install: Vec::new(),
2867            prefer_use: Vec::new(),
2868            plan: None,
2869            base_layer: None,
2870            build_id: None,
2871            stage3: None,
2872            max_pointer_age: Some(DEFAULT_MAX_POINTER_AGE),
2873            mirrors: vec![DEFAULT_MIRROR.to_string()],
2874            cache_dir: None,
2875            keyring: fixture::keyring(key),
2876            fetcher,
2877        }
2878    }
2879
2880    /// A provisioner over a freshly stamped archive signed by a fresh key.
2881    fn current() -> (SignedSecretKey, Gentoo) {
2882        let key = fixture::signing_key("releng <releng@test.invalid>");
2883        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
2884        let gentoo = provisioner(&key, Box::new(archive(&key, Some(now))));
2885        (key, gentoo)
2886    }
2887
2888    /// The provisioner every test below reads the keyring through.
2889    fn gentoo() -> Gentoo {
2890        Gentoo::builder("amd64")
2891            .build()
2892            .expect("amd64 is an ordinary architecture and the keyring is vendored")
2893    }
2894
2895    #[test]
2896    fn the_vendored_keyring_parses_whole() {
2897        // `Keyring::parse` fails the entire keyring if one certificate fails,
2898        // by deliberate design, so this asserts every certificate Gentoo
2899        // publishes is one rPGP reads. The count is the shape of the published
2900        // set: a refresh that changes it is a change worth seeing in a diff.
2901        assert_eq!(gentoo().keyring_horizon().len(), 11);
2902    }
2903
2904    #[test]
2905    fn the_horizon_names_each_certificate_and_its_expiry() {
2906        // The releng certificate is the one this layer actually meets: it signs
2907        // the pointer and the digest documents, through a signing subkey. Both
2908        // it and that subkey run to 2028-07-01.
2909        let horizon = gentoo().keyring_horizon();
2910        let releng = horizon
2911            .iter()
2912            .find(|held| held.fingerprint == "13EBBDBEDE7A12775DFDB1BABB572E0E2D182910")
2913            .expect("the Automated Weekly Release Key is in the vendored keyring");
2914        assert_eq!(releng.expires, Some(1_846_022_400), "2028-07-01");
2915        assert!(releng.signs);
2916        assert_eq!(
2917            releng.user_id.as_deref(),
2918            Some(
2919                "Gentoo Linux Release Engineering (Automated Weekly Release Key) <releng@gentoo.org>"
2920            ),
2921        );
2922
2923        // Every certificate is bounded: Gentoo publishes no perpetual key, and
2924        // one appearing would be worth noticing rather than passing over.
2925        assert!(
2926            horizon.iter().all(|held| held.expires.is_some()),
2927            "every Gentoo certificate carries an expiry",
2928        );
2929    }
2930
2931    #[test]
2932    fn a_certificate_that_can_vouch_for_nothing_says_so() {
2933        // The 2004 release signing key is DSA-1024, which the public-key floor
2934        // refuses, and it delegates to no signing subkey. Its expiry reads like
2935        // every other certificate's, so without this flag it is
2936        // indistinguishable from a working anchor.
2937        let horizon = gentoo().keyring_horizon();
2938        let unusable: Vec<&str> = horizon
2939            .iter()
2940            .filter(|held| !held.signs)
2941            .map(|held| held.fingerprint.as_str())
2942            .collect();
2943        assert_eq!(unusable, ["D99EAC7379A850BCE47DA5F29E6438C817072058"]);
2944    }
2945
2946    #[test]
2947    fn the_certificates_do_not_share_one_expiry() {
2948        // The finding the per-certificate report exists for. Two certificates
2949        // lapse about twenty months before the rest and neither signs anything
2950        // this layer reads, so a single earliest-expiry number would report a
2951        // keyring as nearly stale while the key that matters is good for years.
2952        let horizon = gentoo().keyring_horizon();
2953        let mut dates: Vec<Option<i64>> = horizon.iter().map(|held| held.expires).collect();
2954        dates.sort_unstable();
2955        dates.dedup();
2956        assert_eq!(
2957            dates,
2958            [
2959                Some(1_795_718_963), // 2026-11-26, Gentoo Infra (finch)
2960                Some(1_795_719_147), // 2026-11-26, Gentoo Infra (petrel)
2961                Some(1_814_427_500), // 2027-07-01, GLSAMaker
2962                Some(1_846_022_400), // 2028-07-01, the remaining eight
2963            ],
2964        );
2965    }
2966
2967    #[test]
2968    fn a_user_id_comes_from_a_certification_the_key_actually_made() {
2969        // A certificate reports one user id, always the same one, and it is one
2970        // the certificate itself certified -- an unverified packet must not be
2971        // able to relabel a key in a report a caller reads. Gentoo's ebuild
2972        // repository key carries two user ids, flags neither primary, and last
2973        // certified both in the same second, so it is the case where a rule that
2974        // did not decide the tie would report differently between runs.
2975        let ebuild = |horizon: Vec<CertificateHorizon>| {
2976            horizon
2977                .into_iter()
2978                .find(|held| held.fingerprint == "DCD05B71EAB94199527F44ACDB6B8C1F96D8BF6D")
2979                .expect("the ebuild repository signing key is in the vendored keyring")
2980                .user_id
2981        };
2982        let reported = ebuild(gentoo().keyring_horizon());
2983        assert!(
2984            [
2985                "Gentoo ebuild repository signing key (Automated Signing Key) \
2986                 <infrastructure@gentoo.org>",
2987                "Gentoo Portage Snapshot Signing Key (Automated Signing Key)",
2988            ]
2989            .contains(&reported.as_deref().expect("a UTF-8 user id")),
2990            "an unexpected user id: {reported:?}",
2991        );
2992        assert_eq!(reported, ebuild(gentoo().keyring_horizon()));
2993    }
2994
2995    #[test]
2996    fn an_architecture_a_url_cannot_carry_is_refused_before_anything_is_fetched() {
2997        // The architecture becomes a directory in every URL the layer fetches,
2998        // so a traversal in it is refused at the boundary rather than resolved
2999        // against the archive root.
3000        for architecture in ["../etc", "/etc", "amd64/x32", "", "amd 64"] {
3001            let err = Gentoo::builder(architecture)
3002                .build()
3003                .expect_err("the architecture is refused");
3004            assert!(
3005                matches!(err, GentooError::Config { .. }),
3006                "{architecture:?}: {err}",
3007            );
3008        }
3009        Gentoo::builder("arm64")
3010            .build()
3011            .expect("an ordinary architecture is accepted");
3012    }
3013
3014    #[test]
3015    fn a_resolution_reads_the_digest_the_document_records_for_the_tarball() {
3016        // The whole read half, end to end over signed documents: the pointer
3017        // names the build, the digest document beside it names the SHA-512, and
3018        // the certificate that vouched for the second is reported so a caller
3019        // knows what the chain rests on.
3020        let (key, mut gentoo) = current();
3021        let stage3 = gentoo.resolve().expect("the archive resolves");
3022        assert_eq!(stage3.variant(), "amd64-openrc");
3023        assert_eq!(stage3.build_id(), BUILD);
3024        assert_eq!(
3025            stage3.path(),
3026            format!("{BUILD}/stage3-amd64-openrc-{BUILD}.tar.xz"),
3027        );
3028        // The size comes from the enumeration, so it is the exact length the
3029        // archive published rather than a ceiling.
3030        assert_eq!(stage3.size(), 495_865_168);
3031        // The sidecar's digest sits first in the document, so a reader keying
3032        // on the algorithm alone would have taken it.
3033        assert_eq!(stage3.sha512(), tarball_sha512());
3034        assert_eq!(
3035            stage3.certificate(),
3036            format!("{:X}", key.to_public_key().fingerprint()),
3037            "the certificate that vouched for the digests",
3038        );
3039    }
3040
3041    #[test]
3042    fn the_enumeration_is_what_the_architecture_publishes() {
3043        let (_key, mut gentoo) = current();
3044        let available = gentoo.available().expect("the pointer verifies");
3045        assert_eq!(
3046            available.variants().collect::<Vec<_>>(),
3047            ["amd64-openrc", "amd64-openrc-splitusr"],
3048        );
3049        assert!(available.contains("amd64-openrc-splitusr"));
3050        assert!(!available.contains("amd64"), "no prefix match");
3051        assert_eq!(available.build_id("amd64-openrc"), Some(BUILD));
3052        assert_eq!(available.size("amd64-openrc"), Some(495_865_168));
3053        assert!(available.published().is_some());
3054    }
3055
3056    #[test]
3057    fn a_variant_the_architecture_does_not_publish_is_refused_with_the_ones_it_does() {
3058        // Refused against the signed list before anything is fetched, rather
3059        // than as a 404 on a URL composed from a name Gentoo never published.
3060        let key = fixture::signing_key("releng <releng@test.invalid>");
3061        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3062        let mut gentoo = provisioner(&key, Box::new(archive(&key, Some(now))));
3063        gentoo.variant = Some("amd64-hardened-openrc".to_string());
3064        let err = gentoo.resolve().expect_err("the variant is not published");
3065        let message = err.to_string();
3066        assert!(matches!(err, GentooError::Config { .. }), "{message}");
3067        assert!(message.contains("amd64-hardened-openrc"), "{message}");
3068        assert!(message.contains("amd64-openrc-splitusr"), "{message}");
3069    }
3070
3071    #[test]
3072    fn a_pointer_older_than_the_bound_is_refused() {
3073        // The rollback the bound exists to close: the document is correctly
3074        // signed by a key the keyring holds, the digests match, and every other
3075        // check passes. Only its own regeneration timestamp says it stopped
3076        // being current.
3077        let key = fixture::signing_key("releng <releng@test.invalid>");
3078        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3079        let ancient = now - 400 * 24 * 60 * 60;
3080        let mut gentoo = provisioner(&key, Box::new(archive(&key, Some(ancient))));
3081        let err = gentoo.resolve().expect_err("the pointer is stale");
3082        assert!(
3083            matches!(err, GentooError::Stale { published: Some(at), .. } if at == ancient),
3084            "{err}",
3085        );
3086
3087        // Clearing the bound is what a deliberately archived mirror needs, and
3088        // it is the only thing that accepts the same document.
3089        let mut archived = provisioner(&key, Box::new(archive(&key, Some(ancient))));
3090        archived.max_pointer_age = None;
3091        archived.resolve().expect("an archived mirror is accepted");
3092    }
3093
3094    #[test]
3095    fn a_pointer_stating_no_timestamp_is_refused_rather_than_waved_through() {
3096        // Absence is not permission: the timestamp is the only bound the layer
3097        // has, so reading a missing line as "no limit" would let whoever serves
3098        // the document disable the check by deleting it.
3099        let key = fixture::signing_key("releng <releng@test.invalid>");
3100        let mut gentoo = provisioner(&key, Box::new(archive(&key, None)));
3101        let err = gentoo.resolve().expect_err("an undated pointer is refused");
3102        assert!(
3103            matches!(
3104                err,
3105                GentooError::Stale {
3106                    published: None,
3107                    age: None,
3108                    ..
3109                },
3110            ),
3111            "{err}",
3112        );
3113
3114        // A caller who has cleared the bound has said they do not care, and an
3115        // undated document is then no worse than an old one.
3116        let mut archived = provisioner(&key, Box::new(archive(&key, None)));
3117        archived.max_pointer_age = None;
3118        archived.resolve().expect("the bound is cleared");
3119    }
3120
3121    #[test]
3122    fn a_document_signed_by_a_key_outside_the_keyring_is_refused() {
3123        // The keyring is the whole trust anchor, so a perfectly formed archive
3124        // signed by anyone else verifies nothing -- and the refusal is a
3125        // signature failure rather than a document one, since the bytes never
3126        // became a document at all.
3127        let trusted = fixture::signing_key("releng <releng@test.invalid>");
3128        let forger = fixture::signing_key("forger <forger@test.invalid>");
3129        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3130        let mut gentoo = provisioner(&trusted, Box::new(archive(&forger, Some(now))));
3131        let err = gentoo
3132            .resolve()
3133            .expect_err("a foreign signature verifies nothing");
3134        assert!(matches!(err, GentooError::Signature { .. }), "{err}");
3135    }
3136
3137    #[test]
3138    fn an_unsigned_pointer_is_refused() {
3139        // Gentoo publishes no unsigned path and this layer offers none: a body
3140        // with no cleartext signature is not a document, however plausible its
3141        // contents.
3142        let key = fixture::signing_key("releng <releng@test.invalid>");
3143        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3144        let at = |suffix: &str| format!("{DEFAULT_MIRROR}/releases/{ARCH}/autobuilds/{suffix}");
3145        let bare = Canned::new(HashMap::from([(
3146            at("latest-stage3.txt"),
3147            pointer_body(Some(now)).into_bytes(),
3148        )]));
3149        let mut gentoo = provisioner(&key, Box::new(bare));
3150        let err = gentoo
3151            .resolve()
3152            .expect_err("an unsigned pointer is refused");
3153        assert!(matches!(err, GentooError::Signature { .. }), "{err}");
3154    }
3155
3156    #[test]
3157    fn a_pinned_build_id_skips_the_enumeration_and_is_checked_on_its_way_to_a_url() {
3158        // A pin is the caller saying which build they want, so the pointer --
3159        // which only ever names the current one -- is not consulted. That makes
3160        // the build id a caller's string with nothing between it and a request
3161        // line, which is why it passes the coordinate check.
3162        let key = fixture::signing_key("releng <releng@test.invalid>");
3163        let mut pinned = provisioner(&key, Box::new(archive(&key, Some(0))));
3164        pinned.build_id = Some(BUILD.to_string());
3165        let stage3 = pinned
3166            .resolve()
3167            .expect("a pin resolves without reading the pointer");
3168        assert_eq!(stage3.build_id(), BUILD);
3169        assert_eq!(stage3.sha512(), tarball_sha512());
3170        // Nothing declared a size, so the fetch falls back to the fixed ceiling.
3171        assert_eq!(stage3.size(), MAX_STAGE3_BYTES);
3172
3173        for hostile in ["../../etc", "/etc", "a/b", "", "a b"] {
3174            let err = Gentoo::builder(ARCH)
3175                .build_id(hostile)
3176                .build()
3177                .expect_err("the build id is refused");
3178            assert!(matches!(err, GentooError::Config { .. }), "{hostile:?}");
3179        }
3180    }
3181
3182    #[test]
3183    fn a_resolution_already_in_hand_is_not_performed_again() {
3184        // What lets a caller provision several roots from one answer. The
3185        // transport refuses everything, so a resolve that reached it would fail.
3186        let key = fixture::signing_key("releng <releng@test.invalid>");
3187        let (_, mut resolved) = current();
3188        let stage3 = resolved.resolve().expect("the archive resolves");
3189        let mut carried = provisioner(&key, Box::new(Unreachable));
3190        carried.stage3 = Some(stage3.clone());
3191        assert_eq!(carried.resolve().expect("the answer is carried"), stage3);
3192    }
3193
3194    #[test]
3195    fn a_mirror_that_cannot_serve_is_walked_past_and_one_that_serves_a_forgery_is_not() {
3196        // The distinction the shared failover predicate draws. An unreachable
3197        // mirror is a reason to ask the next; one that answered with something
3198        // that does not verify is not, since what it served was answered for by
3199        // the URL that was asked for.
3200        let key = fixture::signing_key("releng <releng@test.invalid>");
3201        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3202        let mut walked = provisioner(&key, Box::new(archive(&key, Some(now))));
3203        walked.mirrors = vec![
3204            "http://unreachable.invalid".to_string(),
3205            DEFAULT_MIRROR.to_string(),
3206        ];
3207        walked
3208            .resolve()
3209            .expect("the walk advances past a mirror that served nothing");
3210
3211        // Every mirror unreachable reports the transport failure rather than a
3212        // signature one, so a caller can tell an outage from an attack.
3213        let mut lost = provisioner(&key, Box::new(Unreachable));
3214        assert!(matches!(
3215            lost.resolve().expect_err("no mirror answered"),
3216            GentooError::Fetch(_),
3217        ));
3218    }
3219
3220    #[test]
3221    fn the_primary_leads_the_walk_whichever_order_the_two_methods_are_called_in() {
3222        // The two are separate settings composed at build time, as they are on
3223        // the other two builders, so a caller who names a backstop before the
3224        // mirror it backs still has both.
3225        let walked = |gentoo: Gentoo| gentoo.mirrors;
3226        let built = |builder: GentooBuilder| walked(builder.build().expect("the builder is sound"));
3227
3228        assert_eq!(
3229            built(
3230                Gentoo::builder(ARCH)
3231                    .mirror_fallback("http://b.invalid")
3232                    .mirror("http://a.invalid"),
3233            ),
3234            ["http://a.invalid", "http://b.invalid"],
3235        );
3236        assert_eq!(
3237            built(
3238                Gentoo::builder(ARCH)
3239                    .mirror("http://a.invalid")
3240                    .mirror_fallback("http://b.invalid"),
3241            ),
3242            ["http://a.invalid", "http://b.invalid"],
3243        );
3244
3245        // A backstop alone backs the default mirror rather than replacing it,
3246        // and repeating the primary takes the last value.
3247        assert_eq!(
3248            built(Gentoo::builder(ARCH).mirror_fallback("http://b.invalid")),
3249            [DEFAULT_MIRROR, "http://b.invalid"],
3250        );
3251        assert_eq!(
3252            built(
3253                Gentoo::builder(ARCH)
3254                    .mirror("http://a.invalid")
3255                    .mirror("http://c.invalid"),
3256            ),
3257            ["http://c.invalid"],
3258        );
3259        assert_eq!(built(Gentoo::builder(ARCH)), [DEFAULT_MIRROR]);
3260    }
3261
3262    #[test]
3263    fn a_resolution_without_a_variant_says_what_to_name() {
3264        let key = fixture::signing_key("releng <releng@test.invalid>");
3265        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3266        let mut gentoo = provisioner(&key, Box::new(archive(&key, Some(now))));
3267        gentoo.variant = None;
3268        let err = gentoo.resolve().expect_err("no variant was named");
3269        assert!(err.to_string().contains("Gentoo::available"), "{err}");
3270    }
3271
3272    /// The binhost sub-architecture every index test below reads.
3273    const BINHOST: &str = "x86-64";
3274
3275    /// A two-record index document, enough to be a package set.
3276    fn index_body() -> String {
3277        format!(
3278            "TIMESTAMP: 1787145736\nCHOST: x86_64-pc-linux-gnu\nPACKAGES: 1\n\n\
3279             BUILD_ID: 1\nCPV: sys-libs/zlib-1.3.1-r1\n\
3280             MD5: 3a61f698748c7323c87081584f383b3c\n\
3281             PATH: sys-libs/zlib/zlib-1.3.1-r1-1.gpkg.tar\nSIZE: 204800\n\
3282             SLOT: 0/1\nUSE: amd64 minizip\nREPO: gentoo\n\n\
3283             BUILD_ID: 1\nCPV: dev-vcs/git-2.54.0\n\
3284             MD5: 6a539af72c5577fdce267834d3dfd2f1\n\
3285             PATH: dev-vcs/git/git-2.54.0-1.gpkg.tar\nSIZE: 8192000\n\
3286             RDEPEND: >={arch}/zlib-1.3\nUSE: amd64 curl\nREPO: gentoo\n",
3287            arch = "sys-libs",
3288        )
3289    }
3290
3291    /// `body` as a gzip member, which is how the archive publishes the index.
3292    fn gzipped(body: &str) -> Vec<u8> {
3293        let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
3294        encoder.write_all(body.as_bytes()).expect("gzip accepts it");
3295        encoder.finish().expect("the member closes")
3296    }
3297
3298    /// The mirror-relative URL of `name` in the binhost this test reads.
3299    fn binhost_url(name: &str) -> String {
3300        format!(
3301            "{DEFAULT_MIRROR}/{}/{name}",
3302            index::directory(ARCH, BINHOST),
3303        )
3304    }
3305
3306    /// A provisioner reading a binhost served by `fetcher`.
3307    fn binhost_provisioner(fetcher: Box<dyn Fetch>) -> Gentoo {
3308        let key = fixture::signing_key("releng <releng@test.invalid>");
3309        let mut gentoo = provisioner(&key, fetcher);
3310        gentoo.binhost = Some(BINHOST.to_string());
3311        gentoo
3312    }
3313
3314    #[test]
3315    fn the_index_is_read_from_the_compressed_copy_where_one_is_served() {
3316        // A tenth of the bytes for identical content, and the decoder is
3317        // selected by magic rather than by the name -- so the plain copy being
3318        // present too changes nothing about which is read.
3319        let served = HashMap::from([
3320            (binhost_url("Packages.gz"), gzipped(&index_body())),
3321            (binhost_url("Packages"), index_body().into_bytes()),
3322        ]);
3323        let mut gentoo = binhost_provisioner(Box::new(Canned::new(served)));
3324        let catalogue = gentoo.packages().expect("the index is served");
3325        assert_eq!(catalogue.len(), 2);
3326        assert_eq!(catalogue.builds(), 2);
3327        assert!(catalogue.contains("dev-vcs/git"));
3328        assert_eq!(
3329            catalogue.versions("sys-libs/zlib").collect::<Vec<_>>(),
3330            ["1.3.1-r1"],
3331        );
3332        assert_eq!(catalogue.generated(), Some(1_787_145_736));
3333        assert_eq!(catalogue.chost(), Some("x86_64-pc-linux-gnu"));
3334    }
3335
3336    #[test]
3337    fn a_mirror_serving_only_the_plain_copy_is_read_from_it() {
3338        let served = HashMap::from([(binhost_url("Packages"), index_body().into_bytes())]);
3339        let mut gentoo = binhost_provisioner(Box::new(Canned::new(served)));
3340        assert_eq!(gentoo.packages().expect("the plain copy answers").len(), 2);
3341    }
3342
3343    #[test]
3344    fn an_index_cut_short_is_refused_against_its_own_count() {
3345        // A truncated plain-text index parses perfectly and resolves against a
3346        // silently smaller archive, and its own PACKAGES field is the only
3347        // thing that would notice. Not a security property -- a mirror
3348        // composing the stanzas composes the count too -- but the shape a
3349        // half-written mirror actually has.
3350        let body = index_body();
3351        let short = body
3352            .split_once("BUILD_ID: 1\nCPV: dev-vcs/git")
3353            .expect("the fixture holds two records")
3354            .0
3355            .replace("PACKAGES: 1", "PACKAGES: 2");
3356        let served = HashMap::from([(binhost_url("Packages.gz"), gzipped(&short))]);
3357        let mut gentoo = binhost_provisioner(Box::new(Canned::new(served)));
3358        let err = gentoo.packages().expect_err("a short index is refused");
3359        assert!(matches!(err, GentooError::Index { .. }), "{err:?}");
3360        assert!(err.to_string().contains("holds 2 packages"), "{err}");
3361
3362        // An index that holds more than it declares is not short, so it is not
3363        // this check's business. The fixture is one: its preamble says one
3364        // package and it carries two, which is the shape the live archive has
3365        // whenever a record is added between the count being written and the
3366        // stanzas being emitted.
3367        let served = HashMap::from([(binhost_url("Packages.gz"), gzipped(&body))]);
3368        let mut gentoo = binhost_provisioner(Box::new(Canned::new(served)));
3369        assert_eq!(gentoo.packages().expect("a long index reads").len(), 2);
3370    }
3371
3372    #[test]
3373    fn an_index_naming_nothing_this_layer_can_act_on_is_refused() {
3374        // A preamble and nothing else is a document that arrived and is not an
3375        // index. It is refused as one rather than reported as an empty archive,
3376        // which a caller would read as "this binhost publishes nothing".
3377        let served = HashMap::from([(
3378            binhost_url("Packages.gz"),
3379            gzipped("TIMESTAMP: 1787145736\n"),
3380        )]);
3381        let mut gentoo = binhost_provisioner(Box::new(Canned::new(served)));
3382        let err = gentoo.packages().expect_err("an empty index is refused");
3383        assert!(matches!(err, GentooError::Index { .. }), "{err:?}");
3384        // The refusal names the mirror that answered, which is the whole point
3385        // of carrying the URL through the walk.
3386        assert!(err.to_string().contains(DEFAULT_MIRROR), "{err}");
3387    }
3388
3389    #[test]
3390    fn a_binhost_that_was_never_named_is_a_configuration_failure() {
3391        // Not a fetch failure: there is no URL to have failed. The refusal says
3392        // what to call and gives a sub-architecture to call it with, since
3393        // nothing signed enumerates them.
3394        let key = fixture::signing_key("releng <releng@test.invalid>");
3395        let mut gentoo = provisioner(&key, Box::new(Canned::new(HashMap::new())));
3396        let err = gentoo.packages().expect_err("no binhost was named");
3397        assert!(matches!(err, GentooError::Config { .. }), "{err:?}");
3398        assert!(err.to_string().contains("binhost"), "{err}");
3399    }
3400
3401    #[test]
3402    fn a_plan_resolved_for_other_coordinates_is_refused_before_anything_is_fetched() {
3403        // The install wave composes the *builder's* architecture with the
3404        // *plan's* binhost, so a mismatch that reached the wave would fetch
3405        // `releases/<builder>/binpackages/23.0/<plan>/...` and 404 against a
3406        // mirror that was spelled correctly -- and where the two publish a tree
3407        // of the same name it would not fail at all. The refusal says which
3408        // pair disagrees, which is what a 404 cannot.
3409        let plan = Plan::parse_document(
3410            "Format: ferroday-cage-gentoo-plan 1\nArchitecture: arm64\nBinhost: arm64\n",
3411        )
3412        .expect("a fixture plan");
3413
3414        let err = Gentoo::builder("amd64")
3415            .plan(plan.clone())
3416            .build()
3417            .expect_err("a plan for another architecture is refused");
3418        assert!(matches!(err, GentooError::Config { .. }), "{err:?}");
3419        assert!(err.to_string().contains("arm64"), "{err}");
3420
3421        // The same for a binhost the builder named and the plan does not agree
3422        // with; a builder that names none takes the plan's, which is what it
3423        // resolved against.
3424        let err = Gentoo::builder("arm64")
3425            .binhost("x86-64")
3426            .plan(plan.clone())
3427            .build()
3428            .expect_err("a plan for another binhost is refused");
3429        assert!(matches!(err, GentooError::Config { .. }), "{err:?}");
3430        assert!(err.to_string().contains("binhost"), "{err}");
3431
3432        Gentoo::builder("arm64")
3433            .plan(plan.clone())
3434            .build()
3435            .expect("a plan whose architecture is the builder's builds");
3436        Gentoo::builder("arm64")
3437            .binhost("arm64")
3438            .plan(plan)
3439            .build()
3440            .expect("a plan that agrees with the builder on both builds");
3441    }
3442
3443    #[test]
3444    fn a_mirror_that_serves_no_index_at_all_reports_the_fetch_failure() {
3445        let mut gentoo = binhost_provisioner(Box::new(Canned::new(HashMap::new())));
3446        let err = gentoo.packages().expect_err("nothing is served");
3447        assert!(matches!(err, GentooError::Fetch(_)), "{err:?}");
3448    }
3449
3450    #[test]
3451    fn a_bootstrap_publishes_the_verified_tarball_into_a_root() {
3452        // The whole write half, driven exactly as every other provisioner is:
3453        // `ensure` supplies the staging directory and publishes it atomically,
3454        // and this layer fills it.
3455        let scratch = Scratch::for_test("gentoo", "bootstrap");
3456        let (_key, mut gentoo) = current();
3457        gentoo.cache_dir = Some(scratch.join("cache"));
3458        let root = scratch.join("root");
3459        assert_eq!(
3460            crate::provision::ensure(&root, &mut gentoo).expect("the root is provisioned"),
3461            crate::provision::Provisioned::Created,
3462        );
3463        assert_eq!(
3464            std::fs::read_to_string(root.join("etc/marker")).expect("the archive was extracted"),
3465            "a provisioned root\n",
3466        );
3467        // The download stays in the caller's cache, which is what it is for.
3468        assert!(
3469            scratch
3470                .join("cache")
3471                .join(format!("stage3-amd64-openrc-{BUILD}.tar.xz"))
3472                .is_file(),
3473        );
3474    }
3475
3476    #[test]
3477    fn a_bootstrap_resolves_first_when_the_caller_did_not() {
3478        // A caller who only wants a root never has to resolve, so `provision`
3479        // does it as its first step -- and reports what it chose, so a consumer
3480        // sees the exact build `ensure` installed without a second pass.
3481        let scratch = Scratch::for_test("gentoo", "bootstrap-resolves");
3482        let (_key, mut gentoo) = current();
3483        gentoo.cache_dir = Some(scratch.join("cache"));
3484        assert!(gentoo.stage3.is_none(), "nothing was resolved beforehand");
3485
3486        let mut seen: Vec<String> = Vec::new();
3487        crate::provision::Provision::new(scratch.join("root"))
3488            .observe(&mut |event: ProvisionEvent<'_>| {
3489                if let ProvisionEvent::Gentoo(event) = event {
3490                    seen.push(match event {
3491                        GentooEvent::Fetching { .. } => "fetching".to_string(),
3492                        GentooEvent::Resolved { stage3 } => {
3493                            format!("resolved {}", stage3.build_id())
3494                        }
3495                        GentooEvent::Verifying { .. } => "verifying".to_string(),
3496                        GentooEvent::Extracting { .. } => "extracting".to_string(),
3497                        // The install wave's own events, which a bootstrap
3498                        // with no install list never reaches.
3499                        other => format!("unexpected {other:?}"),
3500                    });
3501                }
3502            })
3503            .run(&mut gentoo)
3504            .expect("the root is provisioned");
3505        assert_eq!(
3506            seen.iter()
3507                .filter(|step| *step != "fetching")
3508                .collect::<Vec<_>>(),
3509            [
3510                &format!("resolved {BUILD}"),
3511                &"verifying".to_string(),
3512                &"extracting".to_string(),
3513            ],
3514        );
3515        // Three fetches: the pointer, the digest document, and the tarball.
3516        assert_eq!(seen.iter().filter(|step| *step == "fetching").count(), 3);
3517    }
3518
3519    #[test]
3520    fn a_run_cancelled_at_a_step_boundary_never_reaches_the_download() {
3521        // The resolution is two small documents; the download is most of a
3522        // gigabyte. A caller who cancelled while watching what was resolved
3523        // must not be committed to fetching it by having asked.
3524        struct StopOnResolve {
3525            resolved: bool,
3526        }
3527
3528        impl crate::provision::ProvisionObserver for StopOnResolve {
3529            fn progress(&mut self, event: ProvisionEvent<'_>) {
3530                if let ProvisionEvent::Gentoo(GentooEvent::Resolved { .. }) = event {
3531                    self.resolved = true;
3532                }
3533            }
3534
3535            fn cancelled(&mut self) -> bool {
3536                self.resolved
3537            }
3538        }
3539
3540        let scratch = Scratch::for_test("gentoo", "cancel-boundary");
3541        let (_key, mut gentoo) = current();
3542        let cache = scratch.join("cache");
3543        gentoo.cache_dir = Some(cache.clone());
3544        let root = scratch.join("root");
3545
3546        let mut stop = StopOnResolve { resolved: false };
3547        let err = crate::provision::Provision::new(&root)
3548            .observe(&mut stop)
3549            .run(&mut gentoo)
3550            .expect_err("the observer asked it to stop");
3551        assert!(
3552            matches!(err, crate::provision::ProvisionError::Cancelled),
3553            "{err}",
3554        );
3555        assert!(
3556            stop.resolved,
3557            "the Gentoo events reached the run's observer"
3558        );
3559        assert!(!root.exists(), "no root is published");
3560        assert!(
3561            !cache
3562                .join(format!("stage3-amd64-openrc-{BUILD}.tar.xz"))
3563                .exists(),
3564            "nothing was downloaded",
3565        );
3566    }
3567
3568    #[test]
3569    fn a_run_cancelled_while_the_tarball_arrives_stops_part_way_through_it() {
3570        // The step this layer has and the others do not: one file with no
3571        // package boundary inside it, so the cancellation check has to reach
3572        // into the write. The observer counts how often it was asked once the
3573        // tarball started arriving, which is what tells a boundary-only check
3574        // from one the body itself consults.
3575        struct StopMidBody {
3576            fetches: usize,
3577            asked: usize,
3578        }
3579
3580        impl crate::provision::ProvisionObserver for StopMidBody {
3581            fn progress(&mut self, event: ProvisionEvent<'_>) {
3582                if let ProvisionEvent::Gentoo(GentooEvent::Fetching { .. }) = event {
3583                    self.fetches += 1;
3584                }
3585            }
3586
3587            fn cancelled(&mut self) -> bool {
3588                // The pointer and the digest document are fetched first, so the
3589                // third fetch is the tarball's.
3590                if self.fetches < 3 {
3591                    return false;
3592                }
3593                self.asked += 1;
3594                self.asked > 2
3595            }
3596        }
3597
3598        let scratch = Scratch::for_test("gentoo", "cancel-download");
3599        let key = fixture::signing_key("releng <releng@test.invalid>");
3600        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3601        let mut served = archive(&key, Some(now));
3602        // Small enough that the fixture tarball takes several writes, as a real
3603        // body takes thousands.
3604        served.chunk = 512;
3605        let mut gentoo = provisioner(&key, Box::new(served));
3606        let cache = scratch.join("cache");
3607        gentoo.cache_dir = Some(cache.clone());
3608        let root = scratch.join("root");
3609
3610        let mut stop = StopMidBody {
3611            fetches: 0,
3612            asked: 0,
3613        };
3614        let err = crate::provision::Provision::new(&root)
3615            .observe(&mut stop)
3616            .run(&mut gentoo)
3617            .expect_err("the observer asked it to stop");
3618        assert!(
3619            matches!(err, crate::provision::ProvisionError::Cancelled),
3620            "{err}",
3621        );
3622        assert!(
3623            stop.asked > 2,
3624            "the body itself consulted the check, {} times",
3625            stop.asked,
3626        );
3627        assert!(!root.exists(), "no root is published");
3628        // Neither the staging file nor a published cache entry survives: a
3629        // partial body is not the archive's file, and a later run downloading
3630        // it again is cheaper than one extracting a truncated one.
3631        assert_eq!(
3632            std::fs::read_dir(&cache)
3633                .expect("the cache directory was created")
3634                .count(),
3635            0,
3636            "no partial download is left behind",
3637        );
3638    }
3639
3640    #[test]
3641    fn a_tarball_that_is_not_what_the_document_vouched_for_is_never_extracted() {
3642        // The bytes are verified before the extractor sees them, so an archive
3643        // that is not the one the signed document named leaves no half-written
3644        // root behind -- and the cached file goes, so the next run downloads
3645        // afresh rather than meeting the same refusal out of its own cache.
3646        let scratch = Scratch::for_test("gentoo", "wrong-bytes");
3647        let key = fixture::signing_key("releng <releng@test.invalid>");
3648        let now = crate::provision::now_epoch().expect("the clock is after the epoch");
3649        let mut served = archive(&key, Some(now));
3650        let tarball_url = format!(
3651            "{DEFAULT_MIRROR}/releases/{ARCH}/autobuilds/{BUILD}/stage3-amd64-openrc-{BUILD}.tar.xz",
3652        );
3653        served.served.insert(
3654            tarball_url,
3655            b"not the archive that was vouched for".to_vec(),
3656        );
3657
3658        let mut gentoo = provisioner(&key, Box::new(served));
3659        let cache = scratch.join("cache");
3660        gentoo.cache_dir = Some(cache.clone());
3661        let root = scratch.join("root");
3662        let err =
3663            crate::provision::ensure(&root, &mut gentoo).expect_err("the digest does not match");
3664        let reported = err.to_string();
3665        assert!(reported.contains("does not have the digest"), "{reported}");
3666        assert!(!root.exists(), "no root is published");
3667        assert!(
3668            !cache
3669                .join(format!("stage3-amd64-openrc-{BUILD}.tar.xz"))
3670                .exists(),
3671            "the tarball that did not verify is not left in the cache",
3672        );
3673    }
3674
3675    #[test]
3676    fn a_cached_tarball_is_verified_on_the_read_rather_than_trusted() {
3677        // A cache is bytes on a disk this crate does not own, so a file already
3678        // sitting at the cache path goes through the same check a fresh download
3679        // does. Here the transport would serve the real archive, and the planted
3680        // file is what is refused.
3681        let scratch = Scratch::for_test("gentoo", "cache-hit");
3682        let (_key, mut gentoo) = current();
3683        let cache = scratch.join("cache");
3684        std::fs::create_dir_all(&cache).unwrap();
3685        let planted = cache.join(format!("stage3-amd64-openrc-{BUILD}.tar.xz"));
3686        std::fs::write(&planted, b"planted in the cache by something else").unwrap();
3687        gentoo.cache_dir = Some(cache);
3688
3689        let err = crate::provision::ensure(scratch.join("root"), &mut gentoo)
3690            .expect_err("the planted file does not verify");
3691        assert!(
3692            err.to_string().contains("does not have the digest"),
3693            "{err}"
3694        );
3695        assert!(!planted.exists(), "the refused entry is cleared");
3696
3697        // With it cleared, the same provisioner downloads and succeeds.
3698        crate::provision::ensure(scratch.join("root"), &mut gentoo)
3699            .expect("the second run downloads the real archive");
3700        assert!(scratch.join("root/etc/marker").is_file());
3701    }
3702}