use thiserror::Error;
#[derive(Debug, Error)]
pub enum PbrError {
#[error("existing PBR is {got} bytes; expected exactly 512")]
BadExistingSize { got: usize },
#[error("existing reserved area is {got} bytes; expected exactly 1024 (sector 0 + sector 1)")]
BadExistingMultiSize { got: usize },
#[error("boot blob is {got} bytes; expected exactly 512")]
BadBlobSize { got: usize },
#[error("multi-sector blob is {got} bytes; expected non-zero multiple of 512")]
BadMultiBlobSize { got: usize },
#[error("boot blobs were not embedded; rebuild with --features embed-boot-asm")]
NotEmbedded,
#[error("run list is empty; need at least one LbaRun")]
EmptyRuns,
#[error("run list has {got} entries; bootsect template caps at {max}")]
TooManyRuns { got: usize, max: usize },
#[error(
"total sector count {got} exceeds bootsect cap of {max} \
(≈{} KB at 512 B/sector)",
max * 512 / 1024
)]
TooManySectors { got: usize, max: usize },
#[error("formatter sector 0 missing 0xAA55 boot signature at offset 510")]
MissingBootSignature,
#[error(
"target segment {got:#06x} out of sane range [{min:#06x}, {max:#06x}] \
(too low: overlaps IVT/BDA; too high: wraps real-mode address space)"
)]
BadTargetSegment { got: u16, min: u16, max: u16 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LbaRun {
pub start_lba: u32,
pub sector_count: u16,
}
pub fn build_xp_setup_chain_bootsect(
formatter_sector0: &[u8; 512],
target_segment: u16,
runs: &[LbaRun],
) -> Result<[u8; 512], PbrError> {
let template = crate::blobs::XP_SETUP_CHAIN_BOOTSECT_BOOT;
if template.is_empty() {
return Err(PbrError::NotEmbedded);
}
if runs.is_empty() {
return Err(PbrError::EmptyRuns);
}
if runs.len() > MAX_SETUP_CHAIN_RUNS {
return Err(PbrError::TooManyRuns {
got: runs.len(),
max: MAX_SETUP_CHAIN_RUNS,
});
}
let total_sectors: usize = runs.iter().map(|r| r.sector_count as usize).sum();
if total_sectors > MAX_SETUP_CHAIN_SECTORS {
return Err(PbrError::TooManySectors {
got: total_sectors,
max: MAX_SETUP_CHAIN_SECTORS,
});
}
if formatter_sector0[510] != 0x55 || formatter_sector0[511] != 0xAA {
return Err(PbrError::MissingBootSignature);
}
const MIN_TARGET_SEG: u16 = 0x0050;
const MAX_TARGET_SEG: u16 = 0x9000;
if target_segment < MIN_TARGET_SEG || target_segment > MAX_TARGET_SEG {
return Err(PbrError::BadTargetSegment {
got: target_segment,
min: MIN_TARGET_SEG,
max: MAX_TARGET_SEG,
});
}
let mut out = [0u8; 512];
out[0..3].copy_from_slice(&template[0..3]);
out[3..90].copy_from_slice(&formatter_sector0[3..90]);
out[90..510].copy_from_slice(&template[90..510]);
out[510] = 0x55;
out[511] = 0xAA;
const TARGET_SEG_OFFSET: usize = 0x182;
const RUN_COUNT_OFFSET: usize = 0x184;
const RUN_TABLE_OFFSET: usize = 0x185;
out[TARGET_SEG_OFFSET..TARGET_SEG_OFFSET + 2]
.copy_from_slice(&target_segment.to_le_bytes());
out[RUN_COUNT_OFFSET] = runs.len() as u8;
for (i, run) in runs.iter().enumerate() {
let off = RUN_TABLE_OFFSET + i * 6;
out[off..off + 4].copy_from_slice(&run.start_lba.to_le_bytes());
out[off + 4..off + 6].copy_from_slice(&run.sector_count.to_le_bytes());
}
Ok(out)
}
pub const MAX_SETUP_CHAIN_RUNS: usize = 16;
pub const MAX_SETUP_CHAIN_SECTORS: usize = 1024;
pub fn splice_fat32_pbr(existing: &[u8], boot: &[u8]) -> Result<[u8; 512], PbrError> {
if existing.len() != 512 {
return Err(PbrError::BadExistingSize { got: existing.len() });
}
if boot.is_empty() {
return Err(PbrError::NotEmbedded);
}
if boot.len() != 512 {
return Err(PbrError::BadBlobSize { got: boot.len() });
}
let mut out = [0u8; 512];
out[0..3].copy_from_slice(&boot[0..3]);
out[3..90].copy_from_slice(&existing[3..90]);
out[3..11].copy_from_slice(b"MSWIN4.1");
out[90..510].copy_from_slice(&boot[90..510]);
out[510] = 0x55;
out[511] = 0xAA;
Ok(out)
}
pub fn splice_fat32_pbr_multi(existing: &[u8], blob: &[u8]) -> Result<Vec<u8>, PbrError> {
if existing.len() != 1024 {
return Err(PbrError::BadExistingMultiSize { got: existing.len() });
}
if blob.is_empty() {
return Err(PbrError::NotEmbedded);
}
if blob.len() < 1024 || blob.len() % 512 != 0 {
return Err(PbrError::BadMultiBlobSize { got: blob.len() });
}
let mut out = vec![0u8; blob.len() + 512];
out[0..3].copy_from_slice(&blob[0..3]);
out[3..90].copy_from_slice(&existing[3..90]);
out[3..11].copy_from_slice(b"MSWIN4.1");
out[90..510].copy_from_slice(&blob[90..510]);
out[510] = 0x55;
out[511] = 0xAA;
out[512..1024].copy_from_slice(&existing[512..1024]);
out[1024..].copy_from_slice(&blob[512..]);
Ok(out)
}
pub fn splice_ntfs_pbr_multi(existing: &[u8], blob: &[u8]) -> Result<Vec<u8>, PbrError> {
if existing.len() != 1024 {
return Err(PbrError::BadExistingMultiSize { got: existing.len() });
}
if blob.is_empty() {
return Err(PbrError::NotEmbedded);
}
if blob.len() < 1024 || blob.len() % 512 != 0 {
return Err(PbrError::BadMultiBlobSize { got: blob.len() });
}
let mut out = vec![0u8; blob.len() + 512];
out[0..3].copy_from_slice(&blob[0..3]);
out[3..84].copy_from_slice(&existing[3..84]);
out[84..510].copy_from_slice(&blob[84..510]);
out[510] = 0x55;
out[511] = 0xAA;
out[512..1024].copy_from_slice(&existing[512..1024]);
out[1024..].copy_from_slice(&blob[512..]);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_formatter_sector0() -> [u8; 512] {
let mut s = [0u8; 512];
s[0..3].copy_from_slice(&[0xEB, 0x58, 0x90]);
s[3..11].copy_from_slice(b"BSD 4.4");
s[0x1C..0x20].copy_from_slice(&2048u32.to_le_bytes());
s[510] = 0x55;
s[511] = 0xAA;
s
}
fn one_run(start: u32, count: u16) -> Vec<LbaRun> {
vec![LbaRun {
start_lba: start,
sector_count: count,
}]
}
#[test]
fn setup_chain_emits_512_bytes_with_signature() {
let s0 = synthetic_formatter_sector0();
let out = build_xp_setup_chain_bootsect(&s0, 0x2000, &one_run(1000, 128)).unwrap();
assert_eq!(out.len(), 512);
assert_eq!(&out[510..512], &[0x55, 0xAA]);
assert_eq!(&out[3..90], &s0[3..90]);
}
#[test]
fn setup_chain_patches_target_segment_and_run_table() {
let s0 = synthetic_formatter_sector0();
let runs = vec![
LbaRun { start_lba: 1000, sector_count: 64 },
LbaRun { start_lba: 1100, sector_count: 32 },
];
let out = build_xp_setup_chain_bootsect(&s0, 0x2500, &runs).unwrap();
assert_eq!(u16::from_le_bytes([out[0x180], out[0x181]]), 0);
assert_eq!(u16::from_le_bytes([out[0x182], out[0x183]]), 0x2500);
assert_eq!(out[0x184], 2);
assert_eq!(u32::from_le_bytes(out[0x185..0x189].try_into().unwrap()), 1000);
assert_eq!(u16::from_le_bytes([out[0x189], out[0x18A]]), 64);
assert_eq!(u32::from_le_bytes(out[0x18B..0x18F].try_into().unwrap()), 1100);
assert_eq!(u16::from_le_bytes([out[0x18F], out[0x190]]), 32);
assert!(out[0x191..0x191 + 6].iter().all(|&b| b == 0));
}
#[test]
fn setup_chain_rejects_empty_runs() {
let s0 = synthetic_formatter_sector0();
let err = build_xp_setup_chain_bootsect(&s0, 0x2000, &[]).unwrap_err();
assert!(matches!(err, PbrError::EmptyRuns), "got {err:?}");
}
#[test]
fn setup_chain_rejects_too_many_runs() {
let s0 = synthetic_formatter_sector0();
let many: Vec<LbaRun> = (0..(MAX_SETUP_CHAIN_RUNS + 1) as u32)
.map(|i| LbaRun { start_lba: i * 100, sector_count: 1 })
.collect();
let err = build_xp_setup_chain_bootsect(&s0, 0x2000, &many).unwrap_err();
assert!(matches!(err, PbrError::TooManyRuns { .. }), "got {err:?}");
}
#[test]
fn setup_chain_rejects_too_many_sectors() {
let s0 = synthetic_formatter_sector0();
let runs = vec![LbaRun {
start_lba: 0,
sector_count: (MAX_SETUP_CHAIN_SECTORS + 1) as u16,
}];
let err = build_xp_setup_chain_bootsect(&s0, 0x2000, &runs).unwrap_err();
assert!(matches!(err, PbrError::TooManySectors { .. }), "got {err:?}");
}
#[test]
fn setup_chain_rejects_missing_boot_signature() {
let mut s0 = synthetic_formatter_sector0();
s0[510] = 0;
let err = build_xp_setup_chain_bootsect(&s0, 0x2000, &one_run(1000, 1)).unwrap_err();
assert!(matches!(err, PbrError::MissingBootSignature), "got {err:?}");
}
#[test]
fn setup_chain_rejects_target_segment_out_of_range() {
let s0 = synthetic_formatter_sector0();
for bad in [0x0000u16, 0x0049, 0x9001, 0xFFFF] {
let err = build_xp_setup_chain_bootsect(&s0, bad, &one_run(1000, 1)).unwrap_err();
assert!(
matches!(err, PbrError::BadTargetSegment { .. }),
"seg {bad:#06x} → {err:?}"
);
}
}
#[test]
fn setup_chain_accepts_max_runs_at_cap() {
let s0 = synthetic_formatter_sector0();
let runs: Vec<LbaRun> = (0..MAX_SETUP_CHAIN_RUNS as u32)
.map(|i| LbaRun { start_lba: i * 100, sector_count: 1 })
.collect();
let out = build_xp_setup_chain_bootsect(&s0, 0x2000, &runs).unwrap();
assert_eq!(out[0x184], MAX_SETUP_CHAIN_RUNS as u8);
}
fn fake_blob() -> Vec<u8> {
let mut b = vec![0u8; 512];
b[0] = 0xEB;
b[1] = 0x58;
b[2] = 0x90;
for i in 90..510 {
b[i] = 0xCC; }
b
}
fn fake_existing() -> Vec<u8> {
let mut e = vec![0u8; 512];
for i in 3..90 {
e[i] = 0xBB;
}
e
}
#[test]
fn splice_preserves_bpb() {
let out = splice_fat32_pbr(&fake_existing(), &fake_blob()).unwrap();
assert_eq!(&out[0..3], &[0xEB, 0x58, 0x90], "jump from blob");
assert_eq!(&out[3..11], b"MSWIN4.1", "OEM ID overwritten");
assert!(
out[11..90].iter().all(|&b| b == 0xBB),
"BPB body past OEM preserved from existing"
);
assert!(out[90..510].iter().all(|&b| b == 0xCC), "boot code from blob");
assert_eq!(&out[510..512], &[0x55, 0xAA], "boot signature");
}
#[test]
fn splice_rejects_wrong_sizes() {
assert!(splice_fat32_pbr(&vec![0u8; 256], &fake_blob()).is_err());
assert!(splice_fat32_pbr(&fake_existing(), &vec![0u8; 256]).is_err());
}
#[test]
fn splice_errors_when_blob_missing() {
match splice_fat32_pbr(&fake_existing(), &[]) {
Err(PbrError::NotEmbedded) => {}
other => panic!("expected NotEmbedded, got {other:?}"),
}
}
fn fake_existing_multi() -> Vec<u8> {
let mut e = vec![0u8; 1024];
for i in 3..90 {
e[i] = 0xBB;
}
for i in 512..1024 {
e[i] = 0xF5; }
e
}
fn fake_multi_blob() -> Vec<u8> {
let mut b = vec![0u8; 1024];
b[0] = 0xEB;
b[1] = 0x58;
b[2] = 0x90;
for i in 90..510 {
b[i] = 0xCC;
}
for i in 512..1024 {
b[i] = 0xAB;
}
b
}
#[test]
fn multi_splice_preserves_bpb_fsinfo_and_relocates_stage2() {
let out = splice_fat32_pbr_multi(&fake_existing_multi(), &fake_multi_blob()).unwrap();
assert_eq!(out.len(), 1536);
assert_eq!(&out[0..3], &[0xEB, 0x58, 0x90]);
assert_eq!(&out[3..11], b"MSWIN4.1");
assert!(out[11..90].iter().all(|&b| b == 0xBB), "BPB preserved past OEM");
assert!(out[90..510].iter().all(|&b| b == 0xCC), "stage 1 boot code");
assert_eq!(&out[510..512], &[0x55, 0xAA]);
assert!(
out[512..1024].iter().all(|&b| b == 0xF5),
"FSInfo sector preserved from existing"
);
assert!(
out[1024..1536].iter().all(|&b| b == 0xAB),
"stage 2 lives at LBA 2"
);
}
#[test]
fn multi_splice_rejects_wrong_existing_size() {
assert!(matches!(
splice_fat32_pbr_multi(&vec![0u8; 512], &fake_multi_blob()),
Err(PbrError::BadExistingMultiSize { got: 512 })
));
}
fn fake_ntfs_existing_multi() -> Vec<u8> {
let mut e = vec![0u8; 1024];
e[3..11].copy_from_slice(b"NTFS ");
for i in 11..84 {
e[i] = 0xBB;
}
for i in 512..1024 {
e[i] = 0xF5; }
e
}
fn fake_ntfs_multi_blob() -> Vec<u8> {
let mut b = vec![0u8; 1536];
b[0] = 0xEB;
b[1] = 0x52;
b[2] = 0x90;
for i in 84..510 {
b[i] = 0xCC;
}
for i in 512..1536 {
b[i] = 0xAB;
}
b
}
#[test]
fn ntfs_multi_splice_preserves_bpb_lba1_and_relocates_stage2() {
let out =
splice_ntfs_pbr_multi(&fake_ntfs_existing_multi(), &fake_ntfs_multi_blob()).unwrap();
assert_eq!(out.len(), 2048);
assert_eq!(&out[0..3], &[0xEB, 0x52, 0x90], "jump from blob");
assert_eq!(&out[3..11], b"NTFS ", "OEM preserved");
assert!(out[11..84].iter().all(|&b| b == 0xBB), "BPB preserved");
assert!(out[84..510].iter().all(|&b| b == 0xCC), "stage 1 boot code");
assert_eq!(&out[510..512], &[0x55, 0xAA]);
assert!(
out[512..1024].iter().all(|&b| b == 0xF5),
"LBA 1 preserved from existing"
);
assert!(
out[1024..2048].iter().all(|&b| b == 0xAB),
"stage 2 (2 sectors) lives at LBA 2..3"
);
}
#[test]
fn ntfs_multi_splice_rejects_bad_sizes() {
assert!(matches!(
splice_ntfs_pbr_multi(&vec![0u8; 512], &fake_ntfs_multi_blob()),
Err(PbrError::BadExistingMultiSize { got: 512 })
));
assert!(matches!(
splice_ntfs_pbr_multi(&fake_ntfs_existing_multi(), &vec![0u8; 512]),
Err(PbrError::BadMultiBlobSize { got: 512 })
));
}
#[test]
fn multi_splice_rejects_bad_blob_sizes() {
assert!(matches!(
splice_fat32_pbr_multi(&fake_existing_multi(), &vec![0u8; 512]),
Err(PbrError::BadMultiBlobSize { got: 512 })
));
assert!(matches!(
splice_fat32_pbr_multi(&fake_existing_multi(), &vec![0u8; 1500]),
Err(PbrError::BadMultiBlobSize { got: 1500 })
));
}
}