use crc32c::crc32c;
use crate::crypto::KdfParams;
use crate::format::{
AeadAlgo, BlockKind, CompressionAlgo, FecAlgo, FormatError, KdfAlgo, BLOCK_RECORD_FRAMING_LEN,
BOOTSTRAP_SIDECAR_HEADER_LEN, CRITICAL_METADATA_IMAGE_FIXED_LEN,
CRITICAL_METADATA_RECOVERY_HEADER_LEN, CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN,
CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_EXTENSION_HEADER_LEN, CRYPTO_EXTENSION_MAX_VALUE_LEN,
CRYPTO_HEADER_FIXED_LEN, CRYPTO_HEADER_HMAC_LEN, FORMAT_VERSION, IMAGE_CRC_LEN,
MANIFEST_FOOTER_LEN, READER_MAX_BLOCK_SIZE, READER_MAX_CHUNK_SIZE,
READER_MAX_CRYPTO_HEADER_LEN, READER_MAX_ENVELOPE_TARGET_SIZE, READER_MAX_FEC_CLASS_SHARDS,
READER_MAX_INDEX_FEC_CLASS_SHARDS, READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS,
READER_MAX_PATH_LENGTH, READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN,
READER_MAX_ROOT_AUTH_FOOTER_LEN, READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN,
READER_MAX_STRIPE_WIDTH, ROOT_AUTH_FOOTER_FIXED_LEN, ROOT_AUTH_SPEC_ID,
SERIALIZED_REGION_HEADER_LEN, VOLUME_FORMAT_REV, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
};
use crate::raw_stream_profile::{
validate_raw_stream_content_model_extension, RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG,
};
const TZAP_MAGIC: [u8; 4] = *b"TZAP";
const TZCH_MAGIC: [u8; 4] = *b"TZCH";
const TZBK_MAGIC: [u8; 4] = *b"TZBK";
const TZMF_MAGIC: [u8; 4] = *b"TZMF";
const TZVT_MAGIC: [u8; 4] = *b"TZVT";
const TZRA_MAGIC: [u8; 4] = *b"TZRA";
const TZBS_MAGIC: [u8; 4] = *b"TZBS";
const TZMI_MAGIC: [u8; 4] = *b"TZMI";
const TZCR_MAGIC: [u8; 4] = *b"TZCR";
const TZCS_MAGIC: [u8; 4] = *b"TZCS";
const TZCL_MAGIC: [u8; 4] = *b"TZCL";
const BLOCK_LAST_DATA_FLAG: u8 = 0x01;
const BLOCK_RESERVED_FLAGS: u8 = !BLOCK_LAST_DATA_FLAG;
const SIDECAR_MANIFEST_PRESENT: u32 = 0x01;
const SIDECAR_INDEX_ROOT_PRESENT: u32 = 0x02;
const SIDECAR_DICTIONARY_PRESENT: u32 = 0x04;
const SIDECAR_KNOWN_FLAGS: u32 =
SIDECAR_MANIFEST_PRESENT | SIDECAR_INDEX_ROOT_PRESENT | SIDECAR_DICTIONARY_PRESENT;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VolumeHeader {
pub format_version: u16,
pub volume_format_rev: u16,
pub volume_index: u32,
pub stripe_width: u32,
pub archive_uuid: [u8; 16],
pub session_id: [u8; 16],
pub crypto_header_offset: u32,
pub crypto_header_length: u32,
pub header_crc32c: u32,
}
impl VolumeHeader {
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
expect_len("VolumeHeader", VOLUME_HEADER_LEN, bytes.len())?;
expect_magic("VolumeHeader", TZAP_MAGIC, &bytes[0..4])?;
expect_crc("VolumeHeader", &bytes[..124], read_u32(bytes, 124)?)?;
expect_zero("VolumeHeader", &bytes[56..124])?;
let header = Self {
format_version: read_u16(bytes, 4)?,
volume_format_rev: read_u16(bytes, 6)?,
volume_index: read_u32(bytes, 8)?,
stripe_width: read_u32(bytes, 12)?,
archive_uuid: read_array_16(bytes, 16)?,
session_id: read_array_16(bytes, 32)?,
crypto_header_offset: read_u32(bytes, 48)?,
crypto_header_length: read_u32(bytes, 52)?,
header_crc32c: read_u32(bytes, 124)?,
};
header.validate()?;
Ok(header)
}
pub fn validate(&self) -> Result<(), FormatError> {
if self.format_version != FORMAT_VERSION {
return Err(FormatError::UnsupportedFormatVersion(self.format_version));
}
if self.volume_format_rev != VOLUME_FORMAT_REV {
return Err(FormatError::UnsupportedVolumeFormatRevision(
self.volume_format_rev,
));
}
if self.stripe_width == 0 {
return Err(FormatError::ZeroStripeWidth);
}
if self.volume_index >= self.stripe_width {
return Err(FormatError::VolumeIndexOutOfRange {
volume_index: self.volume_index,
stripe_width: self.stripe_width,
});
}
if self.crypto_header_offset != VOLUME_HEADER_LEN as u32 {
return Err(FormatError::NonCanonicalCryptoHeaderOffset(
self.crypto_header_offset,
));
}
if self.crypto_header_length > READER_MAX_CRYPTO_HEADER_LEN {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "CryptoHeader length",
cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
actual: self.crypto_header_length as u64,
});
}
Ok(())
}
pub fn to_bytes(&self) -> [u8; VOLUME_HEADER_LEN] {
let mut bytes = [0u8; VOLUME_HEADER_LEN];
bytes[0..4].copy_from_slice(&TZAP_MAGIC);
write_u16(&mut bytes, 4, self.format_version);
write_u16(&mut bytes, 6, self.volume_format_rev);
write_u32(&mut bytes, 8, self.volume_index);
write_u32(&mut bytes, 12, self.stripe_width);
bytes[16..32].copy_from_slice(&self.archive_uuid);
bytes[32..48].copy_from_slice(&self.session_id);
write_u32(&mut bytes, 48, self.crypto_header_offset);
write_u32(&mut bytes, 52, self.crypto_header_length);
let crc = crc32c(&bytes[..124]);
write_u32(&mut bytes, 124, crc);
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CryptoHeaderFixed {
pub length: u32,
pub compression_algo: CompressionAlgo,
pub aead_algo: AeadAlgo,
pub fec_algo: FecAlgo,
pub kdf_algo: KdfAlgo,
pub chunk_size: u32,
pub envelope_target_size: u32,
pub block_size: u32,
pub fec_data_shards: u16,
pub fec_parity_shards: u16,
pub index_fec_data_shards: u16,
pub index_fec_parity_shards: u16,
pub index_root_fec_data_shards: u16,
pub index_root_fec_parity_shards: u16,
pub stripe_width: u32,
pub volume_loss_tolerance: u8,
pub bit_rot_buffer_pct: u8,
pub has_dictionary: u8,
pub max_path_length: u32,
pub expected_volume_size: u64,
}
impl CryptoHeaderFixed {
pub fn parse(bytes: &[u8], volume_crypto_header_length: u32) -> Result<Self, FormatError> {
expect_len("CryptoHeaderFixed", CRYPTO_HEADER_FIXED_LEN, bytes.len())?;
expect_magic("CryptoHeaderFixed", TZCH_MAGIC, &bytes[0..4])?;
expect_zero("CryptoHeaderFixed", &bytes[47..48])?;
expect_zero("CryptoHeaderFixed", &bytes[60..76])?;
let length = read_u32(bytes, 4)?;
if length != volume_crypto_header_length {
return Err(FormatError::CryptoHeaderLengthMismatch {
fixed: length,
volume: volume_crypto_header_length,
});
}
if length > READER_MAX_CRYPTO_HEADER_LEN {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "CryptoHeader length",
cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
actual: length as u64,
});
}
let header = Self {
length,
compression_algo: CompressionAlgo::try_from(read_u16(bytes, 8)?)?,
aead_algo: AeadAlgo::try_from(read_u16(bytes, 10)?)?,
fec_algo: FecAlgo::try_from(read_u16(bytes, 12)?)?,
kdf_algo: KdfAlgo::try_from(read_u16(bytes, 14)?)?,
chunk_size: read_u32(bytes, 16)?,
envelope_target_size: read_u32(bytes, 20)?,
block_size: read_u32(bytes, 24)?,
fec_data_shards: read_u16(bytes, 28)?,
fec_parity_shards: read_u16(bytes, 30)?,
index_fec_data_shards: read_u16(bytes, 32)?,
index_fec_parity_shards: read_u16(bytes, 34)?,
index_root_fec_data_shards: read_u16(bytes, 36)?,
index_root_fec_parity_shards: read_u16(bytes, 38)?,
stripe_width: read_u32(bytes, 40)?,
volume_loss_tolerance: bytes[44],
bit_rot_buffer_pct: bytes[45],
has_dictionary: bytes[46],
max_path_length: read_u32(bytes, 48)?,
expected_volume_size: read_u64(bytes, 52)?,
};
header.validate_supported_profile()?;
Ok(header)
}
pub fn validate_supported_profile(&self) -> Result<(), FormatError> {
if self.compression_algo != CompressionAlgo::ZstdFramed {
return Err(FormatError::UnsupportedCompression(self.compression_algo));
}
if self.fec_algo != FecAlgo::ReedSolomonGF16 {
return Err(FormatError::UnsupportedFec(self.fec_algo));
}
match (self.aead_algo, self.kdf_algo) {
(AeadAlgo::None, KdfAlgo::None) => {}
(aead_algo, KdfAlgo::Raw | KdfAlgo::Argon2id) if aead_algo.is_encrypted() => {}
_ => {
return Err(FormatError::InvalidProtectionMode {
aead_algo: self.aead_algo,
kdf_algo: self.kdf_algo,
});
}
}
if self.has_dictionary > 1 {
return Err(FormatError::InvalidBoolean {
field: "has_dictionary",
value: self.has_dictionary,
});
}
if self.stripe_width == 0 {
return Err(FormatError::ZeroStripeWidth);
}
if self.stripe_width > READER_MAX_STRIPE_WIDTH {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "stripe_width",
cap: READER_MAX_STRIPE_WIDTH as u64,
actual: self.stripe_width as u64,
});
}
if self.volume_loss_tolerance as u32 >= self.stripe_width {
return Err(FormatError::VolumeLossToleranceOutOfRange {
volume_loss_tolerance: self.volume_loss_tolerance,
stripe_width: self.stripe_width,
});
}
if self.bit_rot_buffer_pct > 100 {
return Err(FormatError::BitRotBufferPctTooLarge(
self.bit_rot_buffer_pct,
));
}
if self.fec_data_shards == 0 {
return Err(FormatError::ZeroDataShardMaximum {
field: "fec_data_shards",
});
}
if self.index_fec_data_shards == 0 {
return Err(FormatError::ZeroDataShardMaximum {
field: "index_fec_data_shards",
});
}
if self.index_root_fec_data_shards == 0 {
return Err(FormatError::ZeroDataShardMaximum {
field: "index_root_fec_data_shards",
});
}
validate_fec_class_shards(
"fec_data_shards + fec_parity_shards",
self.fec_data_shards,
self.fec_parity_shards,
READER_MAX_FEC_CLASS_SHARDS,
)?;
validate_fec_class_shards(
"index_fec_data_shards + index_fec_parity_shards",
self.index_fec_data_shards,
self.index_fec_parity_shards,
READER_MAX_INDEX_FEC_CLASS_SHARDS,
)?;
validate_fec_class_shards(
"index_root_fec_data_shards + index_root_fec_parity_shards",
self.index_root_fec_data_shards,
self.index_root_fec_parity_shards,
READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS,
)?;
if self.chunk_size == 0 {
return Err(FormatError::ZeroChunkSize);
}
if self.envelope_target_size == 0 {
return Err(FormatError::ZeroEnvelopeTargetSize);
}
if self.chunk_size > self.envelope_target_size {
return Err(FormatError::ChunkSizeExceedsEnvelopeTarget {
chunk_size: self.chunk_size,
envelope_target_size: self.envelope_target_size,
});
}
if self.chunk_size > READER_MAX_CHUNK_SIZE {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "chunk_size",
cap: READER_MAX_CHUNK_SIZE as u64,
actual: self.chunk_size as u64,
});
}
if self.envelope_target_size > READER_MAX_ENVELOPE_TARGET_SIZE {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "envelope_target_size",
cap: READER_MAX_ENVELOPE_TARGET_SIZE as u64,
actual: self.envelope_target_size as u64,
});
}
if self.block_size < 4096 {
return Err(FormatError::BlockSizeTooSmall(self.block_size));
}
if self.block_size % 2 != 0 {
return Err(FormatError::OddBlockSize(self.block_size));
}
validate_fec_class_data_shards("fec_data_shards", self.fec_data_shards, self.block_size)?;
validate_fec_class_data_shards(
"index_fec_data_shards",
self.index_fec_data_shards,
self.block_size,
)?;
validate_fec_class_data_shards(
"index_root_fec_data_shards",
self.index_root_fec_data_shards,
self.block_size,
)?;
if self.block_size > READER_MAX_BLOCK_SIZE {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "block_size",
cap: READER_MAX_BLOCK_SIZE as u64,
actual: self.block_size as u64,
});
}
if self.max_path_length > READER_MAX_PATH_LENGTH {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "max_path_length",
cap: READER_MAX_PATH_LENGTH as u64,
actual: self.max_path_length as u64,
});
}
Ok(())
}
pub fn to_bytes(&self) -> [u8; CRYPTO_HEADER_FIXED_LEN] {
let mut bytes = [0u8; CRYPTO_HEADER_FIXED_LEN];
bytes[0..4].copy_from_slice(&TZCH_MAGIC);
write_u32(&mut bytes, 4, self.length);
write_u16(&mut bytes, 8, self.compression_algo as u16);
write_u16(&mut bytes, 10, self.aead_algo as u16);
write_u16(&mut bytes, 12, self.fec_algo as u16);
write_u16(&mut bytes, 14, self.kdf_algo as u16);
write_u32(&mut bytes, 16, self.chunk_size);
write_u32(&mut bytes, 20, self.envelope_target_size);
write_u32(&mut bytes, 24, self.block_size);
write_u16(&mut bytes, 28, self.fec_data_shards);
write_u16(&mut bytes, 30, self.fec_parity_shards);
write_u16(&mut bytes, 32, self.index_fec_data_shards);
write_u16(&mut bytes, 34, self.index_fec_parity_shards);
write_u16(&mut bytes, 36, self.index_root_fec_data_shards);
write_u16(&mut bytes, 38, self.index_root_fec_parity_shards);
write_u32(&mut bytes, 40, self.stripe_width);
bytes[44] = self.volume_loss_tolerance;
bytes[45] = self.bit_rot_buffer_pct;
bytes[46] = self.has_dictionary;
write_u32(&mut bytes, 48, self.max_path_length);
write_u64(&mut bytes, 52, self.expected_volume_size);
bytes
}
}
fn validate_fec_class_shards(
field: &'static str,
data_shards: u16,
parity_shards: u16,
cap: u32,
) -> Result<(), FormatError> {
let total = data_shards as u32 + parity_shards as u32;
if total > cap {
return Err(FormatError::ReaderResourceLimitExceeded {
field,
cap: cap as u64,
actual: total as u64,
});
}
Ok(())
}
fn validate_fec_class_data_shards(
field: &'static str,
data_shards: u16,
block_size: u32,
) -> Result<(), FormatError> {
let max_data_shards = u32::MAX as u64 / block_size as u64;
if (data_shards as u64) > max_data_shards {
return Err(FormatError::ReaderResourceLimitExceeded {
field,
cap: max_data_shards,
actual: data_shards as u64,
});
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtensionTlv<'a> {
pub tag: u16,
pub value: &'a [u8],
}
pub fn scan_crypto_extension_tlvs(bytes: &[u8]) -> Result<Vec<ExtensionTlv<'_>>, FormatError> {
let mut offset = 0usize;
let mut extensions = Vec::new();
loop {
if offset == bytes.len() {
return Err(FormatError::MissingExtensionTerminator);
}
if bytes.len() - offset < CRYPTO_EXTENSION_HEADER_LEN {
return Err(FormatError::TruncatedExtensionHeader);
}
let tag = read_u16(bytes, offset)?;
let length = read_u32(bytes, offset + 2)?;
offset += CRYPTO_EXTENSION_HEADER_LEN;
if tag == 0 {
if length != 0 {
return Err(FormatError::MalformedExtensionTerminator);
}
if offset != bytes.len() {
return Err(FormatError::BytesAfterExtensionTerminator);
}
return Ok(extensions);
}
if length > CRYPTO_EXTENSION_MAX_VALUE_LEN {
return Err(FormatError::ExtensionPayloadTooLarge(length));
}
let length = length as usize;
if bytes.len() - offset < length {
return Err(FormatError::TruncatedExtensionPayload);
}
extensions.push(ExtensionTlv {
tag,
value: &bytes[offset..offset + length],
});
offset += length;
}
}
pub fn validate_crypto_extension_semantics(
extensions: &[ExtensionTlv<'_>],
) -> Result<(), FormatError> {
let mut seen_known = Vec::new();
for extension in extensions {
let ext_tag = extension.tag & 0x7fff;
let is_critical = extension.tag & 0x8000 != 0;
if matches!(ext_tag, 0x0004 | 0x0006) {
return Err(FormatError::ForbiddenExtensionTag(ext_tag));
}
if ext_tag == RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG {
if is_critical {
if seen_known.contains(&ext_tag) {
return Err(FormatError::DuplicateKnownExtension(ext_tag));
}
validate_raw_stream_content_model_extension(is_critical, extension.value)?;
seen_known.push(ext_tag);
}
continue;
}
if is_known_extension(ext_tag) {
if seen_known.contains(&ext_tag) {
return Err(FormatError::DuplicateKnownExtension(ext_tag));
}
validate_known_extension(ext_tag, extension.value)?;
seen_known.push(ext_tag);
} else if is_critical {
return Err(FormatError::UnknownCriticalExtension(ext_tag));
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CryptoHeader<'a> {
pub fixed: CryptoHeaderFixed,
pub kdf_params: KdfParams,
pub extensions: Vec<ExtensionTlv<'a>>,
pub header_hmac: [u8; 32],
pub hmac_covered_bytes: &'a [u8],
}
impl<'a> CryptoHeader<'a> {
pub fn parse(bytes: &'a [u8], volume_crypto_header_length: u32) -> Result<Self, FormatError> {
let declared_len = volume_crypto_header_length as usize;
if volume_crypto_header_length > READER_MAX_CRYPTO_HEADER_LEN {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "CryptoHeader length",
cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
actual: volume_crypto_header_length as u64,
});
}
if bytes.len() != declared_len {
return Err(FormatError::InvalidLength {
structure: "CryptoHeader",
expected: declared_len,
actual: bytes.len(),
});
}
let min_len =
CRYPTO_HEADER_FIXED_LEN + 2 + CRYPTO_EXTENSION_HEADER_LEN + CRYPTO_HEADER_HMAC_LEN;
if bytes.len() < min_len {
return Err(FormatError::CryptoHeaderTooShort {
min: min_len,
actual: bytes.len(),
});
}
let fixed = CryptoHeaderFixed::parse(
&bytes[..CRYPTO_HEADER_FIXED_LEN],
volume_crypto_header_length,
)?;
let hmac_offset = bytes.len() - CRYPTO_HEADER_HMAC_LEN;
let (kdf_params, kdf_len) =
KdfParams::parse(fixed.kdf_algo, &bytes[CRYPTO_HEADER_FIXED_LEN..hmac_offset])?;
let extension_bytes = &bytes[CRYPTO_HEADER_FIXED_LEN + kdf_len..hmac_offset];
let extensions = scan_crypto_extension_tlvs(extension_bytes)?;
let header_hmac = read_array_32(bytes, hmac_offset)?;
Ok(Self {
fixed,
kdf_params,
extensions,
header_hmac,
hmac_covered_bytes: &bytes[..hmac_offset],
})
}
pub fn validate_extension_semantics(&self) -> Result<(), FormatError> {
validate_crypto_extension_semantics(&self.extensions)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockRecord {
pub block_index: u64,
pub kind: BlockKind,
pub flags: u8,
pub payload: Vec<u8>,
pub record_crc32c: u32,
}
impl BlockRecord {
pub fn parse(bytes: &[u8], block_size: usize) -> Result<Self, FormatError> {
let expected = block_size + BLOCK_RECORD_FRAMING_LEN;
expect_len("BlockRecord", expected, bytes.len())?;
expect_magic("BlockRecord", TZBK_MAGIC, &bytes[0..4])?;
expect_zero("BlockRecord", &bytes[14..16])?;
expect_crc(
"BlockRecord",
&bytes[..16 + block_size],
read_u32(bytes, 16 + block_size)?,
)?;
let kind = BlockKind::try_from(bytes[12])?;
let flags = bytes[13];
if flags & BLOCK_RESERVED_FLAGS != 0 {
return Err(FormatError::InvalidBlockFlags(flags));
}
if kind.is_parity() && flags & BLOCK_LAST_DATA_FLAG != 0 {
return Err(FormatError::ParityBlockHasLastDataFlag);
}
Ok(Self {
block_index: read_u64(bytes, 4)?,
kind,
flags,
payload: bytes[16..16 + block_size].to_vec(),
record_crc32c: read_u32(bytes, 16 + block_size)?,
})
}
pub fn is_last_data(&self) -> bool {
self.flags & BLOCK_LAST_DATA_FLAG != 0
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = vec![0u8; self.payload.len() + BLOCK_RECORD_FRAMING_LEN];
bytes[0..4].copy_from_slice(&TZBK_MAGIC);
write_u64(&mut bytes, 4, self.block_index);
bytes[12] = self.kind as u8;
bytes[13] = self.flags;
bytes[16..16 + self.payload.len()].copy_from_slice(&self.payload);
let crc = crc32c(&bytes[..16 + self.payload.len()]);
let crc_offset = 16 + self.payload.len();
write_u32(&mut bytes, crc_offset, crc);
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManifestFooter {
pub archive_uuid: [u8; 16],
pub session_id: [u8; 16],
pub volume_index: u32,
pub is_authoritative: u8,
pub total_volumes: u32,
pub index_root_first_block: u64,
pub index_root_data_block_count: u32,
pub index_root_parity_block_count: u32,
pub index_root_encrypted_size: u32,
pub index_root_decompressed_size: u32,
pub manifest_hmac: [u8; 32],
}
impl ManifestFooter {
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
expect_len("ManifestFooter", MANIFEST_FOOTER_LEN, bytes.len())?;
expect_magic("ManifestFooter", TZMF_MAGIC, &bytes[0..4])?;
expect_zero("ManifestFooter", &bytes[41..44])?;
expect_zero("ManifestFooter", &bytes[72..104])?;
let is_authoritative = bytes[40];
if is_authoritative > 1 {
return Err(FormatError::InvalidAuthoritativeFlag(is_authoritative));
}
Ok(Self {
archive_uuid: read_array_16(bytes, 4)?,
session_id: read_array_16(bytes, 20)?,
volume_index: read_u32(bytes, 36)?,
is_authoritative,
total_volumes: read_u32(bytes, 44)?,
index_root_first_block: read_u64(bytes, 48)?,
index_root_data_block_count: read_u32(bytes, 56)?,
index_root_parity_block_count: read_u32(bytes, 60)?,
index_root_encrypted_size: read_u32(bytes, 64)?,
index_root_decompressed_size: read_u32(bytes, 68)?,
manifest_hmac: read_array_32(bytes, 104)?,
})
}
pub fn validate_index_root_extent(&self, block_size: u32) -> Result<(), FormatError> {
if self.index_root_data_block_count == 0 || self.index_root_encrypted_size == 0 {
return Err(FormatError::EmptyIndexRootExtent);
}
let expected = self
.index_root_data_block_count
.checked_mul(block_size)
.ok_or(FormatError::IndexRootSizeMismatch)?;
if expected != self.index_root_encrypted_size {
return Err(FormatError::IndexRootSizeMismatch);
}
Ok(())
}
pub fn to_bytes(&self) -> [u8; MANIFEST_FOOTER_LEN] {
let mut bytes = [0u8; MANIFEST_FOOTER_LEN];
bytes[0..4].copy_from_slice(&TZMF_MAGIC);
bytes[4..20].copy_from_slice(&self.archive_uuid);
bytes[20..36].copy_from_slice(&self.session_id);
write_u32(&mut bytes, 36, self.volume_index);
bytes[40] = self.is_authoritative;
write_u32(&mut bytes, 44, self.total_volumes);
write_u64(&mut bytes, 48, self.index_root_first_block);
write_u32(&mut bytes, 56, self.index_root_data_block_count);
write_u32(&mut bytes, 60, self.index_root_parity_block_count);
write_u32(&mut bytes, 64, self.index_root_encrypted_size);
write_u32(&mut bytes, 68, self.index_root_decompressed_size);
bytes[104..136].copy_from_slice(&self.manifest_hmac);
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VolumeTrailer {
pub archive_uuid: [u8; 16],
pub session_id: [u8; 16],
pub volume_index: u32,
pub block_count: u64,
pub bytes_written: u64,
pub manifest_footer_offset: u64,
pub manifest_footer_length: u32,
pub closed_at_ns: i64,
pub root_auth_footer_offset: u64,
pub root_auth_footer_length: u32,
pub root_auth_flags: u32,
pub trailer_hmac: [u8; 32],
}
impl VolumeTrailer {
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
expect_len("VolumeTrailer", VOLUME_TRAILER_LEN, bytes.len())?;
expect_magic("VolumeTrailer", TZVT_MAGIC, &bytes[0..4])?;
expect_zero("VolumeTrailer", &bytes[92..96])?;
let manifest_footer_length = read_u32(bytes, 64)?;
if manifest_footer_length != MANIFEST_FOOTER_LEN as u32 {
return Err(FormatError::InvalidManifestFooterLength(
manifest_footer_length,
));
}
let root_auth_flags = read_u32(bytes, 88)?;
if root_auth_flags & !0x0000_0001 != 0 {
return Err(FormatError::InvalidArchive(
"VolumeTrailer root_auth_flags has unknown bits",
));
}
Ok(Self {
archive_uuid: read_array_16(bytes, 4)?,
session_id: read_array_16(bytes, 20)?,
volume_index: read_u32(bytes, 36)?,
block_count: read_u64(bytes, 40)?,
bytes_written: read_u64(bytes, 48)?,
manifest_footer_offset: read_u64(bytes, 56)?,
manifest_footer_length,
closed_at_ns: read_i64(bytes, 68)?,
root_auth_footer_offset: read_u64(bytes, 76)?,
root_auth_footer_length: read_u32(bytes, 84)?,
root_auth_flags,
trailer_hmac: read_array_32(bytes, 96)?,
})
}
pub fn to_bytes(&self) -> [u8; VOLUME_TRAILER_LEN] {
let mut bytes = [0u8; VOLUME_TRAILER_LEN];
bytes[0..4].copy_from_slice(&TZVT_MAGIC);
bytes[4..20].copy_from_slice(&self.archive_uuid);
bytes[20..36].copy_from_slice(&self.session_id);
write_u32(&mut bytes, 36, self.volume_index);
write_u64(&mut bytes, 40, self.block_count);
write_u64(&mut bytes, 48, self.bytes_written);
write_u64(&mut bytes, 56, self.manifest_footer_offset);
write_u32(&mut bytes, 64, self.manifest_footer_length);
write_i64(&mut bytes, 68, self.closed_at_ns);
write_u64(&mut bytes, 76, self.root_auth_footer_offset);
write_u32(&mut bytes, 84, self.root_auth_footer_length);
write_u32(&mut bytes, 88, self.root_auth_flags);
bytes[96..128].copy_from_slice(&self.trailer_hmac);
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootAuthFooterV1 {
pub archive_uuid: [u8; 16],
pub session_id: [u8; 16],
pub authenticator_id: u16,
pub signer_identity_type: u16,
pub signer_identity_bytes: Vec<u8>,
pub authenticator_value: Vec<u8>,
pub total_data_block_count: u64,
pub critical_metadata_digest: [u8; 32],
pub index_digest: [u8; 32],
pub fec_layout_digest: [u8; 32],
pub data_block_merkle_root: [u8; 32],
pub signer_identity_digest: [u8; 32],
pub archive_root: [u8; 32],
pub footer_crc32c: u32,
}
impl RootAuthFooterV1 {
pub fn footer_length(&self) -> Result<u32, FormatError> {
root_auth_footer_length(
self.signer_identity_bytes.len(),
self.authenticator_value.len(),
)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, FormatError> {
validate_root_auth_variable_lengths(
self.signer_identity_bytes.len(),
self.authenticator_value.len(),
)?;
let footer_length = self.footer_length()?;
let mut bytes = vec![0u8; footer_length as usize];
bytes[0..4].copy_from_slice(&TZRA_MAGIC);
write_u16(&mut bytes, 4, 1);
bytes[6..30].copy_from_slice(&ROOT_AUTH_SPEC_ID);
write_u32(&mut bytes, 30, footer_length);
write_u32(&mut bytes, 34, 0);
bytes[38..54].copy_from_slice(&self.archive_uuid);
bytes[54..70].copy_from_slice(&self.session_id);
write_u16(&mut bytes, 70, FORMAT_VERSION);
write_u16(&mut bytes, 72, VOLUME_FORMAT_REV);
write_u16(&mut bytes, 74, self.authenticator_id);
write_u16(&mut bytes, 76, self.signer_identity_type);
write_u32(
&mut bytes,
78,
u32::try_from(self.signer_identity_bytes.len()).map_err(|_| {
FormatError::InvalidArchive("RootAuthFooterV1 signer identity length overflow")
})?,
);
write_u32(
&mut bytes,
82,
u32::try_from(self.authenticator_value.len()).map_err(|_| {
FormatError::InvalidArchive("RootAuthFooterV1 authenticator length overflow")
})?,
);
write_u64(&mut bytes, 86, self.total_data_block_count);
bytes[94..126].copy_from_slice(&self.critical_metadata_digest);
bytes[126..158].copy_from_slice(&self.index_digest);
bytes[158..190].copy_from_slice(&self.fec_layout_digest);
bytes[190..222].copy_from_slice(&self.data_block_merkle_root);
bytes[222..254].copy_from_slice(&self.signer_identity_digest);
bytes[254..286].copy_from_slice(&self.archive_root);
let signer_start = ROOT_AUTH_FOOTER_FIXED_LEN;
let signer_end = signer_start + self.signer_identity_bytes.len();
bytes[signer_start..signer_end].copy_from_slice(&self.signer_identity_bytes);
let auth_end = signer_end + self.authenticator_value.len();
bytes[signer_end..auth_end].copy_from_slice(&self.authenticator_value);
let crc = crc32c(&bytes[..auth_end]);
write_u32(&mut bytes, auth_end, crc);
Ok(bytes)
}
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
if bytes.len() > READER_MAX_ROOT_AUTH_FOOTER_LEN as usize {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "RootAuthFooterV1 length",
cap: READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
actual: bytes.len() as u64,
});
}
let min_len = ROOT_AUTH_FOOTER_FIXED_LEN + 4;
if bytes.len() < min_len {
return Err(FormatError::InvalidLength {
structure: "RootAuthFooterV1",
expected: min_len,
actual: bytes.len(),
});
}
expect_magic("RootAuthFooterV1", TZRA_MAGIC, &bytes[0..4])?;
let version = read_u16(bytes, 4)?;
if version != 1 {
return Err(FormatError::UnsupportedFormatVersion(version));
}
if read_array_24(bytes, 6)? != ROOT_AUTH_SPEC_ID {
return Err(FormatError::InvalidArchive(
"RootAuthFooterV1 root_auth_spec_id is unsupported",
));
}
let footer_length = read_u32(bytes, 30)?;
if footer_length as usize != bytes.len() {
return Err(FormatError::InvalidLength {
structure: "RootAuthFooterV1",
expected: footer_length as usize,
actual: bytes.len(),
});
}
if read_u32(bytes, 34)? != 0 {
return Err(FormatError::InvalidArchive(
"RootAuthFooterV1 flags must be zero",
));
}
let format_version = read_u16(bytes, 70)?;
if format_version != FORMAT_VERSION {
return Err(FormatError::UnsupportedFormatVersion(format_version));
}
let volume_format_rev = read_u16(bytes, 72)?;
if volume_format_rev != VOLUME_FORMAT_REV {
return Err(FormatError::UnsupportedVolumeFormatRevision(
volume_format_rev,
));
}
let signer_identity_length = read_u32(bytes, 78)?;
let authenticator_value_length = read_u32(bytes, 82)?;
validate_root_auth_variable_lengths(
signer_identity_length as usize,
authenticator_value_length as usize,
)?;
let expected = root_auth_footer_length(
signer_identity_length as usize,
authenticator_value_length as usize,
)?;
if expected != footer_length {
return Err(FormatError::InvalidLength {
structure: "RootAuthFooterV1",
expected: expected as usize,
actual: footer_length as usize,
});
}
expect_zero("RootAuthFooterV1", &bytes[286..318])?;
let crc_offset = bytes.len() - 4;
expect_crc(
"RootAuthFooterV1",
&bytes[..crc_offset],
read_u32(bytes, crc_offset)?,
)?;
let signer_start = ROOT_AUTH_FOOTER_FIXED_LEN;
let signer_end = signer_start + signer_identity_length as usize;
let auth_end = signer_end + authenticator_value_length as usize;
Ok(Self {
archive_uuid: read_array_16(bytes, 38)?,
session_id: read_array_16(bytes, 54)?,
authenticator_id: read_u16(bytes, 74)?,
signer_identity_type: read_u16(bytes, 76)?,
signer_identity_bytes: bytes[signer_start..signer_end].to_vec(),
authenticator_value: bytes[signer_end..auth_end].to_vec(),
total_data_block_count: read_u64(bytes, 86)?,
critical_metadata_digest: read_array_32(bytes, 94)?,
index_digest: read_array_32(bytes, 126)?,
fec_layout_digest: read_array_32(bytes, 158)?,
data_block_merkle_root: read_array_32(bytes, 190)?,
signer_identity_digest: read_array_32(bytes, 222)?,
archive_root: read_array_32(bytes, 254)?,
footer_crc32c: read_u32(bytes, crc_offset)?,
})
}
}
fn root_auth_footer_length(
signer_identity_len: usize,
authenticator_value_len: usize,
) -> Result<u32, FormatError> {
let len = ROOT_AUTH_FOOTER_FIXED_LEN
.checked_add(signer_identity_len)
.and_then(|value| value.checked_add(authenticator_value_len))
.and_then(|value| value.checked_add(4))
.ok_or(FormatError::InvalidArchive(
"RootAuthFooterV1 length overflow",
))?;
if len > READER_MAX_ROOT_AUTH_FOOTER_LEN as usize {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "RootAuthFooterV1 length",
cap: READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
actual: len as u64,
});
}
u32::try_from(len).map_err(|_| FormatError::InvalidArchive("RootAuthFooterV1 length overflow"))
}
fn validate_root_auth_variable_lengths(
signer_identity_len: usize,
authenticator_value_len: usize,
) -> Result<(), FormatError> {
if signer_identity_len > READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN as usize {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "RootAuthFooterV1 signer identity length",
cap: READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN as u64,
actual: signer_identity_len as u64,
});
}
if authenticator_value_len > READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as usize {
return Err(FormatError::ReaderResourceLimitExceeded {
field: "RootAuthFooterV1 authenticator value length",
cap: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as u64,
actual: authenticator_value_len as u64,
});
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SerializedRegion {
pub region_type: u16,
pub offset: u64,
pub bytes: Vec<u8>,
}
impl SerializedRegion {
pub fn encoded_len(&self) -> usize {
SERIALIZED_REGION_HEADER_LEN + self.bytes.len()
}
pub fn to_bytes(&self) -> Result<Vec<u8>, FormatError> {
let length = u32::try_from(self.bytes.len())
.map_err(|_| FormatError::InvalidArchive("SerializedRegion length exceeds u32"))?;
let mut bytes = vec![0u8; self.encoded_len()];
write_u16(&mut bytes, 0, self.region_type);
write_u64(&mut bytes, 4, self.offset);
write_u32(&mut bytes, 12, length);
bytes[SERIALIZED_REGION_HEADER_LEN..].copy_from_slice(&self.bytes);
Ok(bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CriticalMetadataImage {
pub archive_uuid: [u8; 16],
pub session_id: [u8; 16],
pub volume_index: u32,
pub stripe_width: u32,
pub layout_flags: u32,
pub volume_header_offset: u64,
pub volume_header_length: u32,
pub crypto_header_offset: u64,
pub crypto_header_length: u32,
pub block_records_offset: u64,
pub block_records_length: u64,
pub block_count: u64,
pub manifest_footer_offset: u64,
pub manifest_footer_length: u32,
pub root_auth_footer_offset: u64,
pub root_auth_footer_length: u32,
pub volume_trailer_offset: u64,
pub volume_trailer_length: u32,
pub body_bytes_before_cmra: u64,
pub volume_header_sha256: [u8; 32],
pub crypto_header_sha256: [u8; 32],
pub manifest_footer_sha256: [u8; 32],
pub root_auth_footer_sha256: [u8; 32],
pub volume_trailer_sha256: [u8; 32],
pub regions: Vec<SerializedRegion>,
}
impl CriticalMetadataImage {
pub fn to_bytes(&self) -> Result<Vec<u8>, FormatError> {
let region_count = u16::try_from(self.regions.len()).map_err(|_| {
FormatError::InvalidArchive("CriticalMetadataImage has too many regions")
})?;
let variable_len = self.regions.iter().try_fold(0usize, |total, region| {
total
.checked_add(region.encoded_len())
.ok_or(FormatError::InvalidArchive(
"CriticalMetadataImage length overflow",
))
})?;
let mut bytes = vec![
0u8;
CRITICAL_METADATA_IMAGE_FIXED_LEN
.checked_add(variable_len)
.and_then(|value| value.checked_add(IMAGE_CRC_LEN))
.ok_or(FormatError::InvalidArchive(
"CriticalMetadataImage length overflow",
))?
];
bytes[0..4].copy_from_slice(&TZMI_MAGIC);
write_u16(&mut bytes, 4, 1);
write_u16(&mut bytes, 6, VOLUME_FORMAT_REV);
bytes[8..24].copy_from_slice(&self.archive_uuid);
bytes[24..40].copy_from_slice(&self.session_id);
write_u32(&mut bytes, 40, self.volume_index);
write_u32(&mut bytes, 44, self.stripe_width);
write_u32(&mut bytes, 48, self.layout_flags);
write_u64(&mut bytes, 52, self.volume_header_offset);
write_u32(&mut bytes, 60, self.volume_header_length);
write_u64(&mut bytes, 64, self.crypto_header_offset);
write_u32(&mut bytes, 72, self.crypto_header_length);
write_u64(&mut bytes, 76, self.block_records_offset);
write_u64(&mut bytes, 84, self.block_records_length);
write_u64(&mut bytes, 92, self.block_count);
write_u64(&mut bytes, 100, self.manifest_footer_offset);
write_u32(&mut bytes, 108, self.manifest_footer_length);
write_u64(&mut bytes, 112, self.root_auth_footer_offset);
write_u32(&mut bytes, 120, self.root_auth_footer_length);
write_u64(&mut bytes, 124, self.volume_trailer_offset);
write_u32(&mut bytes, 132, self.volume_trailer_length);
write_u64(&mut bytes, 136, self.body_bytes_before_cmra);
bytes[144..176].copy_from_slice(&self.volume_header_sha256);
bytes[176..208].copy_from_slice(&self.crypto_header_sha256);
bytes[208..240].copy_from_slice(&self.manifest_footer_sha256);
bytes[240..272].copy_from_slice(&self.root_auth_footer_sha256);
bytes[272..304].copy_from_slice(&self.volume_trailer_sha256);
write_u16(&mut bytes, 304, region_count);
let mut cursor = CRITICAL_METADATA_IMAGE_FIXED_LEN;
for region in &self.regions {
let region_bytes = region.to_bytes()?;
let end = cursor + region_bytes.len();
bytes[cursor..end].copy_from_slice(®ion_bytes);
cursor = end;
}
let crc = crc32c(&bytes[..cursor]);
write_u32(&mut bytes, cursor, crc);
Ok(bytes)
}
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
if bytes.len() < CRITICAL_METADATA_IMAGE_FIXED_LEN + IMAGE_CRC_LEN {
return Err(FormatError::InvalidLength {
structure: "CriticalMetadataImageV1",
expected: CRITICAL_METADATA_IMAGE_FIXED_LEN + IMAGE_CRC_LEN,
actual: bytes.len(),
});
}
expect_magic("CriticalMetadataImageV1", TZMI_MAGIC, &bytes[0..4])?;
let version = read_u16(bytes, 4)?;
if version != 1 {
return Err(FormatError::UnsupportedFormatVersion(version));
}
let volume_format_rev = read_u16(bytes, 6)?;
if volume_format_rev != VOLUME_FORMAT_REV {
return Err(FormatError::UnsupportedVolumeFormatRevision(
volume_format_rev,
));
}
let layout_flags = read_u32(bytes, 48)?;
if layout_flags & !0x0000_0001 != 0 {
return Err(FormatError::InvalidArchive(
"CriticalMetadataImage layout_flags has unknown bits",
));
}
expect_zero("CriticalMetadataImageV1", &bytes[306..320])?;
let expected_crc_offset =
bytes
.len()
.checked_sub(IMAGE_CRC_LEN)
.ok_or(FormatError::InvalidArchive(
"CriticalMetadataImage length underflow",
))?;
expect_crc(
"CriticalMetadataImageV1",
&bytes[..expected_crc_offset],
read_u32(bytes, expected_crc_offset)?,
)?;
let serialized_region_count = read_u16(bytes, 304)? as usize;
let mut cursor = CRITICAL_METADATA_IMAGE_FIXED_LEN;
let mut regions = Vec::with_capacity(serialized_region_count);
for _ in 0..serialized_region_count {
if cursor + SERIALIZED_REGION_HEADER_LEN > expected_crc_offset {
return Err(FormatError::InvalidLength {
structure: "SerializedRegion",
expected: cursor + SERIALIZED_REGION_HEADER_LEN,
actual: bytes.len(),
});
}
let region_type = read_u16(bytes, cursor)?;
if read_u16(bytes, cursor + 2)? != 0 {
return Err(FormatError::NonZeroReserved {
structure: "SerializedRegion",
});
}
let offset = read_u64(bytes, cursor + 4)?;
let length = read_u32(bytes, cursor + 12)? as usize;
cursor += SERIALIZED_REGION_HEADER_LEN;
let end = cursor
.checked_add(length)
.ok_or(FormatError::InvalidArchive(
"SerializedRegion length overflow",
))?;
if end > expected_crc_offset {
return Err(FormatError::InvalidLength {
structure: "SerializedRegion",
expected: end,
actual: bytes.len(),
});
}
regions.push(SerializedRegion {
region_type,
offset,
bytes: bytes[cursor..end].to_vec(),
});
cursor = end;
}
if cursor != expected_crc_offset {
return Err(FormatError::InvalidArchive(
"CriticalMetadataImage has trailing region bytes",
));
}
Ok(Self {
archive_uuid: read_array_16(bytes, 8)?,
session_id: read_array_16(bytes, 24)?,
volume_index: read_u32(bytes, 40)?,
stripe_width: read_u32(bytes, 44)?,
layout_flags,
volume_header_offset: read_u64(bytes, 52)?,
volume_header_length: read_u32(bytes, 60)?,
crypto_header_offset: read_u64(bytes, 64)?,
crypto_header_length: read_u32(bytes, 72)?,
block_records_offset: read_u64(bytes, 76)?,
block_records_length: read_u64(bytes, 84)?,
block_count: read_u64(bytes, 92)?,
manifest_footer_offset: read_u64(bytes, 100)?,
manifest_footer_length: read_u32(bytes, 108)?,
root_auth_footer_offset: read_u64(bytes, 112)?,
root_auth_footer_length: read_u32(bytes, 120)?,
volume_trailer_offset: read_u64(bytes, 124)?,
volume_trailer_length: read_u32(bytes, 132)?,
body_bytes_before_cmra: read_u64(bytes, 136)?,
volume_header_sha256: read_array_32(bytes, 144)?,
crypto_header_sha256: read_array_32(bytes, 176)?,
manifest_footer_sha256: read_array_32(bytes, 208)?,
root_auth_footer_sha256: read_array_32(bytes, 240)?,
volume_trailer_sha256: read_array_32(bytes, 272)?,
regions,
})
}
pub fn region(&self, region_type: u16) -> Option<&SerializedRegion> {
self.regions
.iter()
.find(|region| region.region_type == region_type)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CriticalMetadataRecoveryHeader {
pub shard_size: u32,
pub data_shard_count: u16,
pub parity_shard_count: u16,
pub image_length: u32,
pub archive_uuid_hint: [u8; 16],
pub session_id_hint: [u8; 16],
pub volume_index_hint: u32,
pub image_sha256: [u8; 32],
pub header_crc32c: u32,
}
impl CriticalMetadataRecoveryHeader {
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
expect_len(
"CriticalMetadataRecoveryHeader",
CRITICAL_METADATA_RECOVERY_HEADER_LEN,
bytes.len(),
)?;
expect_magic("CriticalMetadataRecoveryHeader", TZCR_MAGIC, &bytes[0..4])?;
let version = read_u16(bytes, 4)?;
if version != 1 {
return Err(FormatError::UnsupportedFormatVersion(version));
}
let fec_algo = read_u16(bytes, 6)?;
if fec_algo != FecAlgo::ReedSolomonGF16 as u16 {
return Err(FormatError::UnknownFecAlgo(fec_algo));
}
expect_zero("CriticalMetadataRecoveryHeader", &bytes[88..112])?;
expect_crc(
"CriticalMetadataRecoveryHeader",
&bytes[..112],
read_u32(bytes, 112)?,
)?;
Ok(Self {
shard_size: read_u32(bytes, 8)?,
data_shard_count: read_u16(bytes, 12)?,
parity_shard_count: read_u16(bytes, 14)?,
image_length: read_u32(bytes, 16)?,
archive_uuid_hint: read_array_16(bytes, 20)?,
session_id_hint: read_array_16(bytes, 36)?,
volume_index_hint: read_u32(bytes, 52)?,
image_sha256: read_array_32(bytes, 56)?,
header_crc32c: read_u32(bytes, 112)?,
})
}
pub fn to_bytes(&self) -> [u8; CRITICAL_METADATA_RECOVERY_HEADER_LEN] {
let mut bytes = [0u8; CRITICAL_METADATA_RECOVERY_HEADER_LEN];
bytes[0..4].copy_from_slice(&TZCR_MAGIC);
write_u16(&mut bytes, 4, 1);
write_u16(&mut bytes, 6, FecAlgo::ReedSolomonGF16 as u16);
write_u32(&mut bytes, 8, self.shard_size);
write_u16(&mut bytes, 12, self.data_shard_count);
write_u16(&mut bytes, 14, self.parity_shard_count);
write_u32(&mut bytes, 16, self.image_length);
bytes[20..36].copy_from_slice(&self.archive_uuid_hint);
bytes[36..52].copy_from_slice(&self.session_id_hint);
write_u32(&mut bytes, 52, self.volume_index_hint);
bytes[56..88].copy_from_slice(&self.image_sha256);
let crc = crc32c(&bytes[..112]);
write_u32(&mut bytes, 112, crc);
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CriticalMetadataRecoveryShard {
pub shard_index: u16,
pub shard_role: u8,
pub shard_payload_length: u32,
pub payload: Vec<u8>,
pub shard_crc32c: u32,
}
impl CriticalMetadataRecoveryShard {
pub fn parse(bytes: &[u8], shard_size: usize) -> Result<Self, FormatError> {
let expected = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN
.checked_add(shard_size)
.ok_or(FormatError::InvalidArchive("CMRA shard length overflow"))?;
expect_len("CriticalMetadataRecoveryShard", expected, bytes.len())?;
expect_magic("CriticalMetadataRecoveryShard", TZCS_MAGIC, &bytes[0..4])?;
if bytes[7] != 0 {
return Err(FormatError::NonZeroReserved {
structure: "CriticalMetadataRecoveryShard",
});
}
let shard_role = bytes[6];
if shard_role > 1 {
return Err(FormatError::InvalidArchive(
"CriticalMetadataRecoveryShard has unknown role",
));
}
expect_crc(
"CriticalMetadataRecoveryShard",
&[
&bytes[..12],
&bytes[CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN..],
]
.concat(),
read_u32(bytes, 12)?,
)?;
let shard_payload_length = read_u32(bytes, 8)?;
if shard_payload_length as usize > shard_size {
return Err(FormatError::InvalidArchive(
"CriticalMetadataRecoveryShard payload length exceeds shard size",
));
}
Ok(Self {
shard_index: read_u16(bytes, 4)?,
shard_role,
shard_payload_length,
payload: bytes[CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN..].to_vec(),
shard_crc32c: read_u32(bytes, 12)?,
})
}
pub fn to_bytes(&self, shard_size: usize) -> Result<Vec<u8>, FormatError> {
if self.payload.len() != shard_size {
return Err(FormatError::InvalidArchive(
"CriticalMetadataRecoveryShard payload length mismatch",
));
}
let mut bytes = vec![0u8; CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size];
bytes[0..4].copy_from_slice(&TZCS_MAGIC);
write_u16(&mut bytes, 4, self.shard_index);
bytes[6] = self.shard_role;
write_u32(&mut bytes, 8, self.shard_payload_length);
bytes[CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN..].copy_from_slice(&self.payload);
let mut covered = Vec::with_capacity(12 + shard_size);
covered.extend_from_slice(&bytes[..12]);
covered.extend_from_slice(&bytes[CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN..]);
let crc = crc32c(&covered);
write_u32(&mut bytes, 12, crc);
Ok(bytes)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CriticalRecoveryLocator {
pub cmra_offset: u64,
pub cmra_length: u32,
pub volume_trailer_offset: u64,
pub body_bytes_before_cmra: u64,
pub archive_uuid_hint: [u8; 16],
pub session_id_hint: [u8; 16],
pub volume_index_hint: u32,
pub locator_sequence: u32,
pub cmra_shard_size: u32,
pub cmra_data_shard_count: u16,
pub cmra_parity_shard_count: u16,
pub cmra_image_length: u32,
pub cmra_image_sha256: [u8; 32],
pub locator_crc32c: u32,
}
impl CriticalRecoveryLocator {
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
expect_len(
"CriticalRecoveryLocator",
CRITICAL_RECOVERY_LOCATOR_LEN,
bytes.len(),
)?;
expect_magic("CriticalRecoveryLocator", TZCL_MAGIC, &bytes[0..4])?;
let version = read_u16(bytes, 4)?;
if version != 1 {
return Err(FormatError::UnsupportedFormatVersion(version));
}
let volume_format_rev = read_u16(bytes, 6)?;
if volume_format_rev != VOLUME_FORMAT_REV {
return Err(FormatError::UnsupportedVolumeFormatRevision(
volume_format_rev,
));
}
if read_u16(bytes, 20)? != CRITICAL_METADATA_RECOVERY_HEADER_LEN as u16 {
return Err(FormatError::InvalidArchive(
"CriticalRecoveryLocator CMRA header length is invalid",
));
}
let fec_algo = read_u16(bytes, 22)?;
if fec_algo != FecAlgo::ReedSolomonGF16 as u16 {
return Err(FormatError::UnknownFecAlgo(fec_algo));
}
let locator_sequence = read_u32(bytes, 76)?;
if locator_sequence > 1 {
return Err(FormatError::InvalidArchive(
"CriticalRecoveryLocator has invalid sequence",
));
}
expect_crc(
"CriticalRecoveryLocator",
&bytes[..124],
read_u32(bytes, 124)?,
)?;
Ok(Self {
cmra_offset: read_u64(bytes, 8)?,
cmra_length: read_u32(bytes, 16)?,
volume_trailer_offset: read_u64(bytes, 24)?,
body_bytes_before_cmra: read_u64(bytes, 32)?,
archive_uuid_hint: read_array_16(bytes, 40)?,
session_id_hint: read_array_16(bytes, 56)?,
volume_index_hint: read_u32(bytes, 72)?,
locator_sequence,
cmra_shard_size: read_u32(bytes, 80)?,
cmra_data_shard_count: read_u16(bytes, 84)?,
cmra_parity_shard_count: read_u16(bytes, 86)?,
cmra_image_length: read_u32(bytes, 88)?,
cmra_image_sha256: read_array_32(bytes, 92)?,
locator_crc32c: read_u32(bytes, 124)?,
})
}
pub fn to_bytes(&self) -> [u8; CRITICAL_RECOVERY_LOCATOR_LEN] {
let mut bytes = [0u8; CRITICAL_RECOVERY_LOCATOR_LEN];
bytes[0..4].copy_from_slice(&TZCL_MAGIC);
write_u16(&mut bytes, 4, 1);
write_u16(&mut bytes, 6, VOLUME_FORMAT_REV);
write_u64(&mut bytes, 8, self.cmra_offset);
write_u32(&mut bytes, 16, self.cmra_length);
write_u16(&mut bytes, 20, CRITICAL_METADATA_RECOVERY_HEADER_LEN as u16);
write_u16(&mut bytes, 22, FecAlgo::ReedSolomonGF16 as u16);
write_u64(&mut bytes, 24, self.volume_trailer_offset);
write_u64(&mut bytes, 32, self.body_bytes_before_cmra);
bytes[40..56].copy_from_slice(&self.archive_uuid_hint);
bytes[56..72].copy_from_slice(&self.session_id_hint);
write_u32(&mut bytes, 72, self.volume_index_hint);
write_u32(&mut bytes, 76, self.locator_sequence);
write_u32(&mut bytes, 80, self.cmra_shard_size);
write_u16(&mut bytes, 84, self.cmra_data_shard_count);
write_u16(&mut bytes, 86, self.cmra_parity_shard_count);
write_u32(&mut bytes, 88, self.cmra_image_length);
bytes[92..124].copy_from_slice(&self.cmra_image_sha256);
let crc = crc32c(&bytes[..124]);
write_u32(&mut bytes, 124, crc);
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BootstrapSidecarHeader {
pub archive_uuid: [u8; 16],
pub session_id: [u8; 16],
pub flags: u32,
pub manifest_footer_offset: u64,
pub manifest_footer_length: u32,
pub index_root_records_offset: u64,
pub index_root_records_length: u64,
pub dictionary_records_offset: u64,
pub dictionary_records_length: u64,
pub sidecar_hmac: [u8; 32],
pub header_crc32c: u32,
}
impl BootstrapSidecarHeader {
pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
expect_len(
"BootstrapSidecarHeader",
BOOTSTRAP_SIDECAR_HEADER_LEN,
bytes.len(),
)?;
expect_magic("BootstrapSidecarHeader", TZBS_MAGIC, &bytes[0..4])?;
let version = read_u32(bytes, 4)?;
if version != 1 {
return Err(FormatError::UnsupportedBootstrapSidecarVersion(version));
}
expect_zero("BootstrapSidecarHeader", &bytes[88..92])?;
expect_crc(
"BootstrapSidecarHeader",
&bytes[..124],
read_u32(bytes, 124)?,
)?;
let header = Self {
archive_uuid: read_array_16(bytes, 8)?,
session_id: read_array_16(bytes, 24)?,
flags: read_u32(bytes, 40)?,
manifest_footer_offset: read_u64(bytes, 44)?,
manifest_footer_length: read_u32(bytes, 52)?,
index_root_records_offset: read_u64(bytes, 56)?,
index_root_records_length: read_u64(bytes, 64)?,
dictionary_records_offset: read_u64(bytes, 72)?,
dictionary_records_length: read_u64(bytes, 80)?,
sidecar_hmac: read_array_32(bytes, 92)?,
header_crc32c: read_u32(bytes, 124)?,
};
header.validate_sections()?;
Ok(header)
}
pub fn validate_packed_layout(&self, file_size: u64) -> Result<(), FormatError> {
let mut cursor = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
cursor = self.validate_section_cursor(
self.has_manifest_footer(),
self.manifest_footer_offset,
self.manifest_footer_length as u64,
cursor,
)?;
cursor = self.validate_section_cursor(
self.has_index_root_records(),
self.index_root_records_offset,
self.index_root_records_length,
cursor,
)?;
cursor = self.validate_section_cursor(
self.has_dictionary_records(),
self.dictionary_records_offset,
self.dictionary_records_length,
cursor,
)?;
if cursor != file_size {
return Err(FormatError::NonCanonicalBootstrapSidecarLayout);
}
Ok(())
}
pub fn has_manifest_footer(&self) -> bool {
self.flags & SIDECAR_MANIFEST_PRESENT != 0
}
pub fn has_index_root_records(&self) -> bool {
self.flags & SIDECAR_INDEX_ROOT_PRESENT != 0
}
pub fn has_dictionary_records(&self) -> bool {
self.flags & SIDECAR_DICTIONARY_PRESENT != 0
}
pub fn to_bytes(&self) -> [u8; BOOTSTRAP_SIDECAR_HEADER_LEN] {
let mut bytes = [0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
bytes[0..4].copy_from_slice(&TZBS_MAGIC);
write_u32(&mut bytes, 4, 1);
bytes[8..24].copy_from_slice(&self.archive_uuid);
bytes[24..40].copy_from_slice(&self.session_id);
write_u32(&mut bytes, 40, self.flags);
write_u64(&mut bytes, 44, self.manifest_footer_offset);
write_u32(&mut bytes, 52, self.manifest_footer_length);
write_u64(&mut bytes, 56, self.index_root_records_offset);
write_u64(&mut bytes, 64, self.index_root_records_length);
write_u64(&mut bytes, 72, self.dictionary_records_offset);
write_u64(&mut bytes, 80, self.dictionary_records_length);
bytes[92..124].copy_from_slice(&self.sidecar_hmac);
let crc = crc32c(&bytes[..124]);
write_u32(&mut bytes, 124, crc);
bytes
}
fn validate_sections(&self) -> Result<(), FormatError> {
if self.flags & !SIDECAR_KNOWN_FLAGS != 0 {
return Err(FormatError::UnknownBootstrapSidecarFlags(self.flags));
}
self.validate_presence_fields(
self.has_manifest_footer(),
self.manifest_footer_offset,
self.manifest_footer_length as u64,
)?;
if self.has_manifest_footer() && self.manifest_footer_length != MANIFEST_FOOTER_LEN as u32 {
return Err(FormatError::InvalidManifestFooterLength(
self.manifest_footer_length,
));
}
self.validate_presence_fields(
self.has_index_root_records(),
self.index_root_records_offset,
self.index_root_records_length,
)?;
self.validate_presence_fields(
self.has_dictionary_records(),
self.dictionary_records_offset,
self.dictionary_records_length,
)?;
Ok(())
}
fn validate_presence_fields(
&self,
present: bool,
offset: u64,
length: u64,
) -> Result<(), FormatError> {
match (present, offset, length) {
(true, 0, _) | (true, _, 0) => Err(FormatError::EmptyBootstrapSidecarSection),
(false, 0, 0) => Ok(()),
(false, _, _) => Err(FormatError::NonZeroAbsentBootstrapSidecarSection),
(true, _, _) => Ok(()),
}
}
fn validate_section_cursor(
&self,
present: bool,
offset: u64,
length: u64,
cursor: u64,
) -> Result<u64, FormatError> {
if present {
if offset != cursor {
return Err(FormatError::NonCanonicalBootstrapSidecarLayout);
}
cursor
.checked_add(length)
.ok_or(FormatError::NonCanonicalBootstrapSidecarLayout)
} else {
Ok(cursor)
}
}
}
fn is_known_extension(ext_tag: u16) -> bool {
matches!(ext_tag, 0x0001 | 0x0002 | 0x0003 | 0x0005)
}
fn validate_known_extension(ext_tag: u16, value: &[u8]) -> Result<(), FormatError> {
match ext_tag {
0x0001 | 0x0002 | 0x0005 => std::str::from_utf8(value)
.map(|_| ())
.map_err(|_| FormatError::MalformedKnownExtension(ext_tag)),
0x0003 => {
if value.len() == 8 {
Ok(())
} else {
Err(FormatError::MalformedKnownExtension(ext_tag))
}
}
_ => Ok(()),
}
}
fn expect_len(structure: &'static str, expected: usize, actual: usize) -> Result<(), FormatError> {
if actual != expected {
return Err(FormatError::InvalidLength {
structure,
expected,
actual,
});
}
Ok(())
}
fn expect_magic(
structure: &'static str,
expected: [u8; 4],
actual: &[u8],
) -> Result<(), FormatError> {
if actual != expected {
return Err(FormatError::BadMagic { structure });
}
Ok(())
}
fn expect_zero(structure: &'static str, bytes: &[u8]) -> Result<(), FormatError> {
if bytes.iter().any(|byte| *byte != 0) {
return Err(FormatError::NonZeroReserved { structure });
}
Ok(())
}
fn expect_crc(structure: &'static str, covered: &[u8], expected: u32) -> Result<(), FormatError> {
if crc32c(covered) != expected {
return Err(FormatError::BadCrc { structure });
}
Ok(())
}
fn read_array_16(bytes: &[u8], offset: usize) -> Result<[u8; 16], FormatError> {
let mut value = [0u8; 16];
value.copy_from_slice(
bytes
.get(offset..offset + 16)
.ok_or(FormatError::InvalidLength {
structure: "array16",
expected: offset + 16,
actual: bytes.len(),
})?,
);
Ok(value)
}
fn read_array_24(bytes: &[u8], offset: usize) -> Result<[u8; 24], FormatError> {
let mut value = [0u8; 24];
value.copy_from_slice(
bytes
.get(offset..offset + 24)
.ok_or(FormatError::InvalidLength {
structure: "array24",
expected: offset + 24,
actual: bytes.len(),
})?,
);
Ok(value)
}
fn read_array_32(bytes: &[u8], offset: usize) -> Result<[u8; 32], FormatError> {
let mut value = [0u8; 32];
value.copy_from_slice(
bytes
.get(offset..offset + 32)
.ok_or(FormatError::InvalidLength {
structure: "array32",
expected: offset + 32,
actual: bytes.len(),
})?,
);
Ok(value)
}
fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, FormatError> {
let array: [u8; 2] = bytes
.get(offset..offset + 2)
.ok_or(FormatError::InvalidLength {
structure: "u16",
expected: offset + 2,
actual: bytes.len(),
})?
.try_into()
.expect("slice length checked");
Ok(u16::from_le_bytes(array))
}
fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, FormatError> {
let array: [u8; 4] = bytes
.get(offset..offset + 4)
.ok_or(FormatError::InvalidLength {
structure: "u32",
expected: offset + 4,
actual: bytes.len(),
})?
.try_into()
.expect("slice length checked");
Ok(u32::from_le_bytes(array))
}
fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, FormatError> {
let array: [u8; 8] = bytes
.get(offset..offset + 8)
.ok_or(FormatError::InvalidLength {
structure: "u64",
expected: offset + 8,
actual: bytes.len(),
})?
.try_into()
.expect("slice length checked");
Ok(u64::from_le_bytes(array))
}
fn read_i64(bytes: &[u8], offset: usize) -> Result<i64, FormatError> {
let array: [u8; 8] = bytes
.get(offset..offset + 8)
.ok_or(FormatError::InvalidLength {
structure: "i64",
expected: offset + 8,
actual: bytes.len(),
})?
.try_into()
.expect("slice length checked");
Ok(i64::from_le_bytes(array))
}
fn write_u16(bytes: &mut [u8], offset: usize, value: u16) {
bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
}
fn write_u32(bytes: &mut [u8], offset: usize, value: u32) {
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
fn write_i64(bytes: &mut [u8], offset: usize, value: i64) {
bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::CRYPTO_HEADER_HMAC_LEN;
fn uuid() -> [u8; 16] {
[0x11; 16]
}
fn session() -> [u8; 16] {
[0x22; 16]
}
fn volume_header() -> VolumeHeader {
VolumeHeader {
format_version: FORMAT_VERSION,
volume_format_rev: VOLUME_FORMAT_REV,
volume_index: 0,
stripe_width: 1,
archive_uuid: uuid(),
session_id: session(),
crypto_header_offset: VOLUME_HEADER_LEN as u32,
crypto_header_length: 128,
header_crc32c: 0,
}
}
fn crypto_fixed() -> CryptoHeaderFixed {
CryptoHeaderFixed {
length: (CRYPTO_HEADER_FIXED_LEN + 2 + 6 + CRYPTO_HEADER_HMAC_LEN) as u32,
compression_algo: CompressionAlgo::ZstdFramed,
aead_algo: AeadAlgo::AesGcmSiv256,
fec_algo: FecAlgo::ReedSolomonGF16,
kdf_algo: KdfAlgo::Raw,
chunk_size: 262_144,
envelope_target_size: 4 * 1024 * 1024,
block_size: 4096,
fec_data_shards: 16,
fec_parity_shards: 2,
index_fec_data_shards: 16,
index_fec_parity_shards: 2,
index_root_fec_data_shards: 16,
index_root_fec_parity_shards: 2,
stripe_width: 1,
volume_loss_tolerance: 0,
bit_rot_buffer_pct: 5,
has_dictionary: 0,
max_path_length: 4096,
expected_volume_size: 0,
}
}
fn raw_crypto_header_bytes() -> Vec<u8> {
let fixed = crypto_fixed();
let mut bytes = Vec::new();
bytes.extend_from_slice(&fixed.to_bytes());
bytes.extend_from_slice(&(KdfAlgo::Raw as u16).to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&[0xab; CRYPTO_HEADER_HMAC_LEN]);
bytes
}
#[test]
fn volume_header_round_trips_and_validates() {
let bytes = volume_header().to_bytes();
let parsed = VolumeHeader::parse(&bytes).unwrap();
assert_eq!(parsed.format_version, FORMAT_VERSION);
assert_eq!(parsed.volume_format_rev, VOLUME_FORMAT_REV);
assert_eq!(parsed.crypto_header_offset, VOLUME_HEADER_LEN as u32);
}
#[test]
fn volume_header_rejects_mutations() {
let mut bytes = volume_header().to_bytes();
bytes[0] = b'X';
assert_eq!(
VolumeHeader::parse(&bytes).unwrap_err(),
FormatError::BadMagic {
structure: "VolumeHeader"
}
);
let mut bytes = volume_header().to_bytes();
write_u16(&mut bytes, 6, 35);
let crc = crc32c(&bytes[..124]);
write_u32(&mut bytes, 124, crc);
assert_eq!(
VolumeHeader::parse(&bytes).unwrap_err(),
FormatError::UnsupportedVolumeFormatRevision(35)
);
let mut bytes = volume_header().to_bytes();
bytes[124] ^= 1;
assert_eq!(
VolumeHeader::parse(&bytes).unwrap_err(),
FormatError::BadCrc {
structure: "VolumeHeader"
}
);
let mut bytes = volume_header().to_bytes();
write_u32(&mut bytes, 48, 129);
let crc = crc32c(&bytes[..124]);
write_u32(&mut bytes, 124, crc);
assert_eq!(
VolumeHeader::parse(&bytes).unwrap_err(),
FormatError::NonCanonicalCryptoHeaderOffset(129)
);
let mut bytes = volume_header().to_bytes();
write_u32(&mut bytes, 52, READER_MAX_CRYPTO_HEADER_LEN + 1);
let crc = crc32c(&bytes[..124]);
write_u32(&mut bytes, 124, crc);
assert_eq!(
VolumeHeader::parse(&bytes).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "CryptoHeader length",
cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
actual: (READER_MAX_CRYPTO_HEADER_LEN + 1) as u64,
}
);
}
#[test]
fn fixed_structure_magic_matrix_rejects_all_magic_fields() {
let mut volume = volume_header().to_bytes();
volume[0] ^= 0x01;
assert_eq!(
VolumeHeader::parse(&volume).unwrap_err(),
FormatError::BadMagic {
structure: "VolumeHeader"
}
);
let mut crypto = crypto_fixed().to_bytes();
crypto[0] ^= 0x01;
assert_eq!(
CryptoHeaderFixed::parse(&crypto, crypto_fixed().length).unwrap_err(),
FormatError::BadMagic {
structure: "CryptoHeaderFixed"
}
);
let record = BlockRecord {
block_index: 0,
kind: BlockKind::PayloadData,
flags: BLOCK_LAST_DATA_FLAG,
payload: vec![7; 4096],
record_crc32c: 0,
};
let mut block = record.to_bytes();
block[0] ^= 0x01;
assert_eq!(
BlockRecord::parse(&block, 4096).unwrap_err(),
FormatError::BadMagic {
structure: "BlockRecord"
}
);
let footer = ManifestFooter {
archive_uuid: uuid(),
session_id: session(),
volume_index: 0,
is_authoritative: 1,
total_volumes: 1,
index_root_first_block: 0,
index_root_data_block_count: 1,
index_root_parity_block_count: 0,
index_root_encrypted_size: 4096,
index_root_decompressed_size: 120,
manifest_hmac: [0xaa; 32],
};
let mut manifest = footer.to_bytes();
manifest[0] ^= 0x01;
assert_eq!(
ManifestFooter::parse(&manifest).unwrap_err(),
FormatError::BadMagic {
structure: "ManifestFooter"
}
);
let trailer = VolumeTrailer {
archive_uuid: uuid(),
session_id: session(),
volume_index: 0,
block_count: 3,
bytes_written: 10_000,
manifest_footer_offset: 9_864,
manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
closed_at_ns: 123,
root_auth_footer_offset: 0,
root_auth_footer_length: 0,
root_auth_flags: 0,
trailer_hmac: [0xbb; 32],
};
let mut trailer_bytes = trailer.to_bytes();
trailer_bytes[0] ^= 0x01;
assert_eq!(
VolumeTrailer::parse(&trailer_bytes).unwrap_err(),
FormatError::BadMagic {
structure: "VolumeTrailer"
}
);
let sidecar = BootstrapSidecarHeader {
archive_uuid: uuid(),
session_id: session(),
flags: SIDECAR_MANIFEST_PRESENT,
manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
index_root_records_offset: 0,
index_root_records_length: 0,
dictionary_records_offset: 0,
dictionary_records_length: 0,
sidecar_hmac: [0xcc; 32],
header_crc32c: 0,
};
let mut sidecar_bytes = sidecar.to_bytes();
sidecar_bytes[0] ^= 0x01;
assert_eq!(
BootstrapSidecarHeader::parse(&sidecar_bytes).unwrap_err(),
FormatError::BadMagic {
structure: "BootstrapSidecarHeader"
}
);
}
#[test]
fn crypto_header_fixed_round_trips_and_validates() {
let header = crypto_fixed();
let bytes = header.to_bytes();
let parsed = CryptoHeaderFixed::parse(&bytes, header.length).unwrap();
assert_eq!(parsed.compression_algo, CompressionAlgo::ZstdFramed);
assert_eq!(parsed.fec_algo, FecAlgo::ReedSolomonGF16);
}
#[test]
fn crypto_header_fixed_rejects_unsupported_profile_values() {
let mut header = crypto_fixed();
header.compression_algo = CompressionAlgo::None;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::UnsupportedCompression(CompressionAlgo::None)
);
let mut header = crypto_fixed();
header.block_size = 4097;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::OddBlockSize(4097)
);
let mut bytes = crypto_fixed().to_bytes();
bytes[47] = 1;
assert_eq!(
CryptoHeaderFixed::parse(&bytes, crypto_fixed().length).unwrap_err(),
FormatError::NonZeroReserved {
structure: "CryptoHeaderFixed"
}
);
}
#[test]
fn crypto_header_fixed_validates_v43_protection_mode_pairs() {
let mut header = crypto_fixed();
header.aead_algo = AeadAlgo::None;
header.kdf_algo = KdfAlgo::None;
header.validate_supported_profile().unwrap();
let mut header = crypto_fixed();
header.aead_algo = AeadAlgo::None;
header.kdf_algo = KdfAlgo::Raw;
assert_eq!(
header.validate_supported_profile().unwrap_err(),
FormatError::InvalidProtectionMode {
aead_algo: AeadAlgo::None,
kdf_algo: KdfAlgo::Raw,
}
);
let mut header = crypto_fixed();
header.aead_algo = AeadAlgo::AesGcmSiv256;
header.kdf_algo = KdfAlgo::None;
assert_eq!(
header.validate_supported_profile().unwrap_err(),
FormatError::InvalidProtectionMode {
aead_algo: AeadAlgo::AesGcmSiv256,
kdf_algo: KdfAlgo::None,
}
);
}
#[test]
fn crypto_header_fixed_rejects_parameter_mutation_matrix() {
let mut bytes = crypto_fixed().to_bytes();
write_u16(&mut bytes, 8, 99);
assert_eq!(
CryptoHeaderFixed::parse(&bytes, crypto_fixed().length).unwrap_err(),
FormatError::UnknownCompressionAlgo(99)
);
let mut bytes = crypto_fixed().to_bytes();
write_u16(&mut bytes, 10, 99);
assert_eq!(
CryptoHeaderFixed::parse(&bytes, crypto_fixed().length).unwrap_err(),
FormatError::UnknownAeadAlgo(99)
);
let mut bytes = crypto_fixed().to_bytes();
write_u16(&mut bytes, 12, 99);
assert_eq!(
CryptoHeaderFixed::parse(&bytes, crypto_fixed().length).unwrap_err(),
FormatError::UnknownFecAlgo(99)
);
let mut bytes = crypto_fixed().to_bytes();
write_u16(&mut bytes, 14, 99);
assert_eq!(
CryptoHeaderFixed::parse(&bytes, crypto_fixed().length).unwrap_err(),
FormatError::UnknownKdfAlgo(99)
);
let cases: Vec<(&'static str, CryptoHeaderFixed, FormatError)> = vec![
(
"unsupported FEC None",
CryptoHeaderFixed {
fec_algo: FecAlgo::None,
..crypto_fixed()
},
FormatError::UnsupportedFec(FecAlgo::None),
),
(
"unsupported FEC Wirehair",
CryptoHeaderFixed {
fec_algo: FecAlgo::Wirehair,
..crypto_fixed()
},
FormatError::UnsupportedFec(FecAlgo::Wirehair),
),
(
"invalid dictionary flag",
CryptoHeaderFixed {
has_dictionary: 2,
..crypto_fixed()
},
FormatError::InvalidBoolean {
field: "has_dictionary",
value: 2,
},
),
(
"zero stripe width",
CryptoHeaderFixed {
stripe_width: 0,
..crypto_fixed()
},
FormatError::ZeroStripeWidth,
),
(
"stripe width cap",
CryptoHeaderFixed {
stripe_width: READER_MAX_STRIPE_WIDTH + 1,
..crypto_fixed()
},
FormatError::ReaderResourceLimitExceeded {
field: "stripe_width",
cap: READER_MAX_STRIPE_WIDTH as u64,
actual: (READER_MAX_STRIPE_WIDTH + 1) as u64,
},
),
(
"loss tolerance must be below stripe width",
CryptoHeaderFixed {
stripe_width: 2,
volume_loss_tolerance: 2,
..crypto_fixed()
},
FormatError::VolumeLossToleranceOutOfRange {
volume_loss_tolerance: 2,
stripe_width: 2,
},
),
(
"bit rot pct cap",
CryptoHeaderFixed {
bit_rot_buffer_pct: 101,
..crypto_fixed()
},
FormatError::BitRotBufferPctTooLarge(101),
),
(
"zero payload data shards",
CryptoHeaderFixed {
fec_data_shards: 0,
..crypto_fixed()
},
FormatError::ZeroDataShardMaximum {
field: "fec_data_shards",
},
),
(
"zero index data shards",
CryptoHeaderFixed {
index_fec_data_shards: 0,
..crypto_fixed()
},
FormatError::ZeroDataShardMaximum {
field: "index_fec_data_shards",
},
),
(
"zero index root data shards",
CryptoHeaderFixed {
index_root_fec_data_shards: 0,
..crypto_fixed()
},
FormatError::ZeroDataShardMaximum {
field: "index_root_fec_data_shards",
},
),
(
"zero chunk size",
CryptoHeaderFixed {
chunk_size: 0,
..crypto_fixed()
},
FormatError::ZeroChunkSize,
),
(
"zero envelope target size",
CryptoHeaderFixed {
envelope_target_size: 0,
..crypto_fixed()
},
FormatError::ZeroEnvelopeTargetSize,
),
(
"chunk exceeds envelope",
CryptoHeaderFixed {
chunk_size: 4096,
envelope_target_size: 2048,
..crypto_fixed()
},
FormatError::ChunkSizeExceedsEnvelopeTarget {
chunk_size: 4096,
envelope_target_size: 2048,
},
),
(
"block size too small",
CryptoHeaderFixed {
block_size: 2048,
..crypto_fixed()
},
FormatError::BlockSizeTooSmall(2048),
),
];
for (name, header, expected) in cases {
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
expected,
"{name}"
);
}
}
#[test]
fn crypto_header_fixed_treats_expected_volume_size_as_advisory_not_reserved() {
let mut header = crypto_fixed();
header.expected_volume_size = 1u64 << 56;
let parsed = CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap();
assert_eq!(parsed.expected_volume_size, 1u64 << 56);
}
#[test]
fn crypto_header_fixed_rejects_reader_cap_excesses() {
let mut header = crypto_fixed();
header.chunk_size = READER_MAX_CHUNK_SIZE + 1;
header.envelope_target_size = header.chunk_size;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "chunk_size",
cap: READER_MAX_CHUNK_SIZE as u64,
actual: (READER_MAX_CHUNK_SIZE + 1) as u64,
}
);
let mut header = crypto_fixed();
header.block_size = READER_MAX_BLOCK_SIZE + 2;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "block_size",
cap: READER_MAX_BLOCK_SIZE as u64,
actual: (READER_MAX_BLOCK_SIZE + 2) as u64,
}
);
let mut header = crypto_fixed();
header.max_path_length = READER_MAX_PATH_LENGTH + 1;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "max_path_length",
cap: READER_MAX_PATH_LENGTH as u64,
actual: (READER_MAX_PATH_LENGTH + 1) as u64,
}
);
let mut header = crypto_fixed();
header.fec_data_shards = READER_MAX_FEC_CLASS_SHARDS as u16;
header.fec_parity_shards = 1;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "fec_data_shards + fec_parity_shards",
cap: READER_MAX_FEC_CLASS_SHARDS as u64,
actual: (READER_MAX_FEC_CLASS_SHARDS + 1) as u64,
}
);
let mut header = crypto_fixed();
header.index_fec_data_shards = READER_MAX_INDEX_FEC_CLASS_SHARDS as u16;
header.index_fec_parity_shards = 1;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "index_fec_data_shards + index_fec_parity_shards",
cap: READER_MAX_INDEX_FEC_CLASS_SHARDS as u64,
actual: (READER_MAX_INDEX_FEC_CLASS_SHARDS + 1) as u64,
}
);
let mut header = crypto_fixed();
header.block_size = 1_048_576;
header.fec_data_shards = 4_096;
header.fec_parity_shards = 0;
let max_data_shards = u32::MAX as u64 / header.block_size as u64;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "fec_data_shards",
cap: max_data_shards,
actual: 4_096,
}
);
let mut header = crypto_fixed();
header.block_size = 1_048_576;
header.index_fec_data_shards = 4_096;
header.index_fec_parity_shards = 0;
let max_data_shards = u32::MAX as u64 / header.block_size as u64;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "index_fec_data_shards",
cap: max_data_shards,
actual: 4_096,
}
);
let mut header = crypto_fixed();
header.block_size = 1_048_576;
header.index_root_fec_data_shards = 4_096;
let max_data_shards = u32::MAX as u64 / header.block_size as u64;
assert_eq!(
CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
FormatError::ReaderResourceLimitExceeded {
field: "index_root_fec_data_shards",
cap: max_data_shards,
actual: 4_096,
}
);
}
#[test]
fn crypto_extension_scanner_enforces_terminator_and_caps() {
let bytes = [0u8; CRYPTO_EXTENSION_HEADER_LEN];
assert!(scan_crypto_extension_tlvs(&bytes).unwrap().is_empty());
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x0001u16.to_le_bytes());
bytes.extend_from_slice(&3u32.to_le_bytes());
bytes.extend_from_slice(b"hey");
bytes.extend_from_slice(&0u16.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
let tlvs = scan_crypto_extension_tlvs(&bytes).unwrap();
assert_eq!(tlvs[0].tag, 1);
assert_eq!(tlvs[0].value, b"hey");
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x0001u16.to_le_bytes());
bytes.extend_from_slice(&257u32.to_le_bytes());
assert_eq!(
scan_crypto_extension_tlvs(&bytes).unwrap_err(),
FormatError::ExtensionPayloadTooLarge(257)
);
assert_eq!(
scan_crypto_extension_tlvs(&[]).unwrap_err(),
FormatError::MissingExtensionTerminator
);
}
#[test]
fn crypto_header_parse_splits_fixed_kdf_extensions_and_hmac() {
let bytes = raw_crypto_header_bytes();
let header = CryptoHeader::parse(&bytes, bytes.len() as u32).unwrap();
assert_eq!(header.fixed.kdf_algo, KdfAlgo::Raw);
assert_eq!(header.kdf_params, KdfParams::Raw);
assert!(header.extensions.is_empty());
assert_eq!(header.header_hmac, [0xab; CRYPTO_HEADER_HMAC_LEN]);
assert_eq!(
header.hmac_covered_bytes.len(),
bytes.len() - CRYPTO_HEADER_HMAC_LEN
);
}
#[test]
fn crypto_header_parse_rejects_truncated_and_bad_kdf_params() {
let mut bytes = raw_crypto_header_bytes();
bytes.truncate(CRYPTO_HEADER_FIXED_LEN + 1);
assert_eq!(
CryptoHeader::parse(&bytes, bytes.len() as u32).unwrap_err(),
FormatError::CryptoHeaderTooShort {
min: CRYPTO_HEADER_FIXED_LEN
+ 2
+ CRYPTO_EXTENSION_HEADER_LEN
+ CRYPTO_HEADER_HMAC_LEN,
actual: CRYPTO_HEADER_FIXED_LEN + 1
}
);
let mut bytes = raw_crypto_header_bytes();
write_u16(
&mut bytes,
CRYPTO_HEADER_FIXED_LEN,
KdfAlgo::Argon2id as u16,
);
assert_eq!(
CryptoHeader::parse(&bytes, bytes.len() as u32).unwrap_err(),
FormatError::KdfAlgoTagMismatch {
expected: 0,
actual: 1
}
);
}
#[test]
fn crypto_header_length_mismatch_matrix_rejects_independent_declared_lengths() {
let canonical = raw_crypto_header_bytes();
CryptoHeader::parse(&canonical, canonical.len() as u32).unwrap();
let mut fixed_longer = canonical.clone();
write_u32(&mut fixed_longer, 4, (canonical.len() + 1) as u32);
assert_eq!(
CryptoHeader::parse(&fixed_longer, fixed_longer.len() as u32).unwrap_err(),
FormatError::CryptoHeaderLengthMismatch {
fixed: (canonical.len() + 1) as u32,
volume: canonical.len() as u32,
}
);
let longer_volume_len = (canonical.len() + 1) as u32;
let mut padded = canonical.clone();
padded.insert(canonical.len() - CRYPTO_HEADER_HMAC_LEN, 0);
assert_eq!(
CryptoHeader::parse(&padded, longer_volume_len).unwrap_err(),
FormatError::CryptoHeaderLengthMismatch {
fixed: canonical.len() as u32,
volume: longer_volume_len,
}
);
let mut fixed_shorter = canonical.clone();
write_u32(&mut fixed_shorter, 4, (canonical.len() - 1) as u32);
assert_eq!(
CryptoHeader::parse(&fixed_shorter, fixed_shorter.len() as u32).unwrap_err(),
FormatError::CryptoHeaderLengthMismatch {
fixed: (canonical.len() - 1) as u32,
volume: canonical.len() as u32,
}
);
}
#[test]
fn crypto_extension_semantics_reject_forbidden_duplicate_and_critical() {
let duplicate = vec![
ExtensionTlv {
tag: 0x0001,
value: b"one",
},
ExtensionTlv {
tag: 0x0001,
value: b"two",
},
];
assert_eq!(
validate_crypto_extension_semantics(&duplicate).unwrap_err(),
FormatError::DuplicateKnownExtension(0x0001)
);
let forbidden = vec![ExtensionTlv {
tag: 0x8004,
value: b"",
}];
assert_eq!(
validate_crypto_extension_semantics(&forbidden).unwrap_err(),
FormatError::ForbiddenExtensionTag(0x0004)
);
let unknown_critical = vec![ExtensionTlv {
tag: 0x8123,
value: b"",
}];
assert_eq!(
validate_crypto_extension_semantics(&unknown_critical).unwrap_err(),
FormatError::UnknownCriticalExtension(0x0123)
);
let malformed_known = vec![ExtensionTlv {
tag: 0x0003,
value: b"short",
}];
assert_eq!(
validate_crypto_extension_semantics(&malformed_known).unwrap_err(),
FormatError::MalformedKnownExtension(0x0003)
);
}
#[test]
fn block_record_round_trips_and_validates_crc() {
let record = BlockRecord {
block_index: 0,
kind: BlockKind::PayloadData,
flags: BLOCK_LAST_DATA_FLAG,
payload: vec![7; 4096],
record_crc32c: 0,
};
let bytes = record.to_bytes();
let parsed = BlockRecord::parse(&bytes, 4096).unwrap();
assert_eq!(parsed.kind, BlockKind::PayloadData);
assert!(parsed.is_last_data());
let mut corrupted = bytes;
corrupted[20] ^= 1;
assert_eq!(
BlockRecord::parse(&corrupted, 4096).unwrap_err(),
FormatError::BadCrc {
structure: "BlockRecord"
}
);
}
#[test]
fn block_record_crc_covers_every_record_header_and_payload_byte() {
let record = BlockRecord {
block_index: 0x0102_0304_0506_0708,
kind: BlockKind::PayloadData,
flags: BLOCK_LAST_DATA_FLAG,
payload: (0..4096).map(|idx| (idx & 0xff) as u8).collect(),
record_crc32c: 0,
};
let bytes = record.to_bytes();
let covered_len = 16 + record.payload.len();
for offset in 0..covered_len {
let mut corrupted = bytes.clone();
corrupted[offset] ^= 0x80;
let err = BlockRecord::parse(&corrupted, record.payload.len()).unwrap_err();
if offset < 4 {
assert_eq!(
err,
FormatError::BadMagic {
structure: "BlockRecord"
},
"magic byte {offset}"
);
} else if (14..16).contains(&offset) {
assert_eq!(
err,
FormatError::NonZeroReserved {
structure: "BlockRecord"
},
"reserved byte {offset}"
);
} else {
assert_eq!(
err,
FormatError::BadCrc {
structure: "BlockRecord"
},
"CRC-covered byte {offset}"
);
}
}
let mut corrupted_crc = bytes;
corrupted_crc[covered_len] ^= 0x80;
assert_eq!(
BlockRecord::parse(&corrupted_crc, record.payload.len()).unwrap_err(),
FormatError::BadCrc {
structure: "BlockRecord"
}
);
}
#[test]
fn block_record_rejects_reserved_kind_flags_and_parity_last_flag() {
let mut record = BlockRecord {
block_index: 0,
kind: BlockKind::PayloadData,
flags: 0x02,
payload: vec![0; 4096],
record_crc32c: 0,
};
assert_eq!(
BlockRecord::parse(&record.to_bytes(), 4096).unwrap_err(),
FormatError::InvalidBlockFlags(0x02)
);
record.kind = BlockKind::PayloadParity;
record.flags = BLOCK_LAST_DATA_FLAG;
assert_eq!(
BlockRecord::parse(&record.to_bytes(), 4096).unwrap_err(),
FormatError::ParityBlockHasLastDataFlag
);
let mut bytes = record.to_bytes();
bytes[12] = 10;
let crc = crc32c(&bytes[..4112]);
write_u32(&mut bytes, 4112, crc);
assert_eq!(
BlockRecord::parse(&bytes, 4096).unwrap_err(),
FormatError::UnknownBlockKind(10)
);
for unknown_kind in 10..=u8::MAX {
let mut bytes = record.to_bytes();
bytes[12] = unknown_kind;
let crc = crc32c(&bytes[..4112]);
write_u32(&mut bytes, 4112, crc);
assert_eq!(
BlockRecord::parse(&bytes, 4096).unwrap_err(),
FormatError::UnknownBlockKind(unknown_kind)
);
}
}
#[test]
fn manifest_footer_round_trips_and_validates_index_extent() {
let footer = ManifestFooter {
archive_uuid: uuid(),
session_id: session(),
volume_index: 0,
is_authoritative: 1,
total_volumes: 1,
index_root_first_block: 0,
index_root_data_block_count: 2,
index_root_parity_block_count: 1,
index_root_encrypted_size: 8192,
index_root_decompressed_size: 120,
manifest_hmac: [0xaa; 32],
};
let parsed = ManifestFooter::parse(&footer.to_bytes()).unwrap();
parsed.validate_index_root_extent(4096).unwrap();
let mut bad = footer.clone();
bad.index_root_encrypted_size = 4096;
assert_eq!(
ManifestFooter::parse(&bad.to_bytes())
.unwrap()
.validate_index_root_extent(4096)
.unwrap_err(),
FormatError::IndexRootSizeMismatch
);
}
#[test]
fn volume_trailer_round_trips_and_requires_manifest_length() {
let trailer = VolumeTrailer {
archive_uuid: uuid(),
session_id: session(),
volume_index: 0,
block_count: 3,
bytes_written: 10_000,
manifest_footer_offset: 9_864,
manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
closed_at_ns: 123,
root_auth_footer_offset: 0,
root_auth_footer_length: 0,
root_auth_flags: 0,
trailer_hmac: [0xbb; 32],
};
let parsed = VolumeTrailer::parse(&trailer.to_bytes()).unwrap();
assert_eq!(parsed.block_count, 3);
let mut bad = trailer;
bad.manifest_footer_length = 100;
assert_eq!(
VolumeTrailer::parse(&bad.to_bytes()).unwrap_err(),
FormatError::InvalidManifestFooterLength(100)
);
}
#[test]
fn root_auth_footer_round_trips_and_validates_crc_and_lengths() {
let footer = RootAuthFooterV1 {
archive_uuid: uuid(),
session_id: session(),
authenticator_id: 2,
signer_identity_type: 1,
signer_identity_bytes: b"public-key".to_vec(),
authenticator_value: vec![0x5a; 68],
total_data_block_count: 7,
critical_metadata_digest: [1; 32],
index_digest: [2; 32],
fec_layout_digest: [3; 32],
data_block_merkle_root: [4; 32],
signer_identity_digest: [5; 32],
archive_root: [6; 32],
footer_crc32c: 0,
};
let bytes = footer.to_bytes().unwrap();
let parsed = RootAuthFooterV1::parse(&bytes).unwrap();
assert_eq!(parsed.archive_uuid, uuid());
assert_eq!(parsed.signer_identity_bytes, b"public-key");
assert_eq!(parsed.authenticator_value, vec![0x5a; 68]);
assert_eq!(parsed.footer_length().unwrap() as usize, bytes.len());
let mut bad_crc = bytes.clone();
bad_crc[100] ^= 0x40;
assert_eq!(
RootAuthFooterV1::parse(&bad_crc).unwrap_err(),
FormatError::BadCrc {
structure: "RootAuthFooterV1"
}
);
let mut bad_len = bytes;
write_u32(&mut bad_len, 30, 1);
let crc_offset = bad_len.len() - 4;
let crc = crc32c(&bad_len[..crc_offset]);
write_u32(&mut bad_len, crc_offset, crc);
assert!(matches!(
RootAuthFooterV1::parse(&bad_len).unwrap_err(),
FormatError::InvalidLength {
structure: "RootAuthFooterV1",
..
}
));
}
#[test]
fn bootstrap_sidecar_header_validates_presence_and_layout() {
let header = BootstrapSidecarHeader {
archive_uuid: uuid(),
session_id: session(),
flags: SIDECAR_MANIFEST_PRESENT | SIDECAR_INDEX_ROOT_PRESENT,
manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
index_root_records_offset: (BOOTSTRAP_SIDECAR_HEADER_LEN + MANIFEST_FOOTER_LEN) as u64,
index_root_records_length: 40,
dictionary_records_offset: 0,
dictionary_records_length: 0,
sidecar_hmac: [0xcc; 32],
header_crc32c: 0,
};
let parsed = BootstrapSidecarHeader::parse(&header.to_bytes()).unwrap();
parsed
.validate_packed_layout(
BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + MANIFEST_FOOTER_LEN as u64 + 40,
)
.unwrap();
let mut bad = header.clone();
bad.flags |= 0x08;
assert_eq!(
BootstrapSidecarHeader::parse(&bad.to_bytes()).unwrap_err(),
FormatError::UnknownBootstrapSidecarFlags(0x0b)
);
let mut bad = header;
bad.index_root_records_offset += 1;
let parsed = BootstrapSidecarHeader::parse(&bad.to_bytes()).unwrap();
assert_eq!(
parsed
.validate_packed_layout(
BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + MANIFEST_FOOTER_LEN as u64 + 40
)
.unwrap_err(),
FormatError::NonCanonicalBootstrapSidecarLayout
);
}
}