#![allow(clippy::doc_markdown)]
use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
use disk_forensic::{analyse_disk, DiskReport};
use forensicnomicon::volume_serial::VolumeSerial;
use mbr_partition_forensic::DetectedFs;
use std::io::{Read, Seek, SeekFrom};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncryptionKind {
BitLocker,
BitLockerToGo,
Luks,
UnrecognizedFilesystem,
}
impl EncryptionKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::BitLocker => "BitLocker",
Self::BitLockerToGo => "BitLocker To Go",
Self::Luks => "LUKS",
Self::UnrecognizedFilesystem => {
"unrecognized-filesystem (possible encrypted container)"
}
}
}
const fn rank(self) -> u8 {
match self {
Self::BitLocker | Self::BitLockerToGo => 3,
Self::Luks => 2,
Self::UnrecognizedFilesystem => 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceImage {
pub disk_signature: u32,
pub fat_volume_serial: Option<u32>,
pub encryption: Option<EncryptionKind>,
pub mbr: [u8; 512],
}
#[must_use]
pub fn parse_boot_sectors(image: &[u8]) -> Option<DeviceImage> {
analyse_device_image(&mut std::io::Cursor::new(image), image.len() as u64)
}
pub fn analyse_device_image<R: Read + Seek>(reader: &mut R, disk_size: u64) -> Option<DeviceImage> {
let report = analyse_disk(reader, disk_size).ok()?;
let mbr = match &report {
DiskReport::Mbr(m) | DiskReport::Gpt(m) => m,
DiskReport::Apm(_) => return None,
};
let disk_signature = mbr.disk_serial;
let mbr_bytes: [u8; 512] = read_region(reader, 0, 512)?.try_into().ok()?;
let mut fat_volume_serial = None;
let mut encryption: Option<EncryptionKind> = None;
let mut record = |serial: Option<VolumeSerial>, enc: Option<EncryptionKind>| {
if let Some(kind) = enc {
if encryption.is_none_or(|cur| kind.rank() > cur.rank()) {
encryption = Some(kind);
}
}
if fat_volume_serial.is_none() {
if let Some(VolumeSerial::Short(v)) = serial {
fat_volume_serial = Some(v);
}
}
};
match &mbr.gpt {
None => {
for p in &mbr.partitions {
record(p.volume_serial, encryption_of(p.encryption, p.detected_fs));
}
}
Some(g) => {
for e in g.partitions.iter().filter(|e| e.is_used()) {
record(e.volume_serial, encryption_of(e.encryption, None));
}
}
}
Some(DeviceImage {
disk_signature,
fat_volume_serial,
encryption,
mbr: mbr_bytes,
})
}
fn read_region<R: Read + Seek>(reader: &mut R, offset: u64, len: usize) -> Option<Vec<u8>> {
reader.seek(SeekFrom::Start(offset)).ok()?;
let mut buf = vec![0u8; len];
let mut filled = 0;
while filled < len {
match reader.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(_) => return None,
}
}
(filled != 0).then_some(buf)
}
fn encryption_of(
enc: Option<forensicnomicon::volume_encryption::VolumeEncryption>,
detected_fs: Option<DetectedFs>,
) -> Option<EncryptionKind> {
use forensicnomicon::volume_encryption::VolumeEncryption;
if let Some(e) = enc {
return Some(match e {
VolumeEncryption::BitLocker => EncryptionKind::BitLocker,
VolumeEncryption::BitLockerToGo => EncryptionKind::BitLockerToGo,
});
}
match detected_fs {
Some(DetectedFs::Luks) => Some(EncryptionKind::Luks),
Some(DetectedFs::Unknown) => Some(EncryptionKind::UnrecognizedFilesystem),
_ => None,
}
}
pub struct DeviceImageSource<'a> {
image: &'a DeviceImage,
locator: String,
}
impl<'a> DeviceImageSource<'a> {
#[must_use]
pub fn new(image: &'a DeviceImage, locator: impl Into<String>) -> Self {
Self {
image,
locator: locator.into(),
}
}
}
fn fmt_serial(serial: u32) -> String {
format!("{:04X}-{:04X}", serial >> 16, serial & 0xFFFF)
}
#[must_use]
pub fn export_mbr_hex(image: &DeviceImage, locator: &str) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(
out,
"MBR of {locator} (disk signature {})",
fmt_serial(image.disk_signature)
);
for (i, chunk) in image.mbr.chunks(16).enumerate() {
let hex = chunk.iter().fold(String::new(), |mut acc, b| {
let _ = write!(acc, "{b:02X} ");
acc
});
let ascii: String = chunk
.iter()
.map(|&b| {
if (0x20..0x7F).contains(&b) {
b as char
} else {
'.'
}
})
.collect();
let _ = writeln!(out, "{:08X} {hex:<48} |{ascii}|", i * 16);
}
out
}
impl HistorySource for DeviceImageSource<'_> {
fn claims(&self) -> Vec<Claim> {
let device = DeviceKey(format!("disk-{:08X}", self.image.disk_signature));
let make = |attribute, value| Claim {
device: device.clone(),
attribute,
value: Value::Text(value),
provenance: Provenance {
source: SourceKind::DeviceImage,
locator: self.locator.clone(),
},
};
let mut out = Vec::new();
if let Some(vsn) = self.image.fat_volume_serial {
out.push(make(Attribute::VolumeSerial, fmt_serial(vsn)));
}
if let Some(enc) = self.image.encryption {
out.push(make(Attribute::Encryption, enc.name().to_string()));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fat32_vbr(bs_volid: u32) -> [u8; 512] {
let mut vbr = [0u8; 512];
vbr[3..11].copy_from_slice(b"MSDOS5.0");
vbr[0x52..0x5A].copy_from_slice(b"FAT32 ");
vbr[0x43..0x47].copy_from_slice(&bs_volid.to_le_bytes());
vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
vbr
}
fn fat16_vbr(bs_volid: u32) -> [u8; 512] {
let mut vbr = [0u8; 512];
vbr[3..11].copy_from_slice(b"MSDOS5.0");
vbr[0x36..0x3E].copy_from_slice(b"FAT16 "); vbr[0x27..0x2B].copy_from_slice(&bs_volid.to_le_bytes());
vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
vbr
}
fn ntfs_vbr() -> [u8; 512] {
let mut vbr = [0u8; 512];
vbr[3..11].copy_from_slice(b"NTFS ");
vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
vbr
}
#[test]
fn parses_mbr_disk_signature_and_fat_volume_serial() {
let img = mbr_disk(0xE221_034C, 0x0B, 2, &fat32_vbr(0xB4D8_5399));
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.disk_signature, 0xE221_034C);
assert_eq!(d.fat_volume_serial, Some(0xB4D8_5399));
assert_eq!(d.encryption, None);
}
#[test]
fn a_fat16_partition_reads_bs_volid_at_0x27() {
let img = mbr_disk(1, 0x06, 2, &fat16_vbr(0x1234_5678));
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.fat_volume_serial, Some(0x1234_5678));
}
#[test]
fn a_non_mbr_image_is_rejected() {
assert_eq!(parse_boot_sectors(&[0u8; 512]), None);
assert_eq!(parse_boot_sectors(&[0u8; 10]), None);
}
#[test]
fn read_region_returns_none_past_eof_and_zero_pads_a_short_read() {
use std::io::Cursor;
assert_eq!(
read_region(&mut Cursor::new(vec![0u8; 16]), 4096, 512),
None
);
let mut backing = vec![0xAAu8; 512 + 100];
backing[512..].fill(0xBB);
let s = read_region(&mut Cursor::new(backing), 512, 512)
.expect("short read still yields a buffer");
assert_eq!(s.len(), 512);
assert_eq!(&s[..100], &[0xBBu8; 100]);
assert_eq!(&s[100..], &[0u8; 412]); }
#[test]
fn read_region_propagates_a_read_error() {
struct FailingReader;
impl std::io::Read for FailingReader {
fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("boom"))
}
}
impl std::io::Seek for FailingReader {
fn seek(&mut self, _: std::io::SeekFrom) -> std::io::Result<u64> {
Ok(0)
}
}
assert_eq!(read_region(&mut FailingReader, 0, 512), None);
}
#[test]
fn the_most_specific_encryption_state_wins_across_partitions() {
let mut img = mbr_disk(0x0AAA_0BBB, 0x07, 2, &[0xABu8; 512]); let mut luks = [0u8; 512];
luks[0..6].copy_from_slice(b"LUKS\xba\xbe");
for (i, lba, ptype, vbr) in [(1u8, 4u32, 0x83u8, luks), (2, 6, 0x07, bitlocker_vbr())] {
let e = 0x1BE + i as usize * 16;
img[e + 4] = ptype;
img[e + 8..e + 12].copy_from_slice(&lba.to_le_bytes());
img[e + 12..e + 16].copy_from_slice(&8u32.to_le_bytes());
let off = lba as usize * 512;
img[off..off + 512].copy_from_slice(&vbr);
}
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.encryption, Some(EncryptionKind::BitLocker));
}
#[test]
fn the_first_fat_partitions_serial_is_kept() {
let mut img = mbr_disk(0xF00D_0001, 0x0B, 2, &fat32_vbr(0x1111_2222));
let e = 0x1BE + 16;
img[e + 4] = 0x0B;
img[e + 8..e + 12].copy_from_slice(&8u32.to_le_bytes());
img[e + 12..e + 16].copy_from_slice(&8u32.to_le_bytes());
let off = 8 * 512;
img[off..off + 512].copy_from_slice(&fat32_vbr(0x9999_8888));
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.fat_volume_serial, Some(0x1111_2222));
}
#[test]
fn a_partition_declared_beyond_the_image_is_skipped() {
let mut img = mbr_disk(0xCAFE_0001, 0x0B, 2, &fat32_vbr(0xAABB_CCDD));
let e = 0x1BE + 16;
img[e + 4] = 0x07;
img[e + 8..e + 12].copy_from_slice(&9000u32.to_le_bytes()); img[e + 12..e + 16].copy_from_slice(&8u32.to_le_bytes());
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.fat_volume_serial, Some(0xAABB_CCDD));
}
#[test]
fn an_ntfs_mbr_yields_the_disk_signature_with_no_fat_serial() {
let img = mbr_disk(0xDEAD_BEEF, 0x07, 2, &ntfs_vbr());
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.disk_signature, 0xDEAD_BEEF);
assert_eq!(d.fat_volume_serial, None);
assert_eq!(d.encryption, None);
}
#[test]
fn source_emits_the_fat_volume_serial_keyed_by_disk_signature() {
let img = DeviceImage {
disk_signature: 0xE221_034C,
fat_volume_serial: Some(0xB4D8_5399),
encryption: None,
mbr: [0u8; 512],
};
let claims = DeviceImageSource::new(&img, "rm2.raw").claims();
assert_eq!(claims.len(), 1);
assert_eq!(claims[0].device, DeviceKey("disk-E221034C".to_string()));
assert_eq!(claims[0].attribute, Attribute::VolumeSerial);
assert_eq!(claims[0].value, Value::Text("B4D8-5399".to_string()));
assert_eq!(claims[0].provenance.source, SourceKind::DeviceImage);
assert_eq!(claims[0].provenance.locator, "rm2.raw");
}
#[test]
fn export_mbr_hex_dumps_the_boot_sector_with_signature_and_ascii() {
let img = mbr_disk(0xE221_034C, 0x0B, 2, &fat32_vbr(0xB4D8_5399));
let d = parse_boot_sectors(&img).expect("valid MBR");
let dump = export_mbr_hex(&d, "rm2.raw");
assert!(dump.contains("MBR of rm2.raw"));
assert!(dump.contains("E221-034C"), "disk signature in header");
assert!(dump.contains("00000000 "), "offset column");
assert!(
dump.contains("55 AA"),
"the boot signature bytes are present"
);
assert_eq!(dump.lines().count(), 33);
}
#[test]
fn source_without_a_fat_serial_or_encryption_emits_nothing() {
let img = DeviceImage {
disk_signature: 1,
fat_volume_serial: None,
encryption: None,
mbr: [0u8; 512],
};
assert!(DeviceImageSource::new(&img, "x").claims().is_empty());
}
fn bitlocker_vbr() -> [u8; 512] {
let mut vbr = [0u8; 512];
vbr[0..3].copy_from_slice(&[0xEB, 0x58, 0x90]);
vbr[3..11].copy_from_slice(b"-FVE-FS-");
vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
vbr
}
#[test]
fn bitlocker_signature_is_detected_from_the_vbr() {
let img = mbr_disk(0xABCD_1234, 0x07, 2, &bitlocker_vbr());
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.encryption, Some(EncryptionKind::BitLocker));
assert_eq!(EncryptionKind::BitLocker.name(), "BitLocker");
assert_eq!(d.fat_volume_serial, None);
}
#[test]
fn plain_filesystem_media_is_not_flagged_as_encrypted() {
use forensicnomicon::volume_encryption::VolumeEncryption;
let img = mbr_disk(1, 0x0B, 2, &fat32_vbr(42));
assert_eq!(
parse_boot_sectors(&img).expect("valid MBR").encryption,
None
);
assert_eq!(encryption_of(None, Some(DetectedFs::Ntfs)), None);
assert_eq!(encryption_of(None, None), None);
assert_eq!(
encryption_of(Some(VolumeEncryption::BitLocker), None),
Some(EncryptionKind::BitLocker)
);
assert_eq!(
encryption_of(Some(VolumeEncryption::BitLockerToGo), None),
Some(EncryptionKind::BitLockerToGo)
);
assert_eq!(
encryption_of(None, Some(DetectedFs::Luks)),
Some(EncryptionKind::Luks)
);
assert_eq!(
encryption_of(None, Some(DetectedFs::Unknown)),
Some(EncryptionKind::UnrecognizedFilesystem)
);
}
#[test]
fn an_unrecognized_filesystem_partition_is_flagged_as_possibly_encrypted() {
let img = mbr_disk(7, 0x07, 2, &[0xABu8; 512]);
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.encryption, Some(EncryptionKind::UnrecognizedFilesystem));
assert_eq!(
EncryptionKind::UnrecognizedFilesystem.name(),
"unrecognized-filesystem (possible encrypted container)"
);
}
#[test]
fn source_emits_an_encryption_claim_for_an_encrypted_device() {
let img = DeviceImage {
disk_signature: 0xABCD_1234,
fat_volume_serial: None,
encryption: Some(EncryptionKind::BitLocker),
mbr: [0u8; 512],
};
let claims = DeviceImageSource::new(&img, "x").claims();
assert_eq!(claims.len(), 1);
assert_eq!(claims[0].attribute, Attribute::Encryption);
assert_eq!(claims[0].value, Value::Text("BitLocker".to_string()));
}
fn mbr_disk(disk_sig: u32, ptype: u8, start_lba: u32, vbr: &[u8]) -> Vec<u8> {
let sectors = start_lba as usize + 16;
let mut v = vec![0u8; sectors * 512];
v[0x1B8..0x1BC].copy_from_slice(&disk_sig.to_le_bytes());
v[0x1FE..0x200].copy_from_slice(&[0x55, 0xAA]);
v[0x1BE + 4] = ptype;
v[0x1BE + 8..0x1BE + 12].copy_from_slice(&start_lba.to_le_bytes());
v[0x1BE + 12..0x1BE + 16].copy_from_slice(&8u32.to_le_bytes()); let off = start_lba as usize * 512;
v[off..off + vbr.len()].copy_from_slice(vbr);
v
}
fn to_go_vbr() -> [u8; 512] {
let mut vbr = [0u8; 512];
vbr[0..3].copy_from_slice(&[0xEB, 0x58, 0x90]);
vbr[3..11].copy_from_slice(b"MSWIN4.1");
vbr[0x52..0x5A].copy_from_slice(b"FAT32 ");
vbr[424..440]
.copy_from_slice(&forensicnomicon::volume_encryption::BITLOCKER_IDENTIFIER_GUID);
vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
vbr
}
#[test]
fn bitlocker_to_go_detected_via_identifier_guid_on_a_fat_discovery_volume() {
let img = mbr_disk(0x1111_2222, 0x0B, 2, &to_go_vbr());
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.encryption, Some(EncryptionKind::BitLockerToGo));
assert_eq!(EncryptionKind::BitLockerToGo.name(), "BitLocker To Go");
}
#[test]
fn a_luks_partition_is_flagged_as_luks_encryption() {
let mut vbr = [0u8; 512];
vbr[0..6].copy_from_slice(b"LUKS\xba\xbe"); let img = mbr_disk(0x3333_4444, 0x83, 2, &vbr);
let d = parse_boot_sectors(&img).expect("valid MBR");
assert_eq!(d.encryption, Some(EncryptionKind::Luks));
assert_eq!(EncryptionKind::Luks.name(), "LUKS");
}
#[test]
fn an_apple_partition_map_yields_no_device_image() {
let bs = 512usize;
let mut d = vec![0u8; bs * 2];
d[0..2].copy_from_slice(b"ER"); d[2..4].copy_from_slice(&512u16.to_be_bytes()); d[4..8].copy_from_slice(&4u32.to_be_bytes()); d[bs..bs + 2].copy_from_slice(b"PM"); d[bs + 4..bs + 8].copy_from_slice(&1u32.to_be_bytes()); d[bs + 8..bs + 12].copy_from_slice(&1u32.to_be_bytes()); d[bs + 12..bs + 16].copy_from_slice(&1u32.to_be_bytes()); assert_eq!(parse_boot_sectors(&d), None);
}
fn guid_bytes(s: &str) -> [u8; 16] {
let g: Vec<&str> = s.split('-').collect();
let mut b = [0u8; 16];
b[0..4].copy_from_slice(&u32::from_str_radix(g[0], 16).unwrap().to_le_bytes());
b[4..6].copy_from_slice(&u16::from_str_radix(g[1], 16).unwrap().to_le_bytes());
b[6..8].copy_from_slice(&u16::from_str_radix(g[2], 16).unwrap().to_le_bytes());
b[8..10].copy_from_slice(&u16::from_str_radix(g[3], 16).unwrap().to_be_bytes());
b[10..16].copy_from_slice(&u64::from_str_radix(g[4], 16).unwrap().to_be_bytes()[2..8]);
b
}
fn gpt_entry(type_guid: &str, first: u64, last: u64) -> [u8; 128] {
let mut e = [0u8; 128];
e[0..16].copy_from_slice(&guid_bytes(type_guid));
e[16..32].copy_from_slice(&guid_bytes("00000000-0000-0000-0000-000000000001"));
e[32..40].copy_from_slice(&first.to_le_bytes());
e[40..48].copy_from_slice(&last.to_le_bytes());
e
}
fn gpt_header(my_lba: u64, alt_lba: u64, entry_lba: u64, array_crc: u32) -> [u8; 512] {
let mut s = [0u8; 512];
s[0..8].copy_from_slice(b"EFI PART");
s[8..12].copy_from_slice(&0x0001_0000u32.to_le_bytes());
s[12..16].copy_from_slice(&92u32.to_le_bytes());
s[24..32].copy_from_slice(&my_lba.to_le_bytes());
s[32..40].copy_from_slice(&alt_lba.to_le_bytes());
s[40..48].copy_from_slice(&3u64.to_le_bytes()); s[48..56].copy_from_slice(&61u64.to_le_bytes()); s[56..72].copy_from_slice(&guid_bytes("12345678-1234-5678-1234-567812345678"));
s[72..80].copy_from_slice(&entry_lba.to_le_bytes());
s[80..84].copy_from_slice(&4u32.to_le_bytes()); s[84..88].copy_from_slice(&128u32.to_le_bytes()); s[88..92].copy_from_slice(&array_crc.to_le_bytes());
let crc = gpt_partition_forensic::crc32::checksum(&s[..92]);
s[16..20].copy_from_slice(&crc.to_le_bytes());
s
}
fn build_gpt(bs_volid: u32) -> Vec<u8> {
const SECTOR: usize = 512;
const SECTORS: usize = 64;
let mut disk = vec![0u8; SECTOR * SECTORS];
disk[450] = 0xEE; disk[454..458].copy_from_slice(&1u32.to_le_bytes());
disk[458..462].copy_from_slice(&((SECTORS - 1) as u32).to_le_bytes());
disk[510..512].copy_from_slice(&[0x55, 0xAA]);
let mut array = vec![0u8; 4 * 128];
array[0..128].copy_from_slice(&gpt_entry(
"EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", 3,
30,
));
let array_crc = gpt_partition_forensic::crc32::checksum(&array);
disk[SECTOR..SECTOR + 512].copy_from_slice(&gpt_header(1, 63, 2, array_crc));
disk[2 * SECTOR..2 * SECTOR + array.len()].copy_from_slice(&array);
disk[62 * SECTOR..62 * SECTOR + array.len()].copy_from_slice(&array);
disk[63 * SECTOR..63 * SECTOR + 512].copy_from_slice(&gpt_header(63, 1, 62, array_crc));
let vbr = 3 * SECTOR;
disk[vbr + 3..vbr + 11].copy_from_slice(b"MSDOS5.0");
disk[vbr + 0x52..vbr + 0x5A].copy_from_slice(b"FAT32 ");
disk[vbr + 0x43..vbr + 0x47].copy_from_slice(&bs_volid.to_le_bytes());
disk[vbr + 510..vbr + 512].copy_from_slice(&[0x55, 0xAA]);
disk
}
#[test]
fn a_gpt_disk_is_not_false_flagged_and_its_fat_partition_is_read() {
let img = build_gpt(0xB4D8_5399);
let d = parse_boot_sectors(&img).expect("valid GPT");
assert_eq!(
d.encryption, None,
"GPT header must not be flagged as encrypted"
);
assert_eq!(d.fat_volume_serial, Some(0xB4D8_5399));
}
#[test]
fn a_gpt_partition_beyond_the_image_is_skipped() {
const SECTOR: usize = 512;
const SECTORS: usize = 64;
let mut disk = vec![0u8; SECTOR * SECTORS];
disk[450] = 0xEE;
disk[454..458].copy_from_slice(&1u32.to_le_bytes());
disk[458..462].copy_from_slice(&((SECTORS - 1) as u32).to_le_bytes());
disk[510..512].copy_from_slice(&[0x55, 0xAA]);
let mut array = vec![0u8; 4 * 128];
array[0..128].copy_from_slice(&gpt_entry("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", 3, 30));
array[128..256].copy_from_slice(&gpt_entry(
"EBD0A0A2-B9E5-4433-87C0-68B6B72699C7",
9000,
9030,
));
let array_crc = gpt_partition_forensic::crc32::checksum(&array);
disk[SECTOR..SECTOR + 512].copy_from_slice(&gpt_header(1, 63, 2, array_crc));
disk[2 * SECTOR..2 * SECTOR + array.len()].copy_from_slice(&array);
disk[62 * SECTOR..62 * SECTOR + array.len()].copy_from_slice(&array);
disk[63 * SECTOR..63 * SECTOR + 512].copy_from_slice(&gpt_header(63, 1, 62, array_crc));
let vbr = 3 * SECTOR;
disk[vbr + 3..vbr + 11].copy_from_slice(b"MSDOS5.0");
disk[vbr + 0x52..vbr + 0x5A].copy_from_slice(b"FAT32 ");
disk[vbr + 0x43..vbr + 0x47].copy_from_slice(&0xAABB_CCDDu32.to_le_bytes());
disk[vbr + 510..vbr + 512].copy_from_slice(&[0x55, 0xAA]);
let d = parse_boot_sectors(&disk).expect("valid GPT");
assert_eq!(d.fat_volume_serial, Some(0xAABB_CCDD));
}
}