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}