208 lines
7.8 KiB
Rust
208 lines
7.8 KiB
Rust
//! The fixed 64-byte header (Chapter 8 §"The Fixed Header").
|
|
//!
|
|
//! The header sits at file offset zero, identifies the format, and locates the
|
|
//! superblock slots. It is written once at bundle creation and **never changes
|
|
//! thereafter** (except a major-version rewrite of the whole file), so commits
|
|
//! never touch it — which is exactly why a crash during a commit can never
|
|
//! corrupt the header.
|
|
//!
|
|
//! Layout (little-endian; see `DECISIONS.md` for the prototype byte-layout
|
|
//! choice that anticipates the deferred Binary Format companion):
|
|
//!
|
|
//! | range | field |
|
|
//! |---------|------------------------|
|
|
//! | `0..8` | magic `"MUSCBND\0"` |
|
|
//! | `8..10` | `format_major` (u16) |
|
|
//! | `10..12`| `format_minor` (u16) |
|
|
//! | `12..16`| `header_length` (u32) |
|
|
//! | `16..24`| `superblock_a_offset` |
|
|
//! | `24..32`| `superblock_b_offset` |
|
|
//! | `32..48`| `file_uuid` (16 bytes) |
|
|
//! | `48..60`| reserved (zero) |
|
|
//! | `60..64`| `header_crc` (CRC-32C of `0..60`) |
|
|
|
|
use crate::codec::{DecodeError, Reader, Writer};
|
|
use crate::crc::crc32c;
|
|
use crate::error::BundleError;
|
|
use crate::ids::FileUuid;
|
|
|
|
/// The fixed header length, in bytes. Always 64 in this format version.
|
|
pub const HEADER_LEN: u64 = 64;
|
|
|
|
/// Offset of superblock slot A (immediately after the header).
|
|
pub const SLOT_A_OFFSET: u64 = 64;
|
|
|
|
/// Offset of superblock slot B (after slot A).
|
|
pub const SLOT_B_OFFSET: u64 = 320;
|
|
|
|
/// The format major version this crate writes and understands.
|
|
pub const FORMAT_MAJOR: u16 = 0;
|
|
|
|
/// The format minor version this crate writes.
|
|
pub const FORMAT_MINOR: u16 = 1;
|
|
|
|
/// Byte range covered by the header CRC: everything before the CRC field.
|
|
const HEADER_CRC_RANGE: usize = 60;
|
|
|
|
/// The fixed bundle header.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub struct FixedHeader {
|
|
/// Major format version (non-backward-compatible changes).
|
|
pub format_major: u16,
|
|
/// Minor format version (backward-compatible changes).
|
|
pub format_minor: u16,
|
|
/// Header length in bytes (currently always 64).
|
|
pub header_length: u32,
|
|
/// Offset of superblock slot A (currently always 64).
|
|
pub superblock_a_offset: u64,
|
|
/// Offset of superblock slot B (currently always 320).
|
|
pub superblock_b_offset: u64,
|
|
/// Physical-bundle UUID, set at creation, changed on Save As.
|
|
pub file_uuid: FileUuid,
|
|
}
|
|
|
|
impl FixedHeader {
|
|
/// Builds the canonical header for a freshly created bundle.
|
|
pub fn new(file_uuid: FileUuid) -> Self {
|
|
FixedHeader {
|
|
format_major: FORMAT_MAJOR,
|
|
format_minor: FORMAT_MINOR,
|
|
header_length: HEADER_LEN as u32,
|
|
superblock_a_offset: SLOT_A_OFFSET,
|
|
superblock_b_offset: SLOT_B_OFFSET,
|
|
file_uuid,
|
|
}
|
|
}
|
|
|
|
/// Serializes to the fixed 64-byte form, computing and appending the CRC.
|
|
pub fn encode(&self) -> [u8; HEADER_LEN as usize] {
|
|
let mut w = Writer::with_capacity(HEADER_CRC_RANGE);
|
|
w.put_bytes(&epiphany_determinism::BUNDLE_MAGIC);
|
|
w.put_u16(self.format_major);
|
|
w.put_u16(self.format_minor);
|
|
w.put_u32(self.header_length);
|
|
w.put_u64(self.superblock_a_offset);
|
|
w.put_u64(self.superblock_b_offset);
|
|
self.file_uuid.encode(&mut w);
|
|
w.put_bytes(&[0u8; 12]); // reserved bytes 48..60
|
|
debug_assert_eq!(w.len(), HEADER_CRC_RANGE);
|
|
|
|
let mut buf = [0u8; HEADER_LEN as usize];
|
|
buf[..HEADER_CRC_RANGE].copy_from_slice(w.as_bytes());
|
|
let crc = crc32c(&buf[0..HEADER_CRC_RANGE]);
|
|
buf[60..64].copy_from_slice(&crc.to_le_bytes());
|
|
buf
|
|
}
|
|
|
|
/// Parses and validates a 64-byte header: magic, then CRC, then the
|
|
/// version/length it can interpret (Chapter 8 §"The Fixed Header":
|
|
/// *"Readers MUST verify the magic bytes and the header CRC before
|
|
/// consulting any other part of the file"*; reserved bytes are ignored).
|
|
pub fn decode(bytes: &[u8]) -> Result<Self, BundleError> {
|
|
if bytes.len() < HEADER_LEN as usize {
|
|
return Err(BundleError::HeaderCrcMismatch);
|
|
}
|
|
let buf = &bytes[..HEADER_LEN as usize];
|
|
if buf[0..8] != epiphany_determinism::BUNDLE_MAGIC {
|
|
return Err(BundleError::BadHeaderMagic);
|
|
}
|
|
let stored_crc = u32::from_le_bytes([buf[60], buf[61], buf[62], buf[63]]);
|
|
if stored_crc != crc32c(&buf[0..HEADER_CRC_RANGE]) {
|
|
return Err(BundleError::HeaderCrcMismatch);
|
|
}
|
|
|
|
// Magic and CRC verified; parse the fields from the CRC-covered region.
|
|
let mut r = Reader::new(&buf[0..HEADER_CRC_RANGE]);
|
|
let _magic = r.take_array::<8>()?;
|
|
let format_major = r.get_u16()?;
|
|
let format_minor = r.get_u16()?;
|
|
if format_major != FORMAT_MAJOR {
|
|
return Err(BundleError::UnsupportedFormatVersion {
|
|
major: format_major,
|
|
minor: format_minor,
|
|
});
|
|
}
|
|
let header_length = r.get_u32()?;
|
|
if header_length != HEADER_LEN as u32 {
|
|
return Err(BundleError::UnsupportedHeaderLength {
|
|
declared: header_length,
|
|
});
|
|
}
|
|
let superblock_a_offset = r.get_u64()?;
|
|
let superblock_b_offset = r.get_u64()?;
|
|
// This format version fixes the slot offsets; `Bundle::open` reads them
|
|
// at the constants, so a CRC-valid header that disagrees is foreign and
|
|
// must be rejected rather than silently honored-then-ignored.
|
|
if superblock_a_offset != SLOT_A_OFFSET || superblock_b_offset != SLOT_B_OFFSET {
|
|
return Err(BundleError::Decode(DecodeError::Malformed(
|
|
"header superblock offsets are not the fixed 64 and 320",
|
|
)));
|
|
}
|
|
let file_uuid = FileUuid::decode(&mut r)?;
|
|
// Remaining bytes are reserved and ignored.
|
|
Ok(FixedHeader {
|
|
format_major,
|
|
format_minor,
|
|
header_length,
|
|
superblock_a_offset,
|
|
superblock_b_offset,
|
|
file_uuid,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn header_round_trips() {
|
|
let h = FixedHeader::new(FileUuid([0xAB; 16]));
|
|
let bytes = h.encode();
|
|
assert_eq!(bytes.len(), 64);
|
|
assert_eq!(FixedHeader::decode(&bytes).unwrap(), h);
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_bytes_are_zero_and_offsets_are_fixed() {
|
|
let bytes = FixedHeader::new(FileUuid::ZERO).encode();
|
|
assert_eq!(&bytes[48..60], &[0u8; 12]);
|
|
assert_eq!(&bytes[0..8], &epiphany_determinism::BUNDLE_MAGIC);
|
|
assert_eq!(u64::from_le_bytes(bytes[16..24].try_into().unwrap()), 64);
|
|
assert_eq!(u64::from_le_bytes(bytes[24..32].try_into().unwrap()), 320);
|
|
}
|
|
|
|
#[test]
|
|
fn bad_magic_is_rejected() {
|
|
let mut bytes = FixedHeader::new(FileUuid::ZERO).encode();
|
|
bytes[0] = b'X';
|
|
assert!(matches!(
|
|
FixedHeader::decode(&bytes),
|
|
Err(BundleError::BadHeaderMagic)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn a_corrupt_header_byte_fails_crc() {
|
|
let mut bytes = FixedHeader::new(FileUuid([1; 16])).encode();
|
|
bytes[40] ^= 0xFF; // flip a file_uuid byte
|
|
assert!(matches!(
|
|
FixedHeader::decode(&bytes),
|
|
Err(BundleError::HeaderCrcMismatch)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_bytes_are_ignored_on_read() {
|
|
// A future minor version may use reserved bytes; current readers must
|
|
// ignore them. They are *not* in the CRC-excluded region, though, so to
|
|
// keep the header valid we recompute the CRC after setting them.
|
|
let mut bytes = FixedHeader::new(FileUuid([2; 16])).encode();
|
|
bytes[50] = 0x99;
|
|
let crc = crc32c(&bytes[0..60]);
|
|
bytes[60..64].copy_from_slice(&crc.to_le_bytes());
|
|
// Still decodes; the reserved byte does not affect the parsed fields.
|
|
assert!(FixedHeader::decode(&bytes).is_ok());
|
|
}
|
|
}
|