xtask/src/medium.rs
//! **What a composed installer medium CAN DO, written beside it:
//! `.iso-out/<variant>.medium.json`.**
//!
//! # Why this file exists
//!
//! Until korp-installer 0.1.14 (edda `f4b38bb`) the installer could not read
//! the CD it booted from: the kernel donor had no ATA/SCSI at all (`nuked`
//! vetoes them), and korp-installer probed virtio alone. So every UpCloud
//! re-image uploaded the same ~43 MB medium TWICE — once as the cdrom the
//! firmware boots, once as a virtio disk the installer reads its payload from —
//! and waited for two syncs (mock behaviour 25, ~4 min per re-image).
//!
//! 0.1.14 asks optical drives first, and tunnr `939dbe3` gives the donor
//! ATA + PIIX (i440fx IDE, UpCloud), AHCI (q35) and SR. A medium built from
//! BOTH can be uploaded once. A medium built from either half alone cannot —
//! T16's negative control: a no-ATA medium uploaded only as a cdrom sits in
//! `maintenance` for ever.
//!
//! The re-image (`deploy/upcloud`) must therefore know which kind of medium it
//! holds, and it must learn it from THE MEDIUM, not from a flag an operator has
//! to remember. This is where the medium says so: written at compose time, from
//! the facts that decide it, bound to the ISO by its sha256.
//!
//! # What is judged, and from what
//!
//! * `korp_installer` — the version the APPLIANCE's lock resolved
//! (`installer_pin::PLACES[0]`), the installer this medium carries.
//! * the kernel — `--build kernel` compiles the donor from the sibling `tunnr`
//! checkout; its `variants/kernel/tunnr-hardening.config` is read for the CD
//! block `939dbe3` added. A kernel NAMED from elsewhere, or carried off this
//! box, has no config this verb can read: it is recorded as UNKNOWN, and an
//! unknown kernel never reads its own CD.
//!
//! `cd_driver` is true only when both halves are proven. Everything else —
//! including a missing file — means "upload it twice", which is slower and
//! always safe.
use std::path::{Path, PathBuf};
/// The first korp-installer that asks optical drives first.
pub const FIRST_CD_READING_INSTALLER: (u64, u64, u64) = (0, 1, 14);
/// The kconfig lines tunnr `939dbe3` added to the donor closure. ALL of them,
/// because the two machines this medium meets differ: PIIX is UpCloud's IDE
/// (i440fx), AHCI is the mock's q35, and SR is the cdrom driver on both.
pub const CD_BLOCK: &[&str] = &[
"CONFIG_ATA=y",
"CONFIG_ATA_PIIX=y",
"CONFIG_SATA_AHCI=y",
"CONFIG_SCSI=y",
"CONFIG_BLK_DEV_SR=y",
];
/// Where the donor's config fragment lives in a tunnr checkout.
pub const DONOR_CONFIG: &str = "variants/kernel/tunnr-hardening.config";
/// Where the medium's record lives, beside the ISO.
pub fn record_path(iso: &Path) -> PathBuf {
iso.with_extension("medium.json")
}
/// `0.1.14` → `(0, 1, 14)`; anything else → `None`.
pub fn semver(v: &str) -> Option<(u64, u64, u64)> {
let mut it = v.trim().split('.').map(|p| p.parse::<u64>().ok());
Some((it.next()??, it.next()??, it.next()??))
}
/// The kconfig lines of [`CD_BLOCK`] a fragment does NOT enable. Empty = the
/// donor has the CD block. A commented `# CONFIG_ATA is not set` is not a yes.
pub fn missing_cd_lines(fragment: &str) -> Vec<&'static str> {
CD_BLOCK
.iter()
.copied()
.filter(|want| !fragment.lines().any(|l| l.trim() == *want))
.collect()
}
/// **The decision, pure.** `Ok(why)` when the medium reads its own CD; `Err(why)`
/// when it must be uploaded twice. Both carry the sentence the record keeps.
pub fn cd_driver(
korp_installer: Option<&str>,
donor_fragment: Option<&str>,
) -> Result<String, String> {
let v =
korp_installer.ok_or("the installer's version could not be read off the appliance lock")?;
let parsed = semver(v).ok_or_else(|| format!("korp-installer {v:?} is not a version"))?;
if parsed < FIRST_CD_READING_INSTALLER {
return Err(format!(
"korp-installer {v} predates 0.1.14 and probes virtio alone"
));
}
let frag = donor_fragment.ok_or(
"the kernel was not compiled from the tunnr donor here (named, or carried off this box), \
so its config cannot be read — an unknown kernel is assumed to have no CD driver",
)?;
let missing = missing_cd_lines(frag);
if !missing.is_empty() {
return Err(format!(
"the tunnr donor lacks the CD block (tunnr 939dbe3): {} not enabled",
missing.join(", ")
));
}
Ok(format!(
"korp-installer {v} reads optical drives first, and the donor kernel carries {}",
CD_BLOCK.join(" ")
))
}
fn esc(s: &str) -> String {
let mut o = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => o.push_str("\\\""),
'\\' => o.push_str("\\\\"),
'\n' => o.push_str("\\n"),
c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)),
c => o.push(c),
}
}
o
}
/// The record's JSON.
pub fn record_json(
iso_sha256: &str,
korp_installer: Option<&str>,
kernel: &str,
tunnr_head: Option<&str>,
verdict: &Result<String, String>,
) -> String {
let opt = |v: Option<&str>| {
v.map(|s| format!("\"{}\"", esc(s)))
.unwrap_or_else(|| "null".into())
};
let (cd, why) = match verdict {
Ok(w) => (true, w.as_str()),
Err(w) => (false, w.as_str()),
};
format!(
"{{\n \"iso_sha256\": \"{}\",\n \"korp_installer\": {},\n \"kernel\": \"{}\",\n \"tunnr_head\": {},\n \
\"cd_driver\": {cd},\n \"why\": \"{}\",\n \"note\": \"Written by `cargo xtask iso-installer` beside the ISO whose sha256 it names. \
deploy/upcloud reads it to decide whether the medium is uploaded once (cdrom only) or twice (cdrom + virtio).\"\n}}\n",
esc(iso_sha256),
opt(korp_installer),
esc(kernel),
opt(tunnr_head),
esc(why)
)
}
/// `HEAD` of a checkout, read off `.git` without a git binary. `None` when it
/// cannot be read — it is recorded for the reader, not decided on.
pub fn head_of(checkout: &Path) -> Option<String> {
let git = checkout.join(".git");
let gitdir = if git.is_file() {
let t = std::fs::read_to_string(&git).ok()?;
PathBuf::from(t.trim().strip_prefix("gitdir:")?.trim())
} else {
git
};
let head = std::fs::read_to_string(gitdir.join("HEAD")).ok()?;
let head = head.trim();
match head.strip_prefix("ref:") {
None => Some(head.to_string()),
Some(r) => {
let r = r.trim();
if let Ok(s) = std::fs::read_to_string(gitdir.join(r)) {
return Some(s.trim().to_string());
}
let common = std::fs::read_to_string(gitdir.join("commondir"))
.ok()
.map(|c| gitdir.join(c.trim()))
.unwrap_or(gitdir.clone());
if let Ok(s) = std::fs::read_to_string(common.join(r)) {
return Some(s.trim().to_string());
}
let packed = std::fs::read_to_string(common.join("packed-refs")).ok()?;
packed
.lines()
.find_map(|l| l.strip_suffix(r).map(|h| h.trim().to_string()))
}
}
}
// ── the installer the composer ACTUALLY builds (T15 TD8/TD11, 2026-09-22) ──
//
// The medium's installer is not the one gunnar's lock pins. The composer is
// dwarves, and dwarves links korp-installer by PATH (feature `appliance-iso`,
// `crates/dwarves/Cargo.toml`), so the PID-1 on the medium is built from THAT
// checkout. MEASURED 2026-09-22: pin 0.1.15, record 0.1.15, sibling at f4b38bb
// = 0.1.14, and the twin fell into the 0.1.14 disk election and hung on
// SeaBIOS. So the record names what was built, and a mismatch with the pin is
// refused before the compose starts.
/// The declared exception, spelled as korp-installer's composer spells it
/// (`korp_installer::installer_source::UNPINNED_ENV`): compose from a source
/// that is not the pin, for a proof of an unreleased installer. Recorded.
pub const UNPINNED_ENV: &str = "KORP_INSTALLER_UNPINNED";
/// Where dwarves declares its korp-installer, relative to the dwarves root.
pub const DWARVES_MANIFEST: &str = "crates/dwarves/Cargo.toml";
/// The korp-installer the composer builds: its version, and where it comes from.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Built {
pub version: String,
/// The source directory for a path-dep, `None` for a registry dep.
pub dir: Option<PathBuf>,
/// HEAD of the checkout holding `dir`, when it could be read.
pub revision: Option<String>,
}
/// The `path = "…"` of dwarves' `korp-installer = { … }` line, if any.
pub fn path_dep(dwarves_manifest: &str) -> Option<String> {
dwarves_manifest.lines().find_map(|l| {
let l = l.trim();
if !l.starts_with("korp-installer") || l.starts_with('#') {
return None;
}
let rest = &l[l.find("path")? + 4..];
let rest = &rest[rest.find('"')? + 1..];
Some(rest[..rest.find('"')?].to_string())
})
}
/// The `[package] version` of a manifest.
pub fn package_version(manifest: &str) -> Option<String> {
let mut in_package = false;
for l in manifest.lines().map(str::trim) {
if l.starts_with('[') {
in_package = l == "[package]";
continue;
}
if in_package {
if let Some(v) = l.strip_prefix("version") {
let v = v.trim_start().strip_prefix('=')?.trim();
return Some(v.trim_matches('"').to_string());
}
}
}
None
}
/// The nearest enclosing git checkout's HEAD.
fn head_above(dir: &Path) -> Option<String> {
let mut d = dir.to_path_buf();
loop {
if d.join(".git").exists() {
return head_of(&d);
}
if !d.pop() {
return None;
}
}
}
/// **Which korp-installer dwarves at `dwarves_root` builds into a medium.** A
/// path-dep is read off the sibling's own manifest; a registry dep off
/// dwarves' lock.
pub fn built_installer(dwarves_root: &Path) -> Result<Built, String> {
let manifest_path = dwarves_root.join(DWARVES_MANIFEST);
let manifest = std::fs::read_to_string(&manifest_path)
.map_err(|e| format!("{}: {e}", manifest_path.display()))?;
match path_dep(&manifest) {
Some(rel) => {
let dir = manifest_path.parent().unwrap_or(dwarves_root).join(rel);
let dir = dir.canonicalize().unwrap_or(dir);
let m = dir.join("Cargo.toml");
let text = std::fs::read_to_string(&m).map_err(|e| {
format!(
"{} declares korp-installer at {}, and {e}",
manifest_path.display(),
m.display()
)
})?;
let version = package_version(&text)
.ok_or_else(|| format!("{}: no [package] version", m.display()))?;
Ok(Built {
version,
revision: head_above(&dir),
dir: Some(dir),
})
}
None => {
let lock = dwarves_root.join("Cargo.lock");
let text =
std::fs::read_to_string(&lock).map_err(|e| format!("{}: {e}", lock.display()))?;
let version = crate::installer_pin::resolved_version(&text)
.ok_or_else(|| format!("{} resolves no korp-installer", lock.display()))?;
Ok(Built {
version,
dir: None,
revision: None,
})
}
}
}
/// **Hold the built installer to the appliance's pin.** `Ok(verdict word)`:
/// `match`, or `unpinned` under the exception. `Err` is the refusal.
pub fn check_built_against_pin(
built: &Built,
pin: Option<&str>,
lock: &str,
unpinned_granted: bool,
) -> Result<&'static str, String> {
let Some(pin) = pin else {
return Err(format!("REFUSED [installer-source-not-the-pin] {lock} resolves no korp-installer, so the medium's installer cannot be held to a pin"));
};
if pin == built.version {
return Ok("match");
}
if unpinned_granted {
return Ok("unpinned");
}
Err(format!(
"REFUSED [installer-source-not-the-pin] the composer would build the medium's installer from \
korp-installer {} at {} [{}], but {lock} pins korp-installer {pin}. The record would name the pin \
while the medium carries the other (T15 TD8, 2026-09-22: pin 0.1.15, sibling f4b38bb = 0.1.14, the \
twin booted the wrong disk). Point the edda sibling at the pinned release, bump the pin, or set \
{UNPINNED_ENV}=1 for a proof of an unreleased installer (recorded as unpinned).",
built.version,
built.dir.as_ref().map(|d| d.display().to_string()).unwrap_or_else(|| "the registry (dwarves' lock)".into()),
built.revision.as_deref().unwrap_or("revision unknown"),
))
}
/// The check `iso-installer` runs BEFORE it spawns the compose.
pub fn preflight(gunnar_root: &Path, dwarves_root: &Path) -> Result<(Built, &'static str), String> {
let (_, lock) = crate::installer_pin::PLACES[0];
let pin = std::fs::read_to_string(gunnar_root.join(lock))
.ok()
.and_then(|l| crate::installer_pin::resolved_version(&l));
let built = built_installer(dwarves_root)?;
let granted = std::env::var(UNPINNED_ENV).is_ok_and(|v| !v.trim().is_empty());
let verdict = check_built_against_pin(&built, pin.as_deref(), lock, granted)?;
Ok((built, verdict))
}
/// **Write the record for a freshly composed ISO.** Impure: reads the lock,
/// the donor checkout and the ISO's bytes. `korp_installer` is the version
/// the composer BUILT (see [`preflight`]), never the pin.
pub fn write(
gunnar_root: &Path,
iso: &Path,
kernel_from_source: bool,
kernel_describe: &str,
built: &Built,
verdict: &str,
) -> Result<PathBuf, String> {
let sha = nornir_build_thing::kernel::sha256_file_hex(iso)
.map_err(|e| format!("{}: {e}", iso.display()))?;
let (_, lock) = crate::installer_pin::PLACES[0];
let pin = std::fs::read_to_string(gunnar_root.join(lock))
.ok()
.and_then(|l| crate::installer_pin::resolved_version(&l));
let korp = Some(built.version.clone());
let tunnr = gunnar_root.parent().map(|p| p.join("tunnr"));
let fragment = if kernel_from_source {
tunnr
.as_ref()
.and_then(|t| std::fs::read_to_string(t.join(DONOR_CONFIG)).ok())
} else {
None
};
let head = if kernel_from_source {
tunnr.as_ref().and_then(|t| head_of(t))
} else {
None
};
let cd = cd_driver(korp.as_deref(), fragment.as_deref());
// The record is written FIRST — it is evidence, and evidence of a medium
// that cannot install is exactly what the next reader needs — and then the
// compose REFUSES. See `refuse_unless_cd`.
let path = record_path(iso);
let json = with_installer_source(
record_json(&sha, korp.as_deref(), kernel_describe, head.as_deref(), &cd),
built,
pin.as_deref(),
verdict,
);
std::fs::write(&path, json).map_err(|e| format!("{}: {e}", path.display()))?;
refuse_unless_cd(&cd, iso)?;
Ok(path)
}
/// **A medium that cannot read its own CD is not composed.**
///
/// OWNER'S ORDER 2026-09-23: "refuse when cd_driver is false". Until then the
/// verdict was a FIELD in the record — `cd_driver: false` with the reason —
/// and the compose went on. The ladder uploads a medium ONCE and installs it
/// from the tray; a kernel without the CD block boots, finds no ISO-9660
/// volume, prints FAILED and parks. MEASURED 2026-09-23 22:53: a medium whose
/// record said `cd_driver: false — the tunnr donor lacks the CD block` was
/// composed, uploaded, attached and booted, and the ladder waited 1200 s on a
/// box that had failed at second ten. The record knew. Nothing refused.
///
/// Pure over the verdict, so the refusal is testable without an ISO.
pub fn refuse_unless_cd(cd: &Result<String, String>, iso: &Path) -> Result<(), String> {
match cd {
Ok(_) => Ok(()),
Err(why) => Err(format!(
"REFUSED [medium-no-cd-driver] {} cannot read its own CD: {why}. The ladder uploads a medium \
once and installs it from the tray, so this medium would boot, find no ISO-9660 volume and \
park on FAILED. The record beside it says so (cd_driver: false). Rebuild the donor kernel \
with the CD block (tunnr 939dbe3 or later: CONFIG_ATA=y CONFIG_ATA_PIIX=y CONFIG_SATA_AHCI=y \
CONFIG_SCSI=y CONFIG_BLK_DEV_SR=y) and compose again; the stale kernel in .kernel-out/ is \
what a pin check that looks for EXISTENCE hands back.",
iso.display()
)),
}
}
/// The record plus where its installer came from: the source, its revision,
/// the pin it was held to and the verdict. Appended as the record's last
/// fields, so every existing reader keeps its fields where they were.
pub fn with_installer_source(
record: String,
built: &Built,
pin: Option<&str>,
verdict: &str,
) -> String {
let opt = |v: Option<&str>| {
v.map(|s| format!("\"{}\"", esc(s)))
.unwrap_or_else(|| "null".into())
};
let dir = built.dir.as_ref().map(|d| d.display().to_string());
let tail = format!(
",\n \"korp_installer_source\": {},\n \"korp_installer_revision\": {},\n \"korp_installer_pin\": {},\n \"korp_installer_verdict\": \"{}\"\n}}\n",
opt(dir.as_deref().or(Some("registry (dwarves' Cargo.lock)"))),
opt(built.revision.as_deref()),
opt(pin),
esc(verdict)
);
let body = record
.trim_end()
.strip_suffix('}')
.unwrap_or(&record)
.trim_end()
.to_string();
body + &tail
}
#[cfg(test)]
mod tests {
use super::*;
const WITH_BLOCK: &str = "# the CD block\nCONFIG_ATA=y\nCONFIG_ATA_SFF=y\nCONFIG_ATA_BMDMA=y\nCONFIG_ATA_PIIX=y\nCONFIG_SATA_AHCI=y\nCONFIG_SCSI=y\nCONFIG_BLK_DEV_SR=y\n";
const WITHOUT: &str =
"# CONFIG_ATA is not set\n# CONFIG_SCSI is not set\nCONFIG_VIRTIO_BLK=y\n";
#[test]
fn both_halves_proven_reads_its_own_cd() {
let v = cd_driver(Some("0.1.14"), Some(WITH_BLOCK)).unwrap();
assert!(
v.contains("0.1.14") && v.contains("CONFIG_BLK_DEV_SR=y"),
"{v}"
);
// OWNER 2026-09-23: a false verdict REFUSES the compose, by name, and
// names the kernel options; a true one lets it through.
let iso = std::path::Path::new("/x/gunnar-installer-metered.iso");
assert!(refuse_unless_cd(&Ok(v.clone()), iso).is_ok());
let e =
refuse_unless_cd(&cd_driver(Some("0.1.19"), Some("CONFIG_ATA=y\n")), iso).unwrap_err();
assert!(e.starts_with("REFUSED [medium-no-cd-driver]"), "{e}");
assert!(
e.contains("CONFIG_BLK_DEV_SR=y")
&& e.contains("939dbe3")
&& e.contains("gunnar-installer-metered.iso"),
"{e}"
);
let e = refuse_unless_cd(&cd_driver(Some("0.1.19"), None), iso).unwrap_err();
assert!(
e.contains("unknown kernel is assumed to have no CD driver"),
"an unproven kernel refuses too: {e}"
);
assert!(cd_driver(Some("0.2.0"), Some(WITH_BLOCK)).is_ok());
}
/// Each half alone is not enough — T16's negative control is the second.
#[test]
fn either_half_missing_means_two_uploads() {
assert!(cd_driver(Some("0.1.13"), Some(WITH_BLOCK))
.unwrap_err()
.contains("predates 0.1.14"));
let e = cd_driver(Some("0.1.14"), Some(WITHOUT)).unwrap_err();
assert!(
e.contains("939dbe3") && e.contains("CONFIG_ATA_PIIX=y"),
"{e}"
);
assert!(cd_driver(Some("0.1.14"), None)
.unwrap_err()
.contains("unknown kernel"));
assert!(cd_driver(None, Some(WITH_BLOCK)).is_err());
assert!(cd_driver(Some("zero"), Some(WITH_BLOCK)).is_err());
// One line of the block missing is the whole block missing.
let partial = WITH_BLOCK.replace("CONFIG_SATA_AHCI=y\n", "");
assert_eq!(missing_cd_lines(&partial), vec!["CONFIG_SATA_AHCI=y"]);
}
fn built(v: &str) -> Built {
Built {
version: v.into(),
dir: Some("/t/edda/crates/korp-installer".into()),
revision: Some("f4b38bb738320afde".into()),
}
}
/// The TD8 shape: pin 0.1.15, sibling 0.1.14 at f4b38bb. Refused, naming both.
#[test]
fn a_sibling_that_is_not_the_pin_is_refused_before_the_compose() {
let e = check_built_against_pin(&built("0.1.14"), Some("0.1.15"), "Cargo.lock", false)
.unwrap_err();
assert!(e.contains("REFUSED [installer-source-not-the-pin]"), "{e}");
assert!(
e.contains("korp-installer 0.1.14") && e.contains("pins korp-installer 0.1.15"),
"{e}"
);
assert!(
e.contains("/t/edda/crates/korp-installer") && e.contains("f4b38bb"),
"{e}"
);
assert!(e.contains(UNPINNED_ENV), "{e}");
assert_eq!(
check_built_against_pin(&built("0.1.15"), Some("0.1.15"), "Cargo.lock", false),
Ok("match")
);
assert_eq!(
check_built_against_pin(&built("0.1.18"), Some("0.1.17"), "Cargo.lock", true),
Ok("unpinned")
);
assert!(
check_built_against_pin(&built("0.1.17"), None, "Cargo.lock", true).is_err(),
"no pin is never a pass"
);
}
/// dwarves' own declaration is what is read, and the sibling's manifest
/// is what is judged — the two ends TD11 compares.
#[test]
fn the_built_installer_is_read_off_dwarves_path_dep_and_the_siblings_manifest() {
struct D(std::path::PathBuf);
impl D {
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for D {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
let d = D(std::env::temp_dir().join(format!("gunnar-medium-built-{}", std::process::id())));
let dw = d.path().join("dwarves/crates/dwarves");
let ki = d.path().join("edda/crates/korp-installer");
std::fs::create_dir_all(&dw).unwrap();
std::fs::create_dir_all(&ki).unwrap();
std::fs::write(
dw.join("Cargo.toml"),
"[features]\nappliance-iso = [\"dep:korp-installer\"]\n[dependencies]\n# korp-installer = { path = \"nowhere\" }\nkorp-installer = { version = \"0.1.0\", path = \"../../../edda/crates/korp-installer\", default-features = false, optional = true }\n",
)
.unwrap();
std::fs::write(ki.join("Cargo.toml"), "[package]\nname = \"korp-installer\"\nversion = \"0.1.14\"\n[dependencies]\nfoo = { version = \"9.9.9\" }\n").unwrap();
let b = built_installer(&d.path().join("dwarves")).unwrap();
assert_eq!(b.version, "0.1.14");
assert_eq!(b.dir.as_deref(), Some(ki.canonicalize().unwrap().as_path()));
assert_eq!(
package_version(
"[workspace]\nversion = \"1\"\n[package]\nname = \"x\"\nversion = \"0.2.0\"\n"
)
.as_deref(),
Some("0.2.0")
);
}
/// The record names what was built, and says where it came from.
#[test]
fn the_record_names_the_built_installer_not_the_pin() {
let base = record_json(
"ab",
Some("0.1.14"),
"from source",
None,
&cd_driver(Some("0.1.14"), Some(WITH_BLOCK)),
);
let j = with_installer_source(base, &built("0.1.14"), Some("0.1.15"), "unpinned");
let v = crate::json::Json::parse(&j).expect("valid JSON");
assert_eq!(
v.get("korp_installer").and_then(|x| x.str()),
Some("0.1.14")
);
assert_eq!(
v.get("korp_installer_pin").and_then(|x| x.str()),
Some("0.1.15")
);
assert_eq!(
v.get("korp_installer_source").and_then(|x| x.str()),
Some("/t/edda/crates/korp-installer")
);
assert_eq!(
v.get("korp_installer_revision").and_then(|x| x.str()),
Some("f4b38bb738320afde")
);
assert_eq!(
v.get("korp_installer_verdict").and_then(|x| x.str()),
Some("unpinned")
);
// TD11 reads `"korp_installer": ` off a line; it still finds the built version.
assert!(
j.lines()
.any(|l| l.trim() == "\"korp_installer\": \"0.1.14\","),
"{j}"
);
}
#[test]
fn the_record_is_json_beside_the_iso() {
assert_eq!(
record_path(Path::new(".iso-out/gunnar-installer-metered.iso")),
PathBuf::from(".iso-out/gunnar-installer-metered.medium.json")
);
let j = record_json(
"ab",
Some("0.1.14"),
"from source",
Some("3f7c2ff"),
&cd_driver(Some("0.1.14"), Some(WITH_BLOCK)),
);
let v = crate::json::Json::parse(&j).expect("valid JSON");
assert_eq!(v.get("iso_sha256").and_then(|x| x.str()), Some("ab"));
assert_eq!(v.get("cd_driver"), Some(&crate::json::Json::Bool(true)));
let j = record_json("ab", None, "named \"x\"", None, &cd_driver(None, None));
assert_eq!(
crate::json::Json::parse(&j).unwrap().get("cd_driver"),
Some(&crate::json::Json::Bool(false))
);
}
}