Skip to main content

ferroday_cage/provision/alpine/
mod.rs

1//! The Alpine userland provisioner: bootstrap a release rootfs from an apk
2//! repository.
3//!
4//! The layer talks to the repository directly over the shared
5//! [`Fetch`] transport, as the Debian layer does, and verifies
6//! what it fetches against a [`KeySet`] of RSA public keys — the crate's bundle
7//! for Alpine and postmarketOS, or a caller's own. No `apk` binary is invoked.
8//!
9//! # What binds an apk repository together
10//!
11//! Both formats this layer reads are the same container: independent gzip
12//! segments laid end to end, each holding a tar archive. A package is three of
13//! them — the signature, the control segment, and the file tree — and an index
14//! is two, the signature and the records.
15//!
16//! Every digest the format defines is taken over *compressed* bytes: the RSA
17//! signature covers the raw bytes of the segment after it, an index record's
18//! `C:` field is a SHA-1 over the package's control segment as it lies in the
19//! file, and `.PKGINFO`'s `datahash` is a SHA-256 over the data segment as it
20//! lies in the file. So the chain from a signing key to a file on disk runs
21//! RSA-4096 over SHA-1, then SHA-1 again, then SHA-256 — and the layer verifies
22//! exactly that, because the published format offers nothing stronger and
23//! verifying less than `apk` does would be worse rather than safer.
24//!
25//! # Example
26//!
27//! ```no_run
28//! use ferroday_cage::provision::alpine::Alpine;
29//!
30//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! let mut alpine = Alpine::builder("v3.23")
32//!     .components(["main", "community"])
33//!     .build()?;
34//! assert!(alpine.available()?.contains("busybox"));
35//! # Ok(())
36//! # }
37//! ```
38
39mod apk;
40mod bootstrap;
41mod index;
42mod installed;
43mod keyring;
44mod layer;
45mod pin;
46mod pkginfo;
47mod plan;
48mod repository;
49mod resolve;
50mod sign;
51mod version;
52
53use std::collections::HashMap;
54use std::fmt;
55use std::io::{self, Seek as _};
56use std::path::{Path, PathBuf};
57
58pub use index::Available;
59pub use keyring::KeySet;
60pub use pin::{UnheldPin, UnheldReason};
61pub use plan::{Plan, PlannedPackage, ResolvedIndex};
62pub use repository::{Repository, RepositoryBuilder};
63
64use super::binfmt;
65use super::coordinate::{self, Nesting};
66use super::digest::Algorithm;
67use super::extract::{Extraction, Placement};
68// `super::layer` is the shared one; this module has a `layer` of its own, which
69// reads a provisioned base rather than describing how a cage is rooted on it.
70use super::layer::Rooting;
71use super::rooted::Rooted;
72use super::tar::TarReader;
73use super::{
74    BuildLayer, Failover, Fetch, FetchError, FetchJob, FetchRequest, HttpFetch, LimitedWriter,
75    PackageCache, ProvisionError, ProvisionRequest, Provisioner, Stream, mirror_url, walk_mirrors,
76};
77use crate::IdentityMap;
78use crate::failure::path_io_error;
79use bootstrap::ScriptSubject;
80use index::{Index, Record};
81use installed::{Carried, Claim, Database};
82use pin::Pins;
83
84/// The default mirror: the Alpine content-delivery front end.
85const DEFAULT_MIRROR: &str = "http://dl-cdn.alpinelinux.org/alpine";
86
87/// The ceiling on an index as it arrives, compressed.
88///
89/// Nothing in an apk repository declares an index's size — there is no signed
90/// release above it to record one — so this is the layer's own number rather
91/// than an archive's, and it is why it is not stated through
92/// [`FetchRequest::sized`], which is for a length a verified source declared.
93/// Alpine's largest published index is `edge/community/x86_64` at around 2.5 MB,
94/// so this is generous by a factor of twenty-five and still the reason a hostile
95/// mirror cannot stream forever.
96const MAX_INDEX_FETCH: u64 = 64 * 1024 * 1024;
97
98/// The ceiling on an index's records, decompressed.
99///
100/// The bound the fetch ceiling above cannot give: gzip is a compression format,
101/// so 2.5 MB on the wire can be gigabytes in hand. Alpine's largest index
102/// decompresses to around 11 MB today.
103const MAX_INDEX_RECORDS: u64 = 128 * 1024 * 1024;
104
105/// The suffix a run-owned package cache takes on the tree it is building.
106const APK_CACHE: &str = ".fcage-apks";
107
108/// The mode a bootstrapped rootfs's own root directory takes.
109///
110/// Alpine's packages ship no entry for the root, so nothing in the closure
111/// states it; `0755` is what `alpine-baselayout` gives every top-level directory
112/// and what the published minirootfs unpacks to. Stating it keeps the published
113/// directory independent of the caller's umask.
114const ROOTFS_ROOT_MODE: u32 = 0o755;
115
116/// The ceiling on a package's control segment, decompressed and compressed
117/// alike.
118///
119/// `.PKGINFO` and a handful of shell scripts: the largest across the
120/// `alpine-base` closure is 11 KB, from a kilobyte and a half on the wire. The
121/// bound is here because gzip is a compression format and this segment is read
122/// before anything about the package has been authenticated.
123const MAX_CONTROL_SEGMENT: u64 = 16 * 1024 * 1024;
124
125/// The Alpine architecture name for the host.
126///
127/// This is what [`AlpineBuilder::architecture`] defaults to when a caller names
128/// none, and it is exposed so a caller composing its own defaults — a cache key,
129/// an artifact record — reaches the same answer the bootstrap will.
130///
131/// The name comes from `uname` rather than from `std::env::consts::ARCH`, as the
132/// Debian layer's does and for the same reason: `ARCH` reports the architecture
133/// the *calling binary* was compiled for, and reports `arm` for both the ARMv6
134/// and ARMv7 userlands that Alpine publishes as `armhf` and `armv7`.
135///
136/// Returns the raw machine name when it is not one this table knows, so a caller
137/// on an unusual host can still proceed by naming the architecture explicitly.
138///
139/// # Example
140///
141/// ```no_run
142/// use ferroday_cage::provision::alpine::{Alpine, host_architecture};
143///
144/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
145/// let architecture = host_architecture();
146/// let alpine = Alpine::builder("v3.23").architecture(&architecture).build()?;
147/// # let _ = alpine;
148/// # Ok(())
149/// # }
150/// ```
151pub fn host_architecture() -> String {
152    let uname = rustix::system::uname();
153    let machine = uname.machine().to_string_lossy();
154    match machine.as_ref() {
155        "armv7l" => "armv7",
156        "armv6l" => "armhf",
157        "i686" | "i586" | "i386" => "x86",
158        other => other,
159    }
160    .to_string()
161}
162
163/// Checks one archive coordinate, reporting a refusal as this layer's
164/// configuration failure.
165///
166/// The rule is [`coordinate::check`]'s and is shared with every other layer;
167/// only whose configuration the refusal is about belongs here.
168pub(super) fn check_coordinate(
169    what: &str,
170    value: &str,
171    nesting: Nesting,
172) -> Result<(), AlpineError> {
173    coordinate::check(what, value, nesting).map_err(|reason| AlpineError::Config { reason })
174}
175
176/// An Alpine userland provisioner.
177///
178/// Built with [`Alpine::builder`]. [`available`](Self::available) reports the
179/// names the configured repositories offer, reading and verifying each index
180/// without downloading a package.
181pub struct Alpine {
182    /// The architecture every index is read for, and the one `noarch` joins.
183    architecture: String,
184    /// The repositories, the primary first, in the order a merge applies them.
185    repositories: Vec<Repository>,
186    /// The packages the caller asked for, which is the whole seed of a
187    /// resolution.
188    world: Vec<String>,
189    /// The packages that are never selected.
190    excludes: Vec<String>,
191    /// The versions a resolution is held to, empty where none was given.
192    pins: Pins,
193    /// A plan to install instead of resolving one, where the caller gave one.
194    plan: Option<Plan>,
195    /// Whether to lay the files out and run nothing.
196    extract_only: bool,
197    /// The map the script cages run under.
198    identity_map: IdentityMap,
199    /// Where downloaded packages are kept across runs, where the caller named a
200    /// place.
201    cache_dir: Option<PathBuf>,
202    /// A tree laid over the unpacked closure before the scripts run.
203    overlay: Option<PathBuf>,
204    /// The pristine base a layered build stages its increment over, set by
205    /// [`AlpineBuilder::base_layer`]. `None` for a full bootstrap; required by
206    /// [`Alpine::stage_layer`] and [`Alpine::resolve_layer`].
207    base_layer: Option<PathBuf>,
208    /// The transport every fetch goes through.
209    fetcher: Box<dyn Fetch>,
210}
211
212impl fmt::Debug for Alpine {
213    /// Renders the configuration. The transport is named by neither its type
214    /// nor its state, since a `Fetch` is a caller's value and this crate knows
215    /// nothing renderable about one.
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        f.debug_struct("Alpine")
218            .field("architecture", &self.architecture)
219            .field("repositories", &self.repositories)
220            .field("world", &self.world)
221            .field("excludes", &self.excludes)
222            .field("extract_only", &self.extract_only)
223            .field("identity_map", &self.identity_map)
224            .field("cache_dir", &self.cache_dir)
225            .field("overlay", &self.overlay)
226            .field("base_layer", &self.base_layer)
227            .finish_non_exhaustive()
228    }
229}
230
231impl Alpine {
232    /// Returns a builder for a bootstrap of `release` — `v3.23`, `edge`, or
233    /// postmarketOS's `v26.06`.
234    pub fn builder(release: impl Into<String>) -> AlpineBuilder {
235        AlpineBuilder {
236            release: release.into(),
237            architecture: None,
238            mirror: None,
239            fallbacks: Vec::new(),
240            components: None,
241            keys: None,
242            includes: Vec::new(),
243            excludes: Vec::new(),
244            pin: None,
245            plan: None,
246            extract_only: false,
247            identity_map: IdentityMap::Single,
248            cache_dir: None,
249            overlay: None,
250            base_layer: None,
251            repositories: Vec::new(),
252            fetcher: None,
253        }
254    }
255
256    /// Resolves the install plan for the configured release, downloading and
257    /// unpacking nothing.
258    ///
259    /// This performs the read half of a bootstrap: it fetches each repository's
260    /// index, verifies its signature against that repository's keys, merges
261    /// them, and resolves the caller's [`include`](AlpineBuilder::include)s into
262    /// the exact set of packages a bootstrap would install. It stops there — no
263    /// package is downloaded, and no staging tree is created — and returns the
264    /// resolved [`Plan`].
265    ///
266    /// Because nothing runs, `resolve` needs neither a `qemu-user` binfmt
267    /// handler nor an establishable identity map, and so serves an architecture
268    /// the host cannot execute.
269    ///
270    /// # What seeds a resolution
271    ///
272    /// The included set, and nothing else. apk publishes no priority field and
273    /// no essential flag, so unlike a Debian bootstrap there is no band of
274    /// packages the archive itself nominates as the base — a closure holds what
275    /// was asked for and what that requires. `alpine-base` is the conventional
276    /// minimal set and is a package like any other; name it to get it.
277    ///
278    /// # Errors
279    ///
280    /// Returns [`AlpineError::Config`] when nothing was included or when a
281    /// [`base_layer`](AlpineBuilder::base_layer) is set — such a provisioner
282    /// stages an increment, and [`resolve_layer`](Self::resolve_layer) is its
283    /// read half — [`AlpineError::Fetch`], [`AlpineError::Container`],
284    /// [`AlpineError::Signature`] or [`AlpineError::Index`] for a repository
285    /// that cannot be read or verified, and [`AlpineError::Resolve`] for a
286    /// closure the repositories cannot supply.
287    pub fn resolve(&mut self) -> Result<Plan, AlpineError> {
288        self.observe(&mut Silent).resolve()
289    }
290
291    /// The resolution, over a base layer where there is one.
292    ///
293    /// One body for the whole-root and the layered cases, so the two cannot
294    /// resolve differently: a base contributes packages the closure treats as
295    /// already answered, and nothing else about the resolution changes.
296    fn resolve_over(
297        &mut self,
298        base: Option<&layer::Base>,
299        observer: &mut dyn AlpineObserver,
300    ) -> Result<Plan, AlpineError> {
301        if self.world.is_empty() {
302            return Err(AlpineError::Config {
303                reason: "a resolution installs the packages named with include(), and none were \
304                         named; apk has no priority field, so there is no set the repositories \
305                         nominate on the caller's behalf"
306                    .to_string(),
307            });
308        }
309        let (records, indexes) = self.read_indexes(observer)?;
310        observer.progress(AlpineEvent::Resolving);
311        let merged = Index::merge(records, &self.architecture, &self.pins);
312        // Before the closure, so a pin the repositories have moved past refuses
313        // the resolution rather than quietly resolving to something else.
314        self.pins.check(&merged)?;
315        let carried = base.into_iter().flat_map(layer::Base::records);
316        let packages = resolve::resolve(&merged, &self.world, &self.excludes, carried)?;
317        Ok(Plan::project(
318            self.release(),
319            &self.architecture,
320            &packages,
321            &indexes,
322        ))
323    }
324
325    /// Resolves the increment a layered build would install over its base,
326    /// downloading and unpacking nothing.
327    ///
328    /// The layered counterpart of [`resolve`](Self::resolve): it reads the base
329    /// layer's `lib/apk/db/installed` database, then resolves the configured
330    /// [`include`](AlpineBuilder::include)s against the merged repository index
331    /// while treating everything the base records as already answered, returning
332    /// the [`Plan`] of only the packages the base does not carry. Requires a
333    /// [`base_layer`](AlpineBuilder::base_layer).
334    ///
335    /// The base answers at its recorded versions rather than merely by name, so
336    /// a dependency the base's version is too old for is reported rather than
337    /// quietly skipped. That is the one thing a layered resolution cannot do
338    /// about: an increment installs over a base, and a package the base already
339    /// carries cannot be replaced by a newer one without rebuilding the base.
340    ///
341    /// Like [`resolve`](Self::resolve) it downloads no package and runs nothing,
342    /// so it needs neither a `qemu-user` binfmt handler nor an establishable
343    /// identity map. Use it to preview a layer or to key a build-root cache on
344    /// the increment without staging it.
345    ///
346    /// # Errors
347    ///
348    /// Returns an [`AlpineError`] for the same fetch, signature, and resolution
349    /// failures as [`resolve`](Self::resolve), and [`AlpineError::Config`] when
350    /// no base layer is set or the base is not a provisioned Alpine root of this
351    /// architecture.
352    pub fn resolve_layer(&mut self) -> Result<Plan, AlpineError> {
353        self.observe(&mut Silent).resolve_layer()
354    }
355
356    /// Reads the configured base layer, refusing a provisioner that has none.
357    ///
358    /// `entry` names the method that needs one, so the refusal says which call
359    /// was made rather than only that something was missing.
360    fn read_base_layer(&self, entry: &str) -> Result<layer::Base, AlpineError> {
361        let Some(base) = self.base_layer.as_deref() else {
362            return Err(AlpineError::Config {
363                reason: format!(
364                    "{entry} stages over a base root, and none was set with \
365                     AlpineBuilder::base_layer",
366                ),
367            });
368        };
369        layer::Base::read(base, &self.architecture)
370    }
371
372    /// The release the primary repository publishes, which is the one the
373    /// builder was opened on.
374    fn release(&self) -> &str {
375        self.repositories
376            .first()
377            .expect("a provisioner always holds its primary repository")
378            .release()
379    }
380
381    /// Fetches, verifies and parses every configured index.
382    ///
383    /// The two halves travel together because they are the same walk: the
384    /// records a resolution reads and the record of what was read are produced
385    /// by one pass, and a plan that named an index it had not verified would be
386    /// describing something other than what it resolved against.
387    fn read_indexes(
388        &mut self,
389        observer: &mut dyn AlpineObserver,
390    ) -> Result<(Vec<Vec<Record>>, Vec<ResolvedIndex>), AlpineError> {
391        let mut records = Vec::new();
392        let mut indexes = Vec::new();
393        for repository in &self.repositories {
394            for (component, path) in repository.indexes(&self.architecture) {
395                let from = indexes.len();
396                let (read, resolved) = read_index(
397                    &mut *self.fetcher,
398                    repository,
399                    component,
400                    &path,
401                    from,
402                    observer,
403                )?;
404                records.push(read);
405                indexes.push(resolved);
406            }
407        }
408        Ok((records, indexes))
409    }
410
411    /// The package names the configured repositories offer.
412    ///
413    /// This is the question a resolve cannot answer, since a resolve reports a
414    /// closure rather than a catalogue. Every index is fetched and its signature
415    /// verified, so this is as authenticated as a bootstrap; no package is
416    /// downloaded.
417    ///
418    /// # Errors
419    ///
420    /// Returns [`AlpineError::Fetch`] when no mirror of a repository serves its
421    /// index, [`AlpineError::Container`] or [`AlpineError::Signature`] when what
422    /// one serves is not a signed apk container this key set accepts, and
423    /// [`AlpineError::Index`] when the records it carries are not the ones the
424    /// format defines.
425    pub fn available(&mut self) -> Result<Available, AlpineError> {
426        self.observe(&mut Silent).available()
427    }
428
429    /// Stages this provisioner's packages as an increment over the base layer,
430    /// installing only what the base does not carry into a disposable overlay
431    /// `upper`.
432    ///
433    /// Requires a [`base_layer`](AlpineBuilder::base_layer). It resolves the
434    /// increment [`resolve_layer`](Self::resolve_layer) reports, downloads it,
435    /// extracts it into `upper`, and runs its install scripts in a cage rooted
436    /// on an overlay of the pristine base (the read-only lower) and `upper` (the
437    /// writable increment) — so a script reads the base's shell and the base's
438    /// accounts, and everything it writes lands in `upper`, the base untouched.
439    /// The returned [`BuildLayer`] owns `upper`: root a build cage on
440    /// [`overlay_rootfs(base, layer.path())`](crate::CageBuilder::overlay_rootfs)
441    /// to build against the merged `base + increment` view, then drop the layer
442    /// to discard the increment.
443    ///
444    /// `upper` is created if absent, along with the overlay work directory
445    /// beside it. The identity map, cache directory, additional repositories and
446    /// pre-configure overlay configured on the builder all apply exactly as to a
447    /// full bootstrap, and [`extract_only`](AlpineBuilder::extract_only) stages
448    /// the increment's files and registers them without running a script.
449    ///
450    /// A call that fails leaves nothing behind: the upper and its work directory
451    /// are disposed of exactly as dropping the returned layer would dispose of
452    /// them, since a failure hands the caller no handle to drop. So is the
453    /// package cache, unless the builder named a
454    /// [`cache_dir`](AlpineBuilder::cache_dir), which is the caller's and
455    /// survives either way.
456    ///
457    /// # What the merged root records
458    ///
459    /// An overlay unions files, so the `lib/apk/db/installed` written into
460    /// `upper` shadows the base's outright. It is therefore written to describe
461    /// the whole merged root: the base's records are carried through byte for
462    /// byte and the increment's are sorted in among them, and the same holds for
463    /// the trigger list, the archived scripts, `/etc/apk/world` and
464    /// `/etc/apk/repositories`. An `apk` run inside the build root reads one
465    /// coherent database.
466    ///
467    /// One thing the merged root does not reproduce: a file the increment ships
468    /// over one the base owns shadows it rather than being refused. The base is
469    /// read-only and the shadow lives in the disposable upper, so the collision
470    /// is confined to the layer and reverted with it. A collision *within* the
471    /// increment is refused exactly as a full bootstrap refuses one.
472    ///
473    /// # Preconditions
474    ///
475    /// The increment's scripts run the target's binaries, so a foreign
476    /// architecture needs the same `qemu-user` binfmt handler a full bootstrap
477    /// does, and a range identity map must be establishable — both checked
478    /// before any download. It roots a cage on an unprivileged overlay, so the
479    /// host must support one on `upper`'s filesystem;
480    /// [`host::overlay_blocker`](crate::host::overlay_blocker) reports what is
481    /// missing, and `stage_layer` refuses a host that cannot before downloading.
482    ///
483    /// # Errors
484    ///
485    /// Returns [`ProvisionError`], with an [`AlpineError`] surfaced through
486    /// [`ProvisionError::Other`] for a fetch, resolution, or script failure, an
487    /// unmet host precondition, a base that is not a provisioned Alpine root, or
488    /// no base layer set.
489    pub fn stage_layer(&mut self, upper: impl AsRef<Path>) -> Result<BuildLayer, ProvisionError> {
490        self.observe(&mut Silent).stage_layer(upper)
491    }
492
493    /// The body of [`stage_layer`](Self::stage_layer), reporting to `observer`.
494    ///
495    /// One body for the two public entry points -- the unobserved one above and
496    /// [`Observed::stage_layer`] -- rather than a body on each.
497    fn stage_layer_reporting(
498        &mut self,
499        upper: &Path,
500        observer: &mut dyn AlpineObserver,
501    ) -> Result<BuildLayer, ProvisionError> {
502        // The same fail-early posture the full bootstrap takes, and then the
503        // base itself: reading it refuses a tree that is not a provisioned root
504        // before anything is created or downloaded.
505        preflight(self).map_err(wrap)?;
506        let base = self.read_base_layer("stage_layer").map_err(wrap)?;
507
508        std::fs::create_dir_all(upper)
509            .map_err(|err| ProvisionError::io("creating the overlay upper", upper, err))?;
510        // The handle that owns the upper is taken as soon as the upper exists,
511        // not on the way out, so a failure below disposes of a partly-installed
512        // increment instead of orphaning it: there is no handle in an `Err` for
513        // the caller to drop.
514        let layer = BuildLayer::new(upper, self.identity_map.clone());
515        // Refuse a host that cannot establish an unprivileged overlay on the
516        // upper's filesystem before any download — the preflight the primitive
517        // would make at cage-build time, hoisted ahead of the work. The probe
518        // runs on the upper's parent, its own filesystem, so its scratch files
519        // land beside the upper.
520        //
521        // Not for an extract-only staging, which runs nothing and so roots no
522        // cage of its own, for the same reason `preflight` skips the identity
523        // map and the interpreter there. A caller who goes on to root a build
524        // cage on the result meets the primitive's own check.
525        let scratch = upper
526            .parent()
527            .filter(|parent| !parent.as_os_str().is_empty())
528            .unwrap_or(Path::new("."));
529        if !self.extract_only
530            && let Some(blocker) = crate::host::overlay_blocker(scratch)
531        {
532            return Err(wrap(AlpineError::Config {
533                reason: format!("an overlay-rooted build layer cannot be established: {blocker}"),
534            }));
535        }
536
537        install(self, upper, Some(&base), observer)?;
538        Ok(layer)
539    }
540}
541
542/// Builder for an [`Alpine`] provisioner.
543///
544/// The setters configure the primary repository — its mirrors, components and
545/// keys — and [`repository`](Self::repository) adds further ones after it.
546pub struct AlpineBuilder {
547    release: String,
548    architecture: Option<String>,
549    mirror: Option<String>,
550    fallbacks: Vec<String>,
551    /// `None` where the caller never named any, which is what the default
552    /// applies to; an empty list they named is the componentless layout.
553    components: Option<Vec<String>>,
554    keys: Option<KeySet>,
555    includes: Vec<String>,
556    excludes: Vec<String>,
557    pin: Option<Plan>,
558    plan: Option<Plan>,
559    extract_only: bool,
560    identity_map: IdentityMap,
561    cache_dir: Option<PathBuf>,
562    overlay: Option<PathBuf>,
563    base_layer: Option<PathBuf>,
564    repositories: Vec<Repository>,
565    fetcher: Option<Box<dyn Fetch>>,
566}
567
568impl fmt::Debug for AlpineBuilder {
569    /// Renders the configuration, naming the keys rather than holding them and
570    /// leaving the transport out, as [`Alpine`]'s own rendering does.
571    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572        f.debug_struct("AlpineBuilder")
573            .field("release", &self.release)
574            .field("architecture", &self.architecture)
575            .field("mirror", &self.mirror)
576            .field("fallbacks", &self.fallbacks)
577            .field("components", &self.components)
578            .field("keys", &self.keys)
579            .field("includes", &self.includes)
580            .field("excludes", &self.excludes)
581            .field("pin", &self.pin)
582            .field("plan", &self.plan)
583            .field("extract_only", &self.extract_only)
584            .field("identity_map", &self.identity_map)
585            .field("cache_dir", &self.cache_dir)
586            .field("overlay", &self.overlay)
587            .field("base_layer", &self.base_layer)
588            .field("repositories", &self.repositories)
589            .finish_non_exhaustive()
590    }
591}
592
593impl AlpineBuilder {
594    /// Sets the architecture to bootstrap. The default is
595    /// [`host_architecture`].
596    pub fn architecture(mut self, architecture: impl Into<String>) -> Self {
597        self.architecture = Some(architecture.into());
598        self
599    }
600
601    /// Sets the primary repository's mirror URL, the archive root rather than
602    /// the release directory. The default is Alpine's content-delivery front
603    /// end.
604    pub fn mirror(mut self, url: impl Into<String>) -> Self {
605        self.mirror = Some(url.into());
606        self
607    }
608
609    /// Adds a backstop mirror for the primary repository, tried in order after
610    /// it when a fetch reports the resource missing or fails at the transport.
611    ///
612    /// It backs the packages as well as the index, so a package rebuilt to a
613    /// higher `-r` — which removes its predecessor from a live mirror — is
614    /// served by the backstop for as long as a plan naming the predecessor is
615    /// still being installed.
616    pub fn mirror_fallback(mut self, url: impl Into<String>) -> Self {
617        self.fallbacks.push(url.into());
618        self
619    }
620
621    /// Sets the primary repository's components. The default is `main`.
622    ///
623    /// An empty list is not the default but the other layout: a repository with
624    /// no components is addressed as `<release>/<architecture>`, which is what
625    /// postmarketOS publishes.
626    pub fn components<I, S>(mut self, components: I) -> Self
627    where
628        I: IntoIterator<Item = S>,
629        S: Into<String>,
630    {
631        self.components = Some(components.into_iter().map(Into::into).collect());
632        self
633    }
634
635    /// Verifies the primary repository against `keys` instead of the bundled
636    /// Alpine keys for the architecture being bootstrapped.
637    ///
638    /// The route for a repository of the caller's own is usually to extend the
639    /// bundle rather than replace it: [`KeySet::alpine`] then
640    /// [`KeySet::insert`].
641    pub fn keys(mut self, keys: KeySet) -> Self {
642        self.keys = Some(keys);
643        self
644    }
645
646    /// Adds packages to install, which is the whole seed of a resolution.
647    ///
648    /// apk publishes no priority field and no essential flag, so nothing is
649    /// installed that was not named here or required by something that was.
650    /// `alpine-base` is the conventional minimal set and is an ordinary package;
651    /// name it to get it.
652    ///
653    /// This is the set written to `/etc/apk/world` in the finished rootfs — what
654    /// the root asked for, as against the closure it resolved to.
655    ///
656    /// # Errors
657    ///
658    /// A name carrying a version operator is refused at [`build`](Self::build)
659    /// with [`AlpineError::Config`]. A resolution takes the highest version each
660    /// repository offers; holding one to a version is what [`pin`](Self::pin)
661    /// is for, and reading `busybox>=1.37` as a package name would otherwise
662    /// fail as a package nothing offers.
663    pub fn include<I, S>(mut self, packages: I) -> Self
664    where
665        I: IntoIterator<Item = S>,
666        S: Into<String>,
667    {
668        self.includes.extend(packages.into_iter().map(Into::into));
669        self
670    }
671
672    /// Excludes packages from the resolved install closure.
673    ///
674    /// An excluded package is never selected: it is skipped when a name is
675    /// answered, so a dependency on a name several packages provide resolves
676    /// through one of the others. apk has no alternative groups, so a dependency
677    /// that only the excluded package could have satisfied fails the resolution
678    /// rather than producing a broken closure, as does excluding a package that
679    /// is also an [`include`](Self::include).
680    pub fn exclude<I, S>(mut self, packages: I) -> Self
681    where
682        I: IntoIterator<Item = S>,
683        S: Into<String>,
684    {
685        self.excludes.extend(packages.into_iter().map(Into::into));
686        self
687    }
688
689    /// Holds a resolution to the versions a previously resolved [`Plan`]
690    /// recorded, resolving everything the plan does not name.
691    ///
692    /// The resolution runs as it always does — every index fetched and verified,
693    /// the closure computed over what the repositories offer now — and each
694    /// package the pin names is selected at the pinned version rather than at the
695    /// highest offered. Everything else, including a package added to the closure
696    /// since and a dependency the pinned version pulls in, resolves normally.
697    ///
698    /// # The trust model does not change
699    ///
700    /// Nothing here leaves the repository signature chain: every control
701    /// identity is still read from an index whose own signature was verified.
702    /// The pin narrows which version is selected; it never becomes the authority
703    /// for what the bytes are. A pin whose control identity disagrees with the
704    /// index's for the same version is refused rather than preferred, which is
705    /// how one version published twice over different bytes is caught.
706    ///
707    /// # Errors
708    ///
709    /// [`build`](Self::build) refuses a pin with [`AlpineError::Config`] when it
710    /// names one package twice — a pin is keyed by name, so two entries state
711    /// two versions to hold it at — when it resolves another architecture, or
712    /// when an entry states a version this grammar cannot compare.
713    ///
714    /// A pin the repositories cannot supply fails the resolution with
715    /// [`AlpineError::Pin`], naming every package at once — the repositories
716    /// have published past the pinned version, dropped the package, or record a
717    /// different control segment for it. Alpine keeps one version of each
718    /// package per release, so a pin outlives a publication only where the
719    /// caller keeps a mirror that does.
720    pub fn pin(mut self, pin: Plan) -> Self {
721        self.pin = Some(pin);
722        self
723    }
724
725    /// Installs the packages a previously resolved [`Plan`] names, resolving
726    /// nothing.
727    ///
728    /// No index is fetched and no closure is computed: the plan *is* the
729    /// resolution, and that omission is the whole of what it saves. Each package
730    /// is still fetched from the index directory the plan records, and each is
731    /// still bound to that plan by the control identity recomputed over what
732    /// arrives, so the trust chain is the plan's rather than a fresh
733    /// repository's.
734    ///
735    /// [`pin`](Self::pin) is the other half of the same pair and answers a
736    /// different question: a pin holds a *resolution* to recorded versions while
737    /// still computing one, and so still needs the repositories to agree.
738    ///
739    /// # Errors
740    ///
741    /// [`build`](Self::build) refuses a plan the configuration cannot install,
742    /// with [`AlpineError::Config`]: one resolved for another architecture; one
743    /// naming an index it does not itself carry, or more indexes than the
744    /// configured repositories publish, or an index whose release and component
745    /// are not what the repository in that slot publishes; and one naming a
746    /// package by something no repository could publish it under. It also
747    /// refuses the combinations that would silently drop half of what was asked
748    /// for — [`include`](Self::include), [`exclude`](Self::exclude) and
749    /// [`pin`](Self::pin) all describe a resolution, and a plan is one already.
750    pub fn plan(mut self, plan: Plan) -> Self {
751        self.plan = Some(plan);
752        self
753    }
754
755    /// Lays the packages' files out and runs nothing.
756    ///
757    /// No script runs and no trigger fires, so the tree is what the packages
758    /// ship and nothing a script would have derived from it: no `busybox` applet
759    /// links, no accounts a `.pre-install` would have added. Because nothing
760    /// executes, this serves an architecture the host cannot run.
761    ///
762    /// # This still registers what it installed
763    ///
764    /// Unlike the Debian layer's extract-only, which leaves an empty `dpkg`
765    /// database because registering a package there *means running `dpkg`*, this
766    /// writes the full `/lib/apk/db` and `/etc/apk` state. An apk installed
767    /// record is a file format rather than a program's output, so registration
768    /// costs nothing and needs nothing executed — and a root that knows what it
769    /// holds is worth more than one that does not.
770    pub fn extract_only(mut self, extract_only: bool) -> Self {
771        self.extract_only = extract_only;
772        self
773    }
774
775    /// Runs the install scripts under `map` rather than under the default
776    /// single-identity map.
777    ///
778    /// Alpine's packages are entirely root-owned — every entry of every package
779    /// in the `alpine-base` closure is uid 0, gid 0 — so unlike a Debian
780    /// bootstrap the extraction itself needs nothing from a wider map. What a
781    /// wider map buys is the scripts: `alpine-baselayout`'s `.post-install`
782    /// assigns `/etc/shadow` to the `shadow` group, which the single-identity
783    /// map cannot represent. That call fails under `apk` in the same situation,
784    /// and Alpine's scripts tolerate it, so the default is a working bootstrap
785    /// with the group ownership flattened to root.
786    pub fn identity_map(mut self, map: IdentityMap) -> Self {
787        self.identity_map = map;
788        self
789    }
790
791    /// Keeps downloaded packages in `dir` across runs.
792    ///
793    /// Without one, packages are downloaded into a directory beside the rootfs
794    /// being built and removed with it, so a repeated bootstrap re-downloads
795    /// everything. A cache directory is the caller's: this layer only ever adds
796    /// to it, and never removes an entry it did not just write.
797    ///
798    /// An entry is used only when it reads back as the package the plan names —
799    /// its signature verifies, its control identity matches, and its file tree
800    /// digests to what its own metadata says — so a truncated or substituted
801    /// entry is downloaded again rather than trusted.
802    pub fn cache_dir(mut self, dir: impl AsRef<Path>) -> Self {
803        self.cache_dir = Some(dir.as_ref().to_path_buf());
804        self
805    }
806
807    /// Copies the tree at `source` over the unpacked packages before their
808    /// scripts run.
809    ///
810    /// The window is the one that matters: after every package's files are down,
811    /// so this overwrites what they shipped, and before any script runs, so a
812    /// script reads the injected values. A configuration file placed here is the
813    /// one `busybox`'s applet install or a `.post-install` sees.
814    ///
815    /// It is laid under [`extract_only`](Self::extract_only) too. The tree is
816    /// still assembled there; what does not happen is the scripts that would
817    /// have read it.
818    pub fn pre_configure_overlay(mut self, source: impl AsRef<Path>) -> Self {
819        self.overlay = Some(source.as_ref().to_path_buf());
820        self
821    }
822
823    /// Stages over the provisioned root at `base` rather than bootstrapping a
824    /// whole one.
825    ///
826    /// This turns the [`Alpine`] into a layered provisioner:
827    /// [`Alpine::stage_layer`] resolves only what `base` does not already carry
828    /// and installs that increment into a disposable overlay upper, and
829    /// [`Alpine::resolve_layer`] reports the increment without installing it.
830    ///
831    /// `base` is a root this crate or `apk` provisioned — it is read for its
832    /// `lib/apk/db/installed` database, and refused if it holds none. Unlike the
833    /// Debian layer, an [`extract_only`](Self::extract_only) base is a usable
834    /// one: an apk registration is a file format rather than a program's output,
835    /// so a root laid out without running anything still records exactly what it
836    /// holds.
837    ///
838    /// An [`Alpine`] built with a base layer is used through `stage_layer` or
839    /// `resolve_layer`; it is not passed to [`ensure`](super::ensure), which
840    /// bootstraps a whole root and would ignore the base.
841    pub fn base_layer(mut self, base: impl AsRef<Path>) -> Self {
842        self.base_layer = Some(base.as_ref().to_path_buf());
843        self
844    }
845
846    /// Adds a repository after the primary.
847    ///
848    /// Order is meaning: a repository added later shadows an earlier one where
849    /// both offer a name at the same version, which is what makes postmarketOS's
850    /// repositories an overlay on Alpine's rather than a rival to them. A higher
851    /// version wins wherever it comes from.
852    pub fn repository(mut self, repository: Repository) -> Self {
853        self.repositories.push(repository);
854        self
855    }
856
857    /// Fetches through `fetcher` rather than through the built-in transport.
858    ///
859    /// The built-in [`HttpFetch`] speaks `http://` and `file://`. Every archive
860    /// this layer is written against serves plain HTTP, so a caller needs this
861    /// for a mirror behind TLS, behind a proxy, or on a transport of their own.
862    pub fn fetcher(mut self, fetcher: Box<dyn Fetch>) -> Self {
863        self.fetcher = Some(fetcher);
864        self
865    }
866
867    /// Validates the configuration and freezes it into an [`Alpine`].
868    ///
869    /// # Errors
870    ///
871    /// Returns [`AlpineError::Config`] when the architecture is not one a
872    /// repository can be addressed by, when the bundle holds no keys for it and
873    /// none were given, when the primary repository's release or a component is
874    /// not addressable, or when a [`plan`](Self::plan) does not describe
875    /// something this configuration could install.
876    pub fn build(self) -> Result<Alpine, AlpineError> {
877        // A plan states which architecture it is for, so a builder that names
878        // none adopts it: refusing a plan the caller never contradicted, over a
879        // default the caller never chose, would be a confusing rejection. A pin
880        // states one for the same reason and is adopted the same way. This is
881        // the Debian builder's rule, and it has to be the same rule, because it
882        // is what decides whether the check below ever fires for a caller who
883        // simply did not say.
884        let stated = self.plan.as_ref().or(self.pin.as_ref());
885        let architecture = match (&self.architecture, stated) {
886            (Some(architecture), _) => architecture.clone(),
887            (None, Some(plan)) => plan.architecture.clone(),
888            (None, None) => host_architecture(),
889        };
890        check_coordinate(
891            "the bootstrap's architecture",
892            &architecture,
893            Nesting::Single,
894        )?;
895
896        let keys = match self.keys {
897            Some(keys) => keys,
898            None => KeySet::alpine(&architecture)?,
899        };
900        let mut mirrors = Vec::with_capacity(1 + self.fallbacks.len());
901        mirrors.push(self.mirror.unwrap_or_else(|| DEFAULT_MIRROR.to_string()));
902        mirrors.extend(self.fallbacks);
903        let components = self.components.unwrap_or_else(|| vec!["main".to_string()]);
904
905        for name in self.includes.iter().chain(&self.excludes) {
906            if name.contains(['<', '>', '=', '~']) {
907                return Err(AlpineError::Config {
908                    reason: format!(
909                        "{name:?} names a package and a version, and include() and exclude() \
910                         name packages; hold a resolution to a version with pin() instead",
911                    ),
912                });
913            }
914        }
915
916        let mut repositories = Vec::with_capacity(1 + self.repositories.len());
917        repositories.push(Repository::primary(
918            self.release,
919            mirrors,
920            components,
921            keys,
922        )?);
923        repositories.extend(self.repositories);
924
925        if let Some(plan) = &self.plan {
926            validate_plan(
927                plan,
928                &architecture,
929                &index_slots(&repositories, &architecture),
930                self.pin.is_some(),
931                &self.includes,
932                &self.excludes,
933            )?;
934        }
935        if let Some(pin) = &self.pin {
936            validate_pin(pin, &architecture)?;
937        }
938
939        Ok(Alpine {
940            architecture,
941            repositories,
942            world: self.includes,
943            excludes: self.excludes,
944            pins: match &self.pin {
945                Some(plan) => Pins::of(plan)?,
946                None => Pins::none(),
947            },
948            plan: self.plan,
949            extract_only: self.extract_only,
950            identity_map: self.identity_map,
951            cache_dir: self.cache_dir,
952            overlay: self.overlay,
953            base_layer: self.base_layer,
954            fetcher: self
955                .fetcher
956                .unwrap_or_else(|| Box::new(HttpFetch::new()) as Box<dyn Fetch>),
957        })
958    }
959}
960
961/// One slot of the index walk: which repository publishes it, and what it
962/// publishes there.
963struct IndexSlot<'a> {
964    /// Which of the configured repositories serves it, as an index into them.
965    origin: usize,
966    /// The release, as that repository requested it.
967    release: &'a str,
968    /// The component published there, or `None` for a repository that publishes
969    /// none — the `<release>/<architecture>` layout.
970    component: Option<&'a str>,
971}
972
973/// The index slots a configuration publishes, in the order they are read.
974///
975/// The slots are the walk [`Alpine::read_indexes`] makes — every repository, and
976/// within it every component it publishes — so slot *i* of a plan is the one
977/// described at *i* here. The correspondence is positional, which is what
978/// [`validate_plan`] holds a caller-supplied plan to and what lets a package
979/// reach the mirror list it is fetched over.
980fn index_slots<'a>(repositories: &'a [Repository], architecture: &str) -> Vec<IndexSlot<'a>> {
981    let mut slots = Vec::new();
982    for (origin, repository) in repositories.iter().enumerate() {
983        for (component, _) in repository.indexes(architecture) {
984            slots.push(IndexSlot {
985                origin,
986                release: repository.release(),
987                component,
988            });
989        }
990    }
991    slots
992}
993
994/// What must hold of a [`Plan`] used as a pin.
995///
996/// A pin is read as constraints rather than as a manifest, so it needs less of
997/// the plan than [`validate_plan`] does: no index is consulted and nothing is
998/// fetched by name. What it does need is that every entry states a version this
999/// grammar can compare, because a version it cannot parse compares equal to
1000/// nothing and would be reported as a repository that had published past the pin
1001/// rather than as the malformed entry it is.
1002fn validate_pin(pin: &Plan, architecture: &str) -> Result<(), AlpineError> {
1003    let contradiction = |reason: String| Err(AlpineError::Config { reason });
1004    if pin.architecture != architecture {
1005        return contradiction(format!(
1006            "the pin resolves architecture {} but the bootstrap is configured for {architecture}",
1007            pin.architecture,
1008        ));
1009    }
1010    for package in &pin.packages {
1011        if let Some(reason) = package.unaddressable() {
1012            return contradiction(reason);
1013        }
1014    }
1015    Ok(())
1016}
1017
1018/// What must hold of a caller-supplied [`Plan`] before a bootstrap is built from
1019/// it.
1020///
1021/// A plan reaches a bootstrap without having been resolved by it, so nothing the
1022/// resolution guarantees can be assumed: not that the packages are the
1023/// configuration's architecture, not that their names are ones a repository
1024/// could publish, and not that the indexes they name are ones the configuration
1025/// walks. Every one of those is cheaper to refuse here, where the configuration
1026/// is frozen, than partway through a download — which is where each of them
1027/// would otherwise surface.
1028///
1029/// The counterpart of the Debian layer's `validate_plan`, and it answers the
1030/// same questions in the same order.
1031fn validate_plan(
1032    plan: &Plan,
1033    architecture: &str,
1034    slots: &[IndexSlot<'_>],
1035    pin_set: bool,
1036    includes: &[String],
1037    excludes: &[String],
1038) -> Result<(), AlpineError> {
1039    let contradiction = |reason: String| Err(AlpineError::Config { reason });
1040    if !includes.is_empty() {
1041        return contradiction(
1042            "a plan already names every package to install, so include() has nothing to add; \
1043             drop one of the two"
1044                .to_string(),
1045        );
1046    }
1047    if !excludes.is_empty() {
1048        return contradiction(
1049            "a plan already names every package to install, so exclude() has nothing to remove; \
1050             drop one of the two"
1051                .to_string(),
1052        );
1053    }
1054    if pin_set {
1055        return contradiction(
1056            "plan() installs a resolved plan instead of resolving, so there is no resolution for \
1057             pin() to hold to versions; drop one of the two"
1058                .to_string(),
1059        );
1060    }
1061    if plan.architecture != architecture {
1062        return contradiction(format!(
1063            "the plan resolves architecture {} but the bootstrap is configured for {architecture}",
1064            plan.architecture,
1065        ));
1066    }
1067    // Each package names the index it came from, and a bootstrap fetches it
1068    // through the repository that serves that index, so a plan resolved against
1069    // more indexes than the configuration publishes has packages with no mirror
1070    // to walk.
1071    if plan.indexes.len() > slots.len() {
1072        return contradiction(format!(
1073            "the plan resolved against {} indexes but the configured repositories publish {}, so \
1074             some of its packages name no mirror",
1075            plan.indexes.len(),
1076            slots.len(),
1077        ));
1078    }
1079    // The correspondence is positional, and a package's URL is composed from the
1080    // configured mirror and the plan's own release and component. Two
1081    // configurations that publish the same number of indexes in a different
1082    // order therefore compose URLs no mirror serves, and the walk that follows
1083    // would blame every mirror for a directory none of them was ever asked to
1084    // publish. The mirrors themselves are deliberately not compared: a plan
1085    // carried to a different configuration fetches from that configuration's
1086    // mirrors, which is what makes it portable.
1087    for (at, (index, slot)) in plan.indexes.iter().zip(slots).enumerate() {
1088        let describe = |release: &str, component: Option<&str>| match component {
1089            Some(component) => format!("{release}/{component}"),
1090            None => release.to_string(),
1091        };
1092        if index.release != slot.release || index.component.as_deref() != slot.component {
1093            return contradiction(format!(
1094                "the plan's index {at} is {} and the configured repository publishes {} there, so \
1095                 its packages would be fetched from a directory that repository does not serve",
1096                describe(&index.release, index.component.as_deref()),
1097                describe(slot.release, slot.component),
1098            ));
1099        }
1100    }
1101    for package in &plan.packages {
1102        if package.index >= plan.indexes.len() {
1103            return contradiction(format!(
1104                "the plan's package {} names index {} but the plan records {}, so it names no \
1105                 mirror",
1106                package.name,
1107                package.index,
1108                plan.indexes.len(),
1109            ));
1110        }
1111        // A document is held to this as it is read; a `Plan` is public and
1112        // `Clone`, so one taken from `resolve()` and edited reaches here having
1113        // passed nothing.
1114        if let Some(reason) = package.unaddressable() {
1115            return contradiction(reason);
1116        }
1117    }
1118    Ok(())
1119}
1120
1121/// Fetches, verifies and parses one repository index.
1122///
1123/// `path` is mirror-relative. The mirror list is walked in order, advancing past
1124/// one that [could not serve the index](FetchError::is_failover); a mirror that
1125/// serves something that does not verify is fatal rather than a reason to try
1126/// the next, since what it served was answered for by the URL that was asked
1127/// for. A URL the transport will not accept is fatal for the same reason it is
1128/// everywhere else: it is the configuration rather than the mirror, and walking
1129/// past it would report the failure against a mirror that was spelled fine.
1130fn read_index(
1131    fetcher: &mut dyn Fetch,
1132    repository: &Repository,
1133    component: Option<&str>,
1134    path: &str,
1135    from: usize,
1136    observer: &mut dyn AlpineObserver,
1137) -> Result<(Vec<Record>, ResolvedIndex), AlpineError> {
1138    walk_mirrors(
1139        &repository.mirrors,
1140        |mirror| {
1141            let url = mirror_url(mirror, path);
1142            // Reported for the same reason a package fetch is: this walk spends
1143            // one mirror timeout per unreachable mirror, and it is the slowest
1144            // part of a resolve. An observer that saw only `Resolving` would
1145            // watch a bootstrap that looks stopped.
1146            observer.progress(AlpineEvent::Fetching { url: &url });
1147            let mut bytes = Vec::new();
1148            let request = FetchRequest::new(&url);
1149            fetcher.fetch(
1150                &request,
1151                &mut LimitedWriter::new(&mut bytes, MAX_INDEX_FETCH),
1152            )?;
1153            let (records, signed_by, description) = parse_index(&url, &bytes, repository, from)?;
1154            Ok((
1155                records,
1156                ResolvedIndex {
1157                    mirror: mirror.clone(),
1158                    release: repository.release().to_string(),
1159                    component: component.map(str::to_string),
1160                    // Over the container as it arrived, signature segment
1161                    // included, so the value names the exact bytes whose
1162                    // signature was checked.
1163                    sha256: Algorithm::Sha256.hex_of(&bytes),
1164                    signed_by,
1165                    description,
1166                    // A record built from a live resolution has no document
1167                    // behind it, so there is nothing to carry. Qualified: this
1168                    // module's own `Carried` is the installed database's, a
1169                    // different question with the same name.
1170                    carried: super::document::Carried::new(),
1171                },
1172            ))
1173        },
1174        || AlpineError::Fetch(FetchError::not_found(path.to_string())),
1175    )
1176}
1177
1178/// Verifies an index container and reads the records it carries.
1179///
1180/// `url` names where it came from, for the errors.
1181fn parse_index(
1182    url: &str,
1183    bytes: &[u8],
1184    repository: &Repository,
1185    from: usize,
1186) -> Result<(Vec<Record>, String, Option<String>), AlpineError> {
1187    let mut segments = apk::Segments::new(bytes);
1188    // An index is the trust root: nothing above it vouches for it, so its own
1189    // signature is what is verified, against this repository's keys and no
1190    // other's.
1191    let metadata = apk::read_metadata(
1192        url,
1193        &mut segments,
1194        apk::Anchor::Signature(&repository.keys),
1195        MAX_INDEX_RECORDS,
1196    )?;
1197    // Nothing in an index follows its records, and a trailing segment is
1198    // covered by no digest the format defines, so it is a refusal rather than a
1199    // curiosity.
1200    if !segments
1201        .at_end()
1202        .map_err(|err| AlpineError::container(url, format!("its end is unreadable: {err}")))?
1203    {
1204        return Err(AlpineError::container(
1205            url,
1206            "a segment follows its records, which nothing signed accounts for",
1207        ));
1208    }
1209    let records = metadata
1210        .member("APKINDEX")
1211        .ok_or_else(|| AlpineError::index(url, "its signed segment carries no APKINDEX member"))?;
1212    let text = std::str::from_utf8(&records.data)
1213        .map_err(|err| AlpineError::index(url, format!("its records are not UTF-8: {err}")))?;
1214    // Whatever the repository chose to write there, kept as a fact about the
1215    // publication rather than read for meaning: Alpine writes a build string and
1216    // postmarketOS a timestamp, and the format defines neither.
1217    let description = metadata
1218        .member("DESCRIPTION")
1219        .map(|member| String::from_utf8_lossy(&member.data).trim().to_string())
1220        .filter(|description| !description.is_empty());
1221    Ok((
1222        index::parse(url, text, from)?,
1223        metadata.signed_by,
1224        description,
1225    ))
1226}
1227
1228/// An observer of an Alpine bootstrap's progress.
1229///
1230/// Attached with [`Alpine::observe`]. Every method has a default body, so an
1231/// implementation takes only the events it cares about — and every method added
1232/// in a later release will carry one too, so an existing implementation keeps
1233/// compiling.
1234///
1235/// # Example
1236///
1237/// ```no_run
1238/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1239/// use ferroday_cage::provision::alpine::{Alpine, AlpineEvent, AlpineObserver};
1240///
1241/// struct Progress;
1242///
1243/// impl AlpineObserver for Progress {
1244///     fn progress(&mut self, event: AlpineEvent<'_>) {
1245///         if let AlpineEvent::Downloading { package, index, total, .. } = event {
1246///             eprintln!("{package} ({index}/{total})");
1247///         }
1248///     }
1249/// }
1250///
1251/// let mut alpine = Alpine::builder("v3.23").include(["alpine-base"]).build()?;
1252/// let plan = alpine.observe(&mut Progress).resolve()?;
1253/// # let _ = plan;
1254/// # Ok(())
1255/// # }
1256/// ```
1257pub trait AlpineObserver {
1258    /// Receives one progress event.
1259    fn progress(&mut self, event: AlpineEvent<'_>) {
1260        let _ = event;
1261    }
1262
1263    /// Whether the bootstrap should stop.
1264    ///
1265    /// Consulted where stopping is clean — a package boundary, a script
1266    /// boundary. Returning `true` aborts with [`ProvisionError::Cancelled`],
1267    /// and a run driven through [`provision::ensure`](super::ensure) then
1268    /// removes the staging tree, so a cancelled bootstrap leaves no destination
1269    /// behind.
1270    ///
1271    /// [`ProvisionError::Cancelled`]: super::ProvisionError::Cancelled
1272    fn cancelled(&mut self) -> bool {
1273        false
1274    }
1275}
1276
1277/// A closure is an observer that reports and never cancels.
1278impl<F: FnMut(AlpineEvent<'_>)> AlpineObserver for F {
1279    fn progress(&mut self, event: AlpineEvent<'_>) {
1280        self(event);
1281    }
1282}
1283
1284/// A progress event from an Alpine bootstrap.
1285#[derive(Debug)]
1286#[non_exhaustive]
1287pub enum AlpineEvent<'a> {
1288    /// A URL is being fetched.
1289    #[non_exhaustive]
1290    Fetching {
1291        /// The URL.
1292        url: &'a str,
1293    },
1294    /// The dependency closure is being resolved.
1295    Resolving,
1296    /// The closure has been resolved, carrying the plan the bootstrap will
1297    /// install.
1298    ///
1299    /// Emitted mid-bootstrap, once every index has been verified and before the
1300    /// first package is downloaded, so a consumer sees the exact manifest a
1301    /// [`provision::ensure`](super::ensure) installs without a separate
1302    /// [`Alpine::resolve`] pass.
1303    #[non_exhaustive]
1304    Resolved {
1305        /// The plan the bootstrap will install.
1306        plan: &'a Plan,
1307    },
1308    /// A package is being downloaded.
1309    #[non_exhaustive]
1310    Downloading {
1311        /// The package name.
1312        package: &'a str,
1313        /// Its position among the packages being downloaded, from 1.
1314        index: usize,
1315        /// How many packages are being downloaded, which is the plan's total
1316        /// less whatever the cache already held.
1317        total: usize,
1318    },
1319    /// A package's files are being laid down.
1320    #[non_exhaustive]
1321    Extracting {
1322        /// The package name.
1323        package: &'a str,
1324    },
1325    /// One of a package's scripts is about to run.
1326    #[non_exhaustive]
1327    Script {
1328        /// The package the script belongs to.
1329        package: &'a str,
1330        /// Which script it is: `pre-install`, `post-install` or `trigger`.
1331        script: &'static str,
1332    },
1333    /// Output from a script running inside the cage.
1334    #[non_exhaustive]
1335    CommandOutput {
1336        /// Which standard stream the bytes came from.
1337        stream: Stream,
1338        /// The raw output bytes; not line-buffered, not guaranteed UTF-8.
1339        bytes: &'a [u8],
1340    },
1341}
1342
1343impl Provisioner for Alpine {
1344    /// Bootstraps the configured release into the request's staging directory.
1345    ///
1346    /// Reached through [`provision::ensure`](super::ensure), which publishes the
1347    /// finished tree atomically and removes it on any failure.
1348    fn provision(&mut self, request: &ProvisionRequest<'_>) -> Result<(), ProvisionError> {
1349        let mut observer = RunObserver { request };
1350        self.provision_reporting(request.staging(), &mut observer)
1351    }
1352}
1353
1354impl Alpine {
1355    /// Reports this bootstrap's progress to `sink`, which sees every
1356    /// [`AlpineEvent`] and can stop the run.
1357    ///
1358    /// The returned value is a [`Provisioner`] and carries the read entry points
1359    /// too, so one attachment covers a resolve and the bootstrap that follows
1360    /// it.
1361    pub fn observe<'o>(&'o mut self, sink: &'o mut dyn AlpineObserver) -> Observed<'o> {
1362        Observed { alpine: self, sink }
1363    }
1364
1365    /// The bootstrap, with an observer already chosen.
1366    fn provision_reporting(
1367        &mut self,
1368        staging: &Path,
1369        observer: &mut dyn AlpineObserver,
1370    ) -> Result<(), ProvisionError> {
1371        bootstrap_release(self, staging, observer)
1372    }
1373}
1374
1375/// An [`Alpine`] with an observer attached, from [`Alpine::observe`].
1376pub struct Observed<'o> {
1377    alpine: &'o mut Alpine,
1378    sink: &'o mut dyn AlpineObserver,
1379}
1380
1381impl fmt::Debug for Observed<'_> {
1382    /// Renders the provisioner. The sink is the caller's value, with nothing
1383    /// renderable about one.
1384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1385        f.debug_struct("Observed")
1386            .field("alpine", &self.alpine)
1387            .finish_non_exhaustive()
1388    }
1389}
1390
1391impl Observed<'_> {
1392    /// [`Alpine::resolve`], reporting to the attached sink.
1393    pub fn resolve(&mut self) -> Result<Plan, AlpineError> {
1394        // A provisioner with a base layer stages an increment, which
1395        // `provision::ensure` refuses outright for the same value. Reporting the
1396        // whole-root closure here would answer a question this provisioner never
1397        // asks -- hundreds of packages the base already carries — and read as
1398        // the plan it would install. `resolve_layer` is its read half.
1399        if self.alpine.base_layer.is_some() {
1400            return Err(AlpineError::Config {
1401                reason: "a provisioner with a base layer resolves an increment, and is read \
1402                         through Alpine::resolve_layer rather than through Alpine::resolve"
1403                    .to_string(),
1404            });
1405        }
1406        self.alpine.resolve_over(None, self.sink)
1407    }
1408
1409    /// [`Alpine::available`], reporting to the attached sink.
1410    ///
1411    /// The sink sees one [`Fetching`](AlpineEvent::Fetching) per index, which is
1412    /// the whole of what this call does.
1413    pub fn available(&mut self) -> Result<Available, AlpineError> {
1414        // Unpinned whatever the builder holds: this reports what the
1415        // repositories offer, and a pin describes a selection from it rather
1416        // than a bound on what may be asked about.
1417        let (records, _indexes) = self.alpine.read_indexes(self.sink)?;
1418        Ok(Index::merge(records, &self.alpine.architecture, &Pins::none()).into_available())
1419    }
1420
1421    /// [`Alpine::resolve_layer`], reporting to the attached sink.
1422    pub fn resolve_layer(&mut self) -> Result<Plan, AlpineError> {
1423        let base = self.alpine.read_base_layer("resolve_layer")?;
1424        self.alpine.resolve_over(Some(&base), self.sink)
1425    }
1426
1427    /// [`Alpine::stage_layer`], reporting to the attached sink.
1428    ///
1429    /// The sink sees the increment's downloads and the output of every script
1430    /// run in the overlay-rooted cage, which is the whole of what a layered
1431    /// build has to report.
1432    pub fn stage_layer(&mut self, upper: impl AsRef<Path>) -> Result<BuildLayer, ProvisionError> {
1433        self.alpine.stage_layer_reporting(upper.as_ref(), self.sink)
1434    }
1435}
1436
1437impl Provisioner for Observed<'_> {
1438    /// Bootstraps as [`Alpine`] does, reporting to the attached sink rather than
1439    /// through the run's shared vocabulary.
1440    fn provision(&mut self, request: &ProvisionRequest<'_>) -> Result<(), ProvisionError> {
1441        let mut observer = ObservedRun {
1442            sink: self.sink,
1443            request,
1444        };
1445        self.alpine
1446            .provision_reporting(request.staging(), &mut observer)
1447    }
1448}
1449
1450/// An observer that reports through the run's shared vocabulary and takes its
1451/// cancellation from the run: what an unobserved [`Alpine`] uses.
1452struct RunObserver<'a, 'r> {
1453    request: &'a ProvisionRequest<'r>,
1454}
1455
1456impl AlpineObserver for RunObserver<'_, '_> {
1457    fn progress(&mut self, event: AlpineEvent<'_>) {
1458        self.request.report(super::ProvisionEvent::Alpine(&event));
1459    }
1460
1461    fn cancelled(&mut self) -> bool {
1462        self.request.cancelled()
1463    }
1464}
1465
1466/// An observer that reports to a sink the caller bound with [`Alpine::observe`]
1467/// and takes its cancellation from the run.
1468struct ObservedRun<'a, 'r, 'o> {
1469    sink: &'o mut dyn AlpineObserver,
1470    request: &'a ProvisionRequest<'r>,
1471}
1472
1473impl AlpineObserver for ObservedRun<'_, '_, '_> {
1474    fn progress(&mut self, event: AlpineEvent<'_>) {
1475        self.sink.progress(event);
1476    }
1477
1478    fn cancelled(&mut self) -> bool {
1479        self.request.cancelled()
1480    }
1481}
1482
1483/// An observer that reports nowhere and never cancels: what an entry point the
1484/// caller attached no sink to uses.
1485struct Silent;
1486
1487impl AlpineObserver for Silent {}
1488
1489/// One package acquired and verified, waiting to be laid down.
1490#[derive(Debug)]
1491struct Acquired {
1492    /// The plan entry it answers, which is where its name, version and
1493    /// architecture are checked against what arrived.
1494    planned: PlannedPackage,
1495    /// What the container says about itself, and where its file tree begins.
1496    package: apk::Package,
1497    /// The verified file itself, still open, to be seeked to the file tree the
1498    /// verification already digested.
1499    ///
1500    /// The descriptor rather than the path, and that is the whole point: a cache
1501    /// directory is the caller's and its atomic publish is written for concurrent
1502    /// writers, so re-opening the path would extract whatever is there by then
1503    /// rather than what was verified. Nothing can be swapped underneath an open
1504    /// descriptor — a replacement publishes a new inode and this one keeps
1505    /// reading the old.
1506    file: std::fs::File,
1507    /// Where that file is, for the errors, which name a path rather than a
1508    /// descriptor.
1509    path: PathBuf,
1510    /// The file's length, which the record states as `S:`.
1511    size: u64,
1512}
1513
1514/// What must hold before anything is downloaded.
1515///
1516/// An unavailable identity map or an unrunnable architecture is a configuration
1517/// problem, and both are cheaper to report now than after a closure has been
1518/// fetched. Neither applies to an extract-only run, which executes nothing, so
1519/// such a run reaches none of this.
1520///
1521/// A caller-supplied plan is not checked here: it is checked by
1522/// [`validate_plan`] at `build()`, where the configuration is frozen and where
1523/// the refusal reaches a caller who never provisions at all.
1524fn preflight(alpine: &Alpine) -> Result<(), AlpineError> {
1525    if alpine.extract_only {
1526        return Ok(());
1527    }
1528    preflight_foreign(&host_architecture(), &alpine.architecture)?;
1529    // The identity posture is never quietly downgraded to the single-identity
1530    // form.
1531    if !matches!(alpine.identity_map, IdentityMap::Single)
1532        && let Err(reason) = crate::idmap::resolve_default_chain(&alpine.identity_map)
1533    {
1534        return Err(AlpineError::Config {
1535            reason: format!("the requested identity map is unavailable: {reason}"),
1536        });
1537    }
1538    Ok(())
1539}
1540
1541/// The whole bootstrap: preflight, then install into the staging tree.
1542fn bootstrap_release(
1543    alpine: &mut Alpine,
1544    staging: &Path,
1545    observer: &mut dyn AlpineObserver,
1546) -> Result<(), ProvisionError> {
1547    // A provisioner configured with a base layer is a layered one, and this
1548    // would quietly bootstrap a whole root into a directory the caller meant as
1549    // an increment over that base.
1550    if alpine.base_layer.is_some() {
1551        return Err(wrap(AlpineError::Config {
1552            reason: "a provisioner with a base layer stages an increment, and is used through \
1553                     Alpine::stage_layer rather than through provision::ensure"
1554                .to_string(),
1555        }));
1556    }
1557    preflight(alpine).map_err(wrap)?;
1558    install(alpine, staging, None, observer)
1559}
1560
1561/// Resolve or read the plan, fetch and verify every package, lay the closure
1562/// down, and configure it.
1563///
1564/// `into` is the tree being written — a whole root for a bootstrap, the overlay
1565/// upper for a layered build — and `base` is the provisioned root beneath it
1566/// where there is one. One body serves both so that a layered build differs from
1567/// a bootstrap in what it resolves against and where its scripts are rooted, and
1568/// in nothing else.
1569fn install(
1570    alpine: &mut Alpine,
1571    into: &Path,
1572    base: Option<&layer::Base>,
1573    observer: &mut dyn AlpineObserver,
1574) -> Result<(), ProvisionError> {
1575    // A plan is the resolution: no index is fetched, which is the whole of what
1576    // installing from one saves. It is reported as it stands rather than
1577    // re-projected, so an observer sees the same event either way.
1578    let plan = match alpine.plan.clone() {
1579        Some(plan) => plan,
1580        None => alpine.resolve_over(base, observer).map_err(wrap)?,
1581    };
1582    observer.progress(AlpineEvent::Resolved { plan: &plan });
1583
1584    // The state the finished root carries, composed before anything is
1585    // downloaded so that an increment with nothing in it still writes it. The
1586    // base's own is carried forward rather than replaced: these files shadow
1587    // the base's copies through the overlay, so each has to describe the merged
1588    // root.
1589    //
1590    // A plan names every package rather than the seeds a resolution closed over,
1591    // and `plan()` forbids `include()` — so the configured world is empty and
1592    // the plan's own names are what the root wants. Written as world, they pin
1593    // the closure for a later `apk`: an empty file says nothing is wanted, which
1594    // is what `apk fix` acts on.
1595    let wanted: Vec<String> = match &alpine.plan {
1596        Some(plan) => plan.packages.iter().map(|p| p.name.clone()).collect(),
1597        None => alpine.world.clone(),
1598    };
1599    let world = joined(base.map_or(&[], layer::Base::world), wanted);
1600    let sources = joined(
1601        base.map_or(&[], layer::Base::repositories),
1602        alpine
1603            .repositories
1604            .iter()
1605            .flat_map(super::alpine::Repository::sources),
1606    );
1607    let beneath = base
1608        .map(|base| {
1609            Rooted::open(base.path()).map_err(AlpineError::at("opening the base", base.path()))
1610        })
1611        .transpose()
1612        .map_err(wrap)?;
1613    let state = bootstrap::State {
1614        world: &world,
1615        architecture: &alpine.architecture,
1616        sources: &sources,
1617        repositories: &alpine.repositories,
1618        beneath: beneath.as_ref(),
1619    };
1620
1621    if let Some(base) = base {
1622        // The resolver reports this as the upgrade it is, with the dependency
1623        // that forced it; a plan reaches here without having been resolved
1624        // against this base at all, so the check is made against the plan rather
1625        // than only inside the resolution.
1626        let carried: Vec<&str> = base.records().map(|record| record.name.as_str()).collect();
1627        let doubled: Vec<&str> = plan
1628            .packages
1629            .iter()
1630            .map(|package| package.name.as_str())
1631            .filter(|name| carried.contains(name))
1632            .collect();
1633        if !doubled.is_empty() {
1634            return Err(wrap(AlpineError::Config {
1635                reason: format!(
1636                    "the increment would install {}, which the base layer already carries; an \
1637                     increment installs over a base rather than replacing part of it",
1638                    doubled.join(", "),
1639                ),
1640            }));
1641        }
1642        // An increment with nothing in it needs no download and no script — but
1643        // it still writes its state, so what a caller reads back from the
1644        // merged view does not depend on whether the delta happened to be
1645        // empty. Registration is a file format rather than a program's output,
1646        // so writing it runs nothing.
1647        if plan.packages.is_empty() {
1648            let database = installed::Database::carrying(base.carried());
1649            return bootstrap::register(into, &database, &state).map_err(wrap);
1650        }
1651    }
1652
1653    // The cache disposes of itself when this frame ends, on the failure paths
1654    // below as on the success path.
1655    let cache = PackageCache::beside(into, alpine.cache_dir.as_deref(), APK_CACHE);
1656    let mut acquired = acquire(
1657        &mut *alpine.fetcher,
1658        &plan,
1659        &alpine.repositories,
1660        &alpine.architecture,
1661        cache.path(),
1662        observer,
1663    )?;
1664
1665    let order = install_order(&acquired);
1666    let carried = base.map(layer::Base::carried).unwrap_or_default();
1667    let (database, subjects) = lay_down(into, carried, &mut acquired, &order, observer)?;
1668
1669    // After every package's files are down, so this overwrites what they
1670    // shipped, and before any script runs, so a script reads what was injected.
1671    if let Some(overlay) = &alpine.overlay {
1672        super::extract::overlay_tree(into, overlay)?;
1673    }
1674
1675    if alpine.extract_only {
1676        // Registration is a file format here rather than a program's output, so
1677        // an extract-only root still knows what it holds.
1678        return bootstrap::register(into, &database, &state).map_err(wrap);
1679    }
1680    // A base's own trigger-watching packages fire for the directories this
1681    // install created, which is what `apk` does: a trigger belongs to the root
1682    // rather than to the transaction.
1683    let watching = match base {
1684        Some(base) => base.trigger_subjects().map_err(wrap)?,
1685        None => Vec::new(),
1686    };
1687    bootstrap::configure(
1688        bootstrap::Site {
1689            staging: into,
1690            rooting: Rooting::over(base.map(layer::Base::path)),
1691            identity_map: &alpine.identity_map,
1692        },
1693        &database,
1694        &subjects,
1695        &watching,
1696        &state,
1697        observer,
1698    )
1699    .map_err(wrap)
1700}
1701
1702/// A base layer's lines and this run's, in that order and without a repeat.
1703///
1704/// The base's order is kept because `apk` reads `/etc/apk/repositories` top to
1705/// bottom, and a line the base already records is not written twice.
1706fn joined(carried: &[String], mine: impl IntoIterator<Item = String>) -> Vec<String> {
1707    let mut all = carried.to_vec();
1708    for one in mine {
1709        if !all.contains(&one) {
1710            all.push(one);
1711        }
1712    }
1713    all
1714}
1715
1716/// Fetches every package the plan names into the cache and verifies it there.
1717///
1718/// A cached entry is used only when it reads back as the package the plan names.
1719/// That is a stronger check than a Debian pool's, and it has to be: an apk index
1720/// publishes no digest of the whole file, so what identifies a package is its
1721/// signature, the control identity the index recorded, and the `datahash`
1722/// binding its file tree — all of which this reads anyway.
1723/// Each package is fetched over the mirrors of the repository that serves the
1724/// index it came from, walked in configured order, so a package that has rotated
1725/// off a live mirror is served by that repository's backstop. The plan's own
1726/// record of which mirror served the index is provenance rather than a fetch
1727/// instruction; it is the configuration that says where to fetch from.
1728fn acquire(
1729    fetcher: &mut dyn Fetch,
1730    plan: &Plan,
1731    repositories: &[Repository],
1732    architecture: &str,
1733    cache: &Path,
1734    observer: &mut dyn AlpineObserver,
1735) -> Result<Vec<Acquired>, ProvisionError> {
1736    std::fs::create_dir_all(cache)
1737        .map_err(|err| ProvisionError::io("creating the package directory", cache, err))?;
1738
1739    // Positional, and `validate_plan` has already held the plan to it, so a slot
1740    // a package names is one this resolves.
1741    let slots = index_slots(repositories, architecture);
1742
1743    // The cache pass and the announcement, before anything is fetched: what a
1744    // caller is told it is downloading is what the batch and the walk between
1745    // them go on to fetch, in the order the plan names them.
1746    let mut missing: Vec<Missing<'_>> = Vec::new();
1747    for planned in &plan.packages {
1748        if observer.cancelled() {
1749            return Err(ProvisionError::Cancelled);
1750        }
1751        let index = plan.indexes.get(planned.index).ok_or_else(|| {
1752            wrap(AlpineError::Config {
1753                reason: format!(
1754                    "the plan attributes {} to index {}, and records {}",
1755                    planned.name,
1756                    planned.index,
1757                    plan.indexes.len(),
1758                ),
1759            })
1760        })?;
1761        let mirrors = slots
1762            .get(planned.index)
1763            .map(|slot| repositories[slot.origin].mirrors.as_slice())
1764            .ok_or_else(|| {
1765                wrap(AlpineError::Config {
1766                    reason: format!(
1767                        "the plan attributes {} to index {}, and the configured repositories \
1768                         publish {}",
1769                        planned.name,
1770                        planned.index,
1771                        slots.len(),
1772                    ),
1773                })
1774            })?;
1775        let path = cache.join(planned.file_name());
1776
1777        // A cached entry that does not read back as this package is not an
1778        // error; it is a download waiting to happen.
1779        if read_acquired(&path, planned, architecture).is_err() {
1780            missing.push(Missing {
1781                planned,
1782                index,
1783                mirrors,
1784                path,
1785            });
1786        }
1787    }
1788
1789    // Announced over what is actually being fetched, once the cache has been
1790    // asked about every package: a position among the plan's packages counts
1791    // the ones already cached, so an observer reporting `index` of `total` would
1792    // show a fetch of three packages as reaching nine of ten. The announcement
1793    // precedes the batch because the batch fetches several at once, which is
1794    // the shape a fetch event has throughout this crate.
1795    for (position, entry) in missing.iter().enumerate() {
1796        observer.progress(AlpineEvent::Downloading {
1797            package: &entry.planned.name,
1798            index: position + 1,
1799            total: missing.len(),
1800        });
1801    }
1802
1803    prefetch(fetcher, &missing, architecture, observer)?;
1804
1805    for entry in &missing {
1806        // A package boundary: whatever has been downloaded is complete and the
1807        // staging tree is untouched, so stopping here costs nothing.
1808        if observer.cancelled() {
1809            return Err(ProvisionError::Cancelled);
1810        }
1811        if read_acquired(&entry.path, entry.planned, architecture).is_ok() {
1812            continue;
1813        }
1814        download_over(
1815            fetcher,
1816            entry.mirrors,
1817            entry.index,
1818            entry.planned,
1819            architecture,
1820            &entry.path,
1821            observer,
1822        )
1823        .map_err(wrap)?;
1824    }
1825
1826    plan.packages
1827        .iter()
1828        .map(|planned| {
1829            read_acquired(&cache.join(planned.file_name()), planned, architecture).map_err(wrap)
1830        })
1831        .collect()
1832}
1833
1834/// A package the cache does not hold, and where to fetch it from.
1835struct Missing<'a> {
1836    planned: &'a PlannedPackage,
1837    index: &'a ResolvedIndex,
1838    mirrors: &'a [String],
1839    path: PathBuf,
1840}
1841
1842/// How many packages one batch asks for.
1843///
1844/// Each job holds an open staging file for as long as the batch runs, so this
1845/// is a bound on descriptors rather than on the transport, which decides for
1846/// itself how many of a batch to have in flight. It is also the granularity a
1847/// cancelled bootstrap stops at, which is the reason it is not larger.
1848const BATCH: usize = 32;
1849
1850/// Fills the cache with the packages a bootstrap is about to install, several
1851/// at a time, through [`Fetch::fetch_all`].
1852///
1853/// Every job asks the package's own first mirror, which is the one the walk
1854/// would ask first too. A job that does not arrive leaves the cache without
1855/// that package and the walk that follows fetches it with every mirror
1856/// available to it, so this is an optimization and nothing more. What the
1857/// rename publishes is a *complete* body rather than a verified one, exactly as
1858/// [`download`] does: the verification is the read that follows, which is the
1859/// same read a cache hit goes through.
1860fn prefetch(
1861    fetcher: &mut dyn Fetch,
1862    missing: &[Missing<'_>],
1863    architecture: &str,
1864    observer: &mut dyn AlpineObserver,
1865) -> Result<(), ProvisionError> {
1866    for batch in missing.chunks(BATCH) {
1867        if observer.cancelled() {
1868            return Err(ProvisionError::Cancelled);
1869        }
1870        let mut staged = Vec::with_capacity(batch.len());
1871        for entry in batch {
1872            let Some(mirror) = entry.mirrors.first() else {
1873                continue;
1874            };
1875            let url = mirror_url(
1876                &entry.index.directory_under(mirror),
1877                &format!("{architecture}/{}", entry.planned.file_name()),
1878            );
1879            observer.progress(AlpineEvent::Fetching { url: &url });
1880            let path = super::staging_path(&entry.path);
1881            let Ok(file) = std::fs::OpenOptions::new()
1882                .write(true)
1883                .create_new(true)
1884                .open(&path)
1885            else {
1886                continue;
1887            };
1888            // Capped as the per-package download is: the size travels with
1889            // the request, but a transport is free to ignore it.
1890            let sink = std::io::BufWriter::new(file);
1891            let cap = entry.planned.size.unwrap_or(u64::MAX);
1892            staged.push((entry, url, path, LimitedWriter::new(sink, cap)));
1893        }
1894
1895        let outcomes = {
1896            let mut jobs: Vec<FetchJob<'_>> = staged
1897                .iter_mut()
1898                .map(|(entry, url, _path, sink)| {
1899                    let request = FetchRequest::new(url);
1900                    let request = match entry.planned.size {
1901                        Some(size) => request.sized(size),
1902                        None => request,
1903                    };
1904                    FetchJob::new(request, sink)
1905                })
1906                .collect();
1907            fetcher.fetch_all(&mut jobs)
1908        };
1909
1910        for ((entry, _url, path, sink), outcome) in staged.into_iter().zip(outcomes) {
1911            let published = outcome.is_ok()
1912                && sink.into_inner().into_inner().is_ok()
1913                && std::fs::rename(&path, &entry.path).is_ok();
1914            if !published {
1915                let _ = std::fs::remove_file(&path);
1916            }
1917        }
1918    }
1919    Ok(())
1920}
1921
1922/// Reads a cached `.apk` and holds it to the plan entry it answers.
1923///
1924/// This is the index-to-package binding, and it is the whole of what
1925/// authenticates a package: the control identity is recomputed over the bytes
1926/// that arrived and compared to the one the plan records — which came out of a
1927/// verified index, or out of a plan a caller kept — and the package's own name,
1928/// version and architecture are compared to the plan's.
1929///
1930/// The file the checks were made against is carried out in the [`Acquired`],
1931/// rather than the path being opened again where the tree is laid down: the two
1932/// opens would be two files, and only the first of them was verified.
1933///
1934/// The binding is what a signature alone cannot do. A signature says that *some*
1935/// package was signed; without the binding a mirror answers a request for
1936/// `musl-1.2.5-r23` with a validly signed `musl-1.2.5-r0` and nothing notices.
1937/// The package's own signature, meanwhile, adds nothing the identity has not
1938/// already fixed, and [`apk::Anchor`] records why it is read rather than
1939/// verified.
1940fn read_acquired(
1941    path: &Path,
1942    planned: &PlannedPackage,
1943    architecture: &str,
1944) -> Result<Acquired, AlpineError> {
1945    let subject = planned.file_name();
1946    let mut file = std::fs::File::open(path).map_err(AlpineError::at("reading a package", path))?;
1947    let size = file
1948        .metadata()
1949        .map_err(AlpineError::at("measuring a package", path))?
1950        .len();
1951    let package = apk::read_package(
1952        &subject,
1953        &mut apk::Segments::new(&mut file),
1954        MAX_CONTROL_SEGMENT,
1955    )?;
1956
1957    if package.control_identity != planned.control_identity {
1958        return Err(AlpineError::Digest {
1959            subject,
1960            expected: planned.control_identity.clone(),
1961            actual: package.control_identity,
1962        });
1963    }
1964    // The identity above already fixes the control segment's bytes, so these
1965    // cannot disagree with it unless the plan itself does. They are checked
1966    // because a plan is a document a caller may have edited, and a plan whose
1967    // fields contradict the package it names should be refused rather than
1968    // silently overruled by the digest.
1969    let stated = |what: &str, want: &str, got: &str| AlpineError::Config {
1970        reason: format!(
1971            "the plan names {} at {what} {want:?}, and the package it identifies states \
1972                 {got:?}",
1973            planned.name,
1974        ),
1975    };
1976    if package.info.name != planned.name {
1977        return Err(stated("package", &planned.name, &package.info.name));
1978    }
1979    if package.info.version != planned.version {
1980        return Err(stated("version", &planned.version, &package.info.version));
1981    }
1982    if !package.info.is_noarch() && package.info.architecture != architecture {
1983        return Err(stated(
1984            "architecture",
1985            architecture,
1986            &package.info.architecture,
1987        ));
1988    }
1989
1990    Ok(Acquired {
1991        planned: planned.clone(),
1992        package,
1993        file,
1994        path: path.to_path_buf(),
1995        size,
1996    })
1997}
1998
1999/// Downloads one package, walking the mirrors of the repository that serves it.
2000///
2001/// A mirror that [could not serve it](FetchError::is_failover) advances the walk
2002/// and the last such failure is reported once none is left, which is the
2003/// discipline the index walk and the Debian layer's package walk both follow.
2004/// Nothing else advances it: a body that arrives and does not verify is refused
2005/// where it is read, since what arrived was answered for by the URL that was
2006/// asked for.
2007fn download_over(
2008    fetcher: &mut dyn Fetch,
2009    mirrors: &[String],
2010    index: &ResolvedIndex,
2011    planned: &PlannedPackage,
2012    architecture: &str,
2013    dest: &Path,
2014    observer: &mut dyn AlpineObserver,
2015) -> Result<(), AlpineError> {
2016    walk_mirrors(
2017        mirrors,
2018        |mirror| {
2019            let url = mirror_url(
2020                &index.directory_under(mirror),
2021                &format!("{architecture}/{}", planned.file_name()),
2022            );
2023            observer.progress(AlpineEvent::Fetching { url: &url });
2024            download(fetcher, &url, planned, dest)
2025        },
2026        || AlpineError::Fetch(FetchError::not_found(planned.file_name())),
2027    )
2028}
2029
2030/// Downloads one package into the cache, atomically.
2031///
2032/// The body is written to a staging file beside the destination and renamed onto
2033/// it, so a partial body is never visible at the cache path and a concurrent
2034/// download of the same package publishes its own file rather than consuming
2035/// this one. What the rename publishes is a *complete* body rather than a
2036/// verified one: the verification happens on the read that follows, which is the
2037/// same read a cache hit goes through — one code path, whichever produced the
2038/// file — and an entry that does not verify is downloaded again rather than
2039/// trusted.
2040///
2041/// The index's `S:` bounds what the download may spend. It is a length a
2042/// verified source declared, so it travels as [`FetchRequest::sized`] as well as
2043/// capping the writer; an index that states none leaves the download bounded by
2044/// the fetcher's own ceiling, which is weaker rather than wrong.
2045fn download(
2046    fetcher: &mut dyn Fetch,
2047    url: &str,
2048    planned: &PlannedPackage,
2049    dest: &Path,
2050) -> Result<(), AlpineError> {
2051    let staged = super::staging_path(dest);
2052    let file = std::fs::OpenOptions::new()
2053        .write(true)
2054        .create_new(true)
2055        .open(&staged)
2056        .map_err(AlpineError::at("staging a package", &staged))?;
2057
2058    let mut sink = std::io::BufWriter::new(file);
2059    let request = FetchRequest::new(url);
2060    let fetched = match planned.size {
2061        Some(size) => {
2062            let mut capped = LimitedWriter::new(&mut sink, size);
2063            fetcher.fetch(&request.sized(size), &mut capped)
2064        }
2065        None => fetcher.fetch(&request, &mut sink),
2066    };
2067    // The last buffer is written here rather than by `BufWriter`'s own `Drop`,
2068    // which has nowhere to report a failure and so swallows it. A body whose
2069    // tail never reached the disk would otherwise be renamed into the cache and
2070    // reported by the read that follows as a digest or container failure — an
2071    // authenticity answer to a local write failure, which is the wrong thing to
2072    // tell the caller and the wrong thing for them to act on.
2073    let flushed = sink
2074        .into_inner()
2075        .map(|_| ())
2076        .map_err(|err| AlpineError::at("writing a package", &staged)(err.into_error()));
2077
2078    let published = fetched
2079        .map_err(AlpineError::Fetch)
2080        .and(flushed)
2081        .and_then(|()| {
2082            std::fs::rename(&staged, dest).map_err(AlpineError::at("publishing a package", dest))
2083        });
2084    if published.is_err() {
2085        let _ = std::fs::remove_file(&staged);
2086    }
2087    published
2088}
2089
2090/// The order the closure is laid down and configured in: a package after
2091/// everything it needs.
2092fn install_order(acquired: &[Acquired]) -> Vec<usize> {
2093    let mut provides: HashMap<String, Vec<usize>> = HashMap::new();
2094    for (at, held) in acquired.iter().enumerate() {
2095        provides
2096            .entry(held.package.info.name.clone())
2097            .or_default()
2098            .push(at);
2099        for entry in &held.package.info.provides {
2100            // A `p:` entry may state a version; the name is what a dependency
2101            // names it by.
2102            provides
2103                .entry(version::name_of(entry).to_string())
2104                .or_default()
2105                .push(at);
2106        }
2107    }
2108    let depends: Vec<Vec<String>> = acquired
2109        .iter()
2110        .map(|held| {
2111            held.package
2112                .info
2113                .depends
2114                .iter()
2115                // A `!` entry states what must *not* be installed. The
2116                // resolution already settled that; an install order has no edge
2117                // to draw from it.
2118                .filter(|entry| !entry.starts_with('!'))
2119                .map(|entry| version::name_of(entry).to_string())
2120                .collect()
2121        })
2122        .collect();
2123    bootstrap::dependency_order(&depends, &provides)
2124}
2125
2126/// Lays every package's file tree into the staging tree, building the installed
2127/// database as it goes.
2128///
2129/// One pass per package, in `order`, and the second read of each acquired file:
2130/// the first established that the bytes are the package, and this one decodes
2131/// the file tree it already digested. It is the same open file both times, which
2132/// is what makes "already digested" true of the bytes this reads.
2133fn lay_down(
2134    staging: &Path,
2135    carried: Carried,
2136    acquired: &mut [Acquired],
2137    order: &[usize],
2138    observer: &mut dyn AlpineObserver,
2139) -> Result<(Database, Vec<ScriptSubject>), ProvisionError> {
2140    let mut extraction = Extraction::new(staging)?.root_mode(ROOTFS_ROOT_MODE);
2141    let mut database = Database::carrying(carried);
2142    let mut subjects = Vec::with_capacity(order.len());
2143
2144    for at in order {
2145        let held = &mut acquired[*at];
2146        // A package boundary again: the staging tree is discarded wholesale by
2147        // `ensure` when this returns an error, so a partial layout is no hazard.
2148        if observer.cancelled() {
2149            return Err(ProvisionError::Cancelled);
2150        }
2151        observer.progress(AlpineEvent::Extracting {
2152            package: &held.package.info.name,
2153        });
2154
2155        let record = installed::Record::open(
2156            &held.package.info,
2157            // The plan's rather than `.PKGINFO`'s, since a package built for
2158            // every architecture says `noarch` where the index — and so the
2159            // record apk writes — says the repository's.
2160            &held.planned.architecture,
2161            held.size,
2162            held.package.control_identity.clone(),
2163            held.package.control_identity_hex.clone(),
2164        )
2165        .with_scripts(held.package.scripts.clone());
2166        let at = database.add(record);
2167
2168        // Rewound rather than reopened: the verification left this descriptor at
2169        // the end of the file it authenticated.
2170        held.file
2171            .seek(std::io::SeekFrom::Start(held.package.data_at))
2172            .map_err(|err| ProvisionError::io("reading the package", &held.path, err))?;
2173        let mut archive = TarReader::new(apk::data_reader(&mut held.file));
2174
2175        let mut collision = None;
2176        extraction.extract(&mut archive, &mut |entry, components| {
2177            record_entry(&mut database, at, entry, components, &mut collision)
2178        })?;
2179        if let Some(collision) = collision {
2180            return Err(wrap(collision));
2181        }
2182
2183        subjects.push(ScriptSubject {
2184            name: held.package.info.name.clone(),
2185            version: held.package.info.version.clone(),
2186            scripts: held.package.scripts.clone(),
2187            triggers: held.package.info.triggers.clone(),
2188        });
2189    }
2190
2191    extraction.finalize(staging)?;
2192    Ok((database, subjects))
2193}
2194
2195/// Records one archive entry against the package laying it down, and decides
2196/// whether it may.
2197///
2198/// A kept claim skips the entry rather than failing it: returning an error would
2199/// abandon the package, and what the policy says is that *this file* stays as the
2200/// earlier package left it. The extractor is told so through [`Placement::Skip`],
2201/// which is the only way to say it — the record and the tree have to agree, and a
2202/// record saying the earlier package still owns the path while the later
2203/// package's bytes sat on disk would leave the finished root disagreeing with its
2204/// own `Z:` digest.
2205///
2206/// A contested claim is stashed rather than returned for the same reason it
2207/// cannot be an error here, and reported by the caller once the entry boundary
2208/// has passed.
2209fn record_entry(
2210    database: &mut Database,
2211    at: usize,
2212    entry: &crate::provision::tar::Entry,
2213    components: &[Vec<u8>],
2214    collision: &mut Option<AlpineError>,
2215) -> Result<Placement, ProvisionError> {
2216    let path = components.join(&b'/');
2217    if entry.typeflag == b'5' {
2218        // Directories are shared by construction — every package ships the ones
2219        // it writes into — so they are recorded rather than contested.
2220        database
2221            .record(at)
2222            .directory(&path, entry.uid, entry.gid, entry.mode);
2223        return Ok(Placement::Write);
2224    }
2225
2226    match database.claim(at, &path) {
2227        Claim::Free | Claim::Replaces => {}
2228        Claim::Kept => return Ok(Placement::Skip),
2229        Claim::Contested { owner } => {
2230            collision.get_or_insert(AlpineError::Collision {
2231                owner,
2232                claimant: database.record(at).name.clone(),
2233                path: String::from_utf8_lossy(&path).into_owned(),
2234            });
2235            return Ok(Placement::Skip);
2236        }
2237    }
2238
2239    database
2240        .record(at)
2241        .file(&path, entry.uid, entry.gid, entry.mode, entry.checksum_sha1);
2242    Ok(Placement::Write)
2243}
2244
2245/// Checks that a full bootstrap for `target` can execute on `host`.
2246///
2247/// A bootstrap the host runs natively needs nothing. A foreign one runs the
2248/// target's `busybox` and install scripts through a `qemu-user` binfmt handler,
2249/// which is the kernel-side question [`binfmt::ready`] answers; the names here
2250/// are Alpine's, and the mapping onto qemu's is this layer's own because each
2251/// distribution spells its architectures differently.
2252fn preflight_foreign(host: &str, target: &str) -> Result<(), AlpineError> {
2253    // Identical architectures always run. Beyond that, an x86_64 host runs x86
2254    // binaries through the near-universal IA-32 compatibility mode, and the
2255    // relation is directional. Other nominally-compatible pairs — aarch64
2256    // running armv7 — depend on optional kernel support that is absent on many
2257    // hosts, so they are treated as foreign.
2258    if host == target || (host == "x86_64" && target == "x86") {
2259        return Ok(());
2260    }
2261    let interpreter = match target {
2262        "x86_64" => "x86_64",
2263        "x86" => "i386",
2264        "aarch64" => "aarch64",
2265        "armv7" | "armhf" => "arm",
2266        "ppc64le" => "ppc64le",
2267        "s390x" => "s390x",
2268        "riscv64" => "riscv64",
2269        "loongarch64" => "loongarch64",
2270        _ => {
2271            return Err(AlpineError::Config {
2272                reason: format!(
2273                    "a full bootstrap for {target} on a {host} host needs a qemu-user binfmt \
2274                     handler, and {target} is not a known qemu target; use extract_only instead"
2275                ),
2276            });
2277        }
2278    };
2279    binfmt::ready(interpreter).map_err(|missing| AlpineError::Config {
2280        reason: match missing {
2281            binfmt::Missing::Unregistered => format!(
2282                "a full bootstrap for {target} on a {host} host needs the qemu-{interpreter} \
2283                 binfmt handler registered (install qemu-user-static and binfmt support); use \
2284                 extract_only to lay out the rootfs without running target binaries"
2285            ),
2286            binfmt::Missing::Unusable(reason) => format!(
2287                "the qemu-{interpreter} binfmt handler is registered but {reason}; a foreign \
2288                 bootstrap needs it enabled and registered with the fix-binary (F) flag"
2289            ),
2290        },
2291    })
2292}
2293
2294/// Surfaces a layer failure through the shared provisioning error.
2295fn wrap(error: AlpineError) -> ProvisionError {
2296    match error {
2297        AlpineError::Cancelled => ProvisionError::Cancelled,
2298        other => ProvisionError::other(other),
2299    }
2300}
2301
2302/// A failure provisioning an Alpine rootfs.
2303#[derive(Debug)]
2304#[non_exhaustive]
2305pub enum AlpineError {
2306    /// The configuration cannot be provisioned from as it stands.
2307    #[non_exhaustive]
2308    Config {
2309        /// What is wrong with it, and what to do instead.
2310        reason: String,
2311    },
2312    /// An apk container is malformed, truncated, or carries something the
2313    /// reader refuses.
2314    #[non_exhaustive]
2315    Container {
2316        /// What was being read: a package's path, or the URL of an index.
2317        subject: String,
2318        /// What was wrong with it.
2319        reason: String,
2320    },
2321    /// A signature was not accepted: it did not verify against the key set, or
2322    /// the signature member was refused on its own terms.
2323    #[non_exhaustive]
2324    Signature {
2325        /// What was being verified: a package's path, or the URL of an index.
2326        subject: String,
2327        /// Why the signature was not accepted.
2328        reason: String,
2329    },
2330    /// Bytes did not have the digest the archive recorded for them.
2331    ///
2332    /// The container held together; what it carries is not what something that
2333    /// vouched for it said it would be. Either a mirror served a package that
2334    /// is not the one the index named, or the package's own file tree is not
2335    /// the one its metadata names.
2336    #[non_exhaustive]
2337    Digest {
2338        /// What was being checked: a package's file name, or the identity of
2339        /// its control segment.
2340        subject: String,
2341        /// The digest that was expected, as the archive spells it.
2342        expected: String,
2343        /// The digest the bytes actually have.
2344        actual: String,
2345    },
2346    /// An index's records are not the ones the format defines.
2347    ///
2348    /// The container held together and its signature verified; what it carries
2349    /// is not a readable set of records.
2350    #[non_exhaustive]
2351    Index {
2352        /// The URL of the index.
2353        subject: String,
2354        /// What was wrong with it.
2355        reason: String,
2356    },
2357    /// A pin could not be held: the repositories no longer supply a package at
2358    /// the version and with the bytes a plan recorded.
2359    #[non_exhaustive]
2360    Pin {
2361        /// Every package that could not be held, in name order.
2362        unheld: Vec<UnheldPin>,
2363    },
2364    /// A plan document could not be read, or a plan could not be written as
2365    /// one.
2366    #[non_exhaustive]
2367    PlanDocument {
2368        /// What was wrong with it.
2369        reason: String,
2370    },
2371    /// The closure the repositories offer is not one that can be installed.
2372    #[non_exhaustive]
2373    Resolve {
2374        /// What could not be resolved, and what the repositories offer instead.
2375        reason: String,
2376    },
2377    /// Two packages ship the same path and neither says it may take it from
2378    /// the other.
2379    ///
2380    /// `apk` warns, skips the file, and records the package as broken. A root
2381    /// is something a caller goes on to build on, so this layer refuses instead
2382    /// rather than publish a tree whose contents depend on which package was
2383    /// unpacked first.
2384    #[non_exhaustive]
2385    Collision {
2386        /// The package that laid the path down.
2387        owner: String,
2388        /// The package that would have overwritten it.
2389        claimant: String,
2390        /// The path both ship.
2391        path: String,
2392    },
2393    /// An install script did not succeed.
2394    #[non_exhaustive]
2395    Script {
2396        /// The package whose script it was.
2397        package: String,
2398        /// Which script it was: `pre-install`, `post-install` or `trigger`.
2399        script: &'static str,
2400        /// How it ended.
2401        status: crate::ExitStatus,
2402    },
2403    /// A cage could not be built or run.
2404    Launch(crate::Error),
2405    /// The bootstrap's observer asked it to stop.
2406    ///
2407    /// Surfaces to a caller as [`super::ProvisionError::Cancelled`].
2408    Cancelled,
2409    /// Fetching from the archive failed.
2410    Fetch(FetchError),
2411    /// A host I/O operation failed.
2412    #[non_exhaustive]
2413    Io {
2414        /// What the operation was doing.
2415        op: &'static str,
2416        /// The path concerned.
2417        path: PathBuf,
2418        /// The underlying error.
2419        source: io::Error,
2420    },
2421}
2422
2423impl AlpineError {
2424    /// A malformed container, naming what was being read and what was wrong.
2425    pub(crate) fn container(subject: impl Into<String>, reason: impl Into<String>) -> AlpineError {
2426        AlpineError::Container {
2427            subject: subject.into(),
2428            reason: reason.into(),
2429        }
2430    }
2431
2432    /// A signature that was not accepted, naming what it covered and why.
2433    pub(crate) fn signature(subject: impl Into<String>, reason: impl Into<String>) -> AlpineError {
2434        AlpineError::Signature {
2435            subject: subject.into(),
2436            reason: reason.into(),
2437        }
2438    }
2439
2440    /// An index whose records could not be read, naming which and why.
2441    pub(crate) fn index(subject: impl Into<String>, reason: impl Into<String>) -> AlpineError {
2442        AlpineError::Index {
2443            subject: subject.into(),
2444            reason: reason.into(),
2445        }
2446    }
2447}
2448
2449path_io_error!(AlpineError);
2450
2451impl Failover for AlpineError {
2452    /// Reaches through the wrapper this layer carries a transport failure in.
2453    /// A failure that is not a `Fetch` one at all — a signature or digest
2454    /// mismatch over bytes that did arrive — never advances a walk.
2455    fn is_failover(&self) -> bool {
2456        matches!(self, AlpineError::Fetch(fetch) if fetch.is_failover())
2457    }
2458}
2459
2460impl From<FetchError> for AlpineError {
2461    fn from(err: FetchError) -> AlpineError {
2462        AlpineError::Fetch(err)
2463    }
2464}
2465
2466impl From<resolve::ResolveError> for AlpineError {
2467    fn from(err: resolve::ResolveError) -> AlpineError {
2468        AlpineError::Resolve {
2469            reason: err.to_string(),
2470        }
2471    }
2472}
2473
2474impl fmt::Display for AlpineError {
2475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2476        match self {
2477            AlpineError::Config { reason } => {
2478                write!(f, "the Alpine bootstrap is misconfigured: {reason}")
2479            }
2480            AlpineError::Container { subject, reason } => {
2481                write!(f, "the apk container {subject} is unusable: {reason}")
2482            }
2483            AlpineError::Signature { subject, reason } => {
2484                write!(f, "the signature over {subject} was not accepted: {reason}")
2485            }
2486            AlpineError::Digest {
2487                subject,
2488                expected,
2489                actual,
2490            } => write!(
2491                f,
2492                "{subject} does not have the digest the archive recorded for it: \
2493                 expected {expected}, got {actual}",
2494            ),
2495            AlpineError::Index { subject, reason } => {
2496                write!(f, "the index {subject} is unusable: {reason}")
2497            }
2498            AlpineError::Pin { unheld } => {
2499                write!(f, "the resolution could not be held to its pin: ")?;
2500                for (at, one) in unheld.iter().enumerate() {
2501                    if at > 0 {
2502                        write!(f, "; ")?;
2503                    }
2504                    write!(f, "{one}")?;
2505                }
2506                Ok(())
2507            }
2508            AlpineError::PlanDocument { reason } => {
2509                write!(f, "the Alpine plan document is unusable: {reason}")
2510            }
2511            AlpineError::Resolve { reason } => {
2512                write!(f, "dependency resolution failed: {reason}")
2513            }
2514            AlpineError::Collision {
2515                owner,
2516                claimant,
2517                path,
2518            } => write!(
2519                f,
2520                "{claimant} and {owner} both ship {path}, and neither declares that it replaces \
2521                 the other",
2522            ),
2523            AlpineError::Script {
2524                package,
2525                script,
2526                status,
2527            } => write!(f, "the {package} {script} script {status}"),
2528            AlpineError::Launch(err) => write!(f, "{err}"),
2529            AlpineError::Cancelled => f.write_str("the bootstrap was cancelled"),
2530            AlpineError::Fetch(err) => write!(f, "{err}"),
2531            AlpineError::Io { op, path, source } => write!(
2532                f,
2533                "an Alpine bootstrap step failed while {op} {}: {source}",
2534                path.display(),
2535            ),
2536        }
2537    }
2538}
2539
2540impl std::error::Error for AlpineError {
2541    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2542        match self {
2543            AlpineError::Fetch(err) => Some(err),
2544            AlpineError::Launch(err) => Some(err),
2545            AlpineError::Io { source, .. } => Some(source),
2546            _ => None,
2547        }
2548    }
2549}
2550
2551/// Containers a repository actually published, for the tests that read them.
2552///
2553/// The reader is held to bytes an archive served rather than to bytes this crate
2554/// wrote: a writer that agrees with its own reader proves nothing about either.
2555/// `tests/fixtures/alpine-archive/README.md` records where each came from and
2556/// what it was chosen for, and the crate manifest excludes the directory from
2557/// the packaged tarball.
2558#[cfg(test)]
2559pub(super) mod fixtures {
2560    /// 1320 bytes, and its data segment is an empty tar: the degenerate case a
2561    /// reader that treats an empty third segment as a missing one fails on.
2562    pub(super) const ALPINE_BASE: &[u8] =
2563        include_bytes!("../../../tests/fixtures/alpine-archive/alpine-base-3.23.5-r0.apk");
2564
2565    /// A data segment of directories and regular files, and `arch = x86_64`
2566    /// rather than `noarch`.
2567    pub(super) const ALPINE_KEYS: &[u8] =
2568        include_bytes!("../../../tests/fixtures/alpine-archive/alpine-keys-2.6-r0.apk");
2569
2570    /// Carries a symbolic link, and is large enough that its data segment spans
2571    /// many decoder buffers — the case that catches a reader which hashes the
2572    /// wrong bytes or loses a boundary.
2573    pub(super) const MUSL: &[u8] =
2574        include_bytes!("../../../tests/fixtures/alpine-archive/musl-1.2.5-r23.apk");
2575
2576    /// Four install scripts, a `replaces` with a priority, and a file tree deep
2577    /// enough to exercise every line an installed record can carry.
2578    pub(super) const ALPINE_BASELAYOUT: &[u8] =
2579        include_bytes!("../../../tests/fixtures/alpine-archive/alpine-baselayout-3.7.2-r0.apk");
2580
2581    /// The `installed` records `apk` itself wrote for `alpine-baselayout` and
2582    /// `musl`, lifted from Alpine's own published minirootfs.
2583    ///
2584    /// The database writer is measured against these rather than against its
2585    /// own output: they are what the tool this layer has to agree with produced,
2586    /// for two of the packages committed here.
2587    pub(super) const MINIROOTFS_INSTALLED: &[u8] =
2588        include_bytes!("../../../tests/fixtures/alpine-archive/minirootfs-installed");
2589
2590    /// Alpine's `v3.23/main/x86_64` index.
2591    pub(super) const INDEX: &[u8] =
2592        include_bytes!("../../../tests/fixtures/alpine-archive/APKINDEX.tar.gz");
2593
2594    /// postmarketOS's `v26.06/x86_64` index: the same container, a different
2595    /// key, a path one level shallower.
2596    pub(super) const POSTMARKETOS_INDEX: &[u8] =
2597        include_bytes!("../../../tests/fixtures/alpine-archive/pmos-APKINDEX.tar.gz");
2598
2599    /// A published postmarketOS package, signed by a key no trust anchor holds.
2600    ///
2601    /// Its index is signed by `build.postmarketos.org` and the package by
2602    /// `pmos@local-6a50011b`, which varies from one postmarketOS package to the
2603    /// next. So it is the case that decides how a package is authenticated.
2604    pub(super) const POSTMARKETOS_BASELAYOUT: &[u8] =
2605        include_bytes!("../../../tests/fixtures/alpine-archive/pmos-baselayout-62-r0.apk");
2606
2607    /// The name of the key that signs every Alpine x86_64 fixture here.
2608    pub(super) const ALPINE_KEY_NAME: &str = "alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub";
2609}
2610
2611#[cfg(test)]
2612mod tests {
2613    use std::collections::HashMap;
2614    use std::io::Write;
2615
2616    use super::*;
2617    use crate::scratch::Scratch;
2618
2619    /// A transport serving a fixed set of URLs and nothing else.
2620    struct Canned(HashMap<String, Vec<u8>>);
2621
2622    impl Fetch for Canned {
2623        fn fetch(
2624            &mut self,
2625            request: &FetchRequest<'_>,
2626            sink: &mut dyn Write,
2627        ) -> Result<(), FetchError> {
2628            match self.0.get(request.url()) {
2629                Some(body) => sink
2630                    .write_all(body)
2631                    .map_err(FetchError::at("writing the body", request.url())),
2632                None => Err(FetchError::not_found(request.url())),
2633            }
2634        }
2635    }
2636
2637    /// A transport that refuses one mirror's URLs the way the built-in client
2638    /// refuses one it cannot parse or whose scheme it does not speak, and
2639    /// delegates everything else.
2640    struct Unusable {
2641        refuse: &'static str,
2642        rest: Box<dyn Fetch>,
2643    }
2644
2645    impl Fetch for Unusable {
2646        fn fetch(
2647            &mut self,
2648            request: &FetchRequest<'_>,
2649            sink: &mut dyn Write,
2650        ) -> Result<(), FetchError> {
2651            if request.url().starts_with(self.refuse) {
2652                return Err(FetchError::url(request.url(), "unsupported scheme"));
2653            }
2654            self.rest.fetch(request, sink)
2655        }
2656    }
2657
2658    /// A transport serving the two published index fixtures at the paths their
2659    /// repositories publish them under.
2660    fn canned() -> Box<dyn Fetch> {
2661        Box::new(Canned(HashMap::from([
2662            (
2663                "http://mirror.invalid/alpine/v3.23/main/x86_64/APKINDEX.tar.gz".to_string(),
2664                fixtures::INDEX.to_vec(),
2665            ),
2666            (
2667                "http://mirror.invalid/pmos/v26.06/x86_64/APKINDEX.tar.gz".to_string(),
2668                fixtures::POSTMARKETOS_INDEX.to_vec(),
2669            ),
2670        ])))
2671    }
2672
2673    /// The postmarketOS repository, as a caller configures one.
2674    fn postmarketos() -> Repository {
2675        Repository::builder("v26.06")
2676            .mirror("http://mirror.invalid/pmos")
2677            .keys(KeySet::postmarketos())
2678            .build()
2679            .expect("the repository validates")
2680    }
2681
2682    #[test]
2683    fn a_published_index_is_read_through_the_transport_and_verified() {
2684        let mut alpine = Alpine::builder("v3.23")
2685            .architecture("x86_64")
2686            .mirror("http://mirror.invalid/alpine")
2687            .fetcher(canned())
2688            .build()
2689            .expect("the bootstrap configures");
2690        let available = alpine.available().expect("the published index verifies");
2691        assert!(available.contains("busybox"));
2692        assert!(available.contains("alpine-base"));
2693        assert!(
2694            !available.contains("systemd"),
2695            "and nothing Alpine does not publish"
2696        );
2697        assert!(
2698            available.contains("so:libc.musl-x86_64.so.1"),
2699            "a virtual name the index provides",
2700        );
2701        assert!(
2702            available
2703                .providers("so:libc.musl-x86_64.so.1")
2704                .any(|name| name == "musl"),
2705            "and the package that provides it",
2706        );
2707    }
2708
2709    #[test]
2710    fn a_local_repository_is_read_through_the_built_in_transport() {
2711        // The canned transport above proves the reader; this proves the URL a
2712        // repository formats and the bundled transport that fetches it, by
2713        // laying the published index out at the path Alpine publishes it under
2714        // and reading it back through `file://`.
2715        let scratch = Scratch::new("alpine-repository");
2716        let component = scratch.join("v3.23/main/x86_64");
2717        std::fs::create_dir_all(&component).expect("the repository tree is creatable");
2718        std::fs::write(component.join("APKINDEX.tar.gz"), fixtures::INDEX)
2719            .expect("the index is writable");
2720
2721        let mut alpine = Alpine::builder("v3.23")
2722            .architecture("x86_64")
2723            .mirror(crate::provision::file_url(scratch.path()).expect("the path is nameable"))
2724            .build()
2725            .expect("the bootstrap configures");
2726        assert!(
2727            alpine
2728                .available()
2729                .expect("the local index verifies")
2730                .contains("busybox"),
2731        );
2732    }
2733
2734    #[test]
2735    fn two_repositories_merge_in_the_order_they_were_configured() {
2736        // postmarketOS is Alpine's repositories plus its own, on a path one
2737        // level shallower and under a key of its own. Nothing here is
2738        // postmarketOS-specific but the fixture.
2739        let mut alpine = Alpine::builder("v3.23")
2740            .architecture("x86_64")
2741            .mirror("http://mirror.invalid/alpine")
2742            .repository(postmarketos())
2743            .fetcher(canned())
2744            .build()
2745            .expect("the bootstrap configures");
2746        let available = alpine.available().expect("both indexes verify");
2747        assert!(available.contains("alpine-base"), "from Alpine");
2748        assert!(available.contains("postmarketos-base"), "from postmarketOS",);
2749    }
2750
2751    #[test]
2752    fn an_observed_catalogue_reports_the_index_it_is_fetching() {
2753        // The call is nothing but the index walk, which spends one mirror
2754        // timeout per unreachable mirror, so an observed caller has no other
2755        // way to learn what a long silence is waiting on.
2756        struct Sink(Vec<String>);
2757        impl AlpineObserver for Sink {
2758            fn progress(&mut self, event: AlpineEvent<'_>) {
2759                if let AlpineEvent::Fetching { url } = event {
2760                    self.0.push(url.to_string());
2761                }
2762            }
2763        }
2764
2765        let mut alpine = Alpine::builder("v3.23")
2766            .architecture("x86_64")
2767            .mirror("http://mirror.invalid/alpine")
2768            .repository(postmarketos())
2769            .fetcher(canned())
2770            .build()
2771            .expect("the bootstrap configures");
2772        let mut sink = Sink(Vec::new());
2773        alpine
2774            .observe(&mut sink)
2775            .available()
2776            .expect("both indexes verify");
2777        assert_eq!(
2778            sink.0,
2779            [
2780                "http://mirror.invalid/alpine/v3.23/main/x86_64/APKINDEX.tar.gz",
2781                "http://mirror.invalid/pmos/v26.06/x86_64/APKINDEX.tar.gz",
2782            ],
2783        );
2784    }
2785
2786    #[test]
2787    fn a_repository_verified_against_the_wrong_keys_is_refused() {
2788        // The postmarketOS index under Alpine's keys: the same format, a key
2789        // the set does not hold.
2790        let wrong = Repository::builder("v26.06")
2791            .mirror("http://mirror.invalid/pmos")
2792            .keys(KeySet::alpine("x86_64").expect("x86_64 is in the bundle"))
2793            .build()
2794            .expect("the repository validates");
2795        let mut alpine = Alpine::builder("v3.23")
2796            .architecture("x86_64")
2797            .mirror("http://mirror.invalid/alpine")
2798            .repository(wrong)
2799            .fetcher(canned())
2800            .build()
2801            .expect("the bootstrap configures");
2802        let err = alpine
2803            .available()
2804            .expect_err("postmarketOS does not sign with Alpine's key");
2805        assert!(matches!(err, AlpineError::Signature { .. }), "{err}");
2806    }
2807
2808    #[test]
2809    fn a_mirror_that_does_not_serve_the_index_is_walked_past() {
2810        let mut alpine = Alpine::builder("v3.23")
2811            .architecture("x86_64")
2812            .mirror("http://empty.invalid/alpine")
2813            .mirror_fallback("http://mirror.invalid/alpine")
2814            .fetcher(canned())
2815            .build()
2816            .expect("the bootstrap configures");
2817        assert!(
2818            alpine
2819                .available()
2820                .expect("the backstop serves it")
2821                .contains("busybox"),
2822        );
2823    }
2824
2825    #[test]
2826    fn a_mirror_the_transport_will_not_accept_stops_the_walk() {
2827        // A `Url` failure is the caller's configuration rather than the
2828        // mirror's state, so it is reported against the mirror that carries it
2829        // instead of advancing. The backstop here does serve the index, which is
2830        // what makes the difference observable: a walk that advanced past the
2831        // refusal would succeed and say nothing about the unusable URL.
2832        let mut alpine = Alpine::builder("v3.23")
2833            .architecture("x86_64")
2834            .mirror("http://unusable.invalid/alpine")
2835            .mirror_fallback("http://mirror.invalid/alpine")
2836            .fetcher(Box::new(Unusable {
2837                refuse: "http://unusable.invalid/",
2838                rest: canned(),
2839            }))
2840            .build()
2841            .expect("the bootstrap configures");
2842        let err = alpine
2843            .available()
2844            .expect_err("the primary's URL is not one the transport accepts");
2845        assert!(
2846            matches!(err, AlpineError::Fetch(FetchError::Url { .. })),
2847            "{err}",
2848        );
2849    }
2850
2851    #[test]
2852    fn no_mirror_serving_the_index_is_reported_as_the_fetch_failure_it_is() {
2853        let mut alpine = Alpine::builder("v3.23")
2854            .architecture("x86_64")
2855            .mirror("http://empty.invalid/alpine")
2856            .fetcher(canned())
2857            .build()
2858            .expect("the bootstrap configures");
2859        let err = alpine.available().expect_err("no mirror serves it");
2860        assert!(matches!(err, AlpineError::Fetch(_)), "{err}");
2861        // And it names the mirror that was asked, not the path alone: the walk
2862        // keeps the last failure so the diagnostic survives the walk.
2863        let reason = err.to_string();
2864        assert!(reason.contains("http://empty.invalid/alpine"), "{reason}");
2865    }
2866
2867    #[test]
2868    fn an_exhausted_walk_is_reported_against_the_mirror_it_asked_last() {
2869        // Attribution, which is the whole value of keeping the last failure:
2870        // three mirrors, none serving, and the answer has to name the one the
2871        // walk ended on rather than the one it started with.
2872        let mut alpine = Alpine::builder("v3.23")
2873            .architecture("x86_64")
2874            .mirror("http://empty.invalid/first")
2875            .mirror_fallback("http://empty.invalid/second")
2876            .mirror_fallback("http://empty.invalid/last")
2877            .fetcher(canned())
2878            .build()
2879            .expect("the bootstrap configures");
2880        let reason = alpine
2881            .available()
2882            .expect_err("no mirror serves it")
2883            .to_string();
2884        assert!(reason.contains("http://empty.invalid/last"), "{reason}");
2885        assert!(!reason.contains("/first"), "{reason}");
2886    }
2887
2888    #[test]
2889    fn an_architecture_the_bundle_has_no_keys_for_needs_keys_of_its_own() {
2890        let err = Alpine::builder("v3.23")
2891            .architecture("sparc64")
2892            .build()
2893            .expect_err("the bundle has no sparc64 keys");
2894        assert!(matches!(err, AlpineError::Config { .. }), "{err}");
2895        // And naming them is the whole remedy: the architecture itself is not
2896        // refused, since a mirror may serve one this crate has never heard of.
2897        let mut keys = KeySet::new();
2898        keys.insert(
2899            fixtures::ALPINE_KEY_NAME,
2900            include_str!("keyring/alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub"),
2901        )
2902        .expect("the key parses");
2903        Alpine::builder("v3.23")
2904            .architecture("sparc64")
2905            .keys(keys)
2906            .build()
2907            .expect("a key set of the caller's own is enough");
2908    }
2909
2910    #[test]
2911    fn an_architecture_that_is_not_one_path_segment_is_refused() {
2912        for architecture in ["../etc", "x86_64/sub", "", "  ", "x86 64"] {
2913            let err = Alpine::builder("v3.23")
2914                .architecture(architecture)
2915                .build()
2916                .expect_err("the architecture is not addressable");
2917            assert!(
2918                matches!(err, AlpineError::Config { .. }),
2919                "{architecture:?}: {err}",
2920            );
2921        }
2922    }
2923
2924    #[test]
2925    fn the_primary_defaults_to_main_and_an_empty_list_is_the_other_layout() {
2926        let default = Alpine::builder("v3.23")
2927            .architecture("x86_64")
2928            .build()
2929            .expect("the bootstrap configures");
2930        assert_eq!(
2931            default.repositories[0].indexes("x86_64"),
2932            [(
2933                Some("main"),
2934                "v3.23/main/x86_64/APKINDEX.tar.gz".to_string()
2935            )],
2936        );
2937        let shallow = Alpine::builder("v26.06")
2938            .architecture("x86_64")
2939            .components::<[&str; 0], &str>([])
2940            .build()
2941            .expect("the bootstrap configures");
2942        assert_eq!(
2943            shallow.repositories[0].indexes("x86_64"),
2944            [(None, "v26.06/x86_64/APKINDEX.tar.gz".to_string())],
2945        );
2946    }
2947
2948    #[test]
2949    fn a_published_index_resolves_a_closure_and_records_what_it_resolved_against() {
2950        // The whole read half against 5,869 records an archive actually
2951        // published: the grammar, the merge, and the provider ranking all meet
2952        // real data here rather than the shapes a test writes.
2953        let mut alpine = Alpine::builder("v3.23")
2954            .architecture("x86_64")
2955            .mirror("http://mirror.invalid/alpine")
2956            .include(["alpine-base"])
2957            .fetcher(canned())
2958            .build()
2959            .expect("the bootstrap configures");
2960        let plan = alpine.resolve().expect("alpine-base resolves");
2961
2962        assert_eq!(plan.release, "v3.23");
2963        assert_eq!(plan.architecture, "x86_64");
2964        let names: Vec<&str> = plan
2965            .packages
2966            .iter()
2967            .map(|package| package.name.as_str())
2968            .collect();
2969        assert!(names.contains(&"alpine-base"), "{names:?}");
2970        assert!(names.contains(&"busybox"), "pulled through the closure");
2971        assert!(
2972            names.contains(&"musl"),
2973            "and so is the provider of so:libc.musl-x86_64.so.1",
2974        );
2975        assert!(names.windows(2).all(|pair| pair[0] < pair[1]), "{names:?}");
2976
2977        let busybox = plan
2978            .packages
2979            .iter()
2980            .find(|package| package.name == "busybox")
2981            .expect("busybox is in the closure");
2982        assert!(busybox.control_identity.starts_with("Q1"));
2983        assert!(busybox.size.is_some_and(|size| size > 0));
2984        assert_eq!(
2985            busybox.file_name(),
2986            format!("busybox-{}.apk", busybox.version),
2987        );
2988        assert_eq!(busybox.index, 0);
2989
2990        let [index] = &plan.indexes[..] else {
2991            panic!("one component was configured: {:?}", plan.indexes);
2992        };
2993        assert_eq!(index.mirror, "http://mirror.invalid/alpine");
2994        assert_eq!(index.component.as_deref(), Some("main"));
2995        assert_eq!(index.signed_by, fixtures::ALPINE_KEY_NAME);
2996        assert_eq!(index.sha256, Algorithm::Sha256.hex_of(fixtures::INDEX),);
2997        assert!(
2998            index
2999                .description
3000                .as_deref()
3001                .is_some_and(|built| built.starts_with("v3.23")),
3002            "{:?}",
3003            index.description,
3004        );
3005    }
3006
3007    #[test]
3008    fn a_package_is_attributed_to_the_index_that_offered_it() {
3009        // With two repositories configured, the plan's index number is the only
3010        // thing that says where a package's bytes will be fetched from — and an
3011        // apk package is addressed relative to its own index's directory.
3012        let mut alpine = Alpine::builder("v3.23")
3013            .architecture("x86_64")
3014            .mirror("http://mirror.invalid/alpine")
3015            .repository(postmarketos())
3016            // Not `postmarketos-base`: its closure reaches Alpine's
3017            // `community`, which these two fixtures do not carry. The overlay
3018            // is a real overlay, so most of what it publishes needs more of
3019            // Alpine than one component.
3020            .include(["postmarketos-baselayout"])
3021            .fetcher(canned())
3022            .build()
3023            .expect("the bootstrap configures");
3024        let plan = alpine.resolve().expect("the closure resolves");
3025
3026        let base = plan
3027            .packages
3028            .iter()
3029            .find(|package| package.name == "postmarketos-baselayout")
3030            .expect("it was asked for");
3031        assert_eq!(base.index, 1, "the second index is postmarketOS's");
3032        assert_eq!(plan.indexes[1].component, None, "the shallower layout");
3033        assert!(
3034            plan.indexes[1].signed_by.contains("postmarketos"),
3035            "{}",
3036            plan.indexes[1].signed_by,
3037        );
3038        // And what it needs from Alpine is attributed to Alpine's.
3039        assert!(
3040            plan.packages
3041                .iter()
3042                .any(|package| package.name == "musl" && package.index == 0),
3043            "musl comes from Alpine's main",
3044        );
3045    }
3046
3047    /// Lays `index` out at `path` under `root`, as an archive publishes it.
3048    fn publish_index(root: &std::path::Path, path: &str, index: &[u8]) {
3049        let directory = root.join(path);
3050        std::fs::create_dir_all(&directory).expect("the repository tree is creatable");
3051        std::fs::write(directory.join("APKINDEX.tar.gz"), index).expect("the index is writable");
3052    }
3053
3054    #[test]
3055    fn an_overlay_repository_is_read_from_the_layout_it_publishes() {
3056        // The whole of what postmarketOS costs this layer, end to end and over
3057        // the bundled transport rather than a canned one: two archives, each at
3058        // the path it really publishes -- Alpine's
3059        // <release>/<component>/<architecture> and postmarketOS's
3060        // <release>/<architecture> -- each verified against its own keys, merged
3061        // into one closure.
3062        let scratch = Scratch::new("alpine-overlay");
3063        let alpine_mirror = scratch.join("alpine");
3064        let overlay_mirror = scratch.join("pmos");
3065        publish_index(&alpine_mirror, "v3.23/main/x86_64", fixtures::INDEX);
3066        publish_index(
3067            &overlay_mirror,
3068            "v26.06/x86_64",
3069            fixtures::POSTMARKETOS_INDEX,
3070        );
3071
3072        let overlay = Repository::builder("v26.06")
3073            .mirror(crate::provision::file_url(&overlay_mirror).expect("the path is nameable"))
3074            .keys(KeySet::postmarketos())
3075            .build()
3076            .expect("the repository validates");
3077        let plan = Alpine::builder("v3.23")
3078            .architecture("x86_64")
3079            .mirror(crate::provision::file_url(&alpine_mirror).expect("the path is nameable"))
3080            .repository(overlay)
3081            // `akms` is one of the two names both archives publish, and the only
3082            // one whose closure both together can supply.
3083            .include(["akms"])
3084            .build()
3085            .expect("the bootstrap configures")
3086            .resolve()
3087            .expect("both indexes verify and the closure resolves");
3088
3089        let akms = plan
3090            .packages
3091            .iter()
3092            .find(|package| package.name == "akms")
3093            .expect("it was asked for");
3094        assert_eq!(akms.index, 1, "the overlay's version is the one selected");
3095        assert_eq!(akms.version, "99990.3.0-r1");
3096        assert!(
3097            plan.packages
3098                .iter()
3099                .any(|package| package.name == "bubblewrap" && package.index == 0),
3100            "and what it needs from Alpine comes from Alpine",
3101        );
3102
3103        // The two layouts, recorded as the plan saw them.
3104        assert_eq!(plan.indexes[0].component.as_deref(), Some("main"));
3105        assert_eq!(plan.indexes[0].signed_by, fixtures::ALPINE_KEY_NAME);
3106        assert_eq!(plan.indexes[1].component, None);
3107        assert_eq!(plan.indexes[1].signed_by, "build.postmarketos.org.rsa.pub");
3108    }
3109
3110    #[test]
3111    fn an_overlay_can_answer_a_name_by_providing_it() {
3112        // postmarketOS's second way of overriding an Alpine package, and the
3113        // reason a postmarketOS root is not assembled by naming `alpine-base`:
3114        // `postmarketos-base` provides that name at `1000-r0`, a version no
3115        // Alpine release reaches, so the provider answers it. `apk` resolves the
3116        // two repositories the same way, down to which dependencies it then
3117        // cannot find.
3118        let mut alpine = Alpine::builder("v3.23")
3119            .architecture("x86_64")
3120            .mirror("http://mirror.invalid/alpine")
3121            .repository(postmarketos())
3122            .fetcher(canned())
3123            .build()
3124            .expect("the bootstrap configures");
3125        let available = alpine.available().expect("both indexes verify");
3126        assert!(
3127            available
3128                .providers("alpine-base")
3129                .any(|name| name == "postmarketos-base"),
3130            "the overlay provides the name Alpine publishes as a package",
3131        );
3132    }
3133
3134    #[test]
3135    fn an_overlay_package_is_bound_to_the_index_that_resolved_it() {
3136        // The chain closed over two archives, from bytes each of them published:
3137        // the postmarketOS index verifies against postmarketOS's key, the plan
3138        // records the `C:` that index carries, and the package the mirror serves
3139        // is accepted because it recomputes to that identity. Nothing here
3140        // consults the package's own signature, which is made by a builder key
3141        // no anchor holds.
3142        let scratch = Scratch::new("alpine-overlay-binding");
3143        let alpine_mirror = scratch.join("alpine");
3144        let overlay_mirror = scratch.join("pmos");
3145        publish_index(&alpine_mirror, "v3.23/main/x86_64", fixtures::INDEX);
3146        publish_index(
3147            &overlay_mirror,
3148            "v26.06/x86_64",
3149            fixtures::POSTMARKETOS_INDEX,
3150        );
3151
3152        let overlay = Repository::builder("v26.06")
3153            .mirror(crate::provision::file_url(&overlay_mirror).expect("the path is nameable"))
3154            .keys(KeySet::postmarketos())
3155            .build()
3156            .expect("the repository validates");
3157        let plan = Alpine::builder("v3.23")
3158            .architecture("x86_64")
3159            .mirror(crate::provision::file_url(&alpine_mirror).expect("the path is nameable"))
3160            .repository(overlay)
3161            .include(["postmarketos-baselayout"])
3162            .build()
3163            .expect("the bootstrap configures")
3164            .resolve()
3165            .expect("the closure resolves");
3166
3167        let planned = plan
3168            .packages
3169            .iter()
3170            .find(|package| package.name == "postmarketos-baselayout")
3171            .expect("it was asked for");
3172        let path = scratch.join(planned.file_name());
3173        std::fs::write(&path, fixtures::POSTMARKETOS_BASELAYOUT).expect("the package is writable");
3174        let held = read_acquired(&path, planned, "x86_64")
3175            .expect("the served package is the one the index named");
3176        assert_eq!(held.package.info.name, "postmarketos-baselayout");
3177
3178        // And the binding is doing real work: the same file refused where the
3179        // plan names another version of it.
3180        let mut moved = planned.clone();
3181        moved.version = "61-r0".to_string();
3182        let err = read_acquired(&path, &moved, "x86_64")
3183            .expect_err("the mirror served a package the plan did not name");
3184        assert!(matches!(err, AlpineError::Config { .. }), "{err}");
3185    }
3186
3187    #[test]
3188    fn an_overlay_overrides_by_version_rather_than_by_position() {
3189        // How postmarketOS actually overrides an Alpine package: it republishes
3190        // it at a version no Alpine release will reach -- `akms` at
3191        // `99990.3.0-r1` against Alpine's `0.3.0-r0` -- rather than relying on
3192        // being configured second. So the same package is selected with the
3193        // repositories in either order, and the equal-version shadowing rule
3194        // (proven over `Index::merge`) is not what the published archives lean
3195        // on.
3196        let scratch = Scratch::new("alpine-overlay-order");
3197        let alpine_mirror = scratch.join("alpine");
3198        let overlay_mirror = scratch.join("pmos");
3199        publish_index(&alpine_mirror, "v3.23/main/x86_64", fixtures::INDEX);
3200        publish_index(
3201            &overlay_mirror,
3202            "v26.06/x86_64",
3203            fixtures::POSTMARKETOS_INDEX,
3204        );
3205        let alpine_url = crate::provision::file_url(&alpine_mirror).expect("the path is nameable");
3206        let overlay_url =
3207            crate::provision::file_url(&overlay_mirror).expect("the path is nameable");
3208
3209        // The overlay as the primary, with Alpine merged in after it.
3210        let alpine = Repository::builder("v3.23")
3211            .mirror(&alpine_url)
3212            .components(["main"])
3213            .keys(KeySet::alpine("x86_64").expect("x86_64 is in the bundle"))
3214            .build()
3215            .expect("the repository validates");
3216        let plan = Alpine::builder("v26.06")
3217            .architecture("x86_64")
3218            .mirror(&overlay_url)
3219            .components::<[&str; 0], &str>([])
3220            .keys(KeySet::postmarketos())
3221            .repository(alpine)
3222            .include(["akms"])
3223            .build()
3224            .expect("the bootstrap configures")
3225            .resolve()
3226            .expect("both indexes verify and the closure resolves");
3227
3228        let akms = plan
3229            .packages
3230            .iter()
3231            .find(|package| package.name == "akms")
3232            .expect("it was asked for");
3233        assert_eq!(akms.version, "99990.3.0-r1");
3234        assert_eq!(akms.index, 0, "the overlay is the primary here");
3235    }
3236
3237    /// A provisioner over the published Alpine fixture, installing `world`.
3238    fn provisioner(world: &[&str]) -> AlpineBuilder {
3239        Alpine::builder("v3.23")
3240            .architecture("x86_64")
3241            .mirror("http://mirror.invalid/alpine")
3242            .include(world.iter().copied())
3243            .fetcher(canned())
3244    }
3245
3246    #[test]
3247    fn a_plan_pins_the_resolution_it_came_from() {
3248        // The round trip a reproduce case is: resolve, keep the plan, and
3249        // resolve again holding to it. Nothing has moved, so the closure is the
3250        // same one -- and it went through the whole check rather than round the
3251        // side of it.
3252        let plan = provisioner(&["alpine-base"])
3253            .build()
3254            .expect("the bootstrap configures")
3255            .resolve()
3256            .expect("alpine-base resolves");
3257        let again = provisioner(&["alpine-base"])
3258            .pin(plan.clone())
3259            .build()
3260            .expect("the pin is read")
3261            .resolve()
3262            .expect("the repositories still supply it");
3263        assert_eq!(again, plan);
3264    }
3265
3266    #[test]
3267    fn a_pin_the_repositories_have_moved_past_is_refused_naming_every_package() {
3268        // A publish moves many packages together, so the failure reports them
3269        // all at once rather than one rebuild at a time.
3270        let mut plan = provisioner(&["alpine-base"])
3271            .build()
3272            .expect("the bootstrap configures")
3273            .resolve()
3274            .expect("alpine-base resolves");
3275        for package in &mut plan.packages {
3276            package.version = format!("{}9", package.version);
3277        }
3278        let err = provisioner(&["alpine-base"])
3279            .pin(plan.clone())
3280            .build()
3281            .expect("the pin is read")
3282            .resolve()
3283            .expect_err("the repositories offer none of those versions");
3284        match &err {
3285            AlpineError::Pin { unheld } => {
3286                assert_eq!(unheld.len(), plan.packages.len());
3287                let names: Vec<&str> = unheld.iter().map(|one| one.package.as_str()).collect();
3288                assert!(names.windows(2).all(|pair| pair[0] < pair[1]), "{names:?}");
3289                assert!(
3290                    unheld
3291                        .iter()
3292                        .all(|one| matches!(one.reason, UnheldReason::Version { .. })),
3293                    "{unheld:?}",
3294                );
3295            }
3296            other => panic!("expected a pin refusal, got {other}"),
3297        }
3298    }
3299
3300    #[test]
3301    fn a_version_republished_over_different_bytes_is_refused() {
3302        // The event a recorded control identity exists to catch, and the one a
3303        // version comparison alone cannot see.
3304        let mut plan = provisioner(&["busybox"])
3305            .build()
3306            .expect("the bootstrap configures")
3307            .resolve()
3308            .expect("busybox resolves");
3309        plan.packages[0].control_identity = "Q1".to_string() + &"a".repeat(27) + "=";
3310        let err = provisioner(&["busybox"])
3311            .pin(plan)
3312            .build()
3313            .expect("the pin is read")
3314            .resolve()
3315            .expect_err("the index records other bytes for that version");
3316        assert!(
3317            matches!(&err, AlpineError::Pin { unheld }
3318                if matches!(unheld[0].reason, UnheldReason::Identity { .. })),
3319            "{err}",
3320        );
3321    }
3322
3323    #[test]
3324    fn a_pin_naming_one_package_twice_is_refused_where_it_is_given() {
3325        let mut plan = provisioner(&["busybox"])
3326            .build()
3327            .expect("the bootstrap configures")
3328            .resolve()
3329            .expect("busybox resolves");
3330        let first = plan.packages[0].clone();
3331        plan.packages.push(first);
3332        let err = provisioner(&["busybox"])
3333            .pin(plan)
3334            .build()
3335            .expect_err("there is no version to hold it at");
3336        assert!(
3337            matches!(&err, AlpineError::Config { reason } if reason.contains("twice")),
3338            "{err}",
3339        );
3340    }
3341
3342    #[test]
3343    fn a_resolution_with_nothing_included_is_refused() {
3344        // apk publishes no priority field, so there is no set the repositories
3345        // nominate on the caller's behalf and an empty world is an empty root.
3346        let mut alpine = Alpine::builder("v3.23")
3347            .architecture("x86_64")
3348            .mirror("http://mirror.invalid/alpine")
3349            .fetcher(canned())
3350            .build()
3351            .expect("the bootstrap configures");
3352        let err = alpine.resolve().expect_err("nothing was included");
3353        assert!(
3354            matches!(&err, AlpineError::Config { reason } if reason.contains("include()")),
3355            "{err}",
3356        );
3357    }
3358
3359    #[test]
3360    fn a_requested_name_the_repositories_do_not_offer_is_refused() {
3361        let mut alpine = Alpine::builder("v3.23")
3362            .architecture("x86_64")
3363            .mirror("http://mirror.invalid/alpine")
3364            .include(["systemd"])
3365            .fetcher(canned())
3366            .build()
3367            .expect("the bootstrap configures");
3368        let err = alpine.resolve().expect_err("Alpine publishes no systemd");
3369        assert!(
3370            matches!(&err, AlpineError::Resolve { reason } if reason.contains("systemd")),
3371            "{err}",
3372        );
3373    }
3374
3375    #[test]
3376    fn a_requested_name_carrying_a_version_is_refused_where_it_is_named() {
3377        // Rather than at the resolution, as a package nothing offers: the
3378        // caller wrote a constraint where a name goes, and the message that
3379        // says so belongs at the setter.
3380        for name in ["busybox>=1.37", "musl=1.2.5-r23", "python3~3.12"] {
3381            let err = Alpine::builder("v3.23")
3382                .architecture("x86_64")
3383                .include([name])
3384                .build()
3385                .expect_err("a constraint is not a package name");
3386            assert!(matches!(err, AlpineError::Config { .. }), "{name}: {err}");
3387        }
3388    }
3389
3390    /// A plan entry for a committed package, read out of the package itself.
3391    ///
3392    /// The identity is recomputed rather than written down so the plan is one
3393    /// the binding accepts; what the test is about is the record, and a
3394    /// hand-typed digest would only ever prove that it was typed correctly.
3395    fn planned_from(container: &[u8], architecture: &str) -> PlannedPackage {
3396        let read = apk::read_package(
3397            "the fixture",
3398            &mut apk::Segments::new(container),
3399            MAX_CONTROL_SEGMENT,
3400        )
3401        .expect("the fixture reads");
3402        PlannedPackage {
3403            name: read.info.name.clone(),
3404            version: read.info.version.clone(),
3405            architecture: architecture.to_string(),
3406            control_identity: read.control_identity,
3407            size: Some(container.len() as u64),
3408            installed_size: read.info.installed_size,
3409            origin: read.info.origin.clone(),
3410            index: 0,
3411            carried: crate::provision::document::Carried::new(),
3412        }
3413    }
3414
3415    #[test]
3416    fn the_database_written_is_the_one_apk_writes() {
3417        // The whole point of the writer, measured against the tool it has to
3418        // agree with: these are the records `apk` itself produced for two of
3419        // the committed packages, lifted out of Alpine's published minirootfs.
3420        // Every field, every file line, every digest and every mode has to come
3421        // back the same, in the same order.
3422        let scratch = Scratch::new("alpine-database");
3423        let cache = scratch.join("cache");
3424        std::fs::create_dir_all(&cache).expect("the cache is creatable");
3425        let staging = scratch.join("root");
3426        std::fs::create_dir_all(&staging).expect("the staging tree is creatable");
3427
3428        let mut acquired: Vec<Acquired> = Vec::new();
3429        for container in [fixtures::ALPINE_BASELAYOUT, fixtures::MUSL] {
3430            let planned = planned_from(container, "x86_64");
3431            let path = cache.join(planned.file_name());
3432            std::fs::write(&path, container).expect("the package is writable");
3433            acquired.push(
3434                read_acquired(&path, &planned, "x86_64")
3435                    .expect("the package answers the plan entry"),
3436            );
3437        }
3438
3439        let order: Vec<usize> = (0..acquired.len()).collect();
3440        let (database, subjects) = lay_down(
3441            &staging,
3442            Carried::default(),
3443            &mut acquired,
3444            &order,
3445            &mut Silent,
3446        )
3447        .expect("the closure lays down");
3448
3449        assert_eq!(
3450            String::from_utf8(database.installed()).expect("the database is text"),
3451            String::from_utf8(fixtures::MINIROOTFS_INSTALLED.to_vec())
3452                .expect("the fixture is text"),
3453        );
3454
3455        // And the tree it laid down is the one the records describe.
3456        assert!(staging.join("lib/ld-musl-x86_64.so.1").is_file());
3457        assert_eq!(
3458            std::fs::read_link(staging.join("lib/libc.musl-x86_64.so.1"))
3459                .expect("musl ships the link"),
3460            std::path::Path::new("ld-musl-x86_64.so.1"),
3461        );
3462        assert!(staging.join("etc/profile.d/README").is_file());
3463
3464        // The scripts travelled with it, in the order they are archived.
3465        let baselayout = subjects
3466            .iter()
3467            .find(|subject| subject.name == "alpine-baselayout")
3468            .expect("it was laid down");
3469        let carried: Vec<&str> = baselayout
3470            .scripts
3471            .iter()
3472            .map(|(kind, _)| kind.name())
3473            .collect();
3474        assert_eq!(
3475            carried,
3476            ["pre-install", "post-install", "pre-upgrade", "post-upgrade"],
3477        );
3478    }
3479
3480    #[test]
3481    fn a_package_that_is_not_the_one_the_plan_names_is_refused() {
3482        // The index-to-package binding, which is what makes a per-package
3483        // signature worth checking: this package is validly signed by a key the
3484        // repository publishes, and it is still not the one that was asked for.
3485        let scratch = Scratch::new("alpine-binding");
3486        let path = scratch.join("musl-1.2.5-r23.apk");
3487        std::fs::write(&path, fixtures::MUSL).expect("the package is writable");
3488
3489        let mut planned = planned_from(fixtures::ALPINE_BASELAYOUT, "x86_64");
3490        planned.name = "musl".to_string();
3491        planned.version = "1.2.5-r23".to_string();
3492        let err = read_acquired(&path, &planned, "x86_64")
3493            .expect_err("the served package is not the one the plan identified");
3494        assert!(matches!(err, AlpineError::Digest { .. }), "{err}");
3495
3496        // And a plan whose own fields contradict the package it identifies.
3497        let mut planned = planned_from(fixtures::MUSL, "x86_64");
3498        planned.name = "not-musl".to_string();
3499        let err = read_acquired(&path, &planned, "x86_64")
3500            .expect_err("the plan names a package the container does not");
3501        assert!(
3502            matches!(&err, AlpineError::Config { reason } if reason.contains("not-musl")),
3503            "{err}",
3504        );
3505    }
3506
3507    #[test]
3508    fn a_package_from_an_unpublished_architecture_is_refused() {
3509        let scratch = Scratch::new("alpine-architecture");
3510        let path = scratch.join("musl-1.2.5-r23.apk");
3511        std::fs::write(&path, fixtures::MUSL).expect("the package is writable");
3512        let planned = planned_from(fixtures::MUSL, "aarch64");
3513        let err = read_acquired(&path, &planned, "aarch64")
3514            .expect_err("an x86_64 package is not an aarch64 one");
3515        assert!(
3516            matches!(&err, AlpineError::Config { reason } if reason.contains("architecture")),
3517            "{err}",
3518        );
3519    }
3520
3521    #[test]
3522    fn a_local_repository_bootstraps_a_root_that_records_what_it_holds() {
3523        // The whole layer end to end and entirely offline: a repository laid
3524        // out at the paths Alpine publishes, read through the bundled
3525        // `file://` transport, resolved, verified, unpacked, and registered.
3526        // Extract-only because the committed fixtures are chosen for their
3527        // container shapes rather than for making a runnable root — there is no
3528        // `busybox` here to interpret a script.
3529        let scratch = Scratch::new("alpine-bootstrap");
3530        let component = scratch.join("v3.23/main/x86_64");
3531        std::fs::create_dir_all(&component).expect("the repository tree is creatable");
3532        std::fs::write(component.join("APKINDEX.tar.gz"), fixtures::INDEX)
3533            .expect("the index is writable");
3534        std::fs::write(component.join("musl-1.2.5-r23.apk"), fixtures::MUSL)
3535            .expect("the package is writable");
3536
3537        let root = scratch.join("root");
3538        let mut alpine = Alpine::builder("v3.23")
3539            .architecture("x86_64")
3540            .mirror(crate::provision::file_url(scratch.path()).expect("the path is nameable"))
3541            .include(["musl"])
3542            .extract_only(true)
3543            .build()
3544            .expect("the bootstrap configures");
3545        assert_eq!(
3546            crate::provision::ensure(&root, &mut alpine).expect("the root is provisioned"),
3547            crate::provision::Provisioned::Created,
3548        );
3549
3550        // The files the package ships, including the link that makes it a C
3551        // library rather than a directory of objects.
3552        assert!(root.join("lib/ld-musl-x86_64.so.1").is_file());
3553        assert_eq!(
3554            std::fs::read_link(root.join("lib/libc.musl-x86_64.so.1")).expect("the link is there"),
3555            std::path::Path::new("ld-musl-x86_64.so.1"),
3556        );
3557
3558        // And the state that makes it a root apk recognizes rather than a
3559        // directory of files.
3560        let installed = std::fs::read_to_string(root.join("lib/apk/db/installed"))
3561            .expect("the database is written");
3562        assert!(installed.contains("\nP:musl\n"), "{installed}");
3563        assert!(
3564            installed.ends_with("\n\n"),
3565            "a record ends with a blank line"
3566        );
3567        assert_eq!(
3568            std::fs::read_to_string(root.join("etc/apk/world")).expect("the world is written"),
3569            "musl\n",
3570            "what was asked for, not the closure",
3571        );
3572        assert_eq!(
3573            std::fs::read_to_string(root.join("etc/apk/arch")).expect("the arch is written"),
3574            "x86_64\n",
3575        );
3576        let sources = std::fs::read_to_string(root.join("etc/apk/repositories"))
3577            .expect("the repositories are written");
3578        assert!(sources.trim_end().ends_with("/v3.23/main"), "{sources}");
3579        assert!(
3580            !sources.contains("x86_64"),
3581            "apk appends the architecture itself: {sources}",
3582        );
3583
3584        // The trust anchor, written because nothing in this closure ships one.
3585        assert!(
3586            root.join("etc/apk/keys")
3587                .join(fixtures::ALPINE_KEY_NAME)
3588                .is_file(),
3589        );
3590        // Nothing ran, so nothing was left behind by a script.
3591        assert!(!root.join("lib/apk/exec").exists());
3592        assert_eq!(
3593            std::fs::read(root.join("lib/apk/db/triggers")).expect("the file is written"),
3594            b"",
3595            "musl watches nothing",
3596        );
3597    }
3598
3599    /// A repository laid out at the paths Alpine publishes, serving the index
3600    /// and each named fixture, and the `file://` URL of its root.
3601    fn published(scratch: &Scratch, packages: &[(&str, &[u8])]) -> String {
3602        let component = scratch.join("v3.23/main/x86_64");
3603        std::fs::create_dir_all(&component).expect("the repository tree is creatable");
3604        std::fs::write(component.join("APKINDEX.tar.gz"), fixtures::INDEX)
3605            .expect("the index is writable");
3606        for (name, body) in packages {
3607            std::fs::write(component.join(name), body).expect("the package is writable");
3608        }
3609        crate::provision::file_url(scratch.path()).expect("the path is nameable")
3610    }
3611
3612    /// A base root holding `world`, provisioned offline from `mirror`.
3613    ///
3614    /// Extract-only because the committed fixtures are chosen for their
3615    /// container shapes rather than for making a runnable root: there is no
3616    /// `busybox` here to interpret a script. Everything a layered build reads
3617    /// off a base is written either way, which is the point of registration
3618    /// being a file format.
3619    fn provisioned_base(mirror: &str, at: &Path, world: &[&str]) -> PathBuf {
3620        let mut alpine = Alpine::builder("v3.23")
3621            .architecture("x86_64")
3622            .mirror(mirror)
3623            .include(world.iter().copied())
3624            .extract_only(true)
3625            .build()
3626            .expect("the bootstrap configures");
3627        crate::provision::ensure(at, &mut alpine).expect("the base is provisioned");
3628        at.to_path_buf()
3629    }
3630
3631    /// A hand-written base database holding one package at one version, for the
3632    /// resolution cases a committed fixture cannot express.
3633    fn base_recording(scratch: &Scratch, tag: &str, records: &[(&str, &str, &str)]) -> PathBuf {
3634        let base = scratch.join(tag);
3635        std::fs::create_dir_all(base.join("lib/apk/db")).expect("the tree is creatable");
3636        let mut database = String::new();
3637        let alphabet = "abcdefghijklmnopqrstuvwxyz".repeat(2);
3638        for (at, (name, version, fields)) in records.iter().enumerate() {
3639            // A distinct well-formed control identity per record, which the
3640            // reader checks the shape of and nothing here reads.
3641            let identity = format!("Q1{}=", &alphabet[at..at + 27]);
3642            database.push_str(&format!(
3643                "C:{identity}\nP:{name}\nV:{version}\nA:x86_64\nS:1024\nI:2048\n{fields}\n",
3644            ));
3645        }
3646        std::fs::write(base.join("lib/apk/db/installed"), database)
3647            .expect("the database is writable");
3648        base
3649    }
3650
3651    #[test]
3652    fn an_increment_resolves_only_what_the_base_does_not_carry() {
3653        // The layered read half: the base answers for what it holds, so the
3654        // closure that comes back is the delta and nothing else.
3655        let scratch = Scratch::new("alpine-resolve-layer");
3656        let mirror = published(&scratch, &[("musl-1.2.5-r23.apk", fixtures::MUSL)]);
3657        let base = provisioned_base(&mirror, &scratch.join("base"), &["musl"]);
3658
3659        let plan = Alpine::builder("v3.23")
3660            .architecture("x86_64")
3661            .mirror(&mirror)
3662            .base_layer(&base)
3663            .include(["busybox", "musl"])
3664            .build()
3665            .expect("the bootstrap configures")
3666            .resolve_layer()
3667            .expect("the increment resolves");
3668        let names: Vec<&str> = plan
3669            .packages
3670            .iter()
3671            .map(|package| package.name.as_str())
3672            .collect();
3673        assert_eq!(
3674            names,
3675            ["busybox"],
3676            "musl is in the base, and so is the so: name busybox needs",
3677        );
3678
3679        // And the whole-root resolution of the same request, for contrast: the
3680        // base is the only difference between them.
3681        let whole = Alpine::builder("v3.23")
3682            .architecture("x86_64")
3683            .mirror(&mirror)
3684            .include(["busybox", "musl"])
3685            .build()
3686            .expect("the bootstrap configures")
3687            .resolve()
3688            .expect("the closure resolves");
3689        let names: Vec<&str> = whole
3690            .packages
3691            .iter()
3692            .map(|package| package.name.as_str())
3693            .collect();
3694        assert_eq!(names, ["busybox", "musl"]);
3695    }
3696
3697    #[test]
3698    fn an_increment_that_would_replace_a_base_package_is_refused() {
3699        // The base answers at the version it records rather than merely by name,
3700        // so a dependency its version cannot meet is reported as the upgrade it
3701        // would be. `alpine-baselayout` needs its data package at exactly its own
3702        // version, and this base carries an older one.
3703        let scratch = Scratch::new("alpine-upgrade");
3704        let mirror = published(&scratch, &[]);
3705        let base = base_recording(
3706            &scratch,
3707            "base",
3708            &[
3709                ("alpine-baselayout-data", "3.7.0-r0", ""),
3710                ("busybox", "1.37.0-r30", "p:/bin/sh"),
3711            ],
3712        );
3713
3714        let err = Alpine::builder("v3.23")
3715            .architecture("x86_64")
3716            .mirror(&mirror)
3717            .base_layer(&base)
3718            .include(["alpine-baselayout"])
3719            .build()
3720            .expect("the bootstrap configures")
3721            .resolve_layer()
3722            .expect_err("the base carries an older alpine-baselayout-data");
3723        assert!(
3724            matches!(&err, AlpineError::Resolve { reason }
3725                if reason.contains("3.7.0-r0") && reason.contains("alpine-baselayout-data")),
3726            "{err}",
3727        );
3728
3729        // The same base, with the data package at the version the closure needs:
3730        // now the increment is `alpine-baselayout` alone.
3731        let base = base_recording(
3732            &scratch,
3733            "current",
3734            &[
3735                ("alpine-baselayout-data", "3.7.2-r0", ""),
3736                ("busybox", "1.37.0-r30", "p:/bin/sh"),
3737            ],
3738        );
3739        let plan = Alpine::builder("v3.23")
3740            .architecture("x86_64")
3741            .mirror(&mirror)
3742            .base_layer(&base)
3743            .include(["alpine-baselayout"])
3744            .build()
3745            .expect("the bootstrap configures")
3746            .resolve_layer()
3747            .expect("the base answers both of its dependencies");
3748        let names: Vec<&str> = plan
3749            .packages
3750            .iter()
3751            .map(|package| package.name.as_str())
3752            .collect();
3753        assert_eq!(names, ["alpine-baselayout"]);
3754    }
3755
3756    #[test]
3757    fn a_layered_entry_point_without_a_base_is_refused_where_it_is_called() {
3758        let scratch = Scratch::new("alpine-no-base");
3759        let mirror = published(&scratch, &[("musl-1.2.5-r23.apk", fixtures::MUSL)]);
3760        let mut alpine = Alpine::builder("v3.23")
3761            .architecture("x86_64")
3762            .mirror(&mirror)
3763            .include(["musl"])
3764            .build()
3765            .expect("the bootstrap configures");
3766        let err = alpine.resolve_layer().expect_err("no base layer was set");
3767        assert!(
3768            matches!(&err, AlpineError::Config { reason } if reason.contains("resolve_layer")),
3769            "{err}",
3770        );
3771        let Err(err) = alpine.stage_layer(scratch.join("upper")) else {
3772            panic!("no base layer was set");
3773        };
3774        assert!(err.to_string().contains("stage_layer"), "{err}");
3775
3776        // And the other way round: a provisioner that has a base is a layered
3777        // one, and bootstrapping a whole root into the caller's directory would
3778        // be answering a question they did not ask.
3779        let base = provisioned_base(&mirror, &scratch.join("base"), &["musl"]);
3780        let mut layered = Alpine::builder("v3.23")
3781            .architecture("x86_64")
3782            .mirror(&mirror)
3783            .base_layer(&base)
3784            .include(["musl"])
3785            .extract_only(true)
3786            .build()
3787            .expect("the bootstrap configures");
3788        let Err(err) = crate::provision::ensure(scratch.join("whole"), &mut layered) else {
3789            panic!("a base layer was set");
3790        };
3791        assert!(err.to_string().contains("stage_layer"), "{err}");
3792    }
3793
3794    #[test]
3795    fn an_increment_stages_into_the_upper_and_records_the_merged_root() {
3796        // The layered write half, entirely offline. The base is provisioned
3797        // first, then an increment is staged over it: the increment's files land
3798        // in the upper, the base is not written to, and the database the upper
3799        // carries describes both.
3800        let scratch = Scratch::new("alpine-stage-layer");
3801        let mirror = published(
3802            &scratch,
3803            &[
3804                ("musl-1.2.5-r23.apk", fixtures::MUSL),
3805                ("alpine-keys-2.6-r0.apk", fixtures::ALPINE_KEYS),
3806            ],
3807        );
3808        let base = provisioned_base(&mirror, &scratch.join("base"), &["alpine-keys"]);
3809        let upper = scratch.join("upper");
3810
3811        {
3812            let layer = Alpine::builder("v3.23")
3813                .architecture("x86_64")
3814                .mirror(&mirror)
3815                .base_layer(&base)
3816                .include(["musl", "alpine-keys"])
3817                .extract_only(true)
3818                .build()
3819                .expect("the bootstrap configures")
3820                .stage_layer(&upper)
3821                .expect("the increment stages");
3822            assert_eq!(layer.path(), upper.as_path());
3823
3824            // The increment's files are in the upper and the base is untouched.
3825            assert!(upper.join("lib/ld-musl-x86_64.so.1").is_file());
3826            assert!(
3827                !base.join("lib/ld-musl-x86_64.so.1").exists(),
3828                "the base is the read-only lower and is never written to",
3829            );
3830            // `alpine-keys` was already in the base, so it is not staged again.
3831            assert!(!upper.join("usr/share/apk/keys").exists());
3832
3833            // The database the upper carries describes the merged root: the
3834            // base's record byte for byte, and the increment's sorted in among
3835            // them.
3836            let installed = std::fs::read_to_string(upper.join("lib/apk/db/installed"))
3837                .expect("the database is written");
3838            let names: Vec<&str> = installed
3839                .lines()
3840                .filter_map(|line| line.strip_prefix("P:"))
3841                .collect();
3842            assert_eq!(names, ["alpine-keys", "musl"]);
3843            let carried = std::fs::read_to_string(base.join("lib/apk/db/installed"))
3844                .expect("the base's database is there");
3845            assert!(
3846                installed.contains(carried.trim_end_matches('\n')),
3847                "the base's record is carried through unchanged",
3848            );
3849
3850            // And so does the rest of the state.
3851            assert_eq!(
3852                std::fs::read_to_string(upper.join("etc/apk/world")).expect("the world is written"),
3853                "alpine-keys\nmusl\n",
3854                "what the base asked for and what the increment asked for",
3855            );
3856            // The base already holds the trust anchor and the overlay makes it
3857            // visible, so writing one here would shadow a file rather than add
3858            // one.
3859            assert!(!upper.join("etc/apk/keys").exists());
3860            let sources = std::fs::read_to_string(upper.join("etc/apk/repositories"))
3861                .expect("the repositories are written");
3862            assert_eq!(sources.lines().count(), 1, "not written twice: {sources}");
3863        }
3864
3865        // Dropping the layer discards the increment and leaves the base.
3866        assert!(!upper.exists(), "the upper went with the handle");
3867        assert!(base.join("lib/apk/db/installed").is_file());
3868    }
3869
3870    #[test]
3871    fn a_download_is_announced_over_what_is_being_downloaded() {
3872        // The event is emitted for a cache miss and only for one, so counting
3873        // the plan's packages reports a fetch of one package as reaching two of
3874        // two. What `index` of `total` means has to be the downloads.
3875        let scratch = Scratch::new("alpine-download-count");
3876        let mirror = published(
3877            &scratch,
3878            &[
3879                ("musl-1.2.5-r23.apk", fixtures::MUSL),
3880                ("alpine-keys-2.6-r0.apk", fixtures::ALPINE_KEYS),
3881            ],
3882        );
3883        let cache = scratch.join("cache");
3884        std::fs::create_dir_all(&cache).expect("the cache is creatable");
3885        // One of the two already in the cache, which is the everyday state of a
3886        // second run.
3887        std::fs::write(cache.join("musl-1.2.5-r23.apk"), fixtures::MUSL)
3888            .expect("the cached package is writable");
3889
3890        #[derive(Default)]
3891        struct Counting(Vec<(String, usize, usize)>);
3892        impl AlpineObserver for Counting {
3893            fn progress(&mut self, event: AlpineEvent<'_>) {
3894                if let AlpineEvent::Downloading {
3895                    package,
3896                    index,
3897                    total,
3898                } = event
3899                {
3900                    self.0.push((package.to_string(), index, total));
3901                }
3902            }
3903        }
3904
3905        let mut counting = Counting::default();
3906        let mut alpine = Alpine::builder("v3.23")
3907            .architecture("x86_64")
3908            .mirror(&mirror)
3909            .include(["musl", "alpine-keys"])
3910            .cache_dir(&cache)
3911            .extract_only(true)
3912            .build()
3913            .expect("the bootstrap configures");
3914        crate::provision::ensure(scratch.join("root"), &mut alpine.observe(&mut counting))
3915            .expect("the bootstrap runs");
3916
3917        assert_eq!(
3918            counting.0,
3919            vec![("alpine-keys".to_string(), 1, 1)],
3920            "one package is downloaded, and it is the first of one",
3921        );
3922    }
3923
3924    #[test]
3925    fn a_layered_provisioner_is_read_through_resolve_layer() {
3926        // `provision::ensure` refuses a provisioner with a base layer, and the
3927        // read half has to agree: reporting the whole-root closure answers a
3928        // question this provisioner never asks, and reads as the plan it would
3929        // install.
3930        let scratch = Scratch::new("alpine-layered-resolve");
3931        let mirror = published(&scratch, &[("musl-1.2.5-r23.apk", fixtures::MUSL)]);
3932        let base = provisioned_base(&mirror, &scratch.join("base"), &["musl"]);
3933        let err = Alpine::builder("v3.23")
3934            .architecture("x86_64")
3935            .mirror(&mirror)
3936            .base_layer(&base)
3937            .include(["musl"])
3938            .build()
3939            .expect("the bootstrap configures")
3940            .resolve()
3941            .expect_err("a layered provisioner is not read this way");
3942        assert!(
3943            matches!(&err, AlpineError::Config { reason } if reason.contains("resolve_layer")),
3944            "{err}",
3945        );
3946    }
3947
3948    #[test]
3949    fn an_increment_the_base_already_satisfies_still_says_what_it_wants() {
3950        // Every requested package is in the base, so there is nothing to
3951        // download and no script to run -- but the increment's own state is
3952        // still written. What a caller reads back from the merged view cannot
3953        // depend on whether the delta happened to be empty, and a base holding
3954        // a package says nothing about whether its world names one: every
3955        // package a resolution pulled in as a dependency is installed and
3956        // unwanted.
3957        let scratch = Scratch::new("alpine-empty-increment");
3958        let mirror = published(&scratch, &[("musl-1.2.5-r23.apk", fixtures::MUSL)]);
3959        let base = provisioned_base(&mirror, &scratch.join("base"), &["musl"]);
3960        std::fs::write(base.join("etc/apk/world"), b"").expect("the base's world is writable");
3961        let upper = scratch.join("upper");
3962
3963        let layer = Alpine::builder("v3.23")
3964            .architecture("x86_64")
3965            .mirror(&mirror)
3966            .base_layer(&base)
3967            .include(["musl"])
3968            .extract_only(true)
3969            .build()
3970            .expect("the bootstrap configures")
3971            .stage_layer(&upper)
3972            .expect("the increment stages");
3973        assert!(layer.path().is_dir());
3974        assert_eq!(
3975            std::fs::read_to_string(upper.join("etc/apk/world")).expect("the world is written"),
3976            "musl\n",
3977            "the increment's request reaches the merged view",
3978        );
3979        // Nothing was downloaded and nothing ran, which is still true.
3980        assert!(!upper.join("lib/apk/exec").exists());
3981        assert!(!upper.join("lib/ld-musl-x86_64.so.1").exists());
3982    }
3983
3984    #[test]
3985    fn a_failed_staging_disposes_of_the_upper_it_created() {
3986        // The upper is created before the increment resolves, and a failure
3987        // hands the caller no handle to drop — so the failure path has to
3988        // dispose of it.
3989        let scratch = Scratch::new("alpine-failed-staging");
3990        let mirror = published(&scratch, &[("musl-1.2.5-r23.apk", fixtures::MUSL)]);
3991        let base = provisioned_base(&mirror, &scratch.join("base"), &["musl"]);
3992        let upper = scratch.join("upper");
3993
3994        let Err(err) = Alpine::builder("v3.23")
3995            .architecture("x86_64")
3996            .mirror(&mirror)
3997            .base_layer(&base)
3998            .include(["systemd"])
3999            .extract_only(true)
4000            .build()
4001            .expect("the bootstrap configures")
4002            .stage_layer(&upper)
4003        else {
4004            panic!("Alpine publishes no systemd");
4005        };
4006        assert!(err.to_string().contains("systemd"), "{err}");
4007        assert!(
4008            !upper.exists(),
4009            "the upper it created went with the failure"
4010        );
4011    }
4012
4013    #[test]
4014    fn a_bootstrap_reports_what_it_is_doing_and_can_be_stopped() {
4015        let scratch = Scratch::new("alpine-observed");
4016        let component = scratch.join("v3.23/main/x86_64");
4017        std::fs::create_dir_all(&component).expect("the repository tree is creatable");
4018        std::fs::write(component.join("APKINDEX.tar.gz"), fixtures::INDEX)
4019            .expect("the index is writable");
4020        std::fs::write(component.join("musl-1.2.5-r23.apk"), fixtures::MUSL)
4021            .expect("the package is writable");
4022
4023        /// Records the events it sees, and stops once it has seen a download.
4024        struct Watch {
4025            seen: Vec<String>,
4026            stop: bool,
4027        }
4028
4029        impl AlpineObserver for Watch {
4030            fn progress(&mut self, event: AlpineEvent<'_>) {
4031                match event {
4032                    AlpineEvent::Resolving => self.seen.push("resolving".to_string()),
4033                    AlpineEvent::Resolved { plan } => {
4034                        self.seen.push(format!("resolved {}", plan.packages.len()));
4035                    }
4036                    AlpineEvent::Downloading { package, .. } => {
4037                        self.seen.push(format!("downloading {package}"));
4038                    }
4039                    AlpineEvent::Extracting { package } => {
4040                        self.seen.push(format!("extracting {package}"));
4041                    }
4042                    _ => {}
4043                }
4044            }
4045
4046            fn cancelled(&mut self) -> bool {
4047                // Once the closure is known, which is the first boundary a
4048                // caller watching the plan would decide at.
4049                self.stop && self.seen.iter().any(|seen| seen.starts_with("resolved"))
4050            }
4051        }
4052
4053        let mirror = crate::provision::file_url(scratch.path()).expect("the path is nameable");
4054        let mut watch = Watch {
4055            seen: Vec::new(),
4056            stop: false,
4057        };
4058        let mut alpine = Alpine::builder("v3.23")
4059            .architecture("x86_64")
4060            .mirror(&mirror)
4061            .include(["musl"])
4062            .extract_only(true)
4063            .build()
4064            .expect("the bootstrap configures");
4065        crate::provision::ensure(scratch.join("root"), &mut alpine.observe(&mut watch))
4066            .expect("the root is provisioned");
4067        assert_eq!(
4068            watch.seen,
4069            [
4070                "resolving",
4071                "resolved 1",
4072                "downloading musl",
4073                "extracting musl",
4074            ],
4075        );
4076
4077        // And stopping. A sink bound with `observe` reports; what stops a run
4078        // is the run's own observer, which is the one a caller driving several
4079        // provisioners shares — so that is where the cancellation comes from,
4080        // and the Alpine events still reach it through the shared vocabulary.
4081        struct StopOnResolve {
4082            resolved: bool,
4083        }
4084
4085        impl crate::provision::ProvisionObserver for StopOnResolve {
4086            fn progress(&mut self, event: crate::provision::ProvisionEvent<'_>) {
4087                if let crate::provision::ProvisionEvent::Alpine(AlpineEvent::Resolved { .. }) =
4088                    event
4089                {
4090                    self.resolved = true;
4091                }
4092            }
4093
4094            fn cancelled(&mut self) -> bool {
4095                self.resolved
4096            }
4097        }
4098
4099        let mut alpine = Alpine::builder("v3.23")
4100            .architecture("x86_64")
4101            .mirror(&mirror)
4102            .include(["musl"])
4103            .extract_only(true)
4104            .build()
4105            .expect("the bootstrap configures");
4106        let stopped = scratch.join("stopped");
4107        let mut stop = StopOnResolve { resolved: false };
4108        let err = crate::provision::Provision::new(&stopped)
4109            .observe(&mut stop)
4110            .run(&mut alpine)
4111            .expect_err("the observer asked it to stop");
4112        assert!(
4113            matches!(err, crate::provision::ProvisionError::Cancelled),
4114            "{err}",
4115        );
4116        assert!(
4117            stop.resolved,
4118            "the Alpine events reached the run's observer"
4119        );
4120        assert!(!stopped.exists());
4121    }
4122
4123    #[test]
4124    fn a_plan_installs_without_reading_an_index() {
4125        // What installing from a plan saves is the resolution, and this is the
4126        // proof: the repository serves the package and no index at all.
4127        let scratch = Scratch::new("alpine-planned");
4128        let component = scratch.join("v3.23/main/x86_64");
4129        std::fs::create_dir_all(&component).expect("the repository tree is creatable");
4130        std::fs::write(component.join("APKINDEX.tar.gz"), fixtures::INDEX)
4131            .expect("the index is writable");
4132        std::fs::write(component.join("musl-1.2.5-r23.apk"), fixtures::MUSL)
4133            .expect("the package is writable");
4134        let mirror = crate::provision::file_url(scratch.path()).expect("the path is nameable");
4135
4136        let plan = Alpine::builder("v3.23")
4137            .architecture("x86_64")
4138            .mirror(&mirror)
4139            .include(["musl"])
4140            .build()
4141            .expect("the bootstrap configures")
4142            .resolve()
4143            .expect("musl resolves");
4144
4145        // Round-tripped through the document, which is how a plan actually
4146        // travels between the machine that resolved it and the one that
4147        // installs it.
4148        let document = plan.to_document().expect("the plan is writable");
4149        let kept = Plan::parse_document(&document).expect("the document reads back");
4150        assert_eq!(kept, plan);
4151
4152        std::fs::remove_file(component.join("APKINDEX.tar.gz")).expect("the index is removable");
4153        let root = scratch.join("root");
4154        let mut alpine = Alpine::builder("v3.23")
4155            .architecture("x86_64")
4156            .mirror(&mirror)
4157            .plan(kept)
4158            .extract_only(true)
4159            .build()
4160            .expect("the bootstrap configures");
4161        crate::provision::ensure(&root, &mut alpine).expect("the plan installs");
4162        assert!(root.join("lib/ld-musl-x86_64.so.1").is_file());
4163        let installed = std::fs::read_to_string(root.join("lib/apk/db/installed"))
4164            .expect("the database is written");
4165        assert!(installed.contains("\nP:musl\n"), "{installed}");
4166    }
4167
4168    #[test]
4169    fn a_package_missing_from_the_primary_is_served_by_the_backstop() {
4170        // The plan records the mirror that served the index, and a package is
4171        // fetched over the configured mirror list rather than that record. The
4172        // primary keeps the index the plan resolved from and loses the package,
4173        // which is what a rebuild does to a live mirror; a walk that stopped at
4174        // the recorded mirror could not recover from it.
4175        let scratch = Scratch::new("alpine-package-backstop");
4176        let primary = scratch.join("primary/v3.23/main/x86_64");
4177        let backstop = scratch.join("backstop/v3.23/main/x86_64");
4178        for tree in [&primary, &backstop] {
4179            std::fs::create_dir_all(tree).expect("the repository tree is creatable");
4180        }
4181        std::fs::write(primary.join("APKINDEX.tar.gz"), fixtures::INDEX)
4182            .expect("the index is writable");
4183        std::fs::write(primary.join("musl-1.2.5-r23.apk"), fixtures::MUSL)
4184            .expect("the package is writable");
4185
4186        let primary_url =
4187            crate::provision::file_url(&scratch.join("primary")).expect("the path is nameable");
4188        let backstop_url =
4189            crate::provision::file_url(&scratch.join("backstop")).expect("the path is nameable");
4190
4191        let plan = Alpine::builder("v3.23")
4192            .architecture("x86_64")
4193            .mirror(&primary_url)
4194            .include(["musl"])
4195            .build()
4196            .expect("the bootstrap configures")
4197            .resolve()
4198            .expect("musl resolves");
4199
4200        // The package rotates off the primary onto the backstop. The index it
4201        // was resolved from stays exactly where it was.
4202        std::fs::rename(
4203            primary.join("musl-1.2.5-r23.apk"),
4204            backstop.join("musl-1.2.5-r23.apk"),
4205        )
4206        .expect("the package is movable");
4207
4208        // Without a backstop there is nowhere else to look, which is what makes
4209        // the success below the walk's doing rather than the primary's.
4210        let mut alone = Alpine::builder("v3.23")
4211            .architecture("x86_64")
4212            .mirror(&primary_url)
4213            .plan(plan.clone())
4214            .extract_only(true)
4215            .build()
4216            .expect("the bootstrap configures");
4217        crate::provision::ensure(scratch.join("alone"), &mut alone)
4218            .expect_err("the primary no longer serves the package");
4219
4220        let root = scratch.join("root");
4221        let mut alpine = Alpine::builder("v3.23")
4222            .architecture("x86_64")
4223            .mirror(&primary_url)
4224            .mirror_fallback(&backstop_url)
4225            .plan(plan)
4226            .extract_only(true)
4227            .build()
4228            .expect("the bootstrap configures");
4229        crate::provision::ensure(&root, &mut alpine).expect("the backstop serves the package");
4230        assert!(root.join("lib/ld-musl-x86_64.so.1").is_file());
4231    }
4232
4233    #[test]
4234    fn a_plan_pins_the_closure_it_installed() {
4235        // `plan()` forbids `include()`, so the configured world is empty and an
4236        // empty `/etc/apk/world` is what a plan-based root carried: apk reads
4237        // that as nothing being wanted, and acts on it. A plan names every
4238        // package rather than the seeds a resolution closed over, so its names
4239        // are what the root wants.
4240        let scratch = Scratch::for_test("alpine", "plan-world");
4241        let mirror = published(&scratch, &[("musl-1.2.5-r23.apk", fixtures::MUSL)]);
4242        let plan = Alpine::builder("v3.23")
4243            .architecture("x86_64")
4244            .mirror(&mirror)
4245            .include(["musl"])
4246            .build()
4247            .expect("the bootstrap configures")
4248            .resolve()
4249            .expect("musl resolves");
4250        let mut names: Vec<String> = plan
4251            .packages
4252            .iter()
4253            .map(|package| package.name.clone())
4254            .collect();
4255        assert!(names.iter().any(|name| name == "musl"), "{names:?}");
4256        names.sort();
4257
4258        let root = scratch.join("planned");
4259        let mut alpine = Alpine::builder("v3.23")
4260            .architecture("x86_64")
4261            .mirror(&mirror)
4262            .plan(plan)
4263            .extract_only(true)
4264            .build()
4265            .expect("the bootstrap configures");
4266        crate::provision::ensure(&root, &mut alpine).expect("the plan installs");
4267        let world =
4268            std::fs::read_to_string(root.join("etc/apk/world")).expect("a world is written");
4269        let mut wanted: Vec<String> = world.lines().map(str::to_string).collect();
4270        wanted.sort();
4271        assert_eq!(wanted, names, "the plan's packages are what the root wants");
4272    }
4273
4274    #[test]
4275    fn a_plan_resolved_against_more_indexes_than_are_configured_is_refused() {
4276        // A package names the index it came from, and the mirrors it is fetched
4277        // over are the ones the repository publishing that index is configured
4278        // with. A plan the configuration cannot account for therefore names no
4279        // mirror, and says so before anything is downloaded.
4280        let plan = Alpine::builder("v3.23")
4281            .architecture("x86_64")
4282            .mirror("http://mirror.invalid/alpine")
4283            .repository(postmarketos())
4284            .include(["busybox"])
4285            .fetcher(canned())
4286            .build()
4287            .expect("the bootstrap configures")
4288            .resolve()
4289            .expect("busybox resolves");
4290        assert_eq!(plan.indexes.len(), 2, "one index from each repository");
4291
4292        let err = Alpine::builder("v3.23")
4293            .architecture("x86_64")
4294            .mirror("http://mirror.invalid/alpine")
4295            .plan(plan)
4296            .extract_only(true)
4297            .fetcher(canned())
4298            .build()
4299            .expect_err("the second repository is not configured");
4300        assert!(
4301            matches!(&err, AlpineError::Config { reason } if reason.contains("name no mirror")),
4302            "{err}",
4303        );
4304    }
4305
4306    /// A one-entry uncompressed tar holding `path` with `contents`, as a
4307    /// package's data segment lays a file down.
4308    fn one_file_archive(path: &str, contents: &[u8]) -> Vec<u8> {
4309        let mut out = Vec::new();
4310        out.extend_from_slice(
4311            &crate::provision::tar::ustar_block(
4312                path.as_bytes(),
4313                b"",
4314                0o644,
4315                0,
4316                0,
4317                contents.len() as u64,
4318                0,
4319                b'0',
4320                b"",
4321            )
4322            .expect("the header fields fit"),
4323        );
4324        out.extend_from_slice(contents);
4325        crate::provision::tar::pad(&mut out, contents.len() as u64).expect("padding a vector");
4326        // The two zero blocks that end an archive.
4327        out.extend_from_slice(&[0u8; 1024]);
4328        out
4329    }
4330
4331    /// A record for a package named `name` that carries the extra `.PKGINFO`
4332    /// lines in `extra` — `replaces` and `replaces_priority`, for the collision
4333    /// policy.
4334    fn record_of(name: &str, extra: &str) -> installed::Record {
4335        let info = pkginfo::PkgInfo::parse(
4336            name,
4337            format!("pkgname = {name}\npkgver = 1.0-r0\narch = x86_64\n{extra}").as_bytes(),
4338        )
4339        .expect("the .PKGINFO reads");
4340        installed::Record::open(&info, "x86_64", 1024, format!("Q1{name}"), name.to_string())
4341    }
4342
4343    /// Lays `packages` — each a name, its extra `.PKGINFO` lines, and the
4344    /// contents it ships at `CONTESTED` — down in order into a fresh tree, and
4345    /// returns that tree with the database written over it.
4346    ///
4347    /// Drives the real [`Extraction`] and the real [`record_entry`], which is
4348    /// the pairing the collision policy lives in: `claim` alone cannot say
4349    /// whether the tree agrees with the record of it.
4350    fn lay_contested(packages: &[(&str, &str, &[u8])]) -> (Scratch, Database) {
4351        const CONTESTED: &str = "usr/bin/tool";
4352        let scratch = Scratch::new("alpine-collision");
4353        let staging = scratch.join("root");
4354        std::fs::create_dir_all(&staging).expect("the staging tree is creatable");
4355
4356        let mut extraction = Extraction::new(&staging).expect("the root opens");
4357        let mut database = Database::default();
4358        for (name, extra, contents) in packages {
4359            let at = database.add(record_of(name, extra));
4360            let archive = one_file_archive(CONTESTED, contents);
4361            let mut reader = TarReader::new(&archive[..]);
4362            let mut collision = None;
4363            extraction
4364                .extract(&mut reader, &mut |entry, components| {
4365                    record_entry(&mut database, at, entry, components, &mut collision)
4366                })
4367                .expect("the archive extracts");
4368            assert!(collision.is_none(), "{name}: {collision:?}");
4369        }
4370        extraction.finalize(&staging).expect("the metadata applies");
4371        (scratch, database)
4372    }
4373
4374    #[test]
4375    fn a_kept_claim_leaves_the_earlier_package_s_file_on_disk() {
4376        // The record says the earlier package still owns the path, so the
4377        // earlier package's bytes have to be what is there. Without a way to
4378        // tell the extractor to skip the entry, the later package's bytes
4379        // overwrote them and the finished root disagreed with its own `Z:`
4380        // digest — which `apk audit` reports as a modified file.
4381        let (scratch, database) = lay_contested(&[
4382            (
4383                "aaa-earlier",
4384                "replaces = zzz-later\nreplaces_priority = 1\n",
4385                b"earlier",
4386            ),
4387            ("zzz-later", "", b"later"),
4388        ]);
4389        assert_eq!(
4390            std::fs::read(scratch.join("root/usr/bin/tool")).expect("the file is there"),
4391            b"earlier",
4392        );
4393        let installed = String::from_utf8(database.installed()).expect("the database is text");
4394        assert_eq!(installed.matches("R:tool").count(), 1, "{installed}");
4395    }
4396
4397    #[test]
4398    fn a_replacing_claim_leaves_one_owner_whichever_way_the_names_sort() {
4399        // The replacer taking the path over has to take the file line with it.
4400        // Alpine's own case has the replacer sorting after the replaced, which
4401        // hides a stale earlier line: `apk` reads records in name order, so the
4402        // replacer's digest survives and matches the disk by luck. Reversing the
4403        // sort is what exposes it.
4404        for (earlier, later) in [
4405            ("aaa-replaced", "zzz-replacer"),
4406            ("zzz-replaced", "aaa-replacer"),
4407        ] {
4408            let (scratch, database) = lay_contested(&[
4409                (earlier, "", b"earlier"),
4410                (later, &format!("replaces = {earlier}\n"), b"later"),
4411            ]);
4412            assert_eq!(
4413                std::fs::read(scratch.join("root/usr/bin/tool")).expect("the file is there"),
4414                b"later",
4415                "{later} replaces {earlier}",
4416            );
4417            // One owner, and it is the one whose bytes are on disk.
4418            let installed = String::from_utf8(database.installed()).expect("the database is text");
4419            assert_eq!(
4420                installed.matches("R:tool").count(),
4421                1,
4422                "{earlier} then {later}:\n{installed}",
4423            );
4424            let owner = installed
4425                .split("\n\n")
4426                .find(|record| record.contains("R:tool"))
4427                .expect("some record holds it");
4428            assert!(owner.contains(&format!("P:{later}")), "{owner}");
4429        }
4430    }
4431
4432    /// A plan the canned repository would resolve, for the refusals below to
4433    /// start from something valid.
4434    #[cfg(test)]
4435    fn resolved_plan() -> Plan {
4436        Alpine::builder("v3.23")
4437            .architecture("x86_64")
4438            .mirror("http://mirror.invalid/alpine")
4439            .include(["busybox"])
4440            .fetcher(canned())
4441            .build()
4442            .expect("the bootstrap configures")
4443            .resolve()
4444            .expect("busybox resolves")
4445    }
4446
4447    /// The refusal `build()` gives for `plan`, under `configure`.
4448    fn refuses_plan(plan: Plan, configure: impl FnOnce(AlpineBuilder) -> AlpineBuilder) -> String {
4449        let builder = Alpine::builder("v3.23")
4450            .architecture("x86_64")
4451            .mirror("http://mirror.invalid/alpine")
4452            .extract_only(true)
4453            .fetcher(canned())
4454            .plan(plan);
4455        match configure(builder).build() {
4456            Err(AlpineError::Config { reason }) => reason,
4457            other => panic!("expected a configuration refusal, got {other:?}"),
4458        }
4459    }
4460
4461    #[test]
4462    fn a_plan_resolved_for_another_architecture_is_refused_at_build() {
4463        // The failure this replaces: `build()` succeeded, the first package
4464        // downloaded, and the run failed with a per-package "states
4465        // architecture" error rather than an upfront configuration refusal.
4466        let mut plan = resolved_plan();
4467        plan.architecture = "aarch64".to_string();
4468        let reason = refuses_plan(plan, |builder| builder);
4469        assert!(reason.contains("aarch64"), "{reason}");
4470        assert!(reason.contains("x86_64"), "{reason}");
4471    }
4472
4473    #[test]
4474    fn a_builder_naming_no_architecture_adopts_the_plans() {
4475        // The other half of the check above, and the reason it is not a trap: a
4476        // plan states which architecture it is for, so refusing one the caller
4477        // never contradicted — over a host default they never chose — would be a
4478        // confusing rejection. The Debian builder adopts a plan's architecture
4479        // for exactly this reason, and a pin's the same way.
4480        let mut plan = resolved_plan();
4481        plan.architecture = "aarch64".to_string();
4482        for package in &mut plan.packages {
4483            package.architecture = "aarch64".to_string();
4484        }
4485        let alpine = Alpine::builder("v3.23")
4486            .mirror("http://mirror.invalid/alpine")
4487            .extract_only(true)
4488            .fetcher(canned())
4489            .plan(plan)
4490            .build()
4491            .expect("the plan states the architecture");
4492        assert_eq!(alpine.architecture, "aarch64");
4493    }
4494
4495    #[test]
4496    fn a_plan_whose_index_is_not_the_one_configured_in_that_slot_is_refused() {
4497        // The correspondence is positional and a package's URL is composed from
4498        // the configured mirror with the plan's own release and component, so a
4499        // mismatched slot composes a directory no mirror serves. Without this
4500        // the whole mirror walk 404s and blames mirrors that were never asked
4501        // for that directory.
4502        let mut plan = resolved_plan();
4503        plan.indexes[0].component = Some("community".to_string());
4504        let reason = refuses_plan(plan, |builder| builder);
4505        assert!(reason.contains("v3.23/community"), "{reason}");
4506        assert!(reason.contains("v3.23/main"), "{reason}");
4507
4508        // The mirror is deliberately not compared: a plan carried to another
4509        // configuration fetches from that configuration's mirrors, which is
4510        // what makes it portable.
4511        let mut moved = resolved_plan();
4512        moved.indexes[0].mirror = "http://elsewhere.invalid/alpine".to_string();
4513        Alpine::builder("v3.23")
4514            .architecture("x86_64")
4515            .mirror("http://mirror.invalid/alpine")
4516            .extract_only(true)
4517            .fetcher(canned())
4518            .plan(moved)
4519            .build()
4520            .expect("a plan resolved elsewhere still installs here");
4521    }
4522
4523    #[test]
4524    fn a_plan_naming_a_package_by_a_path_is_refused_at_build() {
4525        // `Plan` is public and `Clone`, so a plan taken from `resolve()` and
4526        // edited reaches `build()` without having passed the index's gate. The
4527        // name composes the cache path the downloaded bytes are published at,
4528        // before anything has checked what they are.
4529        let mut plan = resolved_plan();
4530        plan.packages[0].name = "../../escaped".to_string();
4531        let reason = refuses_plan(plan, |builder| builder);
4532        assert!(reason.contains("addressed by"), "{reason}");
4533    }
4534
4535    #[test]
4536    fn a_plan_beside_a_resolution_of_its_own_is_refused() {
4537        // Each of these describes a resolution, and a plan is one already; the
4538        // bootstrap would install the plan and silently drop the rest.
4539        for (what, configure) in [
4540            (
4541                "include",
4542                Box::new(|builder: AlpineBuilder| builder.include(["busybox"]))
4543                    as Box<dyn FnOnce(AlpineBuilder) -> AlpineBuilder>,
4544            ),
4545            (
4546                "exclude",
4547                Box::new(|builder: AlpineBuilder| builder.exclude(["busybox"])),
4548            ),
4549            (
4550                "pin",
4551                Box::new(|builder: AlpineBuilder| builder.pin(resolved_plan())),
4552            ),
4553        ] {
4554            let reason = refuses_plan(resolved_plan(), configure);
4555            assert!(reason.contains(what), "{what}: {reason}");
4556            assert!(reason.contains("drop one of the two"), "{what}: {reason}");
4557        }
4558    }
4559
4560    #[test]
4561    fn a_rendering_reports_the_configuration_and_not_the_transport() {
4562        let alpine = Alpine::builder("v3.23")
4563            .architecture("x86_64")
4564            .build()
4565            .expect("the bootstrap configures");
4566        let rendering = format!("{alpine:?}");
4567        assert!(rendering.contains("x86_64"), "{rendering}");
4568        assert!(rendering.contains("v3.23"), "{rendering}");
4569        assert!(rendering.contains(fixtures::ALPINE_KEY_NAME), "{rendering}");
4570        assert!(!rendering.contains("BEGIN PUBLIC KEY"), "{rendering}");
4571    }
4572}