//! Windows ISO post-processing. Patches `boot.wim` (image index 2, WinPE) //! with two plain-text files so the client hits our SMB share and runs //! Windows Setup from there. //! //! Credit: the *technique* (not the code) is adapted from Bootimus //! (Apache-2.0, https://github.com/garybowers/bootimus). We reimplement in //! Rust and shell out to `wimlib-imagex` at container runtime because //! there is no maintained pure-Rust wimlib binding. //! //! What we inject — and why these are safe: //! //! * `Windows/System32/winpeshl.ini`: a plain INI that WinPE reads at //! startup and uses to launch `startnet.cmd` instead of the default //! interactive shell. No driver, no executable, no signed code. //! //! * `Windows/System32/startnet.cmd`: a batch file that runs `wpeinit`, //! waits for a DHCP lease, then `net use Z: \\\ /user:guest` //! and invokes `Z:\setup.exe`. Everything the client executes is stock //! Microsoft-signed WinPE + `setup.exe`. We add zero native code to //! the client's boot path. The trust store is untouched. //! //! What we *do not* inject: //! * No `.sys` drivers, signed or otherwise. //! * No `.cer`, no registry hive edits, no `bcdedit` changes. //! * No `bypass*` Windows 11 tweaks (operators who want those can use an //! unattend.xml; they will never be injected silently by us). use std::path::{Path, PathBuf}; use std::process::Command; /// Public identifier of whether/how Windows patching ran for an ISO. /// Stored on `IsoMeta` so the UI can show a clear "SMB ready" indicator. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum WinPatchState { /// Not a Windows ISO, nothing to do. NotApplicable, /// Windows ISO detected but Windows support is disabled in settings. DisabledBySettings, /// wimlib-imagex isn't on PATH — operator needs to install the runtime /// dependency before Windows ISOs can be patched. WimlibMissing, /// Patching succeeded; the ISO's boot.wim was rewritten in-place. Patched { smb_host: String, smb_share: String }, /// wimlib returned an error. Failed { reason: String }, } pub struct WimPatcher { pub smb_host: String, pub smb_share: String, } impl WimPatcher { #[must_use] pub fn new(smb_host: String, smb_share: String) -> Self { Self { smb_host, smb_share, } } /// Apply WinPE patches to `boot.wim` inside `extracted_iso_dir`. Returns /// a state enum — never panics. Designed to be safely re-runnable; each /// call rebuilds image 2 from scratch via `wimlib-imagex update`. pub fn patch(&self, extracted_iso_dir: &Path) -> WinPatchState { if !wimlib_present() { return WinPatchState::WimlibMissing; } let boot_wim = extracted_iso_dir.join("sources").join("boot.wim"); if !boot_wim.exists() { // Not a standard Windows install ISO layout. return WinPatchState::NotApplicable; } let work = match tempfile::tempdir() { Ok(d) => d, Err(e) => { return WinPatchState::Failed { reason: format!("tempdir: {e}"), } } }; // Stage the two files we want present at /Windows/System32/. let staging = work.path().join("stage/Windows/System32"); if let Err(e) = std::fs::create_dir_all(&staging) { return WinPatchState::Failed { reason: format!("staging mkdir: {e}"), }; } if let Err(e) = std::fs::write(staging.join("winpeshl.ini"), WINPESHL_INI) { return WinPatchState::Failed { reason: format!("write winpeshl.ini: {e}"), }; } let startnet = render_startnet(&self.smb_host, &self.smb_share); if let Err(e) = std::fs::write(staging.join("startnet.cmd"), startnet) { return WinPatchState::Failed { reason: format!("write startnet.cmd: {e}"), }; } // Build a wimlib update command file: // add /Windows/System32 /Windows/System32 let update_file = work.path().join("update.cmd"); let update_cmd = format!("add \"{}\" \"/Windows/System32\"\n", staging.display()); if let Err(e) = std::fs::write(&update_file, update_cmd) { return WinPatchState::Failed { reason: format!("write update.cmd: {e}"), }; } // Run wimlib-imagex update against image index 2 (WinPE). let output = Command::new("wimlib-imagex") .arg("update") .arg(&boot_wim) .arg("2") .arg("--rebuild") .arg("--command-file") .arg(&update_file) .output(); match output { Ok(o) if o.status.success() => WinPatchState::Patched { smb_host: self.smb_host.clone(), smb_share: self.smb_share.clone(), }, Ok(o) => WinPatchState::Failed { reason: format!( "wimlib-imagex update failed (exit {:?}): {}", o.status.code(), String::from_utf8_lossy(&o.stderr) ), }, Err(e) => WinPatchState::Failed { reason: format!("spawn wimlib-imagex: {e}"), }, } } } fn wimlib_present() -> bool { which("wimlib-imagex").is_some() } fn which(cmd: &str) -> Option { let paths = std::env::var_os("PATH")?; for dir in std::env::split_paths(&paths) { let p = dir.join(cmd); if p.is_file() { return Some(p); } } None } /// The winpeshl.ini contents. This file tells WinPE "don't run cmd.exe /// interactively; run startnet.cmd and exit when it returns". const WINPESHL_INI: &str = "[LaunchApps]\r\n\ \"%SYSTEMROOT%\\system32\\startnet.cmd\"\r\n"; /// Render startnet.cmd. The script: /// 1. Loads WinPE networking (`wpeinit`) and renews DHCP. /// 2. Waits until the SMB server is reachable. /// 3. Maps the install share to Z: as guest. /// 4. Runs setup.exe from the share. /// /// Uses CRLF line endings because WinPE cmd.exe requires them for .cmd files /// created on unix hosts. fn render_startnet(host: &str, share: &str) -> String { use std::fmt::Write as _; let host = host.trim(); let share = share.trim_matches('/'); let mut s = String::new(); // Windows-style CRLF; consumed verbatim by cmd.exe inside WinPE. // Bootimus v0.1.58 lesson: surface `net use` errors instead of // tight-looping on a blind retry. We retry but log every miss. s.push_str("@echo off\r\n"); s.push_str("echo OpenPXE WinPE bootstrap\r\n"); s.push_str("wpeinit\r\n"); // v0.1.58: explicitly start Workstation before mapping the share — // `net use` otherwise lazily inits SMB-client and races wpeinit. s.push_str("net start Workstation >nul 2>&1\r\n"); s.push_str("ipconfig /renew\r\n"); writeln!(s, "echo Waiting for SMB server {host} to be reachable...\r").unwrap(); writeln!( s, ":waitsmb\r\nping -n 1 -w 500 {host} >nul && goto havenet\r\n\ timeout /t 2 /nobreak >nul\r\ngoto waitsmb\r" ) .unwrap(); s.push_str(":havenet\r\n"); writeln!( s, "echo Mapping install media from \\\\{host}\\{share}...\r" ) .unwrap(); writeln!( s, ":mapshare\r\nnet use Z: \\\\{host}\\{share} /user:guest \"\" /persistent:no && goto mapped\r\n\ timeout /t 3 /nobreak >nul\r\ngoto mapshare\r" ) .unwrap(); s.push_str(":mapped\r\n"); s.push_str("echo Starting Windows Setup\r\n"); s.push_str("Z:\\setup.exe\r\n"); s.push_str("echo Setup exited; dropping to cmd for diagnosis\r\n"); s.push_str("cmd\r\n"); s } #[cfg(test)] mod tests { use super::*; #[test] fn startnet_has_crlf_and_no_testsigning() { let s = render_startnet("10.0.0.5", "win11"); assert!(s.contains("\r\n")); // Hard guard: must never include trust-store or driver-policy mutations. assert!(!s.to_lowercase().contains("bcdedit")); assert!(!s.to_lowercase().contains("testsigning")); assert!(!s.to_lowercase().contains("certutil")); assert!(s.contains("net use Z:")); assert!(s.contains("setup.exe")); } #[test] fn startnet_primes_workstation_and_surfaces_mapping_errors() { let s = render_startnet("10.0.0.5", "win11"); assert!( s.contains("net start Workstation"), "WinPE should explicitly start the SMB client before net use:\n{s}" ); let net_use_line = s .lines() .find(|line| line.contains("net use Z:")) .expect("net use line"); assert!( !net_use_line.contains(">nul"), "net use errors must remain visible in WinPE console: {net_use_line}" ); } #[test] fn patcher_reports_wimlib_missing_gracefully() { // We don't assume wimlib is present in CI; this checks the missing // branch is the noisy-but-survivable one we expect. let patcher = WimPatcher::new("10.0.0.5".into(), "win11".into()); let dir = tempfile::tempdir().unwrap(); // Construct a fake "sources/boot.wim". std::fs::create_dir_all(dir.path().join("sources")).unwrap(); std::fs::write(dir.path().join("sources/boot.wim"), b"placeholder").unwrap(); let result = patcher.patch(dir.path()); // Depending on whether wimlib is installed on the runner, we get // either WimlibMissing or Failed(...). Both mean "no silent // success with trust-store mutation" — that's the invariant. assert!(matches!( result, WinPatchState::WimlibMissing | WinPatchState::Failed { .. } )); } }