//! Fetching a pinned base, and measuring the parameters a refit needs. //! //! Nothing here draws. It answers one question about a base face: what are its //! stroke weights, its cell, and the band its own symbols are fitted into. use std::collections::BTreeMap; use std::io::Read; use std::path::{Path, PathBuf}; use std::process::Command; use read_fonts::tables::cmap::{Cmap, CmapSubtable}; use read_fonts::types::GlyphId; use read_fonts::{FontRef, TableProvider}; use sha2::{Digest, Sha256}; use crate::Error; use crate::pins::Base; /// A base face, verified against its pin and read into memory. pub struct BaseFace { pub style: String, pub bytes: Vec, } /// Everything a drawing recipe is allowed to depend on, measured off the base. /// /// Every field is read from the face rather than configured. That is what makes /// the set a pipeline instead of seven one-offs: point the recipes at a /// different base and the marks refit to its weight and cell. #[derive(Debug, Clone, Copy)] pub struct BaseParams { pub upem: u16, /// The cell width. Marks centre on `advance / 2`. pub advance: u16, pub cap_height: i16, pub x_height: i16, /// `|`'s bbox width: the base's vertical stroke weight. pub stem: i16, /// `-`'s bbox height: the base's horizontal stroke weight. pub stroke: i16, /// `+`'s bbox: the band the base fits its own symbols into. pub band_x0: i16, pub band_x1: i16, pub band_y0: i16, pub band_y1: i16, /// The cell's top, from `hhea`. Positive. /// /// The band is where the base fits its *symbols*; this is the box a /// terminal gives a character. Cell primitives — box drawing, block /// elements — are sized off this and never off the band, because their /// whole job is to meet the cell above and the cell beside them exactly. /// A vertical bar drawn to the band's height would leave a gap at every row /// boundary. /// /// `hhea` rather than OS/2, because `hhea` is what a terminal lays lines /// out with, and it is what `shop`'s shaper takes the baseline from. pub ascent: i16, /// The cell's bottom, from `hhea`. Negative, as the table stores it. pub descent: i16, } impl BaseParams { pub fn band_width(&self) -> f64 { f64::from(self.band_x1 - self.band_x0) } pub fn band_height(&self) -> f64 { f64::from(self.band_y1 - self.band_y0) } pub fn band_center_y(&self) -> f64 { f64::from(self.band_y0 + self.band_y1) / 2.0 } /// Horizontal centre of the cell, which is not the band's centre in a face /// whose symbols are asymmetric. pub fn center_x(&self) -> f64 { f64::from(self.advance) / 2.0 } pub fn x_height_center_y(&self) -> f64 { f64::from(self.x_height) / 2.0 } /// The cell's height: ascender to descender. pub fn cell_height(&self) -> f64 { f64::from(self.ascent - self.descent) } /// Where a horizontal rule sits: the middle of the cell, not of the band. /// /// A rule on the band's centre would be near the x-height, so a table /// border would run through the middle of the text beside it rather than /// between the lines. pub fn cell_center_y(&self) -> f64 { f64::from(self.ascent + self.descent) / 2.0 } } /// The reference glyphs a base must have before it can be refitted against. /// A base missing one cannot be measured, and guessing is worse than refusing. const REFERENCES: [(char, &str); 3] = [ ('|', "the vertical stroke weight"), ('-', "the horizontal stroke weight"), ('+', "the symbol band"), ]; pub fn measure(bytes: &[u8]) -> Result { let font = FontRef::new(bytes).map_err(|e| Error::Font(format!("base is unreadable: {e}")))?; let head = font.head().map_err(table_err("head"))?; let hhea = font.hhea().map_err(table_err("hhea"))?; let os2 = font.os2().map_err(table_err("OS/2"))?; let hmtx = font.hmtx().map_err(table_err("hmtx"))?; let cmap = font.cmap().map_err(table_err("cmap"))?; for (ch, what) in REFERENCES { if lookup(&cmap, ch).is_none() { return Err(Error::UnmeasurableBase { missing: ch, what: what.to_owned(), }); } } let bar = bbox(&font, lookup(&cmap, '|').unwrap())?; let hyphen = bbox(&font, lookup(&cmap, '-').unwrap())?; let plus = bbox(&font, lookup(&cmap, '+').unwrap())?; // A monospace face gives the same advance for every glyph; taking it off a // reference glyph rather than off hhea keeps that assumption checkable. let advance = hmtx .advance(lookup(&cmap, '+').unwrap()) .ok_or_else(|| Error::Font("base has no advance for `+`".into()))?; let cap_height = os2 .s_cap_height() .unwrap_or(bbox_or_zero(&font, &cmap, 'H').3); let x_height = os2.sx_height().unwrap_or(bbox_or_zero(&font, &cmap, 'x').3); Ok(BaseParams { upem: head.units_per_em(), advance, cap_height, x_height, stem: bar.2 - bar.0, stroke: hyphen.3 - hyphen.1, band_x0: plus.0, band_x1: plus.2, band_y0: plus.1, band_y1: plus.3, ascent: hhea.ascender().to_i16(), descent: hhea.descender().to_i16(), }) } /// A base's variation axis, and the style its default instance carries. /// /// One axis only, deliberately. A face with two would need a grid of masters /// and a rule for what happens at the corners, and no base in front of us has /// one; refusing says so instead of interpolating a guess. #[derive(Debug, Clone)] pub struct Variation { pub tag: String, pub min: f32, pub default: f32, pub max: f32, /// The subfamily name of the instance sitting at the axis default. /// /// Read off the face rather than assumed. Atkinson Mono's default is `wght` /// 200 and this reads `ExtraLight`, which is the trap the whole variable /// path is written around. pub default_style: String, } /// One location a mark is drawn at. /// /// `peak` is a normalized coordinate and is only ever -1, 0 or 1. `avar` is /// required to map those three to themselves, so a master at an axis end needs /// no `avar` arithmetic and the deltas mean the same thing to every rasteriser. #[derive(Debug, Clone, Copy)] pub struct Master { /// User coordinate, e.g. `wght` 800. pub user: f32, pub peak: f32, } impl Variation { /// The default location, which is what the glyph itself is drawn at. pub fn default_master(&self) -> Master { Master { user: self.default, peak: 0.0, } } /// The locations that get `gvar` deltas: each end of the axis that is not /// already the default. Atkinson Mono defaults to its own minimum, so it /// has one; a face defaulting to the middle of its range has two. pub fn delta_masters(&self) -> Vec { let mut out = Vec::new(); if self.min < self.default { out.push(Master { user: self.min, peak: -1.0, }); } if self.max > self.default { out.push(Master { user: self.max, peak: 1.0, }); } out } } /// The base's axis, or `None` when the base is static. pub fn variation(bytes: &[u8]) -> Result, Error> { use skrifa::MetadataProvider; let font = skrifa::FontRef::new(bytes).map_err(|e| Error::Font(format!("base is unreadable: {e}")))?; let axes = font.axes(); match axes.len() { 0 => return Ok(None), 1 => {} n => { return Err(Error::Font(format!( "the base varies on {n} axes. The pipeline draws a master per axis end, \ which describes one axis and says nothing about the corners of two. \ Decide the master grid before pinning a base like this." ))); } } let axis = axes.get(0).expect("one axis"); let default = axis.default_value(); let default_style = font .named_instances() .iter() .find(|instance| { instance .user_coords() .next() .is_some_and(|c| (c - default).abs() < f32::EPSILON) }) .and_then(|instance| { font.localized_strings(instance.subfamily_name_id()) .english_or_first() .map(|s| s.chars().collect::()) }) .ok_or_else(|| { Error::Font( "the base names no instance at its own axis default, so there is no \ truthful style name for the face a cut produces" .into(), ) })?; Ok(Some(Variation { tag: axis.tag().to_string(), min: axis.min_value(), default, max: axis.max_value(), default_style, })) } /// Measure a variable base at one location on its axis. /// /// The static path reads bounding boxes straight out of `glyf`, which is only /// the default instance. Here the outline is drawn at the location first, so /// `stroke`, `stem` and the band are the base's real measurements at that /// weight rather than at its default one. pub fn measure_at(bytes: &[u8], variation: &Variation, at: Master) -> Result { use skrifa::MetadataProvider; use skrifa::instance::Size; let font = skrifa::FontRef::new(bytes).map_err(|e| Error::Font(format!("base is unreadable: {e}")))?; let tag = skrifa::Tag::new_checked(variation.tag.as_bytes()) .map_err(|_| Error::Font(format!("`{}` is not an axis tag", variation.tag)))?; let location = font.axes().location([(tag, at.user)]); let charmap = font.charmap(); let outlines = font.outline_glyphs(); let measured = |ch: char, what: &str| -> Result { let gid = charmap.map(ch).ok_or_else(|| Error::UnmeasurableBase { missing: ch, what: what.to_owned(), })?; let glyph = outlines.get(gid).ok_or_else(|| Error::UnmeasurableBase { missing: ch, what: what.to_owned(), })?; let mut pen = Extent::default(); glyph .draw( skrifa::outline::DrawSettings::unhinted(Size::unscaled(), &location), &mut pen, ) .map_err(|e| Error::Font(format!("could not draw `{ch}` at {}: {e}", at.user)))?; if pen.empty() { return Err(Error::UnmeasurableBase { missing: ch, what: what.to_owned(), }); } Ok(pen) }; let bar = measured('|', "the vertical stroke weight")?; let hyphen = measured('-', "the horizontal stroke weight")?; let plus = measured('+', "the symbol band")?; let metrics = font.metrics(Size::unscaled(), &location); let advance = font .glyph_metrics(Size::unscaled(), &location) .advance_width(charmap.map('+').expect("`+` was measured above")) .ok_or_else(|| Error::Font("base has no advance for `+`".into()))?; Ok(BaseParams { upem: metrics.units_per_em, advance: advance.round() as u16, cap_height: round_i16(metrics.cap_height.unwrap_or(0.0)), x_height: round_i16(metrics.x_height.unwrap_or(0.0)), stem: bar.width(), stroke: hyphen.height(), band_x0: round_i16(plus.x0), band_x1: round_i16(plus.x1), band_y0: round_i16(plus.y0), band_y1: round_i16(plus.y1), // The cell does not vary with the axis, and must not: a face whose line // height moved with its weight would reflow a terminal on a bold // heading. Taken from the same metrics call as everything else so it // stays one read of the face. ascent: round_i16(metrics.ascent), descent: round_i16(metrics.descent), }) } /// A drawn outline's extent, accumulated straight off the pen. #[derive(Debug, Clone, Copy)] struct Extent { x0: f32, y0: f32, x1: f32, y1: f32, } impl Default for Extent { fn default() -> Self { Self { x0: f32::MAX, y0: f32::MAX, x1: f32::MIN, y1: f32::MIN, } } } impl Extent { fn empty(self) -> bool { self.x0 > self.x1 || self.y0 > self.y1 } fn width(self) -> i16 { round_i16(self.x1 - self.x0) } fn height(self) -> i16 { round_i16(self.y1 - self.y0) } fn add(&mut self, x: f32, y: f32) { self.x0 = self.x0.min(x); self.y0 = self.y0.min(y); self.x1 = self.x1.max(x); self.y1 = self.y1.max(y); } } /// Control points count toward the extent the same way the stored `glyf` /// bounding box counts them, so the two measurement paths agree on a base that /// happens to be readable both ways. impl skrifa::outline::OutlinePen for Extent { fn move_to(&mut self, x: f32, y: f32) { self.add(x, y); } fn line_to(&mut self, x: f32, y: f32) { self.add(x, y); } fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { self.add(cx0, cy0); self.add(x, y); } fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { self.add(cx0, cy0); self.add(cx1, cy1); self.add(x, y); } fn close(&mut self) {} } fn round_i16(value: f32) -> i16 { value .round() .clamp(f32::from(i16::MIN), f32::from(i16::MAX)) as i16 } fn table_err(tag: &'static str) -> impl Fn(read_fonts::ReadError) -> Error { move |e| Error::Font(format!("base has no readable `{tag}` table: {e}")) } /// The Unicode subtable a base maps through, preferring full-repertoire. pub fn best_subtable<'a>(cmap: &Cmap<'a>) -> Option> { let mut best: Option<(u8, CmapSubtable<'a>)> = None; for record in cmap.encoding_records() { use read_fonts::tables::cmap::PlatformId::{Unicode, Windows}; let rank = match (record.platform_id(), record.encoding_id()) { (Windows, 10) => 4, (Unicode, 4 | 6) => 3, (Windows, 1) => 2, (Unicode, 0..=3) => 1, _ => continue, }; let Ok(subtable) = record.subtable(cmap.offset_data()) else { continue; }; if best.as_ref().is_none_or(|(r, _)| rank > *r) { best = Some((rank, subtable)); } } best.map(|(_, s)| s) } fn lookup(cmap: &Cmap<'_>, ch: char) -> Option { cmap.map_codepoint(ch) } /// Every codepoint the base maps, so the rebuilt cmap keeps all of it. pub fn mappings(bytes: &[u8]) -> Result, Error> { let font = FontRef::new(bytes).map_err(|e| Error::Font(format!("base is unreadable: {e}")))?; let cmap = font.cmap().map_err(table_err("cmap"))?; let subtable = best_subtable(&cmap) .ok_or_else(|| Error::Font("base has no Unicode cmap subtable".into()))?; let mut out = BTreeMap::new(); for (codepoint, gid) in subtable.iter() { if gid.to_u32() != 0 && char::from_u32(codepoint).is_some() { out.insert(codepoint, gid); } } Ok(out) } /// `(x_min, y_min, x_max, y_max)` for a glyph, composites included. fn bbox(font: &FontRef<'_>, gid: GlyphId) -> Result<(i16, i16, i16, i16), Error> { let loca = font.loca(None).map_err(table_err("loca"))?; let glyf = font.glyf().map_err(table_err("glyf"))?; let glyph = loca .get_glyf(gid, &glyf) .map_err(|e| Error::Font(format!("unreadable glyph {gid}: {e}")))? .ok_or_else(|| Error::Font(format!("glyph {gid} is empty")))?; Ok(match glyph { read_fonts::tables::glyf::Glyph::Simple(g) => (g.x_min(), g.y_min(), g.x_max(), g.y_max()), read_fonts::tables::glyf::Glyph::Composite(g) => { (g.x_min(), g.y_min(), g.x_max(), g.y_max()) } }) } fn bbox_or_zero(font: &FontRef<'_>, cmap: &Cmap<'_>, ch: char) -> (i16, i16, i16, i16) { lookup(cmap, ch) .and_then(|gid| bbox(font, gid).ok()) .unwrap_or((0, 0, 0, 0)) } /// Fetch the pinned archive if it is not cached, verify it, and read out the /// faces the pin names. /// /// The archive is verified before anything is read out of it, and each face is /// verified again on the way out. Two checks rather than one because they fail /// differently: the first says upstream moved, the second says the pin names a /// path that no longer holds what it did. pub fn load(base: &Base, cache: &Path, offline: bool) -> Result, Error> { if base.is_archive() { return load_from_archive(base, cache, offline); } let mut faces = Vec::new(); for face in &base.faces { let url = face.url.as_deref().unwrap_or_default(); let path = cache.join(face.cache_name(&base.id, &base.version)); let data = cached(url, &path, offline, &face.sha256)?; verify(&data, &face.sha256).map_err(|found| Error::ArchiveChecksum { path, url: url.to_owned(), expected: face.sha256.clone(), found, })?; faces.push(BaseFace { style: face.style.clone(), bytes: data, }); } Ok(faces) } fn load_from_archive(base: &Base, cache: &Path, offline: bool) -> Result, Error> { let url = base.url.as_deref().unwrap_or_default(); let archive = cache.join(format!("{}-{}.zip", base.id, base.version)); let bytes = cached( url, &archive, offline, base.sha256.as_deref().unwrap_or_default(), )?; verify(&bytes, base.sha256.as_deref().unwrap_or_default()).map_err(|found| { Error::ArchiveChecksum { path: archive.clone(), url: url.to_owned(), expected: base.sha256.clone().unwrap_or_default(), found, } })?; let cursor = std::io::Cursor::new(&bytes); let mut zip = zip::ZipArchive::new(cursor) .map_err(|e| Error::Archive(format!("{} is not a readable zip: {e}", archive.display())))?; let mut faces = Vec::new(); for face in &base.faces { let path = face.path.as_deref().unwrap_or_default(); let data = read_entry(&mut zip, path)?; verify(&data, &face.sha256).map_err(|found| Error::FaceChecksum { path: path.to_owned(), expected: face.sha256.clone(), found, })?; faces.push(BaseFace { style: face.style.clone(), bytes: data, }); } Ok(faces) } /// The environment variable naming a mirror of the pinned files. /// /// A base URL. Every pinned file is addressed under it by the sha256 the pin /// already carries, so `QUASI_TYPE_MIRROR=https://example.invalid/bases` looks /// for `https://example.invalid/bases/`. pub const MIRROR_ENV: &str = "QUASI_TYPE_MIRROR"; /// Where a mirror holds the file whose pinned digest is `sha256`. /// /// Content-addressed on purpose, and it is what keeps this from being a new /// trust assumption: the digest is the one the pin already carries and the /// bytes are verified against it either way, so a mirror can serve the pinned /// file or nothing. It cannot serve a different one. That is also why no /// signature or TLS pinning is wanted here: the integrity guarantee was never /// the transport. /// /// Takes the base rather than reading [`MIRROR_ENV`], so the shaping and the /// order [`cached_from`] tries its two sources in are both testable without /// `set_var`, which is unsafe in a threaded test binary and would set the /// mirror under every other test in the run. fn mirror_url(base: &str, sha256: &str) -> Option { let base = base.trim().trim_end_matches('/'); (!base.is_empty()).then(|| format!("{base}/{sha256}")) } /// Read a pinned file from the cache, fetching it first if it is not there. /// /// **The mirror is tried first and upstream is the fallback**, which is the /// order the outage argues for: the pins name raw.githubusercontent.com, which /// rate-limits by IP, and one clean image build asks it four times. A mirror /// that is consulted only after a failure would still be paying for the /// upstream round trip on every build that works. /// /// Upstream stays reachable, and that is deliberate rather than a leftover: a /// machine with no access to our infrastructure still builds, so the mirror /// adds a source rather than moving the project onto one. /// /// The mirror attempt is quiet and gives up quickly ([`Attempt::Mirror`]), /// because it is spent before a request that is going to be made anyway when it /// fails. Retrying a 404 or printing curl's error would make an absent mirror /// cost more than no mirror, which is the one thing this must not do. /// /// `expect` is the pinned digest, and it is checked *here* for a mirrored file /// rather than only by the caller. A mirror serving the wrong bytes has to fall /// through to upstream, not fail the build: without that, an out-of-date mirror /// would be a worse outcome than no mirror at all. The caller verifies again on /// the way out, which is unchanged — these fail differently, the same way the /// archive's two checks do. fn cached(url: &str, path: &Path, offline: bool, expect: &str) -> Result, Error> { let mirror = std::env::var(MIRROR_ENV).ok(); cached_from(mirror.as_deref(), url, path, offline, expect) } /// [`cached`] against an explicit mirror base, which is where the two sources /// are actually ordered. fn cached_from( mirror: Option<&str>, url: &str, path: &Path, offline: bool, expect: &str, ) -> Result, Error> { if !path.exists() { if offline { return Err(Error::Offline { wanted: path.to_path_buf(), url: url.to_owned(), }); } let mirror = mirror .and_then(|base| mirror_url(base, expect)) .filter(|mirror| { fetch_with(mirror, path, Attempt::Mirror).is_ok() && std::fs::read(path).is_ok_and(|bytes| verify(&bytes, expect).is_ok()) }); if mirror.is_none() { let _ = std::fs::remove_file(path); fetch(url, path)?; } } std::fs::read(path).map_err(|e| Error::Io(path.to_path_buf(), e)) } /// The upstream licence text, which travels with every build the OFL requires /// it to. pub fn license_text(base: &Base, cache: &Path, offline: bool) -> Result, Error> { if let Some(url) = &base.license_url { let path = cache.join(format!("{}-{}-LICENSE.txt", base.id, base.version)); let data = cached( url, &path, offline, base.license_sha256.as_deref().unwrap_or_default(), )?; if let Some(expected) = &base.license_sha256 { verify(&data, expected).map_err(|found| Error::FaceChecksum { path: url.clone(), expected: expected.clone(), found, })?; } return Ok(data); } let archive = cache.join(format!("{}-{}.zip", base.id, base.version)); let bytes = std::fs::read(&archive).map_err(|e| Error::Io(archive.clone(), e))?; let cursor = std::io::Cursor::new(&bytes); let mut zip = zip::ZipArchive::new(cursor) .map_err(|e| Error::Archive(format!("{} is not a readable zip: {e}", archive.display())))?; read_entry(&mut zip, base.license_path.as_deref().unwrap_or_default()) } fn read_entry( zip: &mut zip::ZipArchive, path: &str, ) -> Result, Error> { let mut entry = zip .by_name(path) .map_err(|_| Error::Archive(format!("the pin names `{path}`, which the archive lacks")))?; let mut data = Vec::new(); entry .read_to_end(&mut data) .map_err(|e| Error::Archive(format!("`{path}` is unreadable: {e}")))?; Ok(data) } fn verify(bytes: &[u8], expected: &str) -> Result<(), String> { let found = hex(&Sha256::digest(bytes)); if found == expected { Ok(()) } else { Err(found) } } pub fn hex(bytes: &[u8]) -> String { use std::fmt::Write; bytes.iter().fold(String::new(), |mut out, b| { let _ = write!(out, "{b:02x}"); out }) } /// Fetching shells out to curl rather than linking an HTTP client. /// /// This is a build-time tool that downloads one pinned URL. A TLS stack would /// be the largest thing in the dependency tree and would drag in the crypto /// provider question for no gain: the integrity guarantee here is the sha256 /// below, not the transport. /// /// **It retries, and that is not defensive coding.** The pins point at /// raw.githubusercontent.com, which rate-limits by IP, and a machine that /// builds Alloy asks it four times in one build: the image cuts two slots, and /// shop's build script cuts a third face into its own cache. /// /// This bounds the damage rather than fixing the cause. The cause is that four /// fetches of the same two files leave the machine, and the answer is a mirror /// on infrastructure we own — the same conversation as mirroring the Fedora /// base images (GO alloy `ebf30337`, and the fragility itself is alloy /// `3d41d15a`). [`MIRROR_ENV`] is this end of that: point it at a host we run /// and the pinned files come from there, with upstream still the fallback. fn fetch(url: &str, dest: &Path) -> Result<(), Error> { fetch_with(url, dest, Attempt::Upstream) } /// Which of the two fetches this is, because they want opposite curl flags. /// /// Split out after the first build against a mirror that was not there yet: the /// mirror attempt inherited [`Attempt::Upstream`]'s flags and both of them were /// wrong for it. `--retry-all-errors` retries a 404, so four files the mirror /// did not have cost five retries and four delays each before the build reached /// the url that would answer; and `--show-error` printed `curl: (22) ... 404` /// four times, which reads as a build failing rather than as a fallback working. #[derive(Clone, Copy)] enum Attempt { /// The pinned url. Retry hard: this is the one that has to work, and the /// failure it is retrying through is somebody else's rate limiter. Upstream, /// A mirror, which is allowed not to have the file. Ask once, say nothing, /// and give up quickly, because every second here is spent before the /// request that was always going to be made anyway. Mirror, } fn fetch_with(url: &str, dest: &Path, attempt: Attempt) -> Result<(), Error> { if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_path_buf(), e))?; } let partial = dest.with_extension("part"); let mut curl = Command::new("curl"); curl.args(["--fail", "--location", "--silent"]); match attempt { Attempt::Upstream => { curl.args([ "--show-error", "--retry", "5", "--retry-delay", "2", "--retry-all-errors", ]); } Attempt::Mirror => { curl.args(["--connect-timeout", "10", "--max-time", "120"]); } } let status = curl .arg("--output") .arg(&partial) .arg(url) .status() .map_err(|e| Error::Fetch(format!("could not run curl: {e}")))?; if !status.success() { let _ = std::fs::remove_file(&partial); return Err(Error::Fetch(format!( "curl failed on {url} ({status}), after retrying. A 429 here is this \ machine's IP being rate-limited by the host the base is pinned at, and \ the fix is a mirror rather than another retry." ))); } std::fs::rename(&partial, dest).map_err(|e| Error::Io(dest.to_path_buf(), e))?; Ok(()) } pub fn cache_dir(root: &Path) -> PathBuf { root.join("bases").join("cache") } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::{Arc, Mutex}; /// An HTTP server that answers a fixed table of paths and records every /// path it was asked for. /// /// Real sockets and the real `curl` invocation, because what is under test /// is which host the bytes came from, and a stubbed fetch would be a test /// of the stub. The table is exact: a path the test did not name gets a /// 404, so a mirror URL shaped wrongly reaches the same fallback a mirror /// that lacks the file does. struct Stub { base: String, asked: Arc>>, } impl Stub { fn new(routes: &[(&str, &[u8])]) -> Self { let listener = TcpListener::bind("127.0.0.1:0").expect("binding a stub server"); let port = listener.local_addr().expect("stub address").port(); let asked: Arc>> = Arc::new(Mutex::new(Vec::new())); let table: HashMap> = routes .iter() .map(|(path, body)| ((*path).to_owned(), body.to_vec())) .collect(); let log = Arc::clone(&asked); std::thread::spawn(move || { for stream in listener.incoming() { let Ok(stream) = stream else { continue }; Self::answer(stream, &table, &log); } }); Self { base: format!("http://127.0.0.1:{port}/bases"), asked, } } fn answer( mut stream: TcpStream, table: &HashMap>, log: &Mutex>, ) { let mut request = String::new(); let mut reader = BufReader::new(stream.try_clone().expect("cloning the socket")); loop { let mut line = String::new(); if reader.read_line(&mut line).unwrap_or(0) == 0 || line.trim().is_empty() { break; } if request.is_empty() { request = line; } } let path = request.split_whitespace().nth(1).unwrap_or("").to_owned(); log.lock().expect("the request log").push(path.clone()); let response = match table.get(&path) { Some(body) => { let mut head = format!( "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len(), ) .into_bytes(); head.extend_from_slice(body); head } None => b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" .to_vec(), }; let _ = stream.write_all(&response); let _ = stream.flush(); } /// The URL a pin would carry, for the server standing in for upstream. fn url(&self, path: &str) -> String { format!("{}{path}", self.base.trim_end_matches("/bases")) } fn asked(&self) -> Vec { self.asked.lock().expect("the request log").clone() } } /// A cache directory that removes itself, so a failing assertion leaves no /// tree behind. struct Cache(PathBuf); impl Cache { fn new(label: &str) -> Self { let dir = std::env::temp_dir() .join(format!("quasi-type-mirror-{label}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("cache dir"); Self(dir) } fn file(&self) -> PathBuf { self.0.join("atkinson-mono-2.001-LICENSE.txt") } } impl Drop for Cache { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } // The mirror is addressed by the digest the pin already carries, so a // mirror can serve the pinned file or nothing at all. #[test] fn a_mirrored_file_is_addressed_by_its_pinned_digest() { assert_eq!( mirror_url("https://example.invalid/bases", "abc123").as_deref(), Some("https://example.invalid/bases/abc123"), ); } // A trailing slash in the variable is the obvious way to write it and must // not produce a double slash, which some hosts serve and some 404. #[test] fn a_trailing_slash_on_the_base_is_absorbed() { assert_eq!( mirror_url("https://example.invalid/bases/ ", "abc123").as_deref(), Some("https://example.invalid/bases/abc123"), ); } // Set-but-empty means no mirror, not an empty base URL: an exported // variable someone cleared reads that way, and building a URL out of it // would send every fetch at `/abc123` on nothing. #[test] fn an_empty_base_is_no_mirror() { assert!(mirror_url("", "abc123").is_none()); assert!(mirror_url(" ", "abc123").is_none()); } // The mirror is the first source and upstream is not asked at all when it // answers. Ordering is the whole point of the mirror: upstream rate-limits // by IP, and a mirror consulted only after a failure would still pay for // the upstream round trip on every build that works. Reverse the two and // the upstream server here records a request. #[test] fn a_mirrored_file_is_taken_from_the_mirror_and_upstream_is_not_asked() { let bytes = b"the pinned licence text"; let digest = hex(&Sha256::digest(bytes)); let mirror = Stub::new(&[(&format!("/bases/{digest}"), bytes)]); let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); let cache = Cache::new("hit"); let got = cached_from( Some(&format!("{}/", mirror.base)), &upstream.url("/mono/OFL.txt"), &cache.file(), false, &digest, ) .expect("the mirrored file"); assert_eq!(got, bytes, "the bytes are not the ones the mirror served"); assert_eq!( mirror.asked(), vec![format!("/bases/{digest}")], "the mirror was asked for something other than the pinned digest", ); assert!( upstream.asked().is_empty(), "upstream was asked for a file the mirror had: {:?}", upstream.asked(), ); assert_eq!( std::fs::read(cache.file()).expect("the cached file"), bytes, "the mirrored bytes did not land in the cache", ); } // A mirror serving the wrong bytes falls through to upstream rather than // failing the build, or an out-of-date mirror would be worse than no // mirror. The digest is checked here and again by the caller. #[test] fn a_mirror_serving_the_wrong_bytes_falls_through_to_upstream() { let bytes = b"the pinned licence text"; let digest = hex(&Sha256::digest(bytes)); let mirror = Stub::new(&[(&format!("/bases/{digest}"), b"an older licence")]); let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); let cache = Cache::new("wrong-bytes"); let got = cached_from( Some(&mirror.base), &upstream.url("/mono/OFL.txt"), &cache.file(), false, &digest, ) .expect("the upstream file"); assert_eq!(got, bytes, "the wrong bytes were kept"); assert_eq!( upstream.asked(), vec!["/mono/OFL.txt".to_owned()], "the fallback did not reach upstream", ); assert_eq!( std::fs::read(cache.file()).expect("the cached file"), bytes, "the mirror's bytes were left in the cache for the caller to verify", ); } // A mirror that does not hold the file is a slower build and not a broken // one, which is what lets the variable default to a host that may be down. #[test] fn a_mirror_without_the_file_falls_through_to_upstream() { let bytes = b"the pinned licence text"; let digest = hex(&Sha256::digest(bytes)); let mirror = Stub::new(&[]); let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); let cache = Cache::new("miss"); let got = cached_from( Some(&mirror.base), &upstream.url("/mono/OFL.txt"), &cache.file(), false, &digest, ) .expect("the upstream file"); assert_eq!(got, bytes); assert_eq!( mirror.asked(), vec![format!("/bases/{digest}")], "the mirror was not asked first", ); assert_eq!(upstream.asked(), vec!["/mono/OFL.txt".to_owned()]); } // No mirror named is no request, which is what `QUASI_TYPE_MIRROR=` in a // build is asking for. #[test] fn no_mirror_named_asks_only_upstream() { let bytes = b"the pinned licence text"; let digest = hex(&Sha256::digest(bytes)); let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); let cache = Cache::new("no-mirror"); let got = cached_from( None, &upstream.url("/mono/OFL.txt"), &cache.file(), false, &digest, ) .expect("the upstream file"); assert_eq!(got, bytes); assert_eq!(upstream.asked(), vec!["/mono/OFL.txt".to_owned()]); } }