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}