use std::collections::{BTreeSet, HashMap, HashSet};
use sha2::{Digest, Sha256};
use unicode_normalization::UnicodeNormalization;
use crate::format::FormatError;
const TZIR_MAGIC: [u8; 4] = *b"TZIR";
const TZIS_MAGIC: [u8; 4] = *b"TZIS";
const TZDH_MAGIC: [u8; 4] = *b"TZDH";
pub const INDEX_ROOT_LEN: usize = 160;
pub const SHARD_ENTRY_LEN: usize = 52;
pub const DIRECTORY_HINT_SHARD_ENTRY_LEN: usize = 56;
pub const ENVELOPE_ENTRY_LEN: usize = 48;
pub const FRAME_ENTRY_LEN: usize = 44;
pub const INDEX_SHARD_HEADER_LEN: usize = 64;
pub const FILE_ENTRY_LEN: usize = 56;
pub const DIRECTORY_HINT_TABLE_LEN: usize = 72;
pub const DIRECTORY_HINT_ENTRY_LEN: usize = 40;
const FRAME_KNOWN_FLAGS: u32 = 0x0000_0003;
const DEFAULT_MAX_HASH_COLLISION_SHARD_SCAN: usize = 16;
const REED_SOLOMON_GF16_MAX_TOTAL_SHARDS: u64 = 65_535;
const SHA256_EMPTY: [u8; 32] = [
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MetadataLimits {
pub block_size: u32,
pub max_path_length: u32,
pub max_hash_collision_shard_scan: usize,
pub max_shard_count: u32,
pub max_directory_hint_shards: u32,
pub max_files_per_index_shard: u32,
pub max_entries_per_directory_hint_shard: u64,
pub max_payload_data_shards: u16,
pub max_payload_parity_shards: u16,
pub max_index_data_shards: u16,
pub max_index_parity_shards: u16,
pub max_index_root_data_shards: u16,
pub max_index_root_parity_shards: u16,
}
impl Default for MetadataLimits {
fn default() -> Self {
Self {
block_size: 4096,
max_path_length: 4096,
max_hash_collision_shard_scan: DEFAULT_MAX_HASH_COLLISION_SHARD_SCAN,
max_shard_count: 1_000_000,
max_directory_hint_shards: 1_000_000,
max_files_per_index_shard: 1_000_000,
max_entries_per_directory_hint_shard: 1_000_000,
max_payload_data_shards: u16::MAX,
max_payload_parity_shards: u16::MAX,
max_index_data_shards: u16::MAX,
max_index_parity_shards: u16::MAX,
max_index_root_data_shards: u16::MAX,
max_index_root_parity_shards: u16::MAX,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexRootHeader {
pub version: u32,
pub shard_count: u32,
pub directory_hint_shard_count: u32,
pub frame_count: u64,
pub envelope_count: u64,
pub file_count: u64,
pub payload_block_count: u64,
pub tar_total_size: u64,
pub content_sha256: [u8; 32],
pub shard_table_offset: u64,
pub directory_hint_shard_table_offset: u64,
pub dictionary_first_block: u64,
pub dictionary_data_block_count: u32,
pub dictionary_parity_block_count: u32,
pub dictionary_encrypted_size: u32,
pub dictionary_decompressed_size: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexRoot {
pub header: IndexRootHeader,
pub shards: Vec<ShardEntry>,
pub directory_hint_shards: Vec<DirectoryHintShardEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShardEntry {
pub shard_index: u64,
pub first_block_index: u64,
pub data_block_count: u32,
pub parity_block_count: u32,
pub encrypted_size: u32,
pub decompressed_size: u32,
pub file_count: u32,
pub first_path_hash: [u8; 8],
pub last_path_hash: [u8; 8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectoryHintShardEntry {
pub hint_shard_index: u64,
pub first_dir_hash: [u8; 8],
pub last_dir_hash: [u8; 8],
pub first_block_index: u64,
pub data_block_count: u32,
pub parity_block_count: u32,
pub encrypted_size: u32,
pub decompressed_size: u32,
pub entry_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvelopeEntry {
pub envelope_index: u64,
pub first_block_index: u64,
pub data_block_count: u32,
pub parity_block_count: u32,
pub encrypted_size: u32,
pub plaintext_size: u32,
pub first_frame_index: u64,
pub frame_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameEntry {
pub frame_index: u64,
pub envelope_index: u64,
pub offset_in_envelope: u32,
pub compressed_size: u32,
pub decompressed_size: u32,
pub flags: u32,
pub tar_stream_offset: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexShardHeader {
pub version: u32,
pub shard_index: u64,
pub file_count: u32,
pub frame_count: u32,
pub envelope_count: u32,
pub file_table_offset: u32,
pub frame_table_offset: u32,
pub envelope_table_offset: u32,
pub string_pool_offset: u32,
pub string_pool_size: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexShard {
pub header: IndexShardHeader,
pub files: Vec<FileEntry>,
pub frames: Vec<FrameEntry>,
pub envelopes: Vec<EnvelopeEntry>,
pub string_pool: Vec<u8>,
file_paths: Vec<Vec<u8>>,
file_tar_member_group_starts: Vec<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileEntry {
pub path_hash: [u8; 8],
pub path_offset: u32,
pub path_length: u32,
pub first_frame_index: u64,
pub frame_count: u32,
pub offset_in_first_frame_plaintext: u32,
pub tar_member_group_size: u64,
pub file_data_size: u64,
pub flags: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectoryHintTable {
pub header: DirectoryHintTableHeader,
pub entries: Vec<DirectoryHintEntry>,
pub shard_row_indexes: Vec<u32>,
pub string_pool: Vec<u8>,
entry_paths: Vec<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectoryHintTableHeader {
pub version: u32,
pub hint_shard_index: u64,
pub entry_count: u64,
pub entry_table_offset: u64,
pub shard_list_offset: u64,
pub string_pool_offset: u64,
pub string_pool_size: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectoryHintEntry {
pub dir_hash: [u8; 8],
pub path_offset: u64,
pub path_length: u32,
pub shard_list_start_index: u32,
pub shard_count: u32,
}
impl IndexRoot {
pub fn parse(
bytes: &[u8],
has_dictionary: bool,
limits: MetadataLimits,
) -> Result<Self, FormatError> {
let structure = "IndexRoot";
if bytes.len() < INDEX_ROOT_LEN {
return invalid(structure, "plaintext is shorter than fixed header");
}
expect_magic(structure, TZIR_MAGIC, read_array::<4>(bytes, 0, structure)?)?;
expect_zero(structure, slice(bytes, 128, 32, structure)?)?;
let header = IndexRootHeader {
version: read_u32(bytes, 4, structure)?,
shard_count: read_u32(bytes, 8, structure)?,
directory_hint_shard_count: read_u32(bytes, 12, structure)?,
frame_count: read_u64(bytes, 16, structure)?,
envelope_count: read_u64(bytes, 24, structure)?,
file_count: read_u64(bytes, 32, structure)?,
payload_block_count: read_u64(bytes, 40, structure)?,
tar_total_size: read_u64(bytes, 48, structure)?,
content_sha256: read_array::<32>(bytes, 56, structure)?,
shard_table_offset: read_u64(bytes, 88, structure)?,
directory_hint_shard_table_offset: read_u64(bytes, 96, structure)?,
dictionary_first_block: read_u64(bytes, 104, structure)?,
dictionary_data_block_count: read_u32(bytes, 112, structure)?,
dictionary_parity_block_count: read_u32(bytes, 116, structure)?,
dictionary_encrypted_size: read_u32(bytes, 120, structure)?,
dictionary_decompressed_size: read_u32(bytes, 124, structure)?,
};
if header.version != 1 {
return invalid(structure, "unsupported version");
}
if header.shard_count > limits.max_shard_count {
return invalid(structure, "shard count exceeds resource cap");
}
if header.directory_hint_shard_count > limits.max_directory_hint_shards {
return invalid(structure, "directory hint shard count exceeds resource cap");
}
validate_dictionary_fields(&header, has_dictionary, limits)?;
let mut cursor = INDEX_ROOT_LEN;
let shards = if header.shard_count == 0 {
if header.shard_table_offset != 0 {
return invalid(structure, "absent shard table has non-zero offset");
}
Vec::new()
} else {
expect_offset(structure, "shard table", header.shard_table_offset, cursor)?;
let count = to_usize(header.shard_count as u64, structure)?;
let bytes_len = checked_mul(count, SHARD_ENTRY_LEN, structure)?;
let table = slice(bytes, cursor, bytes_len, structure)?;
cursor = checked_add(cursor, bytes_len, structure)?;
parse_shard_entries(table, limits)?
};
let directory_hint_shards = if header.directory_hint_shard_count == 0 {
if header.directory_hint_shard_table_offset != 0 {
return invalid(
structure,
"absent directory hint shard table has non-zero offset",
);
}
Vec::new()
} else {
if header.shard_count == 0 {
return invalid(structure, "directory hints require at least one shard");
}
expect_offset(
structure,
"directory hint shard table",
header.directory_hint_shard_table_offset,
cursor,
)?;
let count = to_usize(header.directory_hint_shard_count as u64, structure)?;
let bytes_len = checked_mul(count, DIRECTORY_HINT_SHARD_ENTRY_LEN, structure)?;
let table = slice(bytes, cursor, bytes_len, structure)?;
cursor = checked_add(cursor, bytes_len, structure)?;
parse_directory_hint_shard_entries(table, limits)?
};
if bytes.len() != cursor {
return invalid(
structure,
"plaintext length does not match canonical cursor",
);
}
validate_index_root_totals(&header, &shards, has_dictionary)?;
Ok(Self {
header,
shards,
directory_hint_shards,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut header = self.header.clone();
header.shard_count = self.shards.len() as u32;
header.directory_hint_shard_count = self.directory_hint_shards.len() as u32;
header.shard_table_offset = if self.shards.is_empty() {
0
} else {
INDEX_ROOT_LEN as u64
};
header.directory_hint_shard_table_offset = if self.directory_hint_shards.is_empty() {
0
} else {
(INDEX_ROOT_LEN + self.shards.len() * SHARD_ENTRY_LEN) as u64
};
let mut bytes = Vec::with_capacity(
INDEX_ROOT_LEN
+ self.shards.len() * SHARD_ENTRY_LEN
+ self.directory_hint_shards.len() * DIRECTORY_HINT_SHARD_ENTRY_LEN,
);
bytes.extend_from_slice(&header.to_bytes());
for entry in &self.shards {
bytes.extend_from_slice(&entry.to_bytes());
}
for entry in &self.directory_hint_shards {
bytes.extend_from_slice(&entry.to_bytes());
}
bytes
}
pub fn candidate_shard_indexes_for_hash(
&self,
target_hash: [u8; 8],
scan_cap_per_direction: usize,
) -> Result<Vec<usize>, FormatError> {
candidate_interval_indexes(
&self.shards,
target_hash,
scan_cap_per_direction,
|entry| entry.first_path_hash,
|entry| entry.last_path_hash,
)
}
pub fn candidate_shards_for_path(
&self,
normalized_path: &[u8],
limits: MetadataLimits,
) -> Result<Vec<usize>, FormatError> {
self.candidate_shard_indexes_for_hash(
hash_prefix(normalized_path),
limits.max_hash_collision_shard_scan,
)
}
}
impl IndexRootHeader {
pub fn empty() -> Self {
Self {
version: 1,
shard_count: 0,
directory_hint_shard_count: 0,
frame_count: 0,
envelope_count: 0,
file_count: 0,
payload_block_count: 0,
tar_total_size: 0,
content_sha256: SHA256_EMPTY,
shard_table_offset: 0,
directory_hint_shard_table_offset: 0,
dictionary_first_block: 0,
dictionary_data_block_count: 0,
dictionary_parity_block_count: 0,
dictionary_encrypted_size: 0,
dictionary_decompressed_size: 0,
}
}
pub fn to_bytes(&self) -> [u8; INDEX_ROOT_LEN] {
let mut bytes = [0u8; INDEX_ROOT_LEN];
bytes[0..4].copy_from_slice(&TZIR_MAGIC);
write_u32(&mut bytes, 4, self.version);
write_u32(&mut bytes, 8, self.shard_count);
write_u32(&mut bytes, 12, self.directory_hint_shard_count);
write_u64(&mut bytes, 16, self.frame_count);
write_u64(&mut bytes, 24, self.envelope_count);
write_u64(&mut bytes, 32, self.file_count);
write_u64(&mut bytes, 40, self.payload_block_count);
write_u64(&mut bytes, 48, self.tar_total_size);
bytes[56..88].copy_from_slice(&self.content_sha256);
write_u64(&mut bytes, 88, self.shard_table_offset);
write_u64(&mut bytes, 96, self.directory_hint_shard_table_offset);
write_u64(&mut bytes, 104, self.dictionary_first_block);
write_u32(&mut bytes, 112, self.dictionary_data_block_count);
write_u32(&mut bytes, 116, self.dictionary_parity_block_count);
write_u32(&mut bytes, 120, self.dictionary_encrypted_size);
write_u32(&mut bytes, 124, self.dictionary_decompressed_size);
bytes
}
}
impl ShardEntry {
pub fn to_bytes(&self) -> [u8; SHARD_ENTRY_LEN] {
let mut bytes = [0u8; SHARD_ENTRY_LEN];
write_u64(&mut bytes, 0, self.shard_index);
write_u64(&mut bytes, 8, self.first_block_index);
write_u32(&mut bytes, 16, self.data_block_count);
write_u32(&mut bytes, 20, self.parity_block_count);
write_u32(&mut bytes, 24, self.encrypted_size);
write_u32(&mut bytes, 28, self.decompressed_size);
write_u32(&mut bytes, 32, self.file_count);
bytes[36..44].copy_from_slice(&self.first_path_hash);
bytes[44..52].copy_from_slice(&self.last_path_hash);
bytes
}
}
impl DirectoryHintShardEntry {
pub fn to_bytes(&self) -> [u8; DIRECTORY_HINT_SHARD_ENTRY_LEN] {
let mut bytes = [0u8; DIRECTORY_HINT_SHARD_ENTRY_LEN];
write_u64(&mut bytes, 0, self.hint_shard_index);
bytes[8..16].copy_from_slice(&self.first_dir_hash);
bytes[16..24].copy_from_slice(&self.last_dir_hash);
write_u64(&mut bytes, 24, self.first_block_index);
write_u32(&mut bytes, 32, self.data_block_count);
write_u32(&mut bytes, 36, self.parity_block_count);
write_u32(&mut bytes, 40, self.encrypted_size);
write_u32(&mut bytes, 44, self.decompressed_size);
write_u64(&mut bytes, 48, self.entry_count);
bytes
}
}
impl IndexShard {
pub fn parse(
bytes: &[u8],
locating_shard: &ShardEntry,
limits: MetadataLimits,
) -> Result<Self, FormatError> {
let structure = "IndexShard";
if bytes.len() < INDEX_SHARD_HEADER_LEN {
return invalid(structure, "plaintext is shorter than fixed header");
}
expect_magic(structure, TZIS_MAGIC, read_array::<4>(bytes, 0, structure)?)?;
expect_zero(structure, slice(bytes, 48, 16, structure)?)?;
let header = IndexShardHeader {
version: read_u32(bytes, 4, structure)?,
shard_index: read_u64(bytes, 8, structure)?,
file_count: read_u32(bytes, 16, structure)?,
frame_count: read_u32(bytes, 20, structure)?,
envelope_count: read_u32(bytes, 24, structure)?,
file_table_offset: read_u32(bytes, 28, structure)?,
frame_table_offset: read_u32(bytes, 32, structure)?,
envelope_table_offset: read_u32(bytes, 36, structure)?,
string_pool_offset: read_u32(bytes, 40, structure)?,
string_pool_size: read_u32(bytes, 44, structure)?,
};
if header.version != 1 {
return invalid(structure, "unsupported version");
}
if header.file_count == 0 {
return invalid(structure, "index shard must contain at least one file");
}
if header.file_count > limits.max_files_per_index_shard {
return invalid(structure, "file count exceeds resource cap");
}
if header.shard_index != locating_shard.shard_index {
return invalid(structure, "shard index does not match locating ShardEntry");
}
if header.file_count != locating_shard.file_count {
return invalid(structure, "file count does not match locating ShardEntry");
}
let mut cursor = INDEX_SHARD_HEADER_LEN;
let files = parse_counted_table(
bytes,
CountedTableSpec {
structure,
name: "file table",
count: header.file_count as u64,
offset: header.file_table_offset as u64,
entry_len: FILE_ENTRY_LEN,
parse: parse_file_entry,
},
&mut cursor,
)?;
let frames = parse_counted_table(
bytes,
CountedTableSpec {
structure,
name: "frame table",
count: header.frame_count as u64,
offset: header.frame_table_offset as u64,
entry_len: FRAME_ENTRY_LEN,
parse: parse_frame_entry,
},
&mut cursor,
)?;
let envelopes = parse_counted_table(
bytes,
CountedTableSpec {
structure,
name: "envelope table",
count: header.envelope_count as u64,
offset: header.envelope_table_offset as u64,
entry_len: ENVELOPE_ENTRY_LEN,
parse: parse_envelope_entry,
},
&mut cursor,
)?;
let string_pool = if header.string_pool_size == 0 {
if header.string_pool_offset != 0 {
return invalid(structure, "absent string pool has non-zero offset");
}
Vec::new()
} else {
expect_offset(
structure,
"string pool",
header.string_pool_offset as u64,
cursor,
)?;
let len = header.string_pool_size as usize;
let pool = slice(bytes, cursor, len, structure)?.to_vec();
cursor = checked_add(cursor, len, structure)?;
pool
};
if bytes.len() != cursor {
return invalid(
structure,
"plaintext length does not match canonical cursor",
);
}
let (file_paths, file_tar_member_group_starts) = validate_index_shard_tables(
&files,
&frames,
&envelopes,
&string_pool,
locating_shard,
limits,
)?;
Ok(Self {
header,
files,
frames,
envelopes,
string_pool,
file_paths,
file_tar_member_group_starts,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut header = self.header.clone();
header.file_count = self.files.len() as u32;
header.frame_count = self.frames.len() as u32;
header.envelope_count = self.envelopes.len() as u32;
let mut cursor = INDEX_SHARD_HEADER_LEN;
header.file_table_offset = table_offset(self.files.len(), cursor);
cursor += self.files.len() * FILE_ENTRY_LEN;
header.frame_table_offset = table_offset(self.frames.len(), cursor);
cursor += self.frames.len() * FRAME_ENTRY_LEN;
header.envelope_table_offset = table_offset(self.envelopes.len(), cursor);
cursor += self.envelopes.len() * ENVELOPE_ENTRY_LEN;
header.string_pool_size = self.string_pool.len() as u32;
header.string_pool_offset = table_offset(self.string_pool.len(), cursor);
let mut bytes = Vec::with_capacity(cursor + self.string_pool.len());
bytes.extend_from_slice(&header.to_bytes());
for entry in &self.files {
bytes.extend_from_slice(&entry.to_bytes());
}
for entry in &self.frames {
bytes.extend_from_slice(&entry.to_bytes());
}
for entry in &self.envelopes {
bytes.extend_from_slice(&entry.to_bytes());
}
bytes.extend_from_slice(&self.string_pool);
bytes
}
pub fn file_path(&self, file_index: usize) -> Option<&[u8]> {
self.file_paths.get(file_index).map(Vec::as_slice)
}
pub fn tar_member_group_start(&self, file_index: usize) -> Option<u64> {
self.file_tar_member_group_starts.get(file_index).copied()
}
pub fn lookup_file_index(&self, normalized_path: &[u8]) -> Option<usize> {
let target_hash = hash_prefix(normalized_path);
let lower = self.lower_bound_file_key(target_hash, normalized_path);
let mut best = None;
for idx in lower..self.files.len() {
let file = &self.files[idx];
if file.path_hash != target_hash || self.file_paths[idx].as_slice() != normalized_path {
break;
}
best = Some(idx);
}
best
}
fn lower_bound_file_key(&self, target_hash: [u8; 8], target_path: &[u8]) -> usize {
let mut low = 0usize;
let mut high = self.files.len();
while low < high {
let mid = low + (high - low) / 2;
let key_is_less = self.files[mid].path_hash < target_hash
|| (self.files[mid].path_hash == target_hash
&& self.file_paths[mid].as_slice() < target_path);
if key_is_less {
low = mid + 1;
} else {
high = mid;
}
}
low
}
}
impl IndexShardHeader {
pub fn to_bytes(&self) -> [u8; INDEX_SHARD_HEADER_LEN] {
let mut bytes = [0u8; INDEX_SHARD_HEADER_LEN];
bytes[0..4].copy_from_slice(&TZIS_MAGIC);
write_u32(&mut bytes, 4, self.version);
write_u64(&mut bytes, 8, self.shard_index);
write_u32(&mut bytes, 16, self.file_count);
write_u32(&mut bytes, 20, self.frame_count);
write_u32(&mut bytes, 24, self.envelope_count);
write_u32(&mut bytes, 28, self.file_table_offset);
write_u32(&mut bytes, 32, self.frame_table_offset);
write_u32(&mut bytes, 36, self.envelope_table_offset);
write_u32(&mut bytes, 40, self.string_pool_offset);
write_u32(&mut bytes, 44, self.string_pool_size);
bytes
}
}
impl FileEntry {
pub fn to_bytes(&self) -> [u8; FILE_ENTRY_LEN] {
let mut bytes = [0u8; FILE_ENTRY_LEN];
bytes[0..8].copy_from_slice(&self.path_hash);
write_u32(&mut bytes, 8, self.path_offset);
write_u32(&mut bytes, 12, self.path_length);
write_u64(&mut bytes, 16, self.first_frame_index);
write_u32(&mut bytes, 24, self.frame_count);
write_u32(&mut bytes, 28, self.offset_in_first_frame_plaintext);
write_u64(&mut bytes, 32, self.tar_member_group_size);
write_u64(&mut bytes, 40, self.file_data_size);
write_u32(&mut bytes, 48, self.flags);
bytes
}
}
impl FrameEntry {
pub fn to_bytes(&self) -> [u8; FRAME_ENTRY_LEN] {
let mut bytes = [0u8; FRAME_ENTRY_LEN];
write_u64(&mut bytes, 0, self.frame_index);
write_u64(&mut bytes, 8, self.envelope_index);
write_u32(&mut bytes, 16, self.offset_in_envelope);
write_u32(&mut bytes, 20, self.compressed_size);
write_u32(&mut bytes, 24, self.decompressed_size);
write_u32(&mut bytes, 28, self.flags);
write_u64(&mut bytes, 32, self.tar_stream_offset);
bytes
}
}
impl EnvelopeEntry {
pub fn to_bytes(&self) -> [u8; ENVELOPE_ENTRY_LEN] {
let mut bytes = [0u8; ENVELOPE_ENTRY_LEN];
write_u64(&mut bytes, 0, self.envelope_index);
write_u64(&mut bytes, 8, self.first_block_index);
write_u32(&mut bytes, 16, self.data_block_count);
write_u32(&mut bytes, 20, self.parity_block_count);
write_u32(&mut bytes, 24, self.encrypted_size);
write_u32(&mut bytes, 28, self.plaintext_size);
write_u64(&mut bytes, 32, self.first_frame_index);
write_u32(&mut bytes, 40, self.frame_count);
bytes
}
}
impl DirectoryHintTable {
pub fn parse(
bytes: &[u8],
locating_shard: &DirectoryHintShardEntry,
index_root_shard_count: u32,
limits: MetadataLimits,
) -> Result<Self, FormatError> {
let structure = "DirectoryHintTable";
if bytes.len() < DIRECTORY_HINT_TABLE_LEN {
return invalid(structure, "plaintext is shorter than fixed header");
}
expect_magic(structure, TZDH_MAGIC, read_array::<4>(bytes, 0, structure)?)?;
expect_zero(structure, slice(bytes, 56, 16, structure)?)?;
let header = DirectoryHintTableHeader {
version: read_u32(bytes, 4, structure)?,
hint_shard_index: read_u64(bytes, 8, structure)?,
entry_count: read_u64(bytes, 16, structure)?,
entry_table_offset: read_u64(bytes, 24, structure)?,
shard_list_offset: read_u64(bytes, 32, structure)?,
string_pool_offset: read_u64(bytes, 40, structure)?,
string_pool_size: read_u64(bytes, 48, structure)?,
};
if header.version != 1 {
return invalid(structure, "unsupported version");
}
if header.hint_shard_index != locating_shard.hint_shard_index {
return invalid(
structure,
"hint shard index does not match locating DirectoryHintShardEntry",
);
}
if header.entry_count != locating_shard.entry_count {
return invalid(
structure,
"entry count does not match locating DirectoryHintShardEntry",
);
}
if header.entry_count == 0 {
return invalid(structure, "located directory hint shard is empty");
}
if header.entry_count > limits.max_entries_per_directory_hint_shard {
return invalid(structure, "entry count exceeds resource cap");
}
let entry_count = to_usize(header.entry_count, structure)?;
expect_offset(
structure,
"entry table",
header.entry_table_offset,
DIRECTORY_HINT_TABLE_LEN,
)?;
let entry_bytes_len = checked_mul(entry_count, DIRECTORY_HINT_ENTRY_LEN, structure)?;
let entries_end = checked_add(DIRECTORY_HINT_TABLE_LEN, entry_bytes_len, structure)?;
expect_offset(
structure,
"shard list",
header.shard_list_offset,
entries_end,
)?;
if header.shard_list_offset % 4 != 0 {
return invalid(structure, "shard list is not 4-byte aligned");
}
let entry_bytes = slice(bytes, DIRECTORY_HINT_TABLE_LEN, entry_bytes_len, structure)?;
let entries = parse_directory_hint_entries(entry_bytes)?;
let shard_list_len = validate_directory_hint_entries(
&entries,
bytes,
&header,
locating_shard,
index_root_shard_count,
)?;
let shard_list_offset = to_usize(header.shard_list_offset, structure)?;
let shard_list_bytes_len = checked_mul(shard_list_len, 4, structure)?;
let shard_list_end = checked_add(shard_list_offset, shard_list_bytes_len, structure)?;
let shard_list_bytes = slice(bytes, shard_list_offset, shard_list_bytes_len, structure)?;
let shard_row_indexes = parse_u32_array(shard_list_bytes, structure)?;
let string_pool = if header.string_pool_size == 0 {
if header.string_pool_offset != 0 {
return invalid(structure, "absent string pool has non-zero offset");
}
Vec::new()
} else {
expect_offset(
structure,
"string pool",
header.string_pool_offset,
shard_list_end,
)?;
let offset = to_usize(header.string_pool_offset, structure)?;
let size = to_usize(header.string_pool_size, structure)?;
slice(bytes, offset, size, structure)?.to_vec()
};
let final_cursor = if header.string_pool_size == 0 {
shard_list_end
} else {
checked_add(
to_usize(header.string_pool_offset, structure)?,
to_usize(header.string_pool_size, structure)?,
structure,
)?
};
if bytes.len() != final_cursor {
return invalid(
structure,
"plaintext length does not match canonical cursor",
);
}
let entry_paths = validate_directory_hint_paths_and_lists(
&entries,
&shard_row_indexes,
&string_pool,
locating_shard,
index_root_shard_count,
limits.max_path_length,
)?;
Ok(Self {
header,
entries,
shard_row_indexes,
string_pool,
entry_paths,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut header = self.header.clone();
header.entry_count = self.entries.len() as u64;
header.entry_table_offset = if self.entries.is_empty() {
0
} else {
DIRECTORY_HINT_TABLE_LEN as u64
};
header.shard_list_offset = if self.entries.is_empty() {
0
} else {
(DIRECTORY_HINT_TABLE_LEN + self.entries.len() * DIRECTORY_HINT_ENTRY_LEN) as u64
};
header.string_pool_size = self.string_pool.len() as u64;
header.string_pool_offset = if self.string_pool.is_empty() {
0
} else {
header.shard_list_offset + (self.shard_row_indexes.len() as u64) * 4
};
let mut bytes = Vec::with_capacity(
DIRECTORY_HINT_TABLE_LEN
+ self.entries.len() * DIRECTORY_HINT_ENTRY_LEN
+ self.shard_row_indexes.len() * 4
+ self.string_pool.len(),
);
bytes.extend_from_slice(&header.to_bytes());
for entry in &self.entries {
bytes.extend_from_slice(&entry.to_bytes());
}
if !self.entries.is_empty() {
for row in &self.shard_row_indexes {
let mut raw = [0u8; 4];
write_u32(&mut raw, 0, *row);
bytes.extend_from_slice(&raw);
}
}
bytes.extend_from_slice(&self.string_pool);
bytes
}
pub fn entry_path(&self, entry_index: usize) -> Option<&[u8]> {
self.entry_paths.get(entry_index).map(Vec::as_slice)
}
pub fn lookup_directory_index(&self, normalized_dir_path: &[u8]) -> Option<usize> {
let target_hash = hash_prefix(normalized_dir_path);
let lower = self.lower_bound_directory_key(target_hash, normalized_dir_path);
let entry = self.entries.get(lower)?;
if entry.dir_hash == target_hash
&& self.entry_paths[lower].as_slice() == normalized_dir_path
{
Some(lower)
} else {
None
}
}
fn lower_bound_directory_key(&self, target_hash: [u8; 8], target_path: &[u8]) -> usize {
let mut low = 0usize;
let mut high = self.entries.len();
while low < high {
let mid = low + (high - low) / 2;
let key_is_less = self.entries[mid].dir_hash < target_hash
|| (self.entries[mid].dir_hash == target_hash
&& self.entry_paths[mid].as_slice() < target_path);
if key_is_less {
low = mid + 1;
} else {
high = mid;
}
}
low
}
pub fn shard_rows_for_entry(&self, entry_index: usize) -> Option<&[u32]> {
let entry = self.entries.get(entry_index)?;
let start = entry.shard_list_start_index as usize;
let end = start.checked_add(entry.shard_count as usize)?;
self.shard_row_indexes.get(start..end)
}
}
impl DirectoryHintTableHeader {
pub fn to_bytes(&self) -> [u8; DIRECTORY_HINT_TABLE_LEN] {
let mut bytes = [0u8; DIRECTORY_HINT_TABLE_LEN];
bytes[0..4].copy_from_slice(&TZDH_MAGIC);
write_u32(&mut bytes, 4, self.version);
write_u64(&mut bytes, 8, self.hint_shard_index);
write_u64(&mut bytes, 16, self.entry_count);
write_u64(&mut bytes, 24, self.entry_table_offset);
write_u64(&mut bytes, 32, self.shard_list_offset);
write_u64(&mut bytes, 40, self.string_pool_offset);
write_u64(&mut bytes, 48, self.string_pool_size);
bytes
}
}
impl DirectoryHintEntry {
pub fn to_bytes(&self) -> [u8; DIRECTORY_HINT_ENTRY_LEN] {
let mut bytes = [0u8; DIRECTORY_HINT_ENTRY_LEN];
bytes[0..8].copy_from_slice(&self.dir_hash);
write_u64(&mut bytes, 8, self.path_offset);
write_u32(&mut bytes, 16, self.path_length);
write_u32(&mut bytes, 24, self.shard_list_start_index);
write_u32(&mut bytes, 28, self.shard_count);
bytes
}
}
pub fn hash_prefix(bytes: &[u8]) -> [u8; 8] {
let digest = Sha256::digest(bytes);
let mut out = [0u8; 8];
out.copy_from_slice(&digest[..8]);
out
}
pub fn normalize_lookup_file_path(
path: &str,
max_path_length: u32,
) -> Result<Vec<u8>, FormatError> {
let normalized = path.nfc().collect::<String>();
validate_file_path_bytes(normalized.as_bytes(), max_path_length)?;
Ok(normalized.into_bytes())
}
pub fn normalize_lookup_directory_path(
path: &str,
max_path_length: u32,
) -> Result<Vec<u8>, FormatError> {
let trimmed = path.strip_suffix('/').unwrap_or(path);
let normalized = trimmed.nfc().collect::<String>();
validate_directory_path_bytes(normalized.as_bytes(), max_path_length)?;
Ok(normalized.into_bytes())
}
pub fn is_directory_ancestor(directory_path: &[u8], file_path: &[u8]) -> bool {
if directory_path.is_empty() {
return true;
}
file_path.len() > directory_path.len()
&& file_path.starts_with(directory_path)
&& file_path[directory_path.len()] == b'/'
}
fn parse_shard_entries(
bytes: &[u8],
limits: MetadataLimits,
) -> Result<Vec<ShardEntry>, FormatError> {
let mut entries = Vec::with_capacity(bytes.len() / SHARD_ENTRY_LEN);
let mut seen_indexes = HashSet::new();
for chunk in bytes.chunks_exact(SHARD_ENTRY_LEN) {
let entry = ShardEntry {
shard_index: read_u64(chunk, 0, "ShardEntry")?,
first_block_index: read_u64(chunk, 8, "ShardEntry")?,
data_block_count: read_u32(chunk, 16, "ShardEntry")?,
parity_block_count: read_u32(chunk, 20, "ShardEntry")?,
encrypted_size: read_u32(chunk, 24, "ShardEntry")?,
decompressed_size: read_u32(chunk, 28, "ShardEntry")?,
file_count: read_u32(chunk, 32, "ShardEntry")?,
first_path_hash: read_array::<8>(chunk, 36, "ShardEntry")?,
last_path_hash: read_array::<8>(chunk, 44, "ShardEntry")?,
};
if entry.file_count == 0 {
return invalid("ShardEntry", "file count is zero");
}
if entry.decompressed_size == 0 {
return invalid("ShardEntry", "decompressed size is zero");
}
validate_encrypted_extent(
"ShardEntry",
entry.data_block_count,
entry.encrypted_size,
limits.block_size,
)?;
validate_fec_class_extent(
"ShardEntry",
entry.data_block_count,
entry.parity_block_count,
limits.max_index_data_shards,
limits.max_index_parity_shards,
)?;
if entry.first_path_hash > entry.last_path_hash {
return invalid("ShardEntry", "first hash is greater than last hash");
}
if !seen_indexes.insert(entry.shard_index) {
return invalid("ShardEntry", "duplicate shard index");
}
if let Some(previous) = entries.last() {
let previous: &ShardEntry = previous;
if shard_entry_sort_key(previous) >= shard_entry_sort_key(&entry) {
return invalid("IndexRoot", "ShardEntry rows are not sorted");
}
if previous.last_path_hash > entry.first_path_hash {
return invalid("IndexRoot", "ShardEntry hash ranges overlap out of order");
}
}
entries.push(entry);
}
Ok(entries)
}
fn parse_directory_hint_shard_entries(
bytes: &[u8],
limits: MetadataLimits,
) -> Result<Vec<DirectoryHintShardEntry>, FormatError> {
let mut entries = Vec::with_capacity(bytes.len() / DIRECTORY_HINT_SHARD_ENTRY_LEN);
let mut seen_indexes = HashSet::new();
for chunk in bytes.chunks_exact(DIRECTORY_HINT_SHARD_ENTRY_LEN) {
let entry = DirectoryHintShardEntry {
hint_shard_index: read_u64(chunk, 0, "DirectoryHintShardEntry")?,
first_dir_hash: read_array::<8>(chunk, 8, "DirectoryHintShardEntry")?,
last_dir_hash: read_array::<8>(chunk, 16, "DirectoryHintShardEntry")?,
first_block_index: read_u64(chunk, 24, "DirectoryHintShardEntry")?,
data_block_count: read_u32(chunk, 32, "DirectoryHintShardEntry")?,
parity_block_count: read_u32(chunk, 36, "DirectoryHintShardEntry")?,
encrypted_size: read_u32(chunk, 40, "DirectoryHintShardEntry")?,
decompressed_size: read_u32(chunk, 44, "DirectoryHintShardEntry")?,
entry_count: read_u64(chunk, 48, "DirectoryHintShardEntry")?,
};
if entry.entry_count == 0 {
return invalid("DirectoryHintShardEntry", "entry count is zero");
}
if entry.decompressed_size == 0 {
return invalid("DirectoryHintShardEntry", "decompressed size is zero");
}
validate_encrypted_extent(
"DirectoryHintShardEntry",
entry.data_block_count,
entry.encrypted_size,
limits.block_size,
)?;
validate_fec_class_extent(
"DirectoryHintShardEntry",
entry.data_block_count,
entry.parity_block_count,
limits.max_index_data_shards,
limits.max_index_parity_shards,
)?;
if entry.first_dir_hash > entry.last_dir_hash {
return invalid(
"DirectoryHintShardEntry",
"first hash is greater than last hash",
);
}
if !seen_indexes.insert(entry.hint_shard_index) {
return invalid("DirectoryHintShardEntry", "duplicate hint shard index");
}
if let Some(previous) = entries.last() {
let previous: &DirectoryHintShardEntry = previous;
if directory_hint_shard_sort_key(previous) >= directory_hint_shard_sort_key(&entry) {
return invalid("IndexRoot", "DirectoryHintShardEntry rows are not sorted");
}
if previous.last_dir_hash > entry.first_dir_hash {
return invalid(
"IndexRoot",
"DirectoryHintShardEntry hash ranges overlap out of order",
);
}
}
entries.push(entry);
}
Ok(entries)
}
fn parse_file_entry(bytes: &[u8]) -> Result<FileEntry, FormatError> {
expect_zero("FileEntry", slice(bytes, 52, 4, "FileEntry")?)?;
Ok(FileEntry {
path_hash: read_array::<8>(bytes, 0, "FileEntry")?,
path_offset: read_u32(bytes, 8, "FileEntry")?,
path_length: read_u32(bytes, 12, "FileEntry")?,
first_frame_index: read_u64(bytes, 16, "FileEntry")?,
frame_count: read_u32(bytes, 24, "FileEntry")?,
offset_in_first_frame_plaintext: read_u32(bytes, 28, "FileEntry")?,
tar_member_group_size: read_u64(bytes, 32, "FileEntry")?,
file_data_size: read_u64(bytes, 40, "FileEntry")?,
flags: read_u32(bytes, 48, "FileEntry")?,
})
}
fn parse_frame_entry(bytes: &[u8]) -> Result<FrameEntry, FormatError> {
expect_zero("FrameEntry", slice(bytes, 40, 4, "FrameEntry")?)?;
Ok(FrameEntry {
frame_index: read_u64(bytes, 0, "FrameEntry")?,
envelope_index: read_u64(bytes, 8, "FrameEntry")?,
offset_in_envelope: read_u32(bytes, 16, "FrameEntry")?,
compressed_size: read_u32(bytes, 20, "FrameEntry")?,
decompressed_size: read_u32(bytes, 24, "FrameEntry")?,
flags: read_u32(bytes, 28, "FrameEntry")?,
tar_stream_offset: read_u64(bytes, 32, "FrameEntry")?,
})
}
fn parse_envelope_entry(bytes: &[u8]) -> Result<EnvelopeEntry, FormatError> {
expect_zero("EnvelopeEntry", slice(bytes, 44, 4, "EnvelopeEntry")?)?;
Ok(EnvelopeEntry {
envelope_index: read_u64(bytes, 0, "EnvelopeEntry")?,
first_block_index: read_u64(bytes, 8, "EnvelopeEntry")?,
data_block_count: read_u32(bytes, 16, "EnvelopeEntry")?,
parity_block_count: read_u32(bytes, 20, "EnvelopeEntry")?,
encrypted_size: read_u32(bytes, 24, "EnvelopeEntry")?,
plaintext_size: read_u32(bytes, 28, "EnvelopeEntry")?,
first_frame_index: read_u64(bytes, 32, "EnvelopeEntry")?,
frame_count: read_u32(bytes, 40, "EnvelopeEntry")?,
})
}
fn parse_directory_hint_entries(bytes: &[u8]) -> Result<Vec<DirectoryHintEntry>, FormatError> {
let mut entries = Vec::with_capacity(bytes.len() / DIRECTORY_HINT_ENTRY_LEN);
for chunk in bytes.chunks_exact(DIRECTORY_HINT_ENTRY_LEN) {
expect_zero(
"DirectoryHintEntry",
slice(chunk, 20, 4, "DirectoryHintEntry")?,
)?;
expect_zero(
"DirectoryHintEntry",
slice(chunk, 32, 8, "DirectoryHintEntry")?,
)?;
entries.push(DirectoryHintEntry {
dir_hash: read_array::<8>(chunk, 0, "DirectoryHintEntry")?,
path_offset: read_u64(chunk, 8, "DirectoryHintEntry")?,
path_length: read_u32(chunk, 16, "DirectoryHintEntry")?,
shard_list_start_index: read_u32(chunk, 24, "DirectoryHintEntry")?,
shard_count: read_u32(chunk, 28, "DirectoryHintEntry")?,
});
}
Ok(entries)
}
fn validate_index_root_totals(
header: &IndexRootHeader,
shards: &[ShardEntry],
has_dictionary: bool,
) -> Result<(), FormatError> {
if shards.is_empty() {
if header.file_count != 0
|| header.frame_count != 0
|| header.envelope_count != 0
|| header.payload_block_count != 0
|| header.tar_total_size != 0
{
return invalid(
"IndexRoot",
"empty shard table has non-empty archive totals",
);
}
if header.content_sha256 != SHA256_EMPTY {
return invalid(
"IndexRoot",
"empty archive content hash is not SHA-256(empty)",
);
}
if has_dictionary || !index_root_dictionary_fields_are_zero(header) {
return invalid("IndexRoot", "empty archive cannot use dictionary");
}
return Ok(());
}
let mut sum = 0u64;
for shard in shards {
sum = sum.checked_add(shard.file_count as u64).ok_or(
FormatError::MetadataArithmeticOverflow {
structure: "IndexRoot",
},
)?;
}
if sum != header.file_count {
return invalid(
"IndexRoot",
"file_count does not equal sum of ShardEntry rows",
);
}
Ok(())
}
fn validate_dictionary_fields(
header: &IndexRootHeader,
has_dictionary: bool,
limits: MetadataLimits,
) -> Result<(), FormatError> {
if !has_dictionary {
if !index_root_dictionary_fields_are_zero(header) {
return invalid(
"IndexRoot",
"dictionary fields are non-zero while has_dictionary is false",
);
}
return Ok(());
}
if header.dictionary_data_block_count == 0 {
return invalid(
"IndexRoot",
"dictionary data block count is zero while has_dictionary is true",
);
}
if header.dictionary_first_block == 0
|| header.dictionary_encrypted_size == 0
|| header.dictionary_decompressed_size == 0
{
return invalid("IndexRoot", "required dictionary field is zero");
}
validate_encrypted_extent(
"IndexRoot.dictionary",
header.dictionary_data_block_count,
header.dictionary_encrypted_size,
limits.block_size,
)?;
validate_fec_class_extent(
"IndexRoot.dictionary",
header.dictionary_data_block_count,
header.dictionary_parity_block_count,
limits.max_index_root_data_shards,
limits.max_index_root_parity_shards,
)
}
fn index_root_dictionary_fields_are_zero(header: &IndexRootHeader) -> bool {
header.dictionary_first_block == 0
&& header.dictionary_data_block_count == 0
&& header.dictionary_parity_block_count == 0
&& header.dictionary_encrypted_size == 0
&& header.dictionary_decompressed_size == 0
}
fn validate_index_shard_tables(
files: &[FileEntry],
frames: &[FrameEntry],
envelopes: &[EnvelopeEntry],
string_pool: &[u8],
locating_shard: &ShardEntry,
limits: MetadataLimits,
) -> Result<(Vec<Vec<u8>>, Vec<u64>), FormatError> {
validate_frame_table(frames)?;
validate_envelope_table(envelopes, limits)?;
let frame_by_index = frames
.iter()
.enumerate()
.map(|(idx, frame)| (frame.frame_index, idx))
.collect::<HashMap<_, _>>();
let envelope_by_index = envelopes
.iter()
.enumerate()
.map(|(idx, envelope)| (envelope.envelope_index, idx))
.collect::<HashMap<_, _>>();
let mut paths = Vec::with_capacity(files.len());
let mut starts = Vec::with_capacity(files.len());
let mut required_frames = BTreeSet::new();
for file in files {
if file.flags != 0 {
return invalid("FileEntry", "reserved flags are non-zero");
}
if file.path_length == 0 {
return invalid("FileEntry", "path length is zero");
}
if file.path_length > limits.max_path_length {
return invalid("FileEntry", "path length exceeds configured maximum");
}
if file.frame_count == 0 {
return invalid("FileEntry", "frame count is zero");
}
if file.tar_member_group_size < 512 {
return invalid(
"FileEntry",
"tar member group is smaller than one tar record",
);
}
if file.path_hash < locating_shard.first_path_hash
|| file.path_hash > locating_shard.last_path_hash
{
return invalid(
"FileEntry",
"path hash is outside locating ShardEntry bounds",
);
}
let path = string_slice(
string_pool,
file.path_offset as u64,
file.path_length as u64,
"FileEntry",
)?;
validate_file_path_bytes(path, limits.max_path_length)?;
if hash_prefix(path) != file.path_hash {
return invalid("FileEntry", "path hash does not match string-pool path");
}
let first_frame = frame_for_file(file, &frame_by_index, frames, file.first_frame_index)?;
let tar_member_group_start = first_frame
.tar_stream_offset
.checked_add(file.offset_in_first_frame_plaintext as u64)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "FileEntry",
})?;
validate_file_frame_range(file, frames, &frame_by_index)?;
for offset in 0..file.frame_count as u64 {
let index = file.first_frame_index.checked_add(offset).ok_or(
FormatError::MetadataArithmeticOverflow {
structure: "FileEntry",
},
)?;
required_frames.insert(index);
}
paths.push(path.to_vec());
starts.push(tar_member_group_start);
}
validate_file_order(files, &paths, &starts)?;
if required_frames.len() != frames.len()
|| frames
.iter()
.any(|frame| !required_frames.contains(&frame.frame_index))
{
return invalid(
"IndexShard",
"FrameEntry table is not the exact set referenced by FileEntry rows",
);
}
let mut required_envelopes = BTreeSet::new();
for frame in frames {
let envelope = envelope_by_index
.get(&frame.envelope_index)
.and_then(|idx| envelopes.get(*idx))
.ok_or(FormatError::InvalidMetadata {
structure: "FrameEntry",
reason: "referenced EnvelopeEntry is missing",
})?;
validate_frame_envelope_binding(frame, envelope)?;
required_envelopes.insert(frame.envelope_index);
}
if required_envelopes.len() != envelopes.len()
|| envelopes
.iter()
.any(|entry| !required_envelopes.contains(&entry.envelope_index))
{
return invalid(
"IndexShard",
"EnvelopeEntry table is not the exact set referenced by FrameEntry rows",
);
}
validate_frame_slices_by_envelope(frames, envelopes)?;
if let Some(first) = files.first() {
if first.path_hash != locating_shard.first_path_hash {
return invalid(
"IndexShard",
"first FileEntry hash does not match ShardEntry",
);
}
}
if let Some(last) = files.last() {
if last.path_hash != locating_shard.last_path_hash {
return invalid(
"IndexShard",
"last FileEntry hash does not match ShardEntry",
);
}
}
Ok((paths, starts))
}
fn validate_frame_table(frames: &[FrameEntry]) -> Result<(), FormatError> {
for frame in frames {
if frame.compressed_size == 0 || frame.decompressed_size == 0 {
return invalid("FrameEntry", "frame sizes must be non-zero");
}
if frame.flags & !FRAME_KNOWN_FLAGS != 0 {
return invalid("FrameEntry", "reserved flag bits are non-zero");
}
}
for pair in frames.windows(2) {
let previous = &pair[0];
let next = &pair[1];
if previous.frame_index >= next.frame_index {
return invalid("IndexShard", "FrameEntry rows are not sorted and unique");
}
let previous_end = previous
.tar_stream_offset
.checked_add(previous.decompressed_size as u64)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "FrameEntry",
})?;
if next.frame_index == previous.frame_index + 1 {
if next.tar_stream_offset != previous_end {
return invalid(
"FrameEntry",
"consecutive tar stream offsets are not packed",
);
}
} else if next.tar_stream_offset <= previous_end {
return invalid("FrameEntry", "non-consecutive tar stream offsets overlap");
}
}
Ok(())
}
fn validate_envelope_table(
envelopes: &[EnvelopeEntry],
limits: MetadataLimits,
) -> Result<(), FormatError> {
for envelope in envelopes {
if envelope.frame_count == 0 || envelope.plaintext_size == 0 {
return invalid("EnvelopeEntry", "payload envelope has no frame plaintext");
}
validate_encrypted_extent(
"EnvelopeEntry",
envelope.data_block_count,
envelope.encrypted_size,
limits.block_size,
)?;
validate_fec_class_extent(
"EnvelopeEntry",
envelope.data_block_count,
envelope.parity_block_count,
limits.max_payload_data_shards,
limits.max_payload_parity_shards,
)?;
}
for pair in envelopes.windows(2) {
if pair[0].envelope_index >= pair[1].envelope_index {
return invalid("IndexShard", "EnvelopeEntry rows are not sorted and unique");
}
}
Ok(())
}
fn validate_file_order(
files: &[FileEntry],
paths: &[Vec<u8>],
starts: &[u64],
) -> Result<(), FormatError> {
for idx in 1..files.len() {
let previous_key = (
&files[idx - 1].path_hash,
paths[idx - 1].as_slice(),
starts[idx - 1],
);
let current_key = (&files[idx].path_hash, paths[idx].as_slice(), starts[idx]);
if previous_key >= current_key {
return invalid("IndexShard", "FileEntry rows are not sorted and unique");
}
}
Ok(())
}
fn validate_file_frame_range(
file: &FileEntry,
frames: &[FrameEntry],
frame_by_index: &HashMap<u64, usize>,
) -> Result<(), FormatError> {
let first = frame_for_file(file, frame_by_index, frames, file.first_frame_index)?;
if file.offset_in_first_frame_plaintext >= first.decompressed_size {
return invalid(
"FileEntry",
"offset in first frame is outside the first referenced frame",
);
}
let mut bytes_before_last =
first.decompressed_size as u64 - file.offset_in_first_frame_plaintext as u64;
if file.frame_count == 1 {
if file.tar_member_group_size > bytes_before_last {
return invalid(
"FileEntry",
"tar member group exceeds the single referenced frame",
);
}
return Ok(());
}
for offset in 1..(file.frame_count as u64 - 1) {
let frame_index = file.first_frame_index.checked_add(offset).ok_or(
FormatError::MetadataArithmeticOverflow {
structure: "FileEntry",
},
)?;
let frame = frame_for_file(file, frame_by_index, frames, frame_index)?;
bytes_before_last = bytes_before_last
.checked_add(frame.decompressed_size as u64)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "FileEntry",
})?;
}
let last_index = file
.first_frame_index
.checked_add(file.frame_count as u64 - 1)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "FileEntry",
})?;
let last = frame_for_file(file, frame_by_index, frames, last_index)?;
let max_size = bytes_before_last
.checked_add(last.decompressed_size as u64)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "FileEntry",
})?;
if file.tar_member_group_size <= bytes_before_last || file.tar_member_group_size > max_size {
return invalid("FileEntry", "frame range is not minimal");
}
Ok(())
}
fn validate_frame_envelope_binding(
frame: &FrameEntry,
envelope: &EnvelopeEntry,
) -> Result<(), FormatError> {
let envelope_frame_end = envelope
.first_frame_index
.checked_add(envelope.frame_count as u64)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "EnvelopeEntry",
})?;
if frame.frame_index < envelope.first_frame_index || frame.frame_index >= envelope_frame_end {
return invalid("FrameEntry", "frame index is outside envelope frame range");
}
let end = frame
.offset_in_envelope
.checked_add(frame.compressed_size)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "FrameEntry",
})?;
if end > envelope.plaintext_size {
return invalid("FrameEntry", "frame slice exceeds envelope plaintext");
}
Ok(())
}
fn validate_frame_slices_by_envelope(
frames: &[FrameEntry],
envelopes: &[EnvelopeEntry],
) -> Result<(), FormatError> {
for envelope in envelopes {
let mut slices = frames
.iter()
.filter(|frame| frame.envelope_index == envelope.envelope_index)
.map(|frame| {
let end = frame
.offset_in_envelope
.checked_add(frame.compressed_size)
.ok_or(FormatError::MetadataArithmeticOverflow {
structure: "FrameEntry",
})?;
Ok((frame.offset_in_envelope, end, frame.frame_index))
})
.collect::<Result<Vec<_>, FormatError>>()?;
slices.sort_unstable_by_key(|slice| (slice.0, slice.2));
for pair in slices.windows(2) {
if pair[0].1 > pair[1].0 {
return invalid("FrameEntry", "frame slices overlap inside an envelope");
}
}
let contains_complete_global_range = (0..envelope.frame_count as u64).all(|offset| {
envelope
.first_frame_index
.checked_add(offset)
.map(|index| slices.iter().any(|slice| slice.2 == index))
.unwrap_or(false)
});
if contains_complete_global_range {
let mut cursor = 0u32;
for (start, end, _) in slices {
if start != cursor {
return invalid("EnvelopeEntry", "complete local envelope has frame gap");
}
cursor = end;
}
if cursor != envelope.plaintext_size {
return invalid(
"EnvelopeEntry",
"complete local envelope does not cover plaintext",
);
}
}
}
Ok(())
}
fn validate_directory_hint_entries(
entries: &[DirectoryHintEntry],
bytes: &[u8],
header: &DirectoryHintTableHeader,
locating_shard: &DirectoryHintShardEntry,
index_root_shard_count: u32,
) -> Result<usize, FormatError> {
let structure = "DirectoryHintTable";
if index_root_shard_count == 0 {
return invalid(structure, "directory hints require IndexRoot shard rows");
}
if entries.is_empty() {
return invalid(structure, "located directory hint table is empty");
}
if entries[0].dir_hash != locating_shard.first_dir_hash {
return invalid(
structure,
"first DirectoryHintEntry hash does not match locating row",
);
}
if entries[entries.len() - 1].dir_hash != locating_shard.last_dir_hash {
return invalid(
structure,
"last DirectoryHintEntry hash does not match locating row",
);
}
let mut max_shard_list_end = 0usize;
for entry in entries {
if entry.shard_count == 0 {
return invalid("DirectoryHintEntry", "shard count is zero");
}
let start = entry.shard_list_start_index as usize;
let end = start.checked_add(entry.shard_count as usize).ok_or(
FormatError::MetadataArithmeticOverflow {
structure: "DirectoryHintEntry",
},
)?;
max_shard_list_end = max_shard_list_end.max(end);
}
let byte_len = checked_mul(max_shard_list_end, 4, structure)?;
let shard_list_offset = to_usize(header.shard_list_offset, structure)?;
let shard_list_end = checked_add(shard_list_offset, byte_len, structure)?;
if shard_list_end > bytes.len() {
return invalid(structure, "shard list exceeds plaintext");
}
Ok(max_shard_list_end)
}
fn validate_directory_hint_paths_and_lists(
entries: &[DirectoryHintEntry],
shard_row_indexes: &[u32],
string_pool: &[u8],
locating_shard: &DirectoryHintShardEntry,
index_root_shard_count: u32,
max_path_length: u32,
) -> Result<Vec<Vec<u8>>, FormatError> {
let mut paths = Vec::with_capacity(entries.len());
let mut seen_paths = HashSet::new();
for entry in entries {
let path = if entry.path_length == 0 {
if entry.path_offset != 0 || entry.dir_hash != hash_prefix(b"") {
return invalid(
"DirectoryHintEntry",
"root directory entry is not canonical",
);
}
&[][..]
} else {
let path = string_slice(
string_pool,
entry.path_offset,
entry.path_length as u64,
"DirectoryHintEntry",
)?;
validate_directory_path_bytes(path, max_path_length)?;
path
};
if hash_prefix(path) != entry.dir_hash {
return invalid(
"DirectoryHintEntry",
"dir_hash does not match string-pool path",
);
}
if !seen_paths.insert(path.to_vec()) {
return invalid("DirectoryHintEntry", "duplicate directory path");
}
let start = entry.shard_list_start_index as usize;
let end = start.checked_add(entry.shard_count as usize).ok_or(
FormatError::MetadataArithmeticOverflow {
structure: "DirectoryHintEntry",
},
)?;
let rows = shard_row_indexes
.get(start..end)
.ok_or(FormatError::InvalidMetadata {
structure: "DirectoryHintEntry",
reason: "shard-row-index range is out of bounds",
})?;
for pair in rows.windows(2) {
if pair[0] >= pair[1] {
return invalid(
"DirectoryHintEntry",
"shard-row-index list is not sorted and unique",
);
}
}
if rows.iter().any(|row| *row >= index_root_shard_count) {
return invalid(
"DirectoryHintEntry",
"shard-row-index is outside IndexRoot shard table",
);
}
paths.push(path.to_vec());
}
for idx in 1..entries.len() {
let previous_key = (&entries[idx - 1].dir_hash, paths[idx - 1].as_slice());
let current_key = (&entries[idx].dir_hash, paths[idx].as_slice());
if previous_key >= current_key {
return invalid(
"DirectoryHintTable",
"DirectoryHintEntry rows are not sorted and unique",
);
}
}
if entries[0].dir_hash != locating_shard.first_dir_hash
|| entries[entries.len() - 1].dir_hash != locating_shard.last_dir_hash
{
return invalid(
"DirectoryHintTable",
"entry hash bounds do not match locating shard",
);
}
Ok(paths)
}
fn candidate_interval_indexes<T>(
entries: &[T],
target_hash: [u8; 8],
scan_cap_per_direction: usize,
first_hash: impl Fn(&T) -> [u8; 8],
last_hash: impl Fn(&T) -> [u8; 8],
) -> Result<Vec<usize>, FormatError> {
if entries.is_empty() {
return Ok(Vec::new());
}
let upper = entries.partition_point(|entry| first_hash(entry) <= target_hash);
if upper == 0 {
return Ok(Vec::new());
}
let landing = upper - 1;
if last_hash(&entries[landing]) < target_hash {
return Ok(Vec::new());
}
let mut start = landing;
let mut left_scanned = 0usize;
while start > 0
&& first_hash(&entries[start - 1]) <= target_hash
&& last_hash(&entries[start - 1]) >= target_hash
{
left_scanned += 1;
if left_scanned > scan_cap_per_direction {
return Err(FormatError::HashPrefixCollisionRunExceeded);
}
start -= 1;
}
let mut end = landing + 1;
let mut right_scanned = 0usize;
while end < entries.len()
&& first_hash(&entries[end]) <= target_hash
&& last_hash(&entries[end]) >= target_hash
{
right_scanned += 1;
if right_scanned > scan_cap_per_direction {
return Err(FormatError::HashPrefixCollisionRunExceeded);
}
end += 1;
}
Ok((start..end).collect())
}
pub fn validate_file_path_bytes(path: &[u8], max_path_length: u32) -> Result<(), FormatError> {
if path.is_empty() || path.len() > max_path_length as usize {
return Err(FormatError::UnsafeArchivePath);
}
validate_relative_path(path, false)
}
pub fn validate_directory_path_bytes(path: &[u8], max_path_length: u32) -> Result<(), FormatError> {
if path.len() > max_path_length as usize {
return Err(FormatError::UnsafeArchivePath);
}
validate_relative_path(path, true)
}
fn validate_relative_path(path: &[u8], allow_empty_root: bool) -> Result<(), FormatError> {
if path.is_empty() {
return if allow_empty_root {
Ok(())
} else {
Err(FormatError::UnsafeArchivePath)
};
}
if path.contains(&0) || path.contains(&b'\\') || path.contains(&b':') || path[0] == b'/' {
return Err(FormatError::UnsafeArchivePath);
}
let path_str = std::str::from_utf8(path).map_err(|_| FormatError::UnsafeArchivePath)?;
if !path_str.nfc().eq(path_str.chars()) {
return Err(FormatError::UnsafeArchivePath);
}
for component in path_str.split('/') {
if component.is_empty() || component == "." || component == ".." {
return Err(FormatError::UnsafeArchivePath);
}
if is_windows_device_component(component) {
return Err(FormatError::UnsafeArchivePath);
}
}
Ok(())
}
fn is_windows_device_component(component: &str) -> bool {
let stem = component
.split('.')
.next()
.unwrap_or(component)
.trim_end_matches([' ', '.']);
let upper = stem.to_ascii_uppercase();
matches!(
upper.as_str(),
"CON"
| "PRN"
| "AUX"
| "NUL"
| "CLOCK$"
| "COM1"
| "COM2"
| "COM3"
| "COM4"
| "COM5"
| "COM6"
| "COM7"
| "COM8"
| "COM9"
| "COM\u{00b9}"
| "COM\u{00b2}"
| "COM\u{00b3}"
| "LPT1"
| "LPT2"
| "LPT3"
| "LPT4"
| "LPT5"
| "LPT6"
| "LPT7"
| "LPT8"
| "LPT9"
| "LPT\u{00b9}"
| "LPT\u{00b2}"
| "LPT\u{00b3}"
)
}
fn validate_encrypted_extent(
structure: &'static str,
data_block_count: u32,
encrypted_size: u32,
block_size: u32,
) -> Result<(), FormatError> {
if data_block_count == 0 || encrypted_size == 0 {
return invalid(structure, "encrypted object has zero data blocks or size");
}
let expected = (data_block_count as u64)
.checked_mul(block_size as u64)
.ok_or(FormatError::MetadataArithmeticOverflow { structure })?;
if expected > u32::MAX as u64 || expected != encrypted_size as u64 {
return invalid(
structure,
"encrypted_size is not data_block_count * block_size",
);
}
Ok(())
}
fn validate_fec_class_extent(
structure: &'static str,
data_block_count: u32,
parity_block_count: u32,
data_shard_max: u16,
parity_shard_max: u16,
) -> Result<(), FormatError> {
if data_block_count > data_shard_max as u32 {
return invalid(structure, "data_block_count exceeds class maximum");
}
if parity_block_count > parity_shard_max as u32 {
return invalid(structure, "parity_block_count exceeds class maximum");
}
let total = data_block_count as u64 + parity_block_count as u64;
if total > REED_SOLOMON_GF16_MAX_TOTAL_SHARDS {
return invalid(
structure,
"data_block_count + parity_block_count exceeds ReedSolomonGF16 limit",
);
}
Ok(())
}
fn frame_for_file<'a>(
_file: &FileEntry,
frame_by_index: &HashMap<u64, usize>,
frames: &'a [FrameEntry],
frame_index: u64,
) -> Result<&'a FrameEntry, FormatError> {
frame_by_index
.get(&frame_index)
.and_then(|idx| frames.get(*idx))
.ok_or(FormatError::InvalidMetadata {
structure: "FileEntry",
reason: "referenced FrameEntry is missing",
})
}
struct CountedTableSpec<T> {
structure: &'static str,
name: &'static str,
count: u64,
offset: u64,
entry_len: usize,
parse: fn(&[u8]) -> Result<T, FormatError>,
}
fn parse_counted_table<T>(
bytes: &[u8],
spec: CountedTableSpec<T>,
cursor: &mut usize,
) -> Result<Vec<T>, FormatError> {
if spec.count == 0 {
if spec.offset != 0 {
return invalid(spec.structure, "absent counted table has non-zero offset");
}
return Ok(Vec::new());
}
expect_offset(spec.structure, spec.name, spec.offset, *cursor)?;
let count = to_usize(spec.count, spec.structure)?;
let bytes_len = checked_mul(count, spec.entry_len, spec.structure)?;
let table = slice(bytes, *cursor, bytes_len, spec.structure)?;
*cursor = checked_add(*cursor, bytes_len, spec.structure)?;
table.chunks_exact(spec.entry_len).map(spec.parse).collect()
}
fn parse_u32_array(bytes: &[u8], structure: &'static str) -> Result<Vec<u32>, FormatError> {
let mut out = Vec::with_capacity(bytes.len() / 4);
for chunk in bytes.chunks_exact(4) {
out.push(read_u32(chunk, 0, structure)?);
}
Ok(out)
}
fn string_slice<'a>(
string_pool: &'a [u8],
offset: u64,
length: u64,
structure: &'static str,
) -> Result<&'a [u8], FormatError> {
let start = to_usize(offset, structure)?;
let len = to_usize(length, structure)?;
slice(string_pool, start, len, structure)
}
fn shard_entry_sort_key(entry: &ShardEntry) -> ([u8; 8], [u8; 8], u64) {
(
entry.first_path_hash,
entry.last_path_hash,
entry.shard_index,
)
}
fn directory_hint_shard_sort_key(entry: &DirectoryHintShardEntry) -> ([u8; 8], [u8; 8], u64) {
(
entry.first_dir_hash,
entry.last_dir_hash,
entry.hint_shard_index,
)
}
fn table_offset(len: usize, cursor: usize) -> u32 {
if len == 0 {
0
} else {
cursor as u32
}
}
fn expect_magic(
structure: &'static str,
expected: [u8; 4],
actual: [u8; 4],
) -> 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_offset(
structure: &'static str,
name: &'static str,
actual: u64,
expected: usize,
) -> Result<(), FormatError> {
if actual != expected as u64 {
return Err(FormatError::InvalidMetadata {
structure,
reason: name,
});
}
Ok(())
}
fn slice<'a>(
bytes: &'a [u8],
offset: usize,
len: usize,
structure: &'static str,
) -> Result<&'a [u8], FormatError> {
let end = checked_add(offset, len, structure)?;
bytes.get(offset..end).ok_or(FormatError::InvalidMetadata {
structure,
reason: "range is out of bounds",
})
}
fn read_array<const N: usize>(
bytes: &[u8],
offset: usize,
structure: &'static str,
) -> Result<[u8; N], FormatError> {
let mut out = [0u8; N];
out.copy_from_slice(slice(bytes, offset, N, structure)?);
Ok(out)
}
fn read_u32(bytes: &[u8], offset: usize, structure: &'static str) -> Result<u32, FormatError> {
let raw = read_array::<4>(bytes, offset, structure)?;
Ok(u32::from_le_bytes(raw))
}
fn read_u64(bytes: &[u8], offset: usize, structure: &'static str) -> Result<u64, FormatError> {
let raw = read_array::<8>(bytes, offset, structure)?;
Ok(u64::from_le_bytes(raw))
}
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 checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
lhs.checked_add(rhs)
.ok_or(FormatError::MetadataArithmeticOverflow { structure })
}
fn checked_mul(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
lhs.checked_mul(rhs)
.ok_or(FormatError::MetadataArithmeticOverflow { structure })
}
fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
usize::try_from(value).map_err(|_| FormatError::MetadataArithmeticOverflow { structure })
}
fn invalid<T>(structure: &'static str, reason: &'static str) -> Result<T, FormatError> {
Err(FormatError::InvalidMetadata { structure, reason })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_reader_caps_match_v41() {
let limits = MetadataLimits::default();
assert_eq!(limits.max_shard_count, 1_000_000);
assert_eq!(limits.max_directory_hint_shards, 1_000_000);
assert_eq!(limits.max_files_per_index_shard, 1_000_000);
assert_eq!(limits.max_entries_per_directory_hint_shard, 1_000_000);
assert_eq!(limits.max_hash_collision_shard_scan, 16);
}
#[test]
fn index_root_rejects_shard_extent_above_crypto_header_class_limits() {
let path_hash = hash_prefix(b"a.txt");
let root = IndexRoot {
header: IndexRootHeader {
file_count: 1,
..IndexRootHeader::empty()
},
shards: vec![ShardEntry {
shard_index: 0,
first_block_index: 1,
data_block_count: 1,
parity_block_count: 2,
encrypted_size: 4096,
decompressed_size: 64,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
}],
directory_hint_shards: Vec::new(),
};
let limits = MetadataLimits {
max_index_parity_shards: 1,
..MetadataLimits::default()
};
assert_eq!(
IndexRoot::parse(&root.to_bytes(), false, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "ShardEntry",
reason: "parity_block_count exceeds class maximum",
}
);
}
#[test]
fn metadata_fec_extent_rejects_reed_solomon_total_overflow() {
assert_eq!(
validate_fec_class_extent("EnvelopeEntry", 65_535, 1, u16::MAX, u16::MAX).unwrap_err(),
FormatError::InvalidMetadata {
structure: "EnvelopeEntry",
reason: "data_block_count + parity_block_count exceeds ReedSolomonGF16 limit",
}
);
}
#[test]
fn parses_valid_empty_index_root() {
let root = IndexRoot {
header: IndexRootHeader::empty(),
shards: Vec::new(),
directory_hint_shards: Vec::new(),
};
let bytes = root.to_bytes();
let parsed = IndexRoot::parse(&bytes, false, MetadataLimits::default()).unwrap();
assert_eq!(parsed.header.file_count, 0);
assert!(parsed.shards.is_empty());
assert!(parsed.directory_hint_shards.is_empty());
}
#[test]
fn index_root_rejects_nonzero_offsets_for_absent_counted_tables() {
let mut root = IndexRoot {
header: IndexRootHeader::empty(),
shards: Vec::new(),
directory_hint_shards: Vec::new(),
};
let mut bytes = root.to_bytes();
write_u64(&mut bytes, 88, INDEX_ROOT_LEN as u64);
assert_eq!(
IndexRoot::parse(&bytes, false, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "absent shard table has non-zero offset",
}
);
root.header.file_count = 1;
root.shards.push(ShardEntry {
shard_index: 0,
first_block_index: 1,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: 128,
file_count: 1,
first_path_hash: hash_prefix(b"a.txt"),
last_path_hash: hash_prefix(b"a.txt"),
});
let mut bytes = root.to_bytes();
write_u64(&mut bytes, 96, (INDEX_ROOT_LEN + SHARD_ENTRY_LEN) as u64);
assert_eq!(
IndexRoot::parse(&bytes, false, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "absent directory hint shard table has non-zero offset",
}
);
}
#[test]
fn index_root_rejects_has_dictionary_with_zero_dictionary_fields() {
let root = IndexRoot {
header: IndexRootHeader::empty(),
shards: Vec::new(),
directory_hint_shards: Vec::new(),
};
assert_eq!(
IndexRoot::parse(&root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "dictionary data block count is zero while has_dictionary is true",
}
);
}
#[test]
fn index_root_rejects_empty_archive_with_dictionary_extent() {
let root = IndexRoot {
header: IndexRootHeader {
dictionary_first_block: 1,
dictionary_data_block_count: 1,
dictionary_encrypted_size: 4096,
dictionary_decompressed_size: 16,
..IndexRootHeader::empty()
},
shards: Vec::new(),
directory_hint_shards: Vec::new(),
};
assert_eq!(
IndexRoot::parse(&root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "empty archive cannot use dictionary",
}
);
}
#[test]
fn encrypted_object_extents_reject_zero_data_or_size_for_all_metadata_rows() {
assert_eq!(
validate_encrypted_extent("ManifestFooter.IndexRoot", 0, 4096, 4096).unwrap_err(),
FormatError::InvalidMetadata {
structure: "ManifestFooter.IndexRoot",
reason: "encrypted object has zero data blocks or size",
}
);
assert_eq!(
validate_encrypted_extent("EnvelopeEntry", 1, 0, 4096).unwrap_err(),
FormatError::InvalidMetadata {
structure: "EnvelopeEntry",
reason: "encrypted object has zero data blocks or size",
}
);
let path_hash = hash_prefix(b"a.txt");
let mut root = IndexRoot {
header: IndexRootHeader {
file_count: 1,
..IndexRootHeader::empty()
},
shards: vec![ShardEntry {
shard_index: 0,
first_block_index: 1,
data_block_count: 0,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: 128,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
}],
directory_hint_shards: Vec::new(),
};
assert_eq!(
IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "ShardEntry",
reason: "encrypted object has zero data blocks or size",
}
);
root.shards[0].data_block_count = 1;
root.shards[0].encrypted_size = 4096;
root.directory_hint_shards.push(DirectoryHintShardEntry {
hint_shard_index: 0,
first_dir_hash: hash_prefix(b""),
last_dir_hash: hash_prefix(b""),
first_block_index: 2,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 0,
decompressed_size: 72,
entry_count: 1,
});
assert_eq!(
IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintShardEntry",
reason: "encrypted object has zero data blocks or size",
}
);
let mut dict_root = IndexRoot {
header: IndexRootHeader {
file_count: 1,
dictionary_first_block: 10,
dictionary_data_block_count: 0,
dictionary_parity_block_count: 0,
dictionary_encrypted_size: 4096,
dictionary_decompressed_size: 32,
..IndexRootHeader::empty()
},
shards: vec![ShardEntry {
shard_index: 0,
first_block_index: 1,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: 128,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
}],
directory_hint_shards: Vec::new(),
};
assert_eq!(
IndexRoot::parse(&dict_root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "dictionary data block count is zero while has_dictionary is true",
}
);
dict_root.header.dictionary_data_block_count = 1;
dict_root.header.dictionary_encrypted_size = 0;
assert_eq!(
IndexRoot::parse(&dict_root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "required dictionary field is zero",
}
);
}
#[test]
fn index_root_rejects_dictionary_fields_when_crypto_header_has_no_dictionary() {
let mut root = IndexRoot {
header: IndexRootHeader::empty(),
shards: Vec::new(),
directory_hint_shards: Vec::new(),
};
root.header.dictionary_first_block = 1;
root.header.dictionary_data_block_count = 1;
root.header.dictionary_encrypted_size = 4096;
root.header.dictionary_decompressed_size = 16;
assert_eq!(
IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "dictionary fields are non-zero while has_dictionary is false",
}
);
}
#[test]
fn rejects_directory_hint_rows_sorted_by_old_v36_key_only() {
let h = [0x10; 8];
let z = [0x20; 8];
let root = IndexRoot {
header: IndexRootHeader {
file_count: 1,
..IndexRootHeader::empty()
},
shards: vec![ShardEntry {
shard_index: 0,
first_block_index: 0,
data_block_count: 1,
parity_block_count: 1,
encrypted_size: 4096,
decompressed_size: 64,
file_count: 1,
first_path_hash: h,
last_path_hash: z,
}],
directory_hint_shards: vec![
DirectoryHintShardEntry {
hint_shard_index: 0,
first_dir_hash: h,
last_dir_hash: z,
first_block_index: 10,
data_block_count: 1,
parity_block_count: 1,
encrypted_size: 4096,
decompressed_size: 72,
entry_count: 1,
},
DirectoryHintShardEntry {
hint_shard_index: 1,
first_dir_hash: h,
last_dir_hash: h,
first_block_index: 12,
data_block_count: 1,
parity_block_count: 1,
encrypted_size: 4096,
decompressed_size: 72,
entry_count: 1,
},
],
};
assert_eq!(
IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "DirectoryHintShardEntry rows are not sorted"
}
);
}
#[test]
fn directory_hint_shard_count_cap_is_independent_from_index_shard_cap() {
let path_hash = hash_prefix(b"a.txt");
let dir_hash = hash_prefix(b"");
let root = IndexRoot {
header: IndexRootHeader {
file_count: 1,
..IndexRootHeader::empty()
},
shards: vec![ShardEntry {
shard_index: 0,
first_block_index: 1,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: 128,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
}],
directory_hint_shards: vec![DirectoryHintShardEntry {
hint_shard_index: 0,
first_dir_hash: dir_hash,
last_dir_hash: dir_hash,
first_block_index: 2,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: 72,
entry_count: 1,
}],
};
let limits = MetadataLimits {
max_shard_count: 1,
max_directory_hint_shards: 1,
..MetadataLimits::default()
};
IndexRoot::parse(&root.to_bytes(), false, limits).unwrap();
let rejecting_limits = MetadataLimits {
max_directory_hint_shards: 0,
..limits
};
assert_eq!(
IndexRoot::parse(&root.to_bytes(), false, rejecting_limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "directory hint shard count exceeds resource cap",
}
);
}
#[test]
fn directory_hint_paths_obey_configured_max_path_length() {
let path = b"toolong".to_vec();
let table = DirectoryHintTable {
header: DirectoryHintTableHeader {
version: 1,
hint_shard_index: 0,
entry_count: 0,
entry_table_offset: 0,
shard_list_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
entries: vec![DirectoryHintEntry {
dir_hash: hash_prefix(&path),
path_offset: 0,
path_length: path.len() as u32,
shard_list_start_index: 0,
shard_count: 1,
}],
shard_row_indexes: vec![0],
string_pool: path.clone(),
entry_paths: vec![path.clone()],
};
let bytes = table.to_bytes();
let locating = DirectoryHintShardEntry {
hint_shard_index: 0,
first_dir_hash: hash_prefix(&path),
last_dir_hash: hash_prefix(&path),
first_block_index: 0,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: bytes.len() as u32,
entry_count: 1,
};
let limits = MetadataLimits {
max_path_length: 3,
..MetadataLimits::default()
};
assert_eq!(
DirectoryHintTable::parse(&bytes, &locating, 1, limits).unwrap_err(),
FormatError::UnsafeArchivePath
);
}
#[test]
fn directory_hint_table_rejects_wrong_hint_shard_identity() {
let path = b"dir".to_vec();
let table = DirectoryHintTable {
header: DirectoryHintTableHeader {
version: 1,
hint_shard_index: 5,
entry_count: 0,
entry_table_offset: 0,
shard_list_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
entries: vec![DirectoryHintEntry {
dir_hash: hash_prefix(&path),
path_offset: 0,
path_length: path.len() as u32,
shard_list_start_index: 0,
shard_count: 1,
}],
shard_row_indexes: vec![0],
string_pool: path.clone(),
entry_paths: vec![path.clone()],
};
let bytes = table.to_bytes();
let locating = DirectoryHintShardEntry {
hint_shard_index: 6,
first_dir_hash: hash_prefix(&path),
last_dir_hash: hash_prefix(&path),
first_block_index: 0,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: bytes.len() as u32,
entry_count: 1,
};
assert_eq!(
DirectoryHintTable::parse(&bytes, &locating, 1, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintTable",
reason: "hint shard index does not match locating DirectoryHintShardEntry",
}
);
}
#[test]
fn directory_hint_table_rejects_empty_shard_lists() {
let path = b"dir".to_vec();
let table = DirectoryHintTable {
header: DirectoryHintTableHeader {
version: 1,
hint_shard_index: 0,
entry_count: 0,
entry_table_offset: 0,
shard_list_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
entries: vec![DirectoryHintEntry {
dir_hash: hash_prefix(&path),
path_offset: 0,
path_length: path.len() as u32,
shard_list_start_index: 0,
shard_count: 0,
}],
shard_row_indexes: Vec::new(),
string_pool: path.clone(),
entry_paths: vec![path.clone()],
};
let bytes = table.to_bytes();
let locating = DirectoryHintShardEntry {
hint_shard_index: 0,
first_dir_hash: hash_prefix(&path),
last_dir_hash: hash_prefix(&path),
first_block_index: 0,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: bytes.len() as u32,
entry_count: 1,
};
assert_eq!(
DirectoryHintTable::parse(&bytes, &locating, 1, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintEntry",
reason: "shard count is zero",
}
);
}
#[test]
fn index_shard_rejects_unsupported_version_and_zero_count_pointer_offsets() {
let path = b"file.txt";
let path_hash = hash_prefix(path);
let file = FileEntry {
path_hash,
path_offset: 0,
path_length: path.len() as u32,
first_frame_index: 0,
frame_count: 1,
offset_in_first_frame_plaintext: 0,
tar_member_group_size: 512,
file_data_size: 0,
flags: 0,
};
let frame = FrameEntry {
frame_index: 0,
envelope_index: 0,
offset_in_envelope: 0,
compressed_size: 128,
decompressed_size: 512,
flags: 0,
tar_stream_offset: 0,
};
let envelope = EnvelopeEntry {
envelope_index: 0,
first_block_index: 0,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
plaintext_size: 128,
first_frame_index: 0,
frame_count: 1,
};
let shard = IndexShard {
header: IndexShardHeader {
version: 1,
shard_index: 7,
file_count: 0,
frame_count: 0,
envelope_count: 0,
file_table_offset: 0,
frame_table_offset: 0,
envelope_table_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
files: vec![file],
frames: vec![frame],
envelopes: vec![envelope],
string_pool: path.to_vec(),
file_paths: Vec::new(),
file_tar_member_group_starts: Vec::new(),
};
let locating = ShardEntry {
shard_index: 7,
first_block_index: 10,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: shard.to_bytes().len() as u32,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
};
let mut unsupported_version = shard.to_bytes();
write_u32(&mut unsupported_version, 4, 2);
assert_eq!(
IndexShard::parse(&unsupported_version, &locating, MetadataLimits::default())
.unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "unsupported version",
}
);
let mut nonzero_zero_frame_table = shard.to_bytes();
write_u32(&mut nonzero_zero_frame_table, 20, 0);
write_u32(
&mut nonzero_zero_frame_table,
32,
INDEX_SHARD_HEADER_LEN as u32,
);
assert_eq!(
IndexShard::parse(
&nonzero_zero_frame_table,
&locating,
MetadataLimits::default()
)
.unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "absent counted table has non-zero offset",
}
);
let mut nonzero_zero_envelope_table = shard.to_bytes();
write_u32(&mut nonzero_zero_envelope_table, 24, 0);
write_u32(
&mut nonzero_zero_envelope_table,
36,
(INDEX_SHARD_HEADER_LEN + FILE_ENTRY_LEN + FRAME_ENTRY_LEN) as u32,
);
assert_eq!(
IndexShard::parse(
&nonzero_zero_envelope_table,
&locating,
MetadataLimits::default()
)
.unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "absent counted table has non-zero offset",
}
);
}
#[test]
fn directory_hint_table_rejects_zero_count_nonzero_offsets() {
let path = b"dir".to_vec();
let table = DirectoryHintTable {
header: DirectoryHintTableHeader {
version: 1,
hint_shard_index: 5,
entry_count: 0,
entry_table_offset: 0,
shard_list_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
entries: vec![DirectoryHintEntry {
dir_hash: hash_prefix(&path),
path_offset: 0,
path_length: path.len() as u32,
shard_list_start_index: 0,
shard_count: 1,
}],
shard_row_indexes: vec![0],
string_pool: path.clone(),
entry_paths: vec![path.clone()],
};
let locating = DirectoryHintShardEntry {
hint_shard_index: 5,
first_dir_hash: hash_prefix(&path),
last_dir_hash: hash_prefix(&path),
first_block_index: 0,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: table.to_bytes().len() as u32,
entry_count: 1,
};
let mut bytes = table.to_bytes();
let bytes_len = bytes.len() as u64;
write_u64(&mut bytes, 48, 0);
write_u64(&mut bytes, 40, bytes_len);
assert_eq!(
DirectoryHintTable::parse(&bytes, &locating, 1, MetadataLimits::default()).unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintTable",
reason: "absent string pool has non-zero offset",
}
);
}
#[test]
fn index_shard_rejects_non_exact_local_frame_and_envelope_tables() {
let path = b"exact-local.txt";
let path_hash = hash_prefix(path);
let file = FileEntry {
path_hash,
path_offset: 0,
path_length: path.len() as u32,
first_frame_index: 0,
frame_count: 1,
offset_in_first_frame_plaintext: 0,
tar_member_group_size: 512,
file_data_size: 0,
flags: 0,
};
let frame = FrameEntry {
frame_index: 0,
envelope_index: 0,
offset_in_envelope: 0,
compressed_size: 128,
decompressed_size: 512,
flags: 0,
tar_stream_offset: 0,
};
let envelope = EnvelopeEntry {
envelope_index: 0,
first_block_index: 10,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
plaintext_size: 128,
first_frame_index: 0,
frame_count: 1,
};
let shard = IndexShard {
header: IndexShardHeader {
version: 1,
shard_index: 3,
file_count: 0,
frame_count: 0,
envelope_count: 0,
file_table_offset: 0,
frame_table_offset: 0,
envelope_table_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
files: vec![file.clone()],
frames: vec![frame.clone()],
envelopes: vec![envelope.clone()],
string_pool: path.to_vec(),
file_paths: Vec::new(),
file_tar_member_group_starts: Vec::new(),
};
let locating = ShardEntry {
shard_index: 3,
first_block_index: 20,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: shard.to_bytes().len() as u32,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
};
IndexShard::parse(&shard.to_bytes(), &locating, MetadataLimits::default()).unwrap();
let parse_with = |frames: Vec<FrameEntry>, envelopes: Vec<EnvelopeEntry>| {
let mut mutated = shard.clone();
mutated.frames = frames;
mutated.envelopes = envelopes;
let bytes = mutated.to_bytes();
let locating = ShardEntry {
decompressed_size: bytes.len() as u32,
..locating.clone()
};
IndexShard::parse(&bytes, &locating, MetadataLimits::default()).unwrap_err()
};
let mut missing_frame = frame.clone();
missing_frame.frame_index = 1;
assert_eq!(
parse_with(vec![missing_frame], vec![envelope.clone()]),
FormatError::InvalidMetadata {
structure: "FileEntry",
reason: "referenced FrameEntry is missing",
}
);
let mut unreferenced_frame = frame.clone();
unreferenced_frame.frame_index = 9;
unreferenced_frame.tar_stream_offset = 1024;
assert_eq!(
parse_with(
vec![frame.clone(), unreferenced_frame],
vec![envelope.clone()]
),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "FrameEntry table is not the exact set referenced by FileEntry rows",
}
);
assert_eq!(
parse_with(vec![frame.clone(), frame.clone()], vec![envelope.clone()]),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "FrameEntry rows are not sorted and unique",
}
);
let mut missing_envelope = envelope.clone();
missing_envelope.envelope_index = 1;
assert_eq!(
parse_with(vec![frame.clone()], vec![missing_envelope]),
FormatError::InvalidMetadata {
structure: "FrameEntry",
reason: "referenced EnvelopeEntry is missing",
}
);
let mut unreferenced_envelope = envelope.clone();
unreferenced_envelope.envelope_index = 9;
unreferenced_envelope.first_block_index = 11;
unreferenced_envelope.first_frame_index = 9;
assert_eq!(
parse_with(
vec![frame.clone()],
vec![envelope.clone(), unreferenced_envelope]
),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "EnvelopeEntry table is not the exact set referenced by FrameEntry rows",
}
);
assert_eq!(
parse_with(vec![frame], vec![envelope.clone(), envelope]),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "EnvelopeEntry rows are not sorted and unique",
}
);
}
#[test]
fn metadata_parsers_reject_malformed_buffer_corpus() {
let limits = MetadataLimits::default();
let path = b"file.txt";
let path_hash = hash_prefix(path);
let shard_entry = ShardEntry {
shard_index: 0,
first_block_index: 1,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: 0,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
};
let root = IndexRoot {
header: IndexRootHeader {
file_count: 1,
frame_count: 1,
envelope_count: 1,
payload_block_count: 1,
tar_total_size: 512,
..IndexRootHeader::empty()
},
shards: vec![ShardEntry {
decompressed_size: 256,
..shard_entry.clone()
}],
directory_hint_shards: Vec::new(),
};
let root_bytes = root.to_bytes();
IndexRoot::parse(&root_bytes, false, limits).unwrap();
assert_eq!(
IndexRoot::parse(&root_bytes[..INDEX_ROOT_LEN - 1], false, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "plaintext is shorter than fixed header",
}
);
let mut bad_root = root_bytes.clone();
bad_root[0] ^= 1;
assert_eq!(
IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
FormatError::BadMagic {
structure: "IndexRoot"
}
);
let mut bad_root = root_bytes.clone();
write_u32(&mut bad_root, 4, 2);
assert_eq!(
IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "unsupported version",
}
);
let mut bad_root = root_bytes.clone();
bad_root[128] = 1;
assert_eq!(
IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
FormatError::NonZeroReserved {
structure: "IndexRoot"
}
);
let mut bad_root = root_bytes.clone();
write_u64(&mut bad_root, 88, (INDEX_ROOT_LEN + 1) as u64);
assert_eq!(
IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "shard table",
}
);
assert_eq!(
IndexRoot::parse(&root_bytes[..root_bytes.len() - 1], false, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "range is out of bounds",
}
);
let mut bad_root = root_bytes.clone();
bad_root.push(0);
assert_eq!(
IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexRoot",
reason: "plaintext length does not match canonical cursor",
}
);
let file = FileEntry {
path_hash,
path_offset: 0,
path_length: path.len() as u32,
first_frame_index: 0,
frame_count: 1,
offset_in_first_frame_plaintext: 0,
tar_member_group_size: 512,
file_data_size: 0,
flags: 0,
};
let frame = FrameEntry {
frame_index: 0,
envelope_index: 0,
offset_in_envelope: 0,
compressed_size: 128,
decompressed_size: 512,
flags: 0x0000_0003,
tar_stream_offset: 0,
};
let envelope = EnvelopeEntry {
envelope_index: 0,
first_block_index: 1,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
plaintext_size: 128,
first_frame_index: 0,
frame_count: 1,
};
let shard = IndexShard {
header: IndexShardHeader {
version: 1,
shard_index: 0,
file_count: 0,
frame_count: 0,
envelope_count: 0,
file_table_offset: 0,
frame_table_offset: 0,
envelope_table_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
files: vec![file],
frames: vec![frame],
envelopes: vec![envelope],
string_pool: path.to_vec(),
file_paths: Vec::new(),
file_tar_member_group_starts: Vec::new(),
};
let shard_bytes = shard.to_bytes();
let locating = ShardEntry {
decompressed_size: shard_bytes.len() as u32,
..shard_entry
};
IndexShard::parse(&shard_bytes, &locating, limits).unwrap();
assert_eq!(
IndexShard::parse(
&shard_bytes[..INDEX_SHARD_HEADER_LEN - 1],
&locating,
limits
)
.unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "plaintext is shorter than fixed header",
}
);
let mut bad_shard = shard_bytes.clone();
bad_shard[0] ^= 1;
assert_eq!(
IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
FormatError::BadMagic {
structure: "IndexShard"
}
);
let mut bad_shard = shard_bytes.clone();
bad_shard[48] = 1;
assert_eq!(
IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
FormatError::NonZeroReserved {
structure: "IndexShard"
}
);
let mut bad_shard = shard_bytes.clone();
write_u32(&mut bad_shard, 28, INDEX_SHARD_HEADER_LEN as u32 + 1);
assert_eq!(
IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "file table",
}
);
assert_eq!(
IndexShard::parse(&shard_bytes[..shard_bytes.len() - 1], &locating, limits)
.unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "range is out of bounds",
}
);
let mut bad_shard = shard_bytes.clone();
bad_shard.push(0);
assert_eq!(
IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "IndexShard",
reason: "plaintext length does not match canonical cursor",
}
);
let dir_path = b"dir".to_vec();
let dir_hash = hash_prefix(&dir_path);
let table = DirectoryHintTable {
header: DirectoryHintTableHeader {
version: 1,
hint_shard_index: 0,
entry_count: 0,
entry_table_offset: 0,
shard_list_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
entries: vec![DirectoryHintEntry {
dir_hash,
path_offset: 0,
path_length: dir_path.len() as u32,
shard_list_start_index: 0,
shard_count: 1,
}],
shard_row_indexes: vec![0],
string_pool: dir_path.clone(),
entry_paths: Vec::new(),
};
let table_bytes = table.to_bytes();
let locating_hint = DirectoryHintShardEntry {
hint_shard_index: 0,
first_dir_hash: dir_hash,
last_dir_hash: dir_hash,
first_block_index: 2,
data_block_count: 1,
parity_block_count: 0,
encrypted_size: 4096,
decompressed_size: table_bytes.len() as u32,
entry_count: 1,
};
DirectoryHintTable::parse(&table_bytes, &locating_hint, 1, limits).unwrap();
assert_eq!(
DirectoryHintTable::parse(
&table_bytes[..DIRECTORY_HINT_TABLE_LEN - 1],
&locating_hint,
1,
limits,
)
.unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintTable",
reason: "plaintext is shorter than fixed header",
}
);
let mut bad_table = table_bytes.clone();
bad_table[0] ^= 1;
assert_eq!(
DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
FormatError::BadMagic {
structure: "DirectoryHintTable"
}
);
let mut bad_table = table_bytes.clone();
bad_table[56] = 1;
assert_eq!(
DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
FormatError::NonZeroReserved {
structure: "DirectoryHintTable"
}
);
let mut bad_table = table_bytes.clone();
write_u64(&mut bad_table, 24, DIRECTORY_HINT_TABLE_LEN as u64 + 1);
assert_eq!(
DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintTable",
reason: "entry table",
}
);
assert_eq!(
DirectoryHintTable::parse(
&table_bytes[..table_bytes.len() - 1],
&locating_hint,
1,
limits
)
.unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintTable",
reason: "range is out of bounds",
}
);
let mut bad_table = table_bytes.clone();
bad_table.push(0);
assert_eq!(
DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
FormatError::InvalidMetadata {
structure: "DirectoryHintTable",
reason: "plaintext length does not match canonical cursor",
}
);
}
#[test]
fn candidate_path_lookup_uses_supplied_collision_cap() {
let path = b"same-prefix.txt";
let hash = hash_prefix(path);
let root = IndexRoot {
header: IndexRootHeader::empty(),
shards: (0..3)
.map(|idx| ShardEntry {
shard_index: idx,
first_block_index: idx,
data_block_count: 1,
parity_block_count: 1,
encrypted_size: 4096,
decompressed_size: 256,
file_count: 1,
first_path_hash: hash,
last_path_hash: hash,
})
.collect(),
directory_hint_shards: Vec::new(),
};
let mut limits = MetadataLimits {
max_hash_collision_shard_scan: 0,
..MetadataLimits::default()
};
assert_eq!(
root.candidate_shards_for_path(path, limits).unwrap_err(),
FormatError::HashPrefixCollisionRunExceeded
);
limits.max_hash_collision_shard_scan = 2;
assert_eq!(
root.candidate_shards_for_path(path, limits).unwrap(),
vec![0, 1, 2]
);
}
#[test]
fn parses_single_shard_and_finds_final_file_entry() {
let path = b"file.txt";
let path_hash = hash_prefix(path);
let file = FileEntry {
path_hash,
path_offset: 0,
path_length: path.len() as u32,
first_frame_index: 0,
frame_count: 1,
offset_in_first_frame_plaintext: 0,
tar_member_group_size: 512,
file_data_size: 0,
flags: 0,
};
let frame = FrameEntry {
frame_index: 0,
envelope_index: 0,
offset_in_envelope: 0,
compressed_size: 128,
decompressed_size: 512,
flags: 0,
tar_stream_offset: 0,
};
let envelope = EnvelopeEntry {
envelope_index: 0,
first_block_index: 0,
data_block_count: 1,
parity_block_count: 1,
encrypted_size: 4096,
plaintext_size: 128,
first_frame_index: 0,
frame_count: 1,
};
let shard = IndexShard {
header: IndexShardHeader {
version: 1,
shard_index: 7,
file_count: 0,
frame_count: 0,
envelope_count: 0,
file_table_offset: 0,
frame_table_offset: 0,
envelope_table_offset: 0,
string_pool_offset: 0,
string_pool_size: 0,
},
files: vec![file],
frames: vec![frame],
envelopes: vec![envelope],
string_pool: path.to_vec(),
file_paths: Vec::new(),
file_tar_member_group_starts: Vec::new(),
};
let locating = ShardEntry {
shard_index: 7,
first_block_index: 10,
data_block_count: 1,
parity_block_count: 1,
encrypted_size: 4096,
decompressed_size: shard.to_bytes().len() as u32,
file_count: 1,
first_path_hash: path_hash,
last_path_hash: path_hash,
};
let parsed =
IndexShard::parse(&shard.to_bytes(), &locating, MetadataLimits::default()).unwrap();
assert_eq!(parsed.lookup_file_index(path), Some(0));
assert_eq!(parsed.file_path(0), Some(path.as_slice()));
}
}