v0.5.8: Windows ISOs just work (HTTP sanboot) + Storage UX
Windows boot, the "less is more" way. Windows ISOs now boot via iPXE HTTP sanboot of the raw image — iPXE exposes the unmodified ISO as an emulated CD backed by on-demand HTTP range reads, and Windows Setup boots from it. This replaces the wimboot+SMB chain, which needed an SMB server the host often can't provide (:445 collisions), served in-ISO files via an ISO9660 lookup that failed on UDF-only Win11 ISOs, and was gated behind a Settings toggle the WebUI never even exposed (so Windows never booted). Now it needs only the HTTP port — works in any environment, SMB or not — and nothing is injected into Windows (no httpdisk.sys, no test certs, no trust-store changes; fully within the project's hard rules). - iso-store/store.rs: WindowsPe boot entry -> BootKind::SanBootIso of the raw iso/<id>.iso (render_entry already emits `sanboot --no-describe`). - iso-store/introspect.rs: broaden Windows detection for UDF-only Win10/11 ISOs — UTF-16LE markers (boot.wim/bootmgr/install.wim/microsoft), extra ASCII markers, and a filename heuristic, since their volume labels are cryptic and filenames are UTF-16. + unit tests. - http-api/ipxe_script.rs: Windows installers submenu shows whenever a Windows ISO is present — no toggle, no "disabled in Settings". - webui: dashboard no longer flags Windows ISOs (they boot now); the generic large-ISO warning reworded to read sensibly for genuinely non-bootable images (e.g. VMware VCSA appliance bundles). Storage UX: - Available images listed alphabetically by filename. - Upload gains a Cancel button (aborts the chunk + discards the partial). - beforeunload warning while an upload is in flight. 263 tests pass, clippy clean. NOTE: actual Windows boot is validated on real hardware — code/script/range-serving are validated here. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ac433b30e9
commit
9fc9a9a1af
@@ -85,12 +85,37 @@ pub fn introspect(path: &Path) -> IntrospectionReport {
|
||||
read_total += n;
|
||||
}
|
||||
|
||||
// `sources/boot.wim` is the definitive Windows-install-media marker
|
||||
// when the ISO exposes ASCII (ISO9660/Joliet) names. `contains_ascii`
|
||||
// is case-insensitive, so one form covers BOOT.WIM / boot.wim and the
|
||||
// backslash variant.
|
||||
if contains_ascii(&haystack, b"sources/boot.wim")
|
||||
|| contains_ascii(&haystack, b"SOURCES/BOOT.WIM")
|
||||
|| contains_ascii(&haystack, b"SOURCES\\BOOT.WIM")
|
||||
|| contains_ascii(&haystack, b"sources\\boot.wim")
|
||||
{
|
||||
report.has_boot_wim = true;
|
||||
if report.family == DistroFamily::Unknown {
|
||||
report.family = DistroFamily::WindowsPe;
|
||||
}
|
||||
|
||||
// v0.5.8: broaden Windows detection. Modern Windows 10/11 ISOs are
|
||||
// UDF — filenames are stored as UTF-16 (so the ASCII scan above misses
|
||||
// them) and the volume label is a cryptic Microsoft string (so
|
||||
// `family_from_label` misses it too). Booting is via HTTP sanboot of
|
||||
// the raw ISO (no boot.wim extraction), so we only need the *family*.
|
||||
// Catch the common cases: well-known Windows markers in either ASCII
|
||||
// or UTF-16LE within the first 16 MiB, plus a filename hint.
|
||||
if report.family == DistroFamily::Unknown {
|
||||
let head = &haystack[..haystack.len().min(16 * 1024 * 1024)];
|
||||
let ascii_markers: [&[u8]; 4] = [
|
||||
b"bootmgr",
|
||||
b"sources/install.wim",
|
||||
b"sources/install.esd",
|
||||
b"efi/microsoft",
|
||||
];
|
||||
let utf16_markers = ["bootmgr", "boot.wim", "install.wim", "microsoft"];
|
||||
let looks_windows = ascii_markers.iter().any(|m| contains_ascii(head, m))
|
||||
|| utf16_markers.iter().any(|m| contains_utf16le_ci(head, m))
|
||||
|| filename_looks_windows(path);
|
||||
if looks_windows {
|
||||
report.family = DistroFamily::WindowsPe;
|
||||
}
|
||||
}
|
||||
@@ -158,6 +183,45 @@ fn contains_ascii(haystack: &[u8], needle: &[u8]) -> bool {
|
||||
.any(|w| w.eq_ignore_ascii_case(needle))
|
||||
}
|
||||
|
||||
/// Case-insensitive search for an ASCII string encoded as UTF-16LE — the
|
||||
/// way UDF (and thus modern Windows ISOs) store filenames. Each character
|
||||
/// is two bytes: the ASCII low byte (compared case-insensitively) followed
|
||||
/// by a 0 high byte. v0.5.8.
|
||||
fn contains_utf16le_ci(haystack: &[u8], ascii: &str) -> bool {
|
||||
let n = ascii.len();
|
||||
if n == 0 || haystack.len() < n * 2 {
|
||||
return false;
|
||||
}
|
||||
let lower: Vec<u8> = ascii.bytes().map(|b| b.to_ascii_lowercase()).collect();
|
||||
haystack.windows(n * 2).any(|w| {
|
||||
lower
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, &c)| w[i * 2 + 1] == 0 && w[i * 2].to_ascii_lowercase() == c)
|
||||
})
|
||||
}
|
||||
|
||||
/// Filename heuristic: a stock Windows ISO almost always carries an obvious
|
||||
/// token in its name (e.g. `..._windows_11_...`, `Win10`, `winserver`).
|
||||
/// Used only as a last-resort family hint when the content scan and volume
|
||||
/// label are inconclusive. v0.5.8.
|
||||
fn filename_looks_windows(path: &Path) -> bool {
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase();
|
||||
const TOKENS: [&str; 6] = [
|
||||
"windows",
|
||||
"winpe",
|
||||
"win10",
|
||||
"win11",
|
||||
"winserver",
|
||||
"win-server",
|
||||
];
|
||||
TOKENS.iter().any(|t| name.contains(t))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -179,4 +243,38 @@ mod tests {
|
||||
assert_eq!(family_from_label("ARCH_202604"), DistroFamily::Arch);
|
||||
assert_eq!(family_from_label("weird-custom"), DistroFamily::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16le_marker_matches_case_insensitively() {
|
||||
// "boot.wim" encoded UTF-16LE, mixed case — UDF stores Windows
|
||||
// filenames this way, which the ASCII scan can't see.
|
||||
let s = "BOOT.WIM";
|
||||
let utf16: Vec<u8> = s.bytes().flat_map(|b| [b, 0]).collect();
|
||||
let mut hay = vec![0u8; 8];
|
||||
hay.extend_from_slice(&utf16);
|
||||
hay.extend_from_slice(&[1, 2, 3]);
|
||||
assert!(contains_utf16le_ci(&hay, "boot.wim"));
|
||||
assert!(contains_utf16le_ci(&hay, "Boot.Wim"));
|
||||
assert!(!contains_utf16le_ci(&hay, "install.wim"));
|
||||
// An ASCII (not UTF-16) occurrence must NOT match the UTF-16 scan.
|
||||
assert!(!contains_utf16le_ci(b"boot.wim plain ascii", "boot.wim"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_hint_catches_windows_isos() {
|
||||
use std::path::Path;
|
||||
assert!(filename_looks_windows(Path::new(
|
||||
"en-us_windows_11_iot_enterprise_ltsc_2024_x64_dvd.iso"
|
||||
)));
|
||||
assert!(filename_looks_windows(Path::new(
|
||||
"Win10_22H2_English_x64.iso"
|
||||
)));
|
||||
assert!(filename_looks_windows(Path::new("winserver2022.iso")));
|
||||
assert!(!filename_looks_windows(Path::new(
|
||||
"ubuntu-24.04-desktop.iso"
|
||||
)));
|
||||
assert!(!filename_looks_windows(Path::new(
|
||||
"Rocky-9.4-x86_64-dvd.iso"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user