//! iPXE script generator. //! //! ## Menu hierarchy (per Phase 2 spec) //! //! ```text //! Top level: //! Default //! > Boot from Local HDD //! Installers //! > Linux Installers -> submenu of Linux ISOs //! > Windows Installers -> submenu of Windows ISOs (controlled by Settings::windows_enabled) //! Tools //! > Utilities -> memtest, etc. (embedded assets only) //! > OpenPXE Shell -> drop to iPXE shell with branded prompt //! > Network Card Info -> ifstat / config / route dump //! Queued Deployment -> join the deployment queue //! ``` //! //! ## iPXE is entirely backend — users do not see or write iPXE //! //! All user-facing knobs live in `Settings`. Script generation translates //! those knobs into iPXE primitives (chain, menu, item, choose, etc.). //! There is intentionally no UI path to upload a custom `.ipxe` script. use openpxe_core::{Settings, TimeoutAction}; use openpxe_iso_store::introspect::DistroFamily; use openpxe_iso_store::{BootEntry, BootKind, IsoMeta}; use std::fmt::Write as _; /// Top-level OpenPXE boot menu. Serialized identically for BIOS and UEFI /// clients because iPXE normalises the menu primitives across firmwares. /// /// v0.4.69: rendered with an iVentoy-style graphical frame — a /// `console --picture` directive paints a full-screen PNG background /// (the operator's uploaded logo on a dark field, or the default /// OpenPXE mark) with the menu text overlaid below a reserved top /// margin, plus a footer carrying version + arch + firmware kind. On /// iPXE binaries built with `IMAGE_PNG` + `CONSOLE_FRAMEBUFFER` (our /// x86_64 UEFI binaries, compiled from source) the background paints; /// on binaries without PNG support the `|| console` fallback yields a /// clean text menu. The old ASCII wordmark has been removed. #[must_use] pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> String { let mut s = String::new(); let base = base_url.trim_end_matches('/'); let timeout_ms = settings.boot_menu_timeout_secs.saturating_mul(1000); let default_item = match settings.timeout_action { TimeoutAction::QueuedDeployment => "queue", // Stay -> iPXE's `--timeout 0` is "no timeout". Pick any default // label; the client waits for keypress. We use the same label as // LocalHdd to keep the menu's pre-highlight stable. TimeoutAction::LocalHdd | TimeoutAction::Stay => "local", }; let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "# OpenPXE top-level menu - auto-generated, do not edit"); let _ = writeln!(s, "set base-url {base}"); let _ = writeln!(s, "set esc:hex 1b"); let _ = writeln!(s, "set cls ${{esc:string}}[2J"); // v0.4.69: graphical background. `/branding/pxe-logo` always // returns a full-screen 1024×768 PNG now — the operator's logo on a // dark field, or a default OpenPXE mark when none is uploaded. The // `--top 290` reserves the top band (where the logo paints) so the // menu text lands below it. // // v0.5.7: gate the whole command behind `iseq ${platform} efi`. // `console --picture` needs IMAGE_PNG + CONSOLE_FRAMEBUFFER, which // only our from-source UEFI binaries carry (x86_64/arm64 UEFI — see // deploy/docker/Dockerfile). The fetched BIOS `undionly.kpxe` has // neither, and on legacy BIOS the `--picture` attempt misbehaves // *before* the trailing `|| console` fallback can recover (it tries // to set a framebuffer mode the BIOS console can't honour). Guarding // on platform means BIOS clients never issue the command at all — // they drop straight to the plain text menu — while UEFI clients // still get the graphical background. A PNG-less UEFI build (e.g. the // upstream i386-efi baseline) still falls back gracefully through the // same `|| console`. No operator toggle needed; mixed BIOS+UEFI // fleets each get the right treatment automatically. let _ = writeln!( s, "iseq ${{platform}} efi && console --picture {base}/branding/pxe-logo --top 290 || console" ); // Map iPXE's ${{buildarch}} + ${{platform}} into the human form the // user asked for (e.g. "x86 BIOS", "x86_64 UEFI", "arm64 UEFI"). // iPXE evaluates `iseq` lazily, so we only set whichever line // matches. Anything not on the allowlist falls through to a generic // ` ` display. let _ = writeln!(s, "set arch-label ${{buildarch}} ${{platform}}"); let _ = writeln!( s, "iseq ${{buildarch}} i386 && iseq ${{platform}} pcbios && set arch-label x86 BIOS || iseq ${{buildarch}} x86_64 && iseq ${{platform}} efi && set arch-label x86_64 UEFI || iseq ${{buildarch}} arm64 && iseq ${{platform}} efi && set arch-label arm64 UEFI || true" ); let _ = writeln!(s, ":menu"); let _ = writeln!(s, "menu OpenPXE - network boot menu"); // v0.4.69: the ASCII wordmark is gone — the graphical background // (set via `console --picture` above) carries the branding now. let _ = writeln!(s, "item --gap"); let _ = writeln!( s, "item --gap -- ------------------------- Default -------------------------" ); let _ = writeln!(s, "item local Boot from Local HDD"); let _ = writeln!( s, "item --gap -- ----------------------- Installers -----------------------" ); if has_family(isos, is_linux_family) { let _ = writeln!(s, "item linux Linux Installers >"); } else { let _ = writeln!(s, "item --gap -- (no Linux ISOs uploaded)"); } // v0.5.8: Windows just works — no Settings toggle. Show the Windows // installers submenu whenever a Windows ISO is present; entries boot // via HTTP sanboot of the raw ISO, so no SMB/extraction is required. if has_family(isos, is_windows_family) { let _ = writeln!(s, "item windows Windows Installers >"); } else { let _ = writeln!(s, "item --gap -- (no Windows ISOs uploaded)"); } let _ = writeln!( s, "item --gap -- -------------------------- Tools --------------------------" ); let _ = writeln!(s, "item tools Tools >"); let _ = writeln!( s, "item --gap -- ---------------------- Queued Deployment ---------------------" ); let _ = writeln!(s, "item queue Queued Deployment (join queue)"); let _ = writeln!(s, "item --gap"); let _ = writeln!(s, "item --key x exit Exit iPXE"); // v0.4.6 footer line. Sits just above the `choose` line so it's // always visible regardless of how the menu paginates. iPXE // interpolates `${arch-label}` (set near the top of this script) // and `${version}` is the binary-baked iPXE version — *not* the // OpenPXE version — so we hard-code the OpenPXE version string // here. let openpxe_version = env!("CARGO_PKG_VERSION"); let _ = writeln!(s, "item --gap"); let _ = writeln!( s, "item --gap -- OpenPXE v{openpxe_version} - ${{arch-label}}" ); if matches!(settings.timeout_action, TimeoutAction::Stay) { let _ = writeln!(s, "choose --default {default_item} target || goto menu"); } else { let _ = writeln!( s, "choose --default {default_item} --timeout {timeout_ms} target || goto menu" ); } // iPXE's `||` is strict about what follows. Each test uses `goto menu` // as the fallthrough target so the parser never sees a bare `||` with // trailing whitespace — some iPXE builds reject that. let _ = writeln!( s, "iseq ${{target}} local && chain {base}/boot/_local.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} linux && chain {base}/boot/_linux_menu.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} windows && chain {base}/boot/_windows_menu.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} tools && chain {base}/boot/_tools_menu.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} queue && chain {base}/boot/_queue.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} exit && exit || goto menu" ); let _ = writeln!(s, "goto menu"); s } /// Per-family submenu (Linux or Windows). Each item shows the ISO size /// in MiB, iVentoy-style (`[ 4376 MB] ubuntu-22.04-desktop-amd64`). #[must_use] pub fn render_family_menu(isos: &[IsoMeta], base_url: &str, is_windows: bool) -> String { let base = base_url.trim_end_matches('/'); let title = if is_windows { "Windows Installers" } else { "Linux Installers" }; let label = if is_windows { "windows" } else { "linux" }; let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "set base-url {base}"); let _ = writeln!(s, ":menu"); let _ = writeln!(s, "menu OpenPXE - {title}"); let filter: fn(DistroFamily) -> bool = if is_windows { is_windows_family } else { is_linux_family }; let mut count = 0; for iso in isos { if !filter(iso.introspection.family) { continue; } // v0.4.4: ISOs the operator flipped to the Tools category move // out of the OS installer submenus entirely — they only appear // under Tools. Without this filter the operator would see the // same ISO in both menus. if matches!(iso.category, openpxe_iso_store::IsoCategory::Tools) { continue; } for entry in &iso.boot_entries { let size_label = fmt_size_mib(iso.size_bytes); let key = hotkey_for_index(count); // Visual hint: a leading `*` marks password-protected entries. // ASCII only — iPXE's menu console mangles non-ASCII on some // firmwares. let lock = if iso.is_password_protected() { "*" } else { " " }; let _ = writeln!( s, "item {}{} {}[{:>6}] {}", key, entry.id, lock, size_label, escape_label(&entry.title), ); count += 1; } } if count == 0 { let _ = writeln!(s, "item --gap -- (no {label} images uploaded yet)"); } let _ = writeln!(s, "item --gap"); let _ = writeln!(s, "item --key b back < Back to main menu"); let _ = writeln!(s, "choose target || goto menu"); let _ = writeln!( s, "iseq ${{target}} back && chain {base}/boot.ipxe || goto menu" ); // Pass `?mac=${mac}` so the per-entry handler can record the booting // client into the Host log. iPXE substitutes `${mac}` before // the HTTP fetch; if the firmware can't resolve it the literal // `${mac}` is sent and the server treats it as "unknown". let _ = writeln!( s, "chain {base}/boot/${{target}}.ipxe?mac=${{mac}} || goto menu" ); s } /// Format a byte count as `NNNN MB` (iVentoy-style — MB not MiB, to match /// operator expectations from the original tool). fn fmt_size_mib(bytes: u64) -> String { let mib = bytes / (1024 * 1024); format!("{mib} MB") } /// Assign `--key N ` hotkeys 1..9, then nothing for positions >=9. /// iPXE's menu needs the --key prefix as a separate token before the id. fn hotkey_for_index(i: usize) -> String { if i < 9 { format!("--key {} ", i + 1) } else { String::new() } } /// Tools submenu — Utilities, Shell, NIC Info, Reboot, Exit to firmware, /// plus any ISOs the operator flipped to [`IsoCategory::Tools`] in the /// Storage tab. The category-Tools ISOs render first so frequently used /// recovery / hardware tools are reachable with a single number key /// before the built-in shortcuts. #[must_use] pub fn render_tools_menu(isos: &[IsoMeta], base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "set base-url {base}"); let _ = writeln!(s, ":menu"); let _ = writeln!(s, "menu OpenPXE - Tools"); // Operator-categorized tool ISOs (hotkeys 1..9), each chained the // same way as a per-family menu pick — through the boot-entry id // route, carrying `?mac=${mac}` for Host log attribution. let mut count = 0; for iso in isos { if !matches!(iso.category, openpxe_iso_store::IsoCategory::Tools) { continue; } for entry in &iso.boot_entries { let size_label = fmt_size_mib(iso.size_bytes); let key = hotkey_for_index(count); let lock = if iso.is_password_protected() { "*" } else { " " }; let _ = writeln!( s, "item {}{} {}[{:>6}] {}", key, entry.id, lock, size_label, escape_label(&entry.title), ); count += 1; } } if count > 0 { let _ = writeln!(s, "item --gap"); } let _ = writeln!(s, "item --key u util Utilities (memtest, ...)"); let _ = writeln!(s, "item --key s shell OpenPXE Shell"); let _ = writeln!(s, "item --key n nic Network Card Info"); let _ = writeln!(s, "item --gap"); let _ = writeln!(s, "item --key r reboot Reboot Computer"); let _ = writeln!(s, "item --key e firmware Exit and continue BIOS boot"); let _ = writeln!(s, "item --gap"); let _ = writeln!(s, "item --key b back < Back to main menu"); let _ = writeln!(s, "choose target || goto menu"); let _ = writeln!( s, "iseq ${{target}} util && chain {base}/boot/_util.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} shell && chain {base}/boot/_shell.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} nic && chain {base}/boot/_nic.ipxe || goto menu" ); let _ = writeln!( s, "iseq ${{target}} reboot && reboot || goto menu" ); let _ = writeln!( s, "iseq ${{target}} firmware && exit 0 || goto menu" ); let _ = writeln!( s, "iseq ${{target}} back && chain {base}/boot.ipxe || goto menu" ); // Fall-through for category-Tools ISO ids — same as the family // submenu, carrying `?mac=${mac}` for the boot log. let _ = writeln!( s, "chain {base}/boot/${{target}}.ipxe?mac=${{mac}} || goto menu" ); s } /// "Boot from Local HDD". On BIOS, we sanboot the first local drive; on /// UEFI we `exit` so the firmware moves to the next boot entry (normally /// the internal disk). #[must_use] pub fn render_local_hdd(base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "# Boot from Local HDD - platform-sensitive"); let _ = writeln!( s, "iseq ${{platform}} pcbios && sanboot --no-describe --drive 0x80 || goto uefi" ); let _ = writeln!(s, ":uefi"); let _ = writeln!( s, "# UEFI path: fall through to the firmware's next boot entry" ); let _ = writeln!(s, "exit 0"); let _ = writeln!(s, "# If the above exit returns, loop back to the main menu"); let _ = writeln!(s, "chain {base}/boot.ipxe"); s } /// Utilities submenu. For Phase 2 we bundle memtest86+ as an optional /// asset (if absent, the item is listed but errors gracefully). No third- /// party tools are fetched at runtime. #[must_use] pub fn render_util(base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, ":menu"); let _ = writeln!(s, "menu OpenPXE - Utilities"); let _ = writeln!(s, "item memtest MemTest86+ (RAM diagnostic)"); let _ = writeln!(s, "item --gap"); let _ = writeln!(s, "item back < Back"); let _ = writeln!(s, "choose target || goto menu"); let _ = writeln!( s, "iseq ${{target}} memtest && chain {base}/ipxe/memtest.bin || goto menu" ); let _ = writeln!( s, "iseq ${{target}} back && chain {base}/boot/_tools_menu.ipxe || goto menu" ); let _ = writeln!(s, "goto menu"); s } /// "OpenPXE Shell" — iPXE shell, branded. #[must_use] pub fn render_shell(base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "echo =========================================="); let _ = writeln!(s, "echo OpenPXE Shell"); let _ = writeln!(s, "echo 'exit' returns to the main menu"); let _ = writeln!(s, "echo =========================================="); let _ = writeln!(s, "shell"); let _ = writeln!(s, "chain {base}/boot.ipxe"); s } /// "Network Card Info" — print ifstat + route + config. #[must_use] pub fn render_nic_info(base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "echo =========================================="); let _ = writeln!(s, "echo Network Card Info"); let _ = writeln!(s, "echo =========================================="); let _ = writeln!(s, "ifstat"); let _ = writeln!(s, "echo"); let _ = writeln!(s, "route"); let _ = writeln!(s, "echo"); let _ = writeln!(s, "echo 'Press any key to return to menu'"); let _ = writeln!(s, "prompt --timeout 30000"); let _ = writeln!(s, "chain {base}/boot.ipxe"); s } /// Queued Deployment entry point. Joins the queue, then enters a long-poll /// loop (iPXE repeats the chain on 3xx redirects / HTTP errors until a /// real script comes back). #[must_use] pub fn render_queue_entry(base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!( s, "# Queued Deployment - join the queue and wait for operator" ); let _ = writeln!(s, "echo Joining deployment queue..."); // imgfetch writes the body to a file in iPXE's transient FS; we read // the queue entry id out of the Location-style header by asking the server // to put it in the response body as a single token. let _ = writeln!(s, "chain --replace {base}/api/queue/join?mac=${{mac}}"); s } /// Per-entry boot script (same as Phase 1, with extra_kernel_args appended). /// /// `unattended_args` (v0.5.2) carries the per-host unattended-install /// kernel arguments (`inst.ks=…`, `auto=true … url=…`, or /// `autoinstall ds=nocloud-net;s=…`) when the requesting MAC has a /// deployment profile with an answer file selected. It's appended to the /// Linux kernel command line after the operator's global extra args, and /// ignored for Windows (wimboot) / sanboot entries which don't take a /// kernel cmdline. #[must_use] pub fn render_entry( entry: &BootEntry, settings: &Settings, base_url: &str, unattended_args: Option<&str>, ) -> String { let mut s = String::new(); let base = base_url.trim_end_matches('/'); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "set base-url {base}"); match &entry.kind { BootKind::LinuxKernel { kernel_url, initrd_urls, args, } => { let mut cmdline = args.cmdline.replace("${base-url}", base); if !settings.extra_kernel_args.trim().is_empty() { cmdline.push(' '); cmdline.push_str(settings.extra_kernel_args.trim()); } if let Some(extra) = unattended_args { if !extra.trim().is_empty() { cmdline.push(' '); cmdline.push_str(extra.trim()); } } let _ = writeln!(s, "kernel {base}/{kernel_url} {cmdline}"); for u in initrd_urls { let _ = writeln!(s, "initrd {base}/{u}"); } let _ = writeln!(s, "boot || goto failed"); } BootKind::Wimboot { wimboot_url, files } => { let _ = writeln!(s, "kernel {base}/{wimboot_url}"); for (tag, url) in files { let _ = writeln!(s, "initrd --name {tag} {base}/{url} {tag}"); } let _ = writeln!(s, "boot || goto failed"); } BootKind::SanBootIso { iso_url } => { let _ = writeln!(s, "sanboot --no-describe {base}/{iso_url} || goto failed"); } } let _ = writeln!(s, ":failed"); let _ = writeln!(s, "echo Boot failed - returning to menu in 5s"); let _ = writeln!(s, "sleep 5"); let _ = writeln!(s, "chain {base}/boot.ipxe"); s } fn is_linux_family(f: DistroFamily) -> bool { matches!( f, DistroFamily::DebianUbuntu | DistroFamily::RhelFedora | DistroFamily::OpenSuse | DistroFamily::Arch | DistroFamily::Alpine | DistroFamily::Unknown ) } fn is_windows_family(f: DistroFamily) -> bool { matches!(f, DistroFamily::WindowsPe) } fn has_family(isos: &[IsoMeta], pred: fn(DistroFamily) -> bool) -> bool { isos.iter().any(|i| pred(i.introspection.family)) } fn escape_label(s: &str) -> String { s.chars() .map(|c| match c { '\n' | '\r' => ' ', c => c, }) .collect() } /// Render the password-prompt script for a protected boot entry. /// /// Flow on the client: /// 1. iPXE clears any leftover ${password}, prints a banner naming the /// ISO so the operator knows what they're being asked for. /// 2. `read --secret password` accepts input without echoing it to /// the screen. /// 3. An empty input bails back to the main menu (lets the operator /// back out of a misclick). /// 4. Otherwise the script chains the same /boot/.ipxe URL but /// with `?token=${password:uristring}`. iPXE's `:uristring` /// modifier URL-encodes the value so `&`, `?`, `=`, spaces, etc. /// survive transport. /// 5. The server replies with either the boot script (correct /// password) or [`render_password_failed`] (wrong password). On /// transport failure we fall back to the main menu. #[must_use] pub fn render_password_prompt(entry_id: &str, iso_filename: &str, base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let label = escape_label(iso_filename); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "# OpenPXE password prompt for {label}"); let _ = writeln!(s, "echo"); let _ = writeln!(s, "echo =========================================="); let _ = writeln!(s, "echo This image requires a password"); let _ = writeln!(s, "echo {label}"); let _ = writeln!(s, "echo (enter alone returns to main menu)"); let _ = writeln!(s, "echo =========================================="); let _ = writeln!(s, "set password "); let _ = writeln!(s, "read --secret password"); let _ = writeln!( s, "iseq ${{password}} \"\" && chain {base}/boot.ipxe || goto submit" ); let _ = writeln!(s, ":submit"); let _ = writeln!(s, "echo Verifying..."); // Carry `mac=${mac}` alongside the token so a successful unlock // records the actual client MAC into the Host log. On // older iPXE that can't resolve `${mac}` the server just stores it // as "unknown" rather than refusing to boot. let _ = writeln!( s, "chain {base}/boot/{entry_id}.ipxe?token=${{password:uristring}}&mac=${{mac}} \ || chain {base}/boot.ipxe" ); s } /// Render the "wrong password" script. Tells the operator, sleeps for /// two seconds (gives the eye time to register the message and dampens /// brute-force rate without help from the server), and chains back to /// the same entry — which sends them through the prompt flow again. #[must_use] pub fn render_password_failed(entry_id: &str, base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let mut s = String::new(); let _ = writeln!(s, "#!ipxe"); let _ = writeln!(s, "echo"); let _ = writeln!(s, "echo Wrong password."); let _ = writeln!(s, "sleep 2"); let _ = writeln!( s, "chain {base}/boot/{entry_id}.ipxe || chain {base}/boot.ipxe" ); s } #[cfg(test)] mod password_tests { use super::*; #[test] fn prompt_uses_secret_read_and_uri_escape() { let s = render_password_prompt("alpha-linux", "Alpha Test.iso", "http://10.0.0.5"); assert!(s.starts_with("#!ipxe\n")); assert!(s.contains("read --secret password")); assert!(s.contains("Alpha Test.iso")); // URI-string modifier on the var so passwords with `&`/spaces survive. assert!(s.contains("token=${password:uristring}")); // Empty enter sends back to the main menu, not back into the prompt // (avoids a wedged client if the operator chose by mistake). assert!(s.contains("&& chain http://10.0.0.5/boot.ipxe || goto submit")); // Never log/echo the value. assert!(!s.contains("echo ${password")); } #[test] fn failed_chains_back_to_entry() { let s = render_password_failed("alpha-linux", "http://10.0.0.5"); assert!(s.contains("Wrong password.")); // Re-target the entry so the prompt flow runs again. assert!(s.contains("chain http://10.0.0.5/boot/alpha-linux.ipxe")); } #[test] fn top_menu_has_polished_branding_and_arch_footer() { // v0.4.69: the menu emits a `console --picture` line that paints // a full-screen PNG background (the operator's logo, or the // default OpenPXE mark) reserving a top margin for it, then // falls back to a clean text console on iPXE builds without PNG // support. The ASCII wordmark is gone — the graphical // background carries the branding now. A single-line footer // still carries the OpenPXE version + arch. let settings = Settings::default(); let s = render_menu(&[], &settings, "http://10.0.0.5"); assert!( s.contains("console --picture http://10.0.0.5/branding/pxe-logo"), "missing console --picture line:\n{s}" ); // The picture call reserves a top margin for the logo band. assert!(s.contains("--top 290"), "missing --top margin:\n{s}"); // Picture-or-text-console must be a single statement so older // iPXE parsers don't choke on the chain. assert!(s.contains("|| console"), "missing graceful fallback:\n{s}"); // The ASCII wordmark must be GONE — its removal is the whole // point of v0.4.69's graphical background. assert!( !s.contains("___ ___ __ __ ___"), "ASCII banner should have been removed:\n{s}" ); // Footer with version + arch interpolation. The version comes // from CARGO_PKG_VERSION at compile time. let version = env!("CARGO_PKG_VERSION"); assert!( s.contains(&format!("OpenPXE v{version}")), "footer missing OpenPXE version:\n{s}" ); assert!( s.contains("${arch-label}"), "footer missing arch-label interpolation:\n{s}" ); // No website URL — the design brief calls that out as tacky. assert!( !s.to_ascii_lowercase().contains("openpxe.com"), "footer should not advertise the website:\n{s}" ); // Arch-label mapping covers the three labels from the brief: // "x86 BIOS", "x86_64 UEFI", "arm64 UEFI". assert!(s.contains("x86 BIOS"), "{s}"); assert!(s.contains("x86_64 UEFI"), "{s}"); assert!(s.contains("arm64 UEFI"), "{s}"); } // v0.5.4: a full snapshot of the rendered top menu. The fragment // `assert!`s above check specific invariants; this catches *any* other // drift (a reordered item, a dropped line, changed spacing) so it's // reviewed deliberately. The OpenPXE version is filtered out so the // snapshot doesn't churn on every release bump. #[test] fn render_menu_snapshot() { // Normalize the compile-time version so the snapshot doesn't churn // on every release bump (no insta `filters` feature needed). let rendered = render_menu(&[], &Settings::default(), "http://10.0.0.5").replace( concat!("OpenPXE v", env!("CARGO_PKG_VERSION")), "OpenPXE vX.Y.Z", ); insta::assert_snapshot!(rendered); } #[test] fn generated_scripts_do_not_emit_bare_or_trailing_fallbacks() { let settings = Settings::default(); let scripts = [ render_menu(&[], &settings, "http://10.0.0.5"), render_tools_menu(&[], "http://10.0.0.5"), render_local_hdd("http://10.0.0.5"), render_util("http://10.0.0.5"), render_shell("http://10.0.0.5"), render_nic_info("http://10.0.0.5"), render_queue_entry("http://10.0.0.5"), render_password_failed("alpha-linux", "http://10.0.0.5"), ]; for script in scripts { for line in script.lines() { assert!( !line.trim_end().ends_with("||"), "bare iPXE fallback operator in line: {line}\nscript:\n{script}" ); } } } }