1use std::collections::{BTreeSet, HashMap, HashSet};
2
3use sha2::{Digest, Sha256};
4use unicode_normalization::UnicodeNormalization;
5
6use crate::entry_metadata::{EXTENDED_METADATA_V1, FILE_ENTRY_KNOWN_FLAGS};
7use crate::format::FormatError;
8
9const TZIR_MAGIC: [u8; 4] = *b"TZIR";
10const TZIS_MAGIC: [u8; 4] = *b"TZIS";
11const TZDH_MAGIC: [u8; 4] = *b"TZDH";
12
13pub const INDEX_ROOT_LEN: usize = 160;
14pub const SHARD_ENTRY_LEN: usize = 52;
15pub const DIRECTORY_HINT_SHARD_ENTRY_LEN: usize = 56;
16pub const ENVELOPE_ENTRY_LEN: usize = 48;
17pub const FRAME_ENTRY_LEN: usize = 44;
18pub const INDEX_SHARD_HEADER_LEN: usize = 64;
19pub const FILE_ENTRY_LEN: usize = 56;
20pub const DIRECTORY_HINT_TABLE_LEN: usize = 72;
21pub const DIRECTORY_HINT_ENTRY_LEN: usize = 40;
22
23const INDEX_SHARD_VERSION: u32 = 1;
24const FRAME_KNOWN_FLAGS: u32 = 0x0000_0003;
25const DEFAULT_MAX_HASH_COLLISION_SHARD_SCAN: usize = 16;
26const REED_SOLOMON_GF16_MAX_TOTAL_SHARDS: u64 = 65_535;
27const SHA256_EMPTY: [u8; 32] = [
28 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
29 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
30];
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct MetadataLimits {
34 pub block_size: u32,
35 pub max_path_length: u32,
36 pub max_hash_collision_shard_scan: usize,
37 pub max_shard_count: u32,
38 pub max_directory_hint_shards: u32,
39 pub max_files_per_index_shard: u32,
40 pub max_entries_per_directory_hint_shard: u64,
41 pub max_payload_data_shards: u16,
42 pub max_payload_parity_shards: u16,
43 pub max_index_data_shards: u16,
44 pub max_index_parity_shards: u16,
45 pub max_index_root_data_shards: u16,
46 pub max_index_root_parity_shards: u16,
47}
48
49impl Default for MetadataLimits {
50 fn default() -> Self {
51 Self {
52 block_size: 4096,
53 max_path_length: 4096,
54 max_hash_collision_shard_scan: DEFAULT_MAX_HASH_COLLISION_SHARD_SCAN,
55 max_shard_count: 1_000_000,
56 max_directory_hint_shards: 1_000_000,
57 max_files_per_index_shard: 1_000_000,
58 max_entries_per_directory_hint_shard: 1_000_000,
59 max_payload_data_shards: u16::MAX,
60 max_payload_parity_shards: u16::MAX,
61 max_index_data_shards: u16::MAX,
62 max_index_parity_shards: u16::MAX,
63 max_index_root_data_shards: u16::MAX,
64 max_index_root_parity_shards: u16::MAX,
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct IndexRootHeader {
71 pub version: u32,
72 pub shard_count: u32,
73 pub directory_hint_shard_count: u32,
74 pub frame_count: u64,
75 pub envelope_count: u64,
76 pub file_count: u64,
77 pub payload_block_count: u64,
78 pub tar_total_size: u64,
79 pub content_sha256: [u8; 32],
80 pub shard_table_offset: u64,
81 pub directory_hint_shard_table_offset: u64,
82 pub dictionary_first_block: u64,
83 pub dictionary_data_block_count: u32,
84 pub dictionary_parity_block_count: u32,
85 pub dictionary_encrypted_size: u32,
86 pub dictionary_decompressed_size: u32,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct IndexRoot {
91 pub header: IndexRootHeader,
92 pub shards: Vec<ShardEntry>,
93 pub directory_hint_shards: Vec<DirectoryHintShardEntry>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ShardEntry {
98 pub shard_index: u64,
99 pub first_block_index: u64,
100 pub data_block_count: u32,
101 pub parity_block_count: u32,
102 pub encrypted_size: u32,
103 pub decompressed_size: u32,
104 pub file_count: u32,
105 pub first_path_hash: [u8; 8],
106 pub last_path_hash: [u8; 8],
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct DirectoryHintShardEntry {
111 pub hint_shard_index: u64,
112 pub first_dir_hash: [u8; 8],
113 pub last_dir_hash: [u8; 8],
114 pub first_block_index: u64,
115 pub data_block_count: u32,
116 pub parity_block_count: u32,
117 pub encrypted_size: u32,
118 pub decompressed_size: u32,
119 pub entry_count: u64,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct EnvelopeEntry {
124 pub envelope_index: u64,
125 pub first_block_index: u64,
126 pub data_block_count: u32,
127 pub parity_block_count: u32,
128 pub encrypted_size: u32,
129 pub plaintext_size: u32,
130 pub first_frame_index: u64,
131 pub frame_count: u32,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct FrameEntry {
136 pub frame_index: u64,
137 pub envelope_index: u64,
138 pub offset_in_envelope: u32,
139 pub compressed_size: u32,
140 pub decompressed_size: u32,
141 pub flags: u32,
142 pub tar_stream_offset: u64,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct IndexShardHeader {
147 pub version: u32,
148 pub shard_index: u64,
149 pub file_count: u32,
150 pub frame_count: u32,
151 pub envelope_count: u32,
152 pub file_table_offset: u32,
153 pub frame_table_offset: u32,
154 pub envelope_table_offset: u32,
155 pub string_pool_offset: u32,
156 pub string_pool_size: u32,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct IndexShard {
161 pub header: IndexShardHeader,
162 pub files: Vec<FileEntry>,
163 pub frames: Vec<FrameEntry>,
164 pub envelopes: Vec<EnvelopeEntry>,
165 pub string_pool: Vec<u8>,
166 file_paths: Vec<Vec<u8>>,
167 file_tar_member_group_starts: Vec<u64>,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct FileEntry {
172 pub path_hash: [u8; 8],
173 pub path_offset: u32,
174 pub path_length: u32,
175 pub first_frame_index: u64,
176 pub frame_count: u32,
177 pub offset_in_first_frame_plaintext: u32,
178 pub tar_member_group_size: u64,
179 pub file_data_size: u64,
180 pub flags: u32,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct DirectoryHintTable {
185 pub header: DirectoryHintTableHeader,
186 pub entries: Vec<DirectoryHintEntry>,
187 pub shard_row_indexes: Vec<u32>,
188 pub string_pool: Vec<u8>,
189 entry_paths: Vec<Vec<u8>>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct DirectoryHintTableHeader {
194 pub version: u32,
195 pub hint_shard_index: u64,
196 pub entry_count: u64,
197 pub entry_table_offset: u64,
198 pub shard_list_offset: u64,
199 pub string_pool_offset: u64,
200 pub string_pool_size: u64,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct DirectoryHintEntry {
205 pub dir_hash: [u8; 8],
206 pub path_offset: u64,
207 pub path_length: u32,
208 pub shard_list_start_index: u32,
209 pub shard_count: u32,
210}
211
212impl IndexRoot {
213 pub fn parse(
214 bytes: &[u8],
215 has_dictionary: bool,
216 limits: MetadataLimits,
217 ) -> Result<Self, FormatError> {
218 let structure = "IndexRoot";
219 if bytes.len() < INDEX_ROOT_LEN {
220 return invalid(structure, "plaintext is shorter than fixed header");
221 }
222 expect_magic(structure, TZIR_MAGIC, read_array::<4>(bytes, 0, structure)?)?;
223 expect_zero(structure, slice(bytes, 128, 32, structure)?)?;
224
225 let header = IndexRootHeader {
226 version: read_u32(bytes, 4, structure)?,
227 shard_count: read_u32(bytes, 8, structure)?,
228 directory_hint_shard_count: read_u32(bytes, 12, structure)?,
229 frame_count: read_u64(bytes, 16, structure)?,
230 envelope_count: read_u64(bytes, 24, structure)?,
231 file_count: read_u64(bytes, 32, structure)?,
232 payload_block_count: read_u64(bytes, 40, structure)?,
233 tar_total_size: read_u64(bytes, 48, structure)?,
234 content_sha256: read_array::<32>(bytes, 56, structure)?,
235 shard_table_offset: read_u64(bytes, 88, structure)?,
236 directory_hint_shard_table_offset: read_u64(bytes, 96, structure)?,
237 dictionary_first_block: read_u64(bytes, 104, structure)?,
238 dictionary_data_block_count: read_u32(bytes, 112, structure)?,
239 dictionary_parity_block_count: read_u32(bytes, 116, structure)?,
240 dictionary_encrypted_size: read_u32(bytes, 120, structure)?,
241 dictionary_decompressed_size: read_u32(bytes, 124, structure)?,
242 };
243
244 if header.version != 1 {
245 return invalid(structure, "unsupported version");
246 }
247 if header.shard_count > limits.max_shard_count {
248 return invalid(structure, "shard count exceeds resource cap");
249 }
250 if header.directory_hint_shard_count > limits.max_directory_hint_shards {
251 return invalid(structure, "directory hint shard count exceeds resource cap");
252 }
253 validate_dictionary_fields(&header, has_dictionary, limits)?;
254
255 let mut cursor = INDEX_ROOT_LEN;
256 let shards = if header.shard_count == 0 {
257 if header.shard_table_offset != 0 {
258 return invalid(structure, "absent shard table has non-zero offset");
259 }
260 Vec::new()
261 } else {
262 expect_offset(structure, "shard table", header.shard_table_offset, cursor)?;
263 let count = to_usize(header.shard_count as u64, structure)?;
264 let bytes_len = checked_mul(count, SHARD_ENTRY_LEN, structure)?;
265 let table = slice(bytes, cursor, bytes_len, structure)?;
266 cursor = checked_add(cursor, bytes_len, structure)?;
267 parse_shard_entries(table, limits)?
268 };
269
270 let directory_hint_shards = if header.directory_hint_shard_count == 0 {
271 if header.directory_hint_shard_table_offset != 0 {
272 return invalid(
273 structure,
274 "absent directory hint shard table has non-zero offset",
275 );
276 }
277 Vec::new()
278 } else {
279 if header.shard_count == 0 {
280 return invalid(structure, "directory hints require at least one shard");
281 }
282 expect_offset(
283 structure,
284 "directory hint shard table",
285 header.directory_hint_shard_table_offset,
286 cursor,
287 )?;
288 let count = to_usize(header.directory_hint_shard_count as u64, structure)?;
289 let bytes_len = checked_mul(count, DIRECTORY_HINT_SHARD_ENTRY_LEN, structure)?;
290 let table = slice(bytes, cursor, bytes_len, structure)?;
291 cursor = checked_add(cursor, bytes_len, structure)?;
292 parse_directory_hint_shard_entries(table, limits)?
293 };
294
295 if bytes.len() != cursor {
296 return invalid(
297 structure,
298 "plaintext length does not match canonical cursor",
299 );
300 }
301 validate_index_root_totals(&header, &shards, has_dictionary)?;
302
303 Ok(Self {
304 header,
305 shards,
306 directory_hint_shards,
307 })
308 }
309
310 pub fn to_bytes(&self) -> Vec<u8> {
311 let mut header = self.header.clone();
312 header.shard_count = self.shards.len() as u32;
313 header.directory_hint_shard_count = self.directory_hint_shards.len() as u32;
314 header.shard_table_offset = if self.shards.is_empty() {
315 0
316 } else {
317 INDEX_ROOT_LEN as u64
318 };
319 header.directory_hint_shard_table_offset = if self.directory_hint_shards.is_empty() {
320 0
321 } else {
322 (INDEX_ROOT_LEN + self.shards.len() * SHARD_ENTRY_LEN) as u64
323 };
324
325 let mut bytes = Vec::with_capacity(
326 INDEX_ROOT_LEN
327 + self.shards.len() * SHARD_ENTRY_LEN
328 + self.directory_hint_shards.len() * DIRECTORY_HINT_SHARD_ENTRY_LEN,
329 );
330 bytes.extend_from_slice(&header.to_bytes());
331 for entry in &self.shards {
332 bytes.extend_from_slice(&entry.to_bytes());
333 }
334 for entry in &self.directory_hint_shards {
335 bytes.extend_from_slice(&entry.to_bytes());
336 }
337 bytes
338 }
339
340 pub fn candidate_shard_indexes_for_hash(
341 &self,
342 target_hash: [u8; 8],
343 scan_cap_per_direction: usize,
344 ) -> Result<Vec<usize>, FormatError> {
345 candidate_interval_indexes(
346 &self.shards,
347 target_hash,
348 scan_cap_per_direction,
349 |entry| entry.first_path_hash,
350 |entry| entry.last_path_hash,
351 )
352 }
353
354 pub fn candidate_shards_for_path(
355 &self,
356 normalized_path: &[u8],
357 limits: MetadataLimits,
358 ) -> Result<Vec<usize>, FormatError> {
359 self.candidate_shard_indexes_for_hash(
360 hash_prefix(normalized_path),
361 limits.max_hash_collision_shard_scan,
362 )
363 }
364}
365
366impl IndexRootHeader {
367 pub fn empty() -> Self {
368 Self {
369 version: 1,
370 shard_count: 0,
371 directory_hint_shard_count: 0,
372 frame_count: 0,
373 envelope_count: 0,
374 file_count: 0,
375 payload_block_count: 0,
376 tar_total_size: 0,
377 content_sha256: SHA256_EMPTY,
378 shard_table_offset: 0,
379 directory_hint_shard_table_offset: 0,
380 dictionary_first_block: 0,
381 dictionary_data_block_count: 0,
382 dictionary_parity_block_count: 0,
383 dictionary_encrypted_size: 0,
384 dictionary_decompressed_size: 0,
385 }
386 }
387
388 pub fn to_bytes(&self) -> [u8; INDEX_ROOT_LEN] {
389 let mut bytes = [0u8; INDEX_ROOT_LEN];
390 bytes[0..4].copy_from_slice(&TZIR_MAGIC);
391 write_u32(&mut bytes, 4, self.version);
392 write_u32(&mut bytes, 8, self.shard_count);
393 write_u32(&mut bytes, 12, self.directory_hint_shard_count);
394 write_u64(&mut bytes, 16, self.frame_count);
395 write_u64(&mut bytes, 24, self.envelope_count);
396 write_u64(&mut bytes, 32, self.file_count);
397 write_u64(&mut bytes, 40, self.payload_block_count);
398 write_u64(&mut bytes, 48, self.tar_total_size);
399 bytes[56..88].copy_from_slice(&self.content_sha256);
400 write_u64(&mut bytes, 88, self.shard_table_offset);
401 write_u64(&mut bytes, 96, self.directory_hint_shard_table_offset);
402 write_u64(&mut bytes, 104, self.dictionary_first_block);
403 write_u32(&mut bytes, 112, self.dictionary_data_block_count);
404 write_u32(&mut bytes, 116, self.dictionary_parity_block_count);
405 write_u32(&mut bytes, 120, self.dictionary_encrypted_size);
406 write_u32(&mut bytes, 124, self.dictionary_decompressed_size);
407 bytes
408 }
409}
410
411impl ShardEntry {
412 pub fn to_bytes(&self) -> [u8; SHARD_ENTRY_LEN] {
413 let mut bytes = [0u8; SHARD_ENTRY_LEN];
414 write_u64(&mut bytes, 0, self.shard_index);
415 write_u64(&mut bytes, 8, self.first_block_index);
416 write_u32(&mut bytes, 16, self.data_block_count);
417 write_u32(&mut bytes, 20, self.parity_block_count);
418 write_u32(&mut bytes, 24, self.encrypted_size);
419 write_u32(&mut bytes, 28, self.decompressed_size);
420 write_u32(&mut bytes, 32, self.file_count);
421 bytes[36..44].copy_from_slice(&self.first_path_hash);
422 bytes[44..52].copy_from_slice(&self.last_path_hash);
423 bytes
424 }
425}
426
427impl DirectoryHintShardEntry {
428 pub fn to_bytes(&self) -> [u8; DIRECTORY_HINT_SHARD_ENTRY_LEN] {
429 let mut bytes = [0u8; DIRECTORY_HINT_SHARD_ENTRY_LEN];
430 write_u64(&mut bytes, 0, self.hint_shard_index);
431 bytes[8..16].copy_from_slice(&self.first_dir_hash);
432 bytes[16..24].copy_from_slice(&self.last_dir_hash);
433 write_u64(&mut bytes, 24, self.first_block_index);
434 write_u32(&mut bytes, 32, self.data_block_count);
435 write_u32(&mut bytes, 36, self.parity_block_count);
436 write_u32(&mut bytes, 40, self.encrypted_size);
437 write_u32(&mut bytes, 44, self.decompressed_size);
438 write_u64(&mut bytes, 48, self.entry_count);
439 bytes
440 }
441}
442
443impl IndexShard {
444 pub fn parse(
445 bytes: &[u8],
446 locating_shard: &ShardEntry,
447 limits: MetadataLimits,
448 ) -> Result<Self, FormatError> {
449 let structure = "IndexShard";
450 if bytes.len() < INDEX_SHARD_HEADER_LEN {
451 return invalid(structure, "plaintext is shorter than fixed header");
452 }
453 expect_magic(structure, TZIS_MAGIC, read_array::<4>(bytes, 0, structure)?)?;
454 expect_zero(structure, slice(bytes, 48, 16, structure)?)?;
455
456 let header = IndexShardHeader {
457 version: read_u32(bytes, 4, structure)?,
458 shard_index: read_u64(bytes, 8, structure)?,
459 file_count: read_u32(bytes, 16, structure)?,
460 frame_count: read_u32(bytes, 20, structure)?,
461 envelope_count: read_u32(bytes, 24, structure)?,
462 file_table_offset: read_u32(bytes, 28, structure)?,
463 frame_table_offset: read_u32(bytes, 32, structure)?,
464 envelope_table_offset: read_u32(bytes, 36, structure)?,
465 string_pool_offset: read_u32(bytes, 40, structure)?,
466 string_pool_size: read_u32(bytes, 44, structure)?,
467 };
468
469 if header.version != INDEX_SHARD_VERSION {
470 return invalid(structure, "unsupported version");
471 }
472 if header.file_count == 0 {
473 return invalid(structure, "index shard must contain at least one file");
474 }
475 if header.file_count > limits.max_files_per_index_shard {
476 return invalid(structure, "file count exceeds resource cap");
477 }
478 if header.shard_index != locating_shard.shard_index {
479 return invalid(structure, "shard index does not match locating ShardEntry");
480 }
481 if header.file_count != locating_shard.file_count {
482 return invalid(structure, "file count does not match locating ShardEntry");
483 }
484
485 let mut cursor = INDEX_SHARD_HEADER_LEN;
486 let files = parse_counted_table(
487 bytes,
488 CountedTableSpec {
489 structure,
490 name: "file table",
491 count: header.file_count as u64,
492 offset: header.file_table_offset as u64,
493 entry_len: FILE_ENTRY_LEN,
494 parse: parse_file_entry,
495 },
496 &mut cursor,
497 )?;
498 let frames = parse_counted_table(
499 bytes,
500 CountedTableSpec {
501 structure,
502 name: "frame table",
503 count: header.frame_count as u64,
504 offset: header.frame_table_offset as u64,
505 entry_len: FRAME_ENTRY_LEN,
506 parse: parse_frame_entry,
507 },
508 &mut cursor,
509 )?;
510 let envelopes = parse_counted_table(
511 bytes,
512 CountedTableSpec {
513 structure,
514 name: "envelope table",
515 count: header.envelope_count as u64,
516 offset: header.envelope_table_offset as u64,
517 entry_len: ENVELOPE_ENTRY_LEN,
518 parse: parse_envelope_entry,
519 },
520 &mut cursor,
521 )?;
522 let string_pool = if header.string_pool_size == 0 {
523 if header.string_pool_offset != 0 {
524 return invalid(structure, "absent string pool has non-zero offset");
525 }
526 Vec::new()
527 } else {
528 expect_offset(
529 structure,
530 "string pool",
531 header.string_pool_offset as u64,
532 cursor,
533 )?;
534 let len = header.string_pool_size as usize;
535 let pool = slice(bytes, cursor, len, structure)?.to_vec();
536 cursor = checked_add(cursor, len, structure)?;
537 pool
538 };
539 if bytes.len() != cursor {
540 return invalid(
541 structure,
542 "plaintext length does not match canonical cursor",
543 );
544 }
545
546 let (file_paths, file_tar_member_group_starts) = validate_index_shard_tables(
547 &files,
548 &frames,
549 &envelopes,
550 &string_pool,
551 locating_shard,
552 limits,
553 )?;
554
555 Ok(Self {
556 header,
557 files,
558 frames,
559 envelopes,
560 string_pool,
561 file_paths,
562 file_tar_member_group_starts,
563 })
564 }
565
566 pub fn to_bytes(&self) -> Vec<u8> {
567 let mut header = self.header.clone();
568 header.file_count = self.files.len() as u32;
569 header.frame_count = self.frames.len() as u32;
570 header.envelope_count = self.envelopes.len() as u32;
571
572 let mut cursor = INDEX_SHARD_HEADER_LEN;
573 header.file_table_offset = table_offset(self.files.len(), cursor);
574 cursor += self.files.len() * FILE_ENTRY_LEN;
575 header.frame_table_offset = table_offset(self.frames.len(), cursor);
576 cursor += self.frames.len() * FRAME_ENTRY_LEN;
577 header.envelope_table_offset = table_offset(self.envelopes.len(), cursor);
578 cursor += self.envelopes.len() * ENVELOPE_ENTRY_LEN;
579 header.string_pool_size = self.string_pool.len() as u32;
580 header.string_pool_offset = table_offset(self.string_pool.len(), cursor);
581
582 let mut bytes = Vec::with_capacity(cursor + self.string_pool.len());
583 bytes.extend_from_slice(&header.to_bytes());
584 for entry in &self.files {
585 bytes.extend_from_slice(&entry.to_bytes());
586 }
587 for entry in &self.frames {
588 bytes.extend_from_slice(&entry.to_bytes());
589 }
590 for entry in &self.envelopes {
591 bytes.extend_from_slice(&entry.to_bytes());
592 }
593 bytes.extend_from_slice(&self.string_pool);
594 bytes
595 }
596
597 pub fn file_path(&self, file_index: usize) -> Option<&[u8]> {
598 self.file_paths.get(file_index).map(Vec::as_slice)
599 }
600
601 pub fn tar_member_group_start(&self, file_index: usize) -> Option<u64> {
602 self.file_tar_member_group_starts.get(file_index).copied()
603 }
604
605 pub fn lookup_file_index(&self, normalized_path: &[u8]) -> Option<usize> {
606 let target_hash = hash_prefix(normalized_path);
607 let lower = self.lower_bound_file_key(target_hash, normalized_path);
608
609 let mut best = None;
610 for idx in lower..self.files.len() {
611 let file = &self.files[idx];
612 if file.path_hash != target_hash || self.file_paths[idx].as_slice() != normalized_path {
613 break;
614 }
615 best = Some(idx);
616 }
617 best
618 }
619
620 fn lower_bound_file_key(&self, target_hash: [u8; 8], target_path: &[u8]) -> usize {
621 let mut low = 0usize;
622 let mut high = self.files.len();
623 while low < high {
624 let mid = low + (high - low) / 2;
625 let key_is_less = self.files[mid].path_hash < target_hash
626 || (self.files[mid].path_hash == target_hash
627 && self.file_paths[mid].as_slice() < target_path);
628 if key_is_less {
629 low = mid + 1;
630 } else {
631 high = mid;
632 }
633 }
634 low
635 }
636}
637
638impl IndexShardHeader {
639 pub fn to_bytes(&self) -> [u8; INDEX_SHARD_HEADER_LEN] {
640 let mut bytes = [0u8; INDEX_SHARD_HEADER_LEN];
641 bytes[0..4].copy_from_slice(&TZIS_MAGIC);
642 write_u32(&mut bytes, 4, self.version);
643 write_u64(&mut bytes, 8, self.shard_index);
644 write_u32(&mut bytes, 16, self.file_count);
645 write_u32(&mut bytes, 20, self.frame_count);
646 write_u32(&mut bytes, 24, self.envelope_count);
647 write_u32(&mut bytes, 28, self.file_table_offset);
648 write_u32(&mut bytes, 32, self.frame_table_offset);
649 write_u32(&mut bytes, 36, self.envelope_table_offset);
650 write_u32(&mut bytes, 40, self.string_pool_offset);
651 write_u32(&mut bytes, 44, self.string_pool_size);
652 bytes
653 }
654}
655
656impl FileEntry {
657 pub fn to_bytes(&self) -> [u8; FILE_ENTRY_LEN] {
658 let mut bytes = [0u8; FILE_ENTRY_LEN];
659 bytes[0..8].copy_from_slice(&self.path_hash);
660 write_u32(&mut bytes, 8, self.path_offset);
661 write_u32(&mut bytes, 12, self.path_length);
662 write_u64(&mut bytes, 16, self.first_frame_index);
663 write_u32(&mut bytes, 24, self.frame_count);
664 write_u32(&mut bytes, 28, self.offset_in_first_frame_plaintext);
665 write_u64(&mut bytes, 32, self.tar_member_group_size);
666 write_u64(&mut bytes, 40, self.file_data_size);
667 write_u32(&mut bytes, 48, self.flags);
668 bytes
669 }
670}
671
672impl FrameEntry {
673 pub fn to_bytes(&self) -> [u8; FRAME_ENTRY_LEN] {
674 let mut bytes = [0u8; FRAME_ENTRY_LEN];
675 write_u64(&mut bytes, 0, self.frame_index);
676 write_u64(&mut bytes, 8, self.envelope_index);
677 write_u32(&mut bytes, 16, self.offset_in_envelope);
678 write_u32(&mut bytes, 20, self.compressed_size);
679 write_u32(&mut bytes, 24, self.decompressed_size);
680 write_u32(&mut bytes, 28, self.flags);
681 write_u64(&mut bytes, 32, self.tar_stream_offset);
682 bytes
683 }
684}
685
686impl EnvelopeEntry {
687 pub fn to_bytes(&self) -> [u8; ENVELOPE_ENTRY_LEN] {
688 let mut bytes = [0u8; ENVELOPE_ENTRY_LEN];
689 write_u64(&mut bytes, 0, self.envelope_index);
690 write_u64(&mut bytes, 8, self.first_block_index);
691 write_u32(&mut bytes, 16, self.data_block_count);
692 write_u32(&mut bytes, 20, self.parity_block_count);
693 write_u32(&mut bytes, 24, self.encrypted_size);
694 write_u32(&mut bytes, 28, self.plaintext_size);
695 write_u64(&mut bytes, 32, self.first_frame_index);
696 write_u32(&mut bytes, 40, self.frame_count);
697 bytes
698 }
699}
700
701impl DirectoryHintTable {
702 pub fn parse(
703 bytes: &[u8],
704 locating_shard: &DirectoryHintShardEntry,
705 index_root_shard_count: u32,
706 limits: MetadataLimits,
707 ) -> Result<Self, FormatError> {
708 let structure = "DirectoryHintTable";
709 if bytes.len() < DIRECTORY_HINT_TABLE_LEN {
710 return invalid(structure, "plaintext is shorter than fixed header");
711 }
712 expect_magic(structure, TZDH_MAGIC, read_array::<4>(bytes, 0, structure)?)?;
713 expect_zero(structure, slice(bytes, 56, 16, structure)?)?;
714
715 let header = DirectoryHintTableHeader {
716 version: read_u32(bytes, 4, structure)?,
717 hint_shard_index: read_u64(bytes, 8, structure)?,
718 entry_count: read_u64(bytes, 16, structure)?,
719 entry_table_offset: read_u64(bytes, 24, structure)?,
720 shard_list_offset: read_u64(bytes, 32, structure)?,
721 string_pool_offset: read_u64(bytes, 40, structure)?,
722 string_pool_size: read_u64(bytes, 48, structure)?,
723 };
724 if header.version != 1 {
725 return invalid(structure, "unsupported version");
726 }
727 if header.hint_shard_index != locating_shard.hint_shard_index {
728 return invalid(
729 structure,
730 "hint shard index does not match locating DirectoryHintShardEntry",
731 );
732 }
733 if header.entry_count != locating_shard.entry_count {
734 return invalid(
735 structure,
736 "entry count does not match locating DirectoryHintShardEntry",
737 );
738 }
739 if header.entry_count == 0 {
740 return invalid(structure, "located directory hint shard is empty");
741 }
742 if header.entry_count > limits.max_entries_per_directory_hint_shard {
743 return invalid(structure, "entry count exceeds resource cap");
744 }
745
746 let entry_count = to_usize(header.entry_count, structure)?;
747 expect_offset(
748 structure,
749 "entry table",
750 header.entry_table_offset,
751 DIRECTORY_HINT_TABLE_LEN,
752 )?;
753 let entry_bytes_len = checked_mul(entry_count, DIRECTORY_HINT_ENTRY_LEN, structure)?;
754 let entries_end = checked_add(DIRECTORY_HINT_TABLE_LEN, entry_bytes_len, structure)?;
755 expect_offset(
756 structure,
757 "shard list",
758 header.shard_list_offset,
759 entries_end,
760 )?;
761 if header.shard_list_offset % 4 != 0 {
762 return invalid(structure, "shard list is not 4-byte aligned");
763 }
764
765 let entry_bytes = slice(bytes, DIRECTORY_HINT_TABLE_LEN, entry_bytes_len, structure)?;
766 let entries = parse_directory_hint_entries(entry_bytes)?;
767 let shard_list_len = validate_directory_hint_entries(
768 &entries,
769 bytes,
770 &header,
771 locating_shard,
772 index_root_shard_count,
773 )?;
774 let shard_list_offset = to_usize(header.shard_list_offset, structure)?;
775 let shard_list_bytes_len = checked_mul(shard_list_len, 4, structure)?;
776 let shard_list_end = checked_add(shard_list_offset, shard_list_bytes_len, structure)?;
777 let shard_list_bytes = slice(bytes, shard_list_offset, shard_list_bytes_len, structure)?;
778 let shard_row_indexes = parse_u32_array(shard_list_bytes, structure)?;
779
780 let string_pool = if header.string_pool_size == 0 {
781 if header.string_pool_offset != 0 {
782 return invalid(structure, "absent string pool has non-zero offset");
783 }
784 Vec::new()
785 } else {
786 expect_offset(
787 structure,
788 "string pool",
789 header.string_pool_offset,
790 shard_list_end,
791 )?;
792 let offset = to_usize(header.string_pool_offset, structure)?;
793 let size = to_usize(header.string_pool_size, structure)?;
794 slice(bytes, offset, size, structure)?.to_vec()
795 };
796 let final_cursor = if header.string_pool_size == 0 {
797 shard_list_end
798 } else {
799 checked_add(
800 to_usize(header.string_pool_offset, structure)?,
801 to_usize(header.string_pool_size, structure)?,
802 structure,
803 )?
804 };
805 if bytes.len() != final_cursor {
806 return invalid(
807 structure,
808 "plaintext length does not match canonical cursor",
809 );
810 }
811
812 let entry_paths = validate_directory_hint_paths_and_lists(
813 &entries,
814 &shard_row_indexes,
815 &string_pool,
816 locating_shard,
817 index_root_shard_count,
818 limits.max_path_length,
819 )?;
820
821 Ok(Self {
822 header,
823 entries,
824 shard_row_indexes,
825 string_pool,
826 entry_paths,
827 })
828 }
829
830 pub fn to_bytes(&self) -> Vec<u8> {
831 let mut header = self.header.clone();
832 header.entry_count = self.entries.len() as u64;
833 header.entry_table_offset = if self.entries.is_empty() {
834 0
835 } else {
836 DIRECTORY_HINT_TABLE_LEN as u64
837 };
838 header.shard_list_offset = if self.entries.is_empty() {
839 0
840 } else {
841 (DIRECTORY_HINT_TABLE_LEN + self.entries.len() * DIRECTORY_HINT_ENTRY_LEN) as u64
842 };
843 header.string_pool_size = self.string_pool.len() as u64;
844 header.string_pool_offset = if self.string_pool.is_empty() {
845 0
846 } else {
847 header.shard_list_offset + (self.shard_row_indexes.len() as u64) * 4
848 };
849
850 let mut bytes = Vec::with_capacity(
851 DIRECTORY_HINT_TABLE_LEN
852 + self.entries.len() * DIRECTORY_HINT_ENTRY_LEN
853 + self.shard_row_indexes.len() * 4
854 + self.string_pool.len(),
855 );
856 bytes.extend_from_slice(&header.to_bytes());
857 for entry in &self.entries {
858 bytes.extend_from_slice(&entry.to_bytes());
859 }
860 if !self.entries.is_empty() {
861 for row in &self.shard_row_indexes {
862 let mut raw = [0u8; 4];
863 write_u32(&mut raw, 0, *row);
864 bytes.extend_from_slice(&raw);
865 }
866 }
867 bytes.extend_from_slice(&self.string_pool);
868 bytes
869 }
870
871 pub fn entry_path(&self, entry_index: usize) -> Option<&[u8]> {
872 self.entry_paths.get(entry_index).map(Vec::as_slice)
873 }
874
875 pub fn lookup_directory_index(&self, normalized_dir_path: &[u8]) -> Option<usize> {
876 let target_hash = hash_prefix(normalized_dir_path);
877 let lower = self.lower_bound_directory_key(target_hash, normalized_dir_path);
878 let entry = self.entries.get(lower)?;
879 if entry.dir_hash == target_hash
880 && self.entry_paths[lower].as_slice() == normalized_dir_path
881 {
882 Some(lower)
883 } else {
884 None
885 }
886 }
887
888 fn lower_bound_directory_key(&self, target_hash: [u8; 8], target_path: &[u8]) -> usize {
889 let mut low = 0usize;
890 let mut high = self.entries.len();
891 while low < high {
892 let mid = low + (high - low) / 2;
893 let key_is_less = self.entries[mid].dir_hash < target_hash
894 || (self.entries[mid].dir_hash == target_hash
895 && self.entry_paths[mid].as_slice() < target_path);
896 if key_is_less {
897 low = mid + 1;
898 } else {
899 high = mid;
900 }
901 }
902 low
903 }
904
905 pub fn shard_rows_for_entry(&self, entry_index: usize) -> Option<&[u32]> {
906 let entry = self.entries.get(entry_index)?;
907 let start = entry.shard_list_start_index as usize;
908 let end = start.checked_add(entry.shard_count as usize)?;
909 self.shard_row_indexes.get(start..end)
910 }
911}
912
913impl DirectoryHintTableHeader {
914 pub fn to_bytes(&self) -> [u8; DIRECTORY_HINT_TABLE_LEN] {
915 let mut bytes = [0u8; DIRECTORY_HINT_TABLE_LEN];
916 bytes[0..4].copy_from_slice(&TZDH_MAGIC);
917 write_u32(&mut bytes, 4, self.version);
918 write_u64(&mut bytes, 8, self.hint_shard_index);
919 write_u64(&mut bytes, 16, self.entry_count);
920 write_u64(&mut bytes, 24, self.entry_table_offset);
921 write_u64(&mut bytes, 32, self.shard_list_offset);
922 write_u64(&mut bytes, 40, self.string_pool_offset);
923 write_u64(&mut bytes, 48, self.string_pool_size);
924 bytes
925 }
926}
927
928impl DirectoryHintEntry {
929 pub fn to_bytes(&self) -> [u8; DIRECTORY_HINT_ENTRY_LEN] {
930 let mut bytes = [0u8; DIRECTORY_HINT_ENTRY_LEN];
931 bytes[0..8].copy_from_slice(&self.dir_hash);
932 write_u64(&mut bytes, 8, self.path_offset);
933 write_u32(&mut bytes, 16, self.path_length);
934 write_u32(&mut bytes, 24, self.shard_list_start_index);
935 write_u32(&mut bytes, 28, self.shard_count);
936 bytes
937 }
938}
939
940pub fn hash_prefix(bytes: &[u8]) -> [u8; 8] {
941 let digest = Sha256::digest(bytes);
942 let mut out = [0u8; 8];
943 out.copy_from_slice(&digest[..8]);
944 out
945}
946
947pub fn normalize_lookup_file_path(
948 path: &str,
949 max_path_length: u32,
950) -> Result<Vec<u8>, FormatError> {
951 let normalized = path.nfc().collect::<String>();
952 validate_file_path_bytes(normalized.as_bytes(), max_path_length)?;
953 Ok(normalized.into_bytes())
954}
955
956pub fn normalize_lookup_directory_path(
957 path: &str,
958 max_path_length: u32,
959) -> Result<Vec<u8>, FormatError> {
960 let trimmed = path.strip_suffix('/').unwrap_or(path);
961 let normalized = trimmed.nfc().collect::<String>();
962 validate_directory_path_bytes(normalized.as_bytes(), max_path_length)?;
963 Ok(normalized.into_bytes())
964}
965
966pub fn is_directory_ancestor(directory_path: &[u8], file_path: &[u8]) -> bool {
967 if directory_path.is_empty() {
968 return true;
969 }
970 file_path.len() > directory_path.len()
971 && file_path.starts_with(directory_path)
972 && file_path[directory_path.len()] == b'/'
973}
974
975fn parse_shard_entries(
976 bytes: &[u8],
977 limits: MetadataLimits,
978) -> Result<Vec<ShardEntry>, FormatError> {
979 let mut entries = Vec::with_capacity(bytes.len() / SHARD_ENTRY_LEN);
980 let mut seen_indexes = HashSet::new();
981 for chunk in bytes.chunks_exact(SHARD_ENTRY_LEN) {
982 let entry = ShardEntry {
983 shard_index: read_u64(chunk, 0, "ShardEntry")?,
984 first_block_index: read_u64(chunk, 8, "ShardEntry")?,
985 data_block_count: read_u32(chunk, 16, "ShardEntry")?,
986 parity_block_count: read_u32(chunk, 20, "ShardEntry")?,
987 encrypted_size: read_u32(chunk, 24, "ShardEntry")?,
988 decompressed_size: read_u32(chunk, 28, "ShardEntry")?,
989 file_count: read_u32(chunk, 32, "ShardEntry")?,
990 first_path_hash: read_array::<8>(chunk, 36, "ShardEntry")?,
991 last_path_hash: read_array::<8>(chunk, 44, "ShardEntry")?,
992 };
993 if entry.file_count == 0 {
994 return invalid("ShardEntry", "file count is zero");
995 }
996 if entry.decompressed_size == 0 {
997 return invalid("ShardEntry", "decompressed size is zero");
998 }
999 validate_encrypted_extent(
1000 "ShardEntry",
1001 entry.data_block_count,
1002 entry.encrypted_size,
1003 limits.block_size,
1004 )?;
1005 validate_fec_class_extent(
1006 "ShardEntry",
1007 entry.data_block_count,
1008 entry.parity_block_count,
1009 limits.max_index_data_shards,
1010 limits.max_index_parity_shards,
1011 )?;
1012 if entry.first_path_hash > entry.last_path_hash {
1013 return invalid("ShardEntry", "first hash is greater than last hash");
1014 }
1015 if !seen_indexes.insert(entry.shard_index) {
1016 return invalid("ShardEntry", "duplicate shard index");
1017 }
1018 if let Some(previous) = entries.last() {
1019 let previous: &ShardEntry = previous;
1020 if shard_entry_sort_key(previous) >= shard_entry_sort_key(&entry) {
1021 return invalid("IndexRoot", "ShardEntry rows are not sorted");
1022 }
1023 if previous.last_path_hash > entry.first_path_hash {
1024 return invalid("IndexRoot", "ShardEntry hash ranges overlap out of order");
1025 }
1026 }
1027 entries.push(entry);
1028 }
1029 Ok(entries)
1030}
1031
1032fn parse_directory_hint_shard_entries(
1033 bytes: &[u8],
1034 limits: MetadataLimits,
1035) -> Result<Vec<DirectoryHintShardEntry>, FormatError> {
1036 let mut entries = Vec::with_capacity(bytes.len() / DIRECTORY_HINT_SHARD_ENTRY_LEN);
1037 let mut seen_indexes = HashSet::new();
1038 for chunk in bytes.chunks_exact(DIRECTORY_HINT_SHARD_ENTRY_LEN) {
1039 let entry = DirectoryHintShardEntry {
1040 hint_shard_index: read_u64(chunk, 0, "DirectoryHintShardEntry")?,
1041 first_dir_hash: read_array::<8>(chunk, 8, "DirectoryHintShardEntry")?,
1042 last_dir_hash: read_array::<8>(chunk, 16, "DirectoryHintShardEntry")?,
1043 first_block_index: read_u64(chunk, 24, "DirectoryHintShardEntry")?,
1044 data_block_count: read_u32(chunk, 32, "DirectoryHintShardEntry")?,
1045 parity_block_count: read_u32(chunk, 36, "DirectoryHintShardEntry")?,
1046 encrypted_size: read_u32(chunk, 40, "DirectoryHintShardEntry")?,
1047 decompressed_size: read_u32(chunk, 44, "DirectoryHintShardEntry")?,
1048 entry_count: read_u64(chunk, 48, "DirectoryHintShardEntry")?,
1049 };
1050 if entry.entry_count == 0 {
1051 return invalid("DirectoryHintShardEntry", "entry count is zero");
1052 }
1053 if entry.decompressed_size == 0 {
1054 return invalid("DirectoryHintShardEntry", "decompressed size is zero");
1055 }
1056 validate_encrypted_extent(
1057 "DirectoryHintShardEntry",
1058 entry.data_block_count,
1059 entry.encrypted_size,
1060 limits.block_size,
1061 )?;
1062 validate_fec_class_extent(
1063 "DirectoryHintShardEntry",
1064 entry.data_block_count,
1065 entry.parity_block_count,
1066 limits.max_index_data_shards,
1067 limits.max_index_parity_shards,
1068 )?;
1069 if entry.first_dir_hash > entry.last_dir_hash {
1070 return invalid(
1071 "DirectoryHintShardEntry",
1072 "first hash is greater than last hash",
1073 );
1074 }
1075 if !seen_indexes.insert(entry.hint_shard_index) {
1076 return invalid("DirectoryHintShardEntry", "duplicate hint shard index");
1077 }
1078 if let Some(previous) = entries.last() {
1079 let previous: &DirectoryHintShardEntry = previous;
1080 if directory_hint_shard_sort_key(previous) >= directory_hint_shard_sort_key(&entry) {
1081 return invalid("IndexRoot", "DirectoryHintShardEntry rows are not sorted");
1082 }
1083 if previous.last_dir_hash > entry.first_dir_hash {
1084 return invalid(
1085 "IndexRoot",
1086 "DirectoryHintShardEntry hash ranges overlap out of order",
1087 );
1088 }
1089 }
1090 entries.push(entry);
1091 }
1092 Ok(entries)
1093}
1094
1095fn parse_file_entry(bytes: &[u8]) -> Result<FileEntry, FormatError> {
1096 expect_zero("FileEntry", slice(bytes, 52, 4, "FileEntry")?)?;
1097 Ok(FileEntry {
1098 path_hash: read_array::<8>(bytes, 0, "FileEntry")?,
1099 path_offset: read_u32(bytes, 8, "FileEntry")?,
1100 path_length: read_u32(bytes, 12, "FileEntry")?,
1101 first_frame_index: read_u64(bytes, 16, "FileEntry")?,
1102 frame_count: read_u32(bytes, 24, "FileEntry")?,
1103 offset_in_first_frame_plaintext: read_u32(bytes, 28, "FileEntry")?,
1104 tar_member_group_size: read_u64(bytes, 32, "FileEntry")?,
1105 file_data_size: read_u64(bytes, 40, "FileEntry")?,
1106 flags: read_u32(bytes, 48, "FileEntry")?,
1107 })
1108}
1109
1110fn parse_frame_entry(bytes: &[u8]) -> Result<FrameEntry, FormatError> {
1111 expect_zero("FrameEntry", slice(bytes, 40, 4, "FrameEntry")?)?;
1112 Ok(FrameEntry {
1113 frame_index: read_u64(bytes, 0, "FrameEntry")?,
1114 envelope_index: read_u64(bytes, 8, "FrameEntry")?,
1115 offset_in_envelope: read_u32(bytes, 16, "FrameEntry")?,
1116 compressed_size: read_u32(bytes, 20, "FrameEntry")?,
1117 decompressed_size: read_u32(bytes, 24, "FrameEntry")?,
1118 flags: read_u32(bytes, 28, "FrameEntry")?,
1119 tar_stream_offset: read_u64(bytes, 32, "FrameEntry")?,
1120 })
1121}
1122
1123fn parse_envelope_entry(bytes: &[u8]) -> Result<EnvelopeEntry, FormatError> {
1124 expect_zero("EnvelopeEntry", slice(bytes, 44, 4, "EnvelopeEntry")?)?;
1125 Ok(EnvelopeEntry {
1126 envelope_index: read_u64(bytes, 0, "EnvelopeEntry")?,
1127 first_block_index: read_u64(bytes, 8, "EnvelopeEntry")?,
1128 data_block_count: read_u32(bytes, 16, "EnvelopeEntry")?,
1129 parity_block_count: read_u32(bytes, 20, "EnvelopeEntry")?,
1130 encrypted_size: read_u32(bytes, 24, "EnvelopeEntry")?,
1131 plaintext_size: read_u32(bytes, 28, "EnvelopeEntry")?,
1132 first_frame_index: read_u64(bytes, 32, "EnvelopeEntry")?,
1133 frame_count: read_u32(bytes, 40, "EnvelopeEntry")?,
1134 })
1135}
1136
1137fn parse_directory_hint_entries(bytes: &[u8]) -> Result<Vec<DirectoryHintEntry>, FormatError> {
1138 let mut entries = Vec::with_capacity(bytes.len() / DIRECTORY_HINT_ENTRY_LEN);
1139 for chunk in bytes.chunks_exact(DIRECTORY_HINT_ENTRY_LEN) {
1140 expect_zero(
1141 "DirectoryHintEntry",
1142 slice(chunk, 20, 4, "DirectoryHintEntry")?,
1143 )?;
1144 expect_zero(
1145 "DirectoryHintEntry",
1146 slice(chunk, 32, 8, "DirectoryHintEntry")?,
1147 )?;
1148 entries.push(DirectoryHintEntry {
1149 dir_hash: read_array::<8>(chunk, 0, "DirectoryHintEntry")?,
1150 path_offset: read_u64(chunk, 8, "DirectoryHintEntry")?,
1151 path_length: read_u32(chunk, 16, "DirectoryHintEntry")?,
1152 shard_list_start_index: read_u32(chunk, 24, "DirectoryHintEntry")?,
1153 shard_count: read_u32(chunk, 28, "DirectoryHintEntry")?,
1154 });
1155 }
1156 Ok(entries)
1157}
1158
1159fn validate_index_root_totals(
1160 header: &IndexRootHeader,
1161 shards: &[ShardEntry],
1162 has_dictionary: bool,
1163) -> Result<(), FormatError> {
1164 if shards.is_empty() {
1165 if header.file_count != 0
1166 || header.frame_count != 0
1167 || header.envelope_count != 0
1168 || header.payload_block_count != 0
1169 || header.tar_total_size != 0
1170 {
1171 return invalid(
1172 "IndexRoot",
1173 "empty shard table has non-empty archive totals",
1174 );
1175 }
1176 if header.content_sha256 != SHA256_EMPTY {
1177 return invalid(
1178 "IndexRoot",
1179 "empty archive content hash is not SHA-256(empty)",
1180 );
1181 }
1182 if has_dictionary || !index_root_dictionary_fields_are_zero(header) {
1183 return invalid("IndexRoot", "empty archive cannot use dictionary");
1184 }
1185 return Ok(());
1186 }
1187
1188 let mut sum = 0u64;
1189 for shard in shards {
1190 sum = sum.checked_add(shard.file_count as u64).ok_or(
1191 FormatError::MetadataArithmeticOverflow {
1192 structure: "IndexRoot",
1193 },
1194 )?;
1195 }
1196 if sum != header.file_count {
1197 return invalid(
1198 "IndexRoot",
1199 "file_count does not equal sum of ShardEntry rows",
1200 );
1201 }
1202 Ok(())
1203}
1204
1205fn validate_dictionary_fields(
1206 header: &IndexRootHeader,
1207 has_dictionary: bool,
1208 limits: MetadataLimits,
1209) -> Result<(), FormatError> {
1210 if !has_dictionary {
1211 if !index_root_dictionary_fields_are_zero(header) {
1212 return invalid(
1213 "IndexRoot",
1214 "dictionary fields are non-zero while has_dictionary is false",
1215 );
1216 }
1217 return Ok(());
1218 }
1219
1220 if header.dictionary_data_block_count == 0 {
1221 return invalid(
1222 "IndexRoot",
1223 "dictionary data block count is zero while has_dictionary is true",
1224 );
1225 }
1226 if header.dictionary_first_block == 0
1227 || header.dictionary_encrypted_size == 0
1228 || header.dictionary_decompressed_size == 0
1229 {
1230 return invalid("IndexRoot", "required dictionary field is zero");
1231 }
1232 validate_encrypted_extent(
1233 "IndexRoot.dictionary",
1234 header.dictionary_data_block_count,
1235 header.dictionary_encrypted_size,
1236 limits.block_size,
1237 )?;
1238 validate_fec_class_extent(
1239 "IndexRoot.dictionary",
1240 header.dictionary_data_block_count,
1241 header.dictionary_parity_block_count,
1242 limits.max_index_root_data_shards,
1243 limits.max_index_root_parity_shards,
1244 )
1245}
1246
1247fn index_root_dictionary_fields_are_zero(header: &IndexRootHeader) -> bool {
1248 header.dictionary_first_block == 0
1249 && header.dictionary_data_block_count == 0
1250 && header.dictionary_parity_block_count == 0
1251 && header.dictionary_encrypted_size == 0
1252 && header.dictionary_decompressed_size == 0
1253}
1254
1255fn validate_index_shard_tables(
1256 files: &[FileEntry],
1257 frames: &[FrameEntry],
1258 envelopes: &[EnvelopeEntry],
1259 string_pool: &[u8],
1260 locating_shard: &ShardEntry,
1261 limits: MetadataLimits,
1262) -> Result<(Vec<Vec<u8>>, Vec<u64>), FormatError> {
1263 validate_frame_table(frames)?;
1264 validate_envelope_table(envelopes, limits)?;
1265
1266 let frame_by_index = frames
1267 .iter()
1268 .enumerate()
1269 .map(|(idx, frame)| (frame.frame_index, idx))
1270 .collect::<HashMap<_, _>>();
1271 let envelope_by_index = envelopes
1272 .iter()
1273 .enumerate()
1274 .map(|(idx, envelope)| (envelope.envelope_index, idx))
1275 .collect::<HashMap<_, _>>();
1276
1277 let mut paths = Vec::with_capacity(files.len());
1278 let mut starts = Vec::with_capacity(files.len());
1279 let mut required_frames = BTreeSet::new();
1280
1281 for file in files {
1282 if file.flags & !FILE_ENTRY_KNOWN_FLAGS != 0 {
1283 return invalid("FileEntry", "unknown revision-45 flags are non-zero");
1284 }
1285 if file.flags & EXTENDED_METADATA_V1 == 0 {
1286 return invalid("FileEntry", "EXTENDED_METADATA_V1 flag is not set");
1287 }
1288 if file.path_length == 0 {
1289 return invalid("FileEntry", "path length is zero");
1290 }
1291 if file.path_length > limits.max_path_length {
1292 return invalid("FileEntry", "path length exceeds configured maximum");
1293 }
1294 if file.frame_count == 0 {
1295 return invalid("FileEntry", "frame count is zero");
1296 }
1297 if file.tar_member_group_size < 1536 {
1298 return invalid(
1299 "FileEntry",
1300 "tar member group is smaller than the revision-45 minimum",
1301 );
1302 }
1303 if file.path_hash < locating_shard.first_path_hash
1304 || file.path_hash > locating_shard.last_path_hash
1305 {
1306 return invalid(
1307 "FileEntry",
1308 "path hash is outside locating ShardEntry bounds",
1309 );
1310 }
1311
1312 let path = string_slice(
1313 string_pool,
1314 file.path_offset as u64,
1315 file.path_length as u64,
1316 "FileEntry",
1317 )?;
1318 validate_file_path_bytes(path, limits.max_path_length)?;
1319 if hash_prefix(path) != file.path_hash {
1320 return invalid("FileEntry", "path hash does not match string-pool path");
1321 }
1322
1323 let first_frame = frame_for_file(file, &frame_by_index, frames, file.first_frame_index)?;
1324 let tar_member_group_start = first_frame
1325 .tar_stream_offset
1326 .checked_add(file.offset_in_first_frame_plaintext as u64)
1327 .ok_or(FormatError::MetadataArithmeticOverflow {
1328 structure: "FileEntry",
1329 })?;
1330 validate_file_frame_range(file, frames, &frame_by_index)?;
1331 for offset in 0..file.frame_count as u64 {
1332 let index = file.first_frame_index.checked_add(offset).ok_or(
1333 FormatError::MetadataArithmeticOverflow {
1334 structure: "FileEntry",
1335 },
1336 )?;
1337 required_frames.insert(index);
1338 }
1339 paths.push(path.to_vec());
1340 starts.push(tar_member_group_start);
1341 }
1342
1343 validate_file_order(files, &paths, &starts)?;
1344 if required_frames.len() != frames.len()
1345 || frames
1346 .iter()
1347 .any(|frame| !required_frames.contains(&frame.frame_index))
1348 {
1349 return invalid(
1350 "IndexShard",
1351 "FrameEntry table is not the exact set referenced by FileEntry rows",
1352 );
1353 }
1354
1355 let mut required_envelopes = BTreeSet::new();
1356 for frame in frames {
1357 let envelope = envelope_by_index
1358 .get(&frame.envelope_index)
1359 .and_then(|idx| envelopes.get(*idx))
1360 .ok_or(FormatError::InvalidMetadata {
1361 structure: "FrameEntry",
1362 reason: "referenced EnvelopeEntry is missing",
1363 })?;
1364 validate_frame_envelope_binding(frame, envelope)?;
1365 required_envelopes.insert(frame.envelope_index);
1366 }
1367 if required_envelopes.len() != envelopes.len()
1368 || envelopes
1369 .iter()
1370 .any(|entry| !required_envelopes.contains(&entry.envelope_index))
1371 {
1372 return invalid(
1373 "IndexShard",
1374 "EnvelopeEntry table is not the exact set referenced by FrameEntry rows",
1375 );
1376 }
1377 validate_frame_slices_by_envelope(frames, envelopes)?;
1378
1379 if let Some(first) = files.first() {
1380 if first.path_hash != locating_shard.first_path_hash {
1381 return invalid(
1382 "IndexShard",
1383 "first FileEntry hash does not match ShardEntry",
1384 );
1385 }
1386 }
1387 if let Some(last) = files.last() {
1388 if last.path_hash != locating_shard.last_path_hash {
1389 return invalid(
1390 "IndexShard",
1391 "last FileEntry hash does not match ShardEntry",
1392 );
1393 }
1394 }
1395
1396 Ok((paths, starts))
1397}
1398
1399fn validate_frame_table(frames: &[FrameEntry]) -> Result<(), FormatError> {
1400 for frame in frames {
1401 if frame.compressed_size == 0 || frame.decompressed_size == 0 {
1402 return invalid("FrameEntry", "frame sizes must be non-zero");
1403 }
1404 if frame.flags & !FRAME_KNOWN_FLAGS != 0 {
1405 return invalid("FrameEntry", "reserved flag bits are non-zero");
1406 }
1407 }
1408 for pair in frames.windows(2) {
1409 let previous = &pair[0];
1410 let next = &pair[1];
1411 if previous.frame_index >= next.frame_index {
1412 return invalid("IndexShard", "FrameEntry rows are not sorted and unique");
1413 }
1414 let previous_end = previous
1415 .tar_stream_offset
1416 .checked_add(previous.decompressed_size as u64)
1417 .ok_or(FormatError::MetadataArithmeticOverflow {
1418 structure: "FrameEntry",
1419 })?;
1420 if next.frame_index == previous.frame_index + 1 {
1421 if next.tar_stream_offset != previous_end {
1422 return invalid(
1423 "FrameEntry",
1424 "consecutive tar stream offsets are not packed",
1425 );
1426 }
1427 } else if next.tar_stream_offset <= previous_end {
1428 return invalid("FrameEntry", "non-consecutive tar stream offsets overlap");
1429 }
1430 }
1431 Ok(())
1432}
1433
1434fn validate_envelope_table(
1435 envelopes: &[EnvelopeEntry],
1436 limits: MetadataLimits,
1437) -> Result<(), FormatError> {
1438 for envelope in envelopes {
1439 if envelope.frame_count == 0 || envelope.plaintext_size == 0 {
1440 return invalid("EnvelopeEntry", "payload envelope has no frame plaintext");
1441 }
1442 validate_encrypted_extent(
1443 "EnvelopeEntry",
1444 envelope.data_block_count,
1445 envelope.encrypted_size,
1446 limits.block_size,
1447 )?;
1448 validate_fec_class_extent(
1449 "EnvelopeEntry",
1450 envelope.data_block_count,
1451 envelope.parity_block_count,
1452 limits.max_payload_data_shards,
1453 limits.max_payload_parity_shards,
1454 )?;
1455 }
1456 for pair in envelopes.windows(2) {
1457 if pair[0].envelope_index >= pair[1].envelope_index {
1458 return invalid("IndexShard", "EnvelopeEntry rows are not sorted and unique");
1459 }
1460 }
1461 Ok(())
1462}
1463
1464fn validate_file_order(
1465 files: &[FileEntry],
1466 paths: &[Vec<u8>],
1467 starts: &[u64],
1468) -> Result<(), FormatError> {
1469 for idx in 1..files.len() {
1470 let previous_key = (
1471 &files[idx - 1].path_hash,
1472 paths[idx - 1].as_slice(),
1473 starts[idx - 1],
1474 );
1475 let current_key = (&files[idx].path_hash, paths[idx].as_slice(), starts[idx]);
1476 if previous_key >= current_key {
1477 return invalid("IndexShard", "FileEntry rows are not sorted and unique");
1478 }
1479 }
1480 Ok(())
1481}
1482
1483fn validate_file_frame_range(
1484 file: &FileEntry,
1485 frames: &[FrameEntry],
1486 frame_by_index: &HashMap<u64, usize>,
1487) -> Result<(), FormatError> {
1488 let first = frame_for_file(file, frame_by_index, frames, file.first_frame_index)?;
1489 if file.offset_in_first_frame_plaintext >= first.decompressed_size {
1490 return invalid(
1491 "FileEntry",
1492 "offset in first frame is outside the first referenced frame",
1493 );
1494 }
1495
1496 let mut bytes_before_last =
1497 first.decompressed_size as u64 - file.offset_in_first_frame_plaintext as u64;
1498 if file.frame_count == 1 {
1499 if file.tar_member_group_size > bytes_before_last {
1500 return invalid(
1501 "FileEntry",
1502 "tar member group exceeds the single referenced frame",
1503 );
1504 }
1505 return Ok(());
1506 }
1507
1508 for offset in 1..(file.frame_count as u64 - 1) {
1509 let frame_index = file.first_frame_index.checked_add(offset).ok_or(
1510 FormatError::MetadataArithmeticOverflow {
1511 structure: "FileEntry",
1512 },
1513 )?;
1514 let frame = frame_for_file(file, frame_by_index, frames, frame_index)?;
1515 bytes_before_last = bytes_before_last
1516 .checked_add(frame.decompressed_size as u64)
1517 .ok_or(FormatError::MetadataArithmeticOverflow {
1518 structure: "FileEntry",
1519 })?;
1520 }
1521
1522 let last_index = file
1523 .first_frame_index
1524 .checked_add(file.frame_count as u64 - 1)
1525 .ok_or(FormatError::MetadataArithmeticOverflow {
1526 structure: "FileEntry",
1527 })?;
1528 let last = frame_for_file(file, frame_by_index, frames, last_index)?;
1529 let max_size = bytes_before_last
1530 .checked_add(last.decompressed_size as u64)
1531 .ok_or(FormatError::MetadataArithmeticOverflow {
1532 structure: "FileEntry",
1533 })?;
1534 if file.tar_member_group_size <= bytes_before_last || file.tar_member_group_size > max_size {
1535 return invalid("FileEntry", "frame range is not minimal");
1536 }
1537 Ok(())
1538}
1539
1540fn validate_frame_envelope_binding(
1541 frame: &FrameEntry,
1542 envelope: &EnvelopeEntry,
1543) -> Result<(), FormatError> {
1544 let envelope_frame_end = envelope
1545 .first_frame_index
1546 .checked_add(envelope.frame_count as u64)
1547 .ok_or(FormatError::MetadataArithmeticOverflow {
1548 structure: "EnvelopeEntry",
1549 })?;
1550 if frame.frame_index < envelope.first_frame_index || frame.frame_index >= envelope_frame_end {
1551 return invalid("FrameEntry", "frame index is outside envelope frame range");
1552 }
1553 let end = frame
1554 .offset_in_envelope
1555 .checked_add(frame.compressed_size)
1556 .ok_or(FormatError::MetadataArithmeticOverflow {
1557 structure: "FrameEntry",
1558 })?;
1559 if end > envelope.plaintext_size {
1560 return invalid("FrameEntry", "frame slice exceeds envelope plaintext");
1561 }
1562 Ok(())
1563}
1564
1565fn validate_frame_slices_by_envelope(
1566 frames: &[FrameEntry],
1567 envelopes: &[EnvelopeEntry],
1568) -> Result<(), FormatError> {
1569 for envelope in envelopes {
1570 let mut slices = frames
1571 .iter()
1572 .filter(|frame| frame.envelope_index == envelope.envelope_index)
1573 .map(|frame| {
1574 let end = frame
1575 .offset_in_envelope
1576 .checked_add(frame.compressed_size)
1577 .ok_or(FormatError::MetadataArithmeticOverflow {
1578 structure: "FrameEntry",
1579 })?;
1580 Ok((frame.offset_in_envelope, end, frame.frame_index))
1581 })
1582 .collect::<Result<Vec<_>, FormatError>>()?;
1583 slices.sort_unstable_by_key(|slice| (slice.0, slice.2));
1584 for pair in slices.windows(2) {
1585 if pair[0].1 > pair[1].0 {
1586 return invalid("FrameEntry", "frame slices overlap inside an envelope");
1587 }
1588 }
1589
1590 let contains_complete_global_range = (0..envelope.frame_count as u64).all(|offset| {
1591 envelope
1592 .first_frame_index
1593 .checked_add(offset)
1594 .map(|index| slices.iter().any(|slice| slice.2 == index))
1595 .unwrap_or(false)
1596 });
1597 if contains_complete_global_range {
1598 let mut cursor = 0u32;
1599 for (start, end, _) in slices {
1600 if start != cursor {
1601 return invalid("EnvelopeEntry", "complete local envelope has frame gap");
1602 }
1603 cursor = end;
1604 }
1605 if cursor != envelope.plaintext_size {
1606 return invalid(
1607 "EnvelopeEntry",
1608 "complete local envelope does not cover plaintext",
1609 );
1610 }
1611 }
1612 }
1613 Ok(())
1614}
1615
1616fn validate_directory_hint_entries(
1617 entries: &[DirectoryHintEntry],
1618 bytes: &[u8],
1619 header: &DirectoryHintTableHeader,
1620 locating_shard: &DirectoryHintShardEntry,
1621 index_root_shard_count: u32,
1622) -> Result<usize, FormatError> {
1623 let structure = "DirectoryHintTable";
1624 if index_root_shard_count == 0 {
1625 return invalid(structure, "directory hints require IndexRoot shard rows");
1626 }
1627 if entries.is_empty() {
1628 return invalid(structure, "located directory hint table is empty");
1629 }
1630 if entries[0].dir_hash != locating_shard.first_dir_hash {
1631 return invalid(
1632 structure,
1633 "first DirectoryHintEntry hash does not match locating row",
1634 );
1635 }
1636 if entries[entries.len() - 1].dir_hash != locating_shard.last_dir_hash {
1637 return invalid(
1638 structure,
1639 "last DirectoryHintEntry hash does not match locating row",
1640 );
1641 }
1642
1643 let mut max_shard_list_end = 0usize;
1644 for entry in entries {
1645 if entry.shard_count == 0 {
1646 return invalid("DirectoryHintEntry", "shard count is zero");
1647 }
1648 let start = entry.shard_list_start_index as usize;
1649 let end = start.checked_add(entry.shard_count as usize).ok_or(
1650 FormatError::MetadataArithmeticOverflow {
1651 structure: "DirectoryHintEntry",
1652 },
1653 )?;
1654 max_shard_list_end = max_shard_list_end.max(end);
1655 }
1656 let byte_len = checked_mul(max_shard_list_end, 4, structure)?;
1657 let shard_list_offset = to_usize(header.shard_list_offset, structure)?;
1658 let shard_list_end = checked_add(shard_list_offset, byte_len, structure)?;
1659 if shard_list_end > bytes.len() {
1660 return invalid(structure, "shard list exceeds plaintext");
1661 }
1662 Ok(max_shard_list_end)
1663}
1664
1665fn validate_directory_hint_paths_and_lists(
1666 entries: &[DirectoryHintEntry],
1667 shard_row_indexes: &[u32],
1668 string_pool: &[u8],
1669 locating_shard: &DirectoryHintShardEntry,
1670 index_root_shard_count: u32,
1671 max_path_length: u32,
1672) -> Result<Vec<Vec<u8>>, FormatError> {
1673 let mut paths = Vec::with_capacity(entries.len());
1674 let mut seen_paths = HashSet::new();
1675 for entry in entries {
1676 let path = if entry.path_length == 0 {
1677 if entry.path_offset != 0 || entry.dir_hash != hash_prefix(b"") {
1678 return invalid(
1679 "DirectoryHintEntry",
1680 "root directory entry is not canonical",
1681 );
1682 }
1683 &[][..]
1684 } else {
1685 let path = string_slice(
1686 string_pool,
1687 entry.path_offset,
1688 entry.path_length as u64,
1689 "DirectoryHintEntry",
1690 )?;
1691 validate_directory_path_bytes(path, max_path_length)?;
1692 path
1693 };
1694 if hash_prefix(path) != entry.dir_hash {
1695 return invalid(
1696 "DirectoryHintEntry",
1697 "dir_hash does not match string-pool path",
1698 );
1699 }
1700 if !seen_paths.insert(path.to_vec()) {
1701 return invalid("DirectoryHintEntry", "duplicate directory path");
1702 }
1703
1704 let start = entry.shard_list_start_index as usize;
1705 let end = start.checked_add(entry.shard_count as usize).ok_or(
1706 FormatError::MetadataArithmeticOverflow {
1707 structure: "DirectoryHintEntry",
1708 },
1709 )?;
1710 let rows = shard_row_indexes
1711 .get(start..end)
1712 .ok_or(FormatError::InvalidMetadata {
1713 structure: "DirectoryHintEntry",
1714 reason: "shard-row-index range is out of bounds",
1715 })?;
1716 for pair in rows.windows(2) {
1717 if pair[0] >= pair[1] {
1718 return invalid(
1719 "DirectoryHintEntry",
1720 "shard-row-index list is not sorted and unique",
1721 );
1722 }
1723 }
1724 if rows.iter().any(|row| *row >= index_root_shard_count) {
1725 return invalid(
1726 "DirectoryHintEntry",
1727 "shard-row-index is outside IndexRoot shard table",
1728 );
1729 }
1730 paths.push(path.to_vec());
1731 }
1732
1733 for idx in 1..entries.len() {
1734 let previous_key = (&entries[idx - 1].dir_hash, paths[idx - 1].as_slice());
1735 let current_key = (&entries[idx].dir_hash, paths[idx].as_slice());
1736 if previous_key >= current_key {
1737 return invalid(
1738 "DirectoryHintTable",
1739 "DirectoryHintEntry rows are not sorted and unique",
1740 );
1741 }
1742 }
1743 if entries[0].dir_hash != locating_shard.first_dir_hash
1744 || entries[entries.len() - 1].dir_hash != locating_shard.last_dir_hash
1745 {
1746 return invalid(
1747 "DirectoryHintTable",
1748 "entry hash bounds do not match locating shard",
1749 );
1750 }
1751
1752 Ok(paths)
1753}
1754
1755fn candidate_interval_indexes<T>(
1756 entries: &[T],
1757 target_hash: [u8; 8],
1758 scan_cap_per_direction: usize,
1759 first_hash: impl Fn(&T) -> [u8; 8],
1760 last_hash: impl Fn(&T) -> [u8; 8],
1761) -> Result<Vec<usize>, FormatError> {
1762 if entries.is_empty() {
1763 return Ok(Vec::new());
1764 }
1765 let upper = entries.partition_point(|entry| first_hash(entry) <= target_hash);
1766 if upper == 0 {
1767 return Ok(Vec::new());
1768 }
1769 let landing = upper - 1;
1770 if last_hash(&entries[landing]) < target_hash {
1771 return Ok(Vec::new());
1772 }
1773
1774 let mut start = landing;
1775 let mut left_scanned = 0usize;
1776 while start > 0
1777 && first_hash(&entries[start - 1]) <= target_hash
1778 && last_hash(&entries[start - 1]) >= target_hash
1779 {
1780 left_scanned += 1;
1781 if left_scanned > scan_cap_per_direction {
1782 return Err(FormatError::HashPrefixCollisionRunExceeded);
1783 }
1784 start -= 1;
1785 }
1786
1787 let mut end = landing + 1;
1788 let mut right_scanned = 0usize;
1789 while end < entries.len()
1790 && first_hash(&entries[end]) <= target_hash
1791 && last_hash(&entries[end]) >= target_hash
1792 {
1793 right_scanned += 1;
1794 if right_scanned > scan_cap_per_direction {
1795 return Err(FormatError::HashPrefixCollisionRunExceeded);
1796 }
1797 end += 1;
1798 }
1799
1800 Ok((start..end).collect())
1801}
1802
1803pub fn validate_file_path_bytes(path: &[u8], max_path_length: u32) -> Result<(), FormatError> {
1804 if path.is_empty() || path.len() > max_path_length as usize {
1805 return Err(FormatError::UnsafeArchivePath);
1806 }
1807 validate_relative_path(path, false)
1808}
1809
1810pub fn validate_directory_path_bytes(path: &[u8], max_path_length: u32) -> Result<(), FormatError> {
1811 if path.len() > max_path_length as usize {
1812 return Err(FormatError::UnsafeArchivePath);
1813 }
1814 validate_relative_path(path, true)
1815}
1816
1817fn validate_relative_path(path: &[u8], allow_empty_root: bool) -> Result<(), FormatError> {
1818 if path.is_empty() {
1819 return if allow_empty_root {
1820 Ok(())
1821 } else {
1822 Err(FormatError::UnsafeArchivePath)
1823 };
1824 }
1825 if path.contains(&0) || path.contains(&b'\\') || path.contains(&b':') || path[0] == b'/' {
1826 return Err(FormatError::UnsafeArchivePath);
1827 }
1828 let path_str = std::str::from_utf8(path).map_err(|_| FormatError::UnsafeArchivePath)?;
1829 if !path_str.nfc().eq(path_str.chars()) {
1830 return Err(FormatError::UnsafeArchivePath);
1831 }
1832 for component in path_str.split('/') {
1833 if component.is_empty() || component == "." || component == ".." {
1834 return Err(FormatError::UnsafeArchivePath);
1835 }
1836 if is_windows_device_component(component) {
1837 return Err(FormatError::UnsafeArchivePath);
1838 }
1839 }
1840 Ok(())
1841}
1842
1843fn is_windows_device_component(component: &str) -> bool {
1844 let stem = component
1845 .split('.')
1846 .next()
1847 .unwrap_or(component)
1848 .trim_end_matches([' ', '.']);
1849 let upper = stem.to_ascii_uppercase();
1850 matches!(
1851 upper.as_str(),
1852 "CON"
1853 | "PRN"
1854 | "AUX"
1855 | "NUL"
1856 | "CLOCK$"
1857 | "COM1"
1858 | "COM2"
1859 | "COM3"
1860 | "COM4"
1861 | "COM5"
1862 | "COM6"
1863 | "COM7"
1864 | "COM8"
1865 | "COM9"
1866 | "COM\u{00b9}"
1867 | "COM\u{00b2}"
1868 | "COM\u{00b3}"
1869 | "LPT1"
1870 | "LPT2"
1871 | "LPT3"
1872 | "LPT4"
1873 | "LPT5"
1874 | "LPT6"
1875 | "LPT7"
1876 | "LPT8"
1877 | "LPT9"
1878 | "LPT\u{00b9}"
1879 | "LPT\u{00b2}"
1880 | "LPT\u{00b3}"
1881 )
1882}
1883
1884fn validate_encrypted_extent(
1885 structure: &'static str,
1886 data_block_count: u32,
1887 encrypted_size: u32,
1888 block_size: u32,
1889) -> Result<(), FormatError> {
1890 if data_block_count == 0 || encrypted_size == 0 {
1891 return invalid(structure, "encrypted object has zero data blocks or size");
1892 }
1893 let expected = (data_block_count as u64)
1894 .checked_mul(block_size as u64)
1895 .ok_or(FormatError::MetadataArithmeticOverflow { structure })?;
1896 if expected > u32::MAX as u64 || expected != encrypted_size as u64 {
1897 return invalid(
1898 structure,
1899 "encrypted_size is not data_block_count * block_size",
1900 );
1901 }
1902 Ok(())
1903}
1904
1905fn validate_fec_class_extent(
1906 structure: &'static str,
1907 data_block_count: u32,
1908 parity_block_count: u32,
1909 data_shard_max: u16,
1910 parity_shard_max: u16,
1911) -> Result<(), FormatError> {
1912 if data_block_count > data_shard_max as u32 {
1913 return invalid(structure, "data_block_count exceeds class maximum");
1914 }
1915 if parity_block_count > parity_shard_max as u32 {
1916 return invalid(structure, "parity_block_count exceeds class maximum");
1917 }
1918 let total = data_block_count as u64 + parity_block_count as u64;
1919 if total > REED_SOLOMON_GF16_MAX_TOTAL_SHARDS {
1920 return invalid(
1921 structure,
1922 "data_block_count + parity_block_count exceeds ReedSolomonGF16 limit",
1923 );
1924 }
1925 Ok(())
1926}
1927
1928fn frame_for_file<'a>(
1929 _file: &FileEntry,
1930 frame_by_index: &HashMap<u64, usize>,
1931 frames: &'a [FrameEntry],
1932 frame_index: u64,
1933) -> Result<&'a FrameEntry, FormatError> {
1934 frame_by_index
1935 .get(&frame_index)
1936 .and_then(|idx| frames.get(*idx))
1937 .ok_or(FormatError::InvalidMetadata {
1938 structure: "FileEntry",
1939 reason: "referenced FrameEntry is missing",
1940 })
1941}
1942
1943struct CountedTableSpec<T> {
1944 structure: &'static str,
1945 name: &'static str,
1946 count: u64,
1947 offset: u64,
1948 entry_len: usize,
1949 parse: fn(&[u8]) -> Result<T, FormatError>,
1950}
1951
1952fn parse_counted_table<T>(
1953 bytes: &[u8],
1954 spec: CountedTableSpec<T>,
1955 cursor: &mut usize,
1956) -> Result<Vec<T>, FormatError> {
1957 if spec.count == 0 {
1958 if spec.offset != 0 {
1959 return invalid(spec.structure, "absent counted table has non-zero offset");
1960 }
1961 return Ok(Vec::new());
1962 }
1963 expect_offset(spec.structure, spec.name, spec.offset, *cursor)?;
1964 let count = to_usize(spec.count, spec.structure)?;
1965 let bytes_len = checked_mul(count, spec.entry_len, spec.structure)?;
1966 let table = slice(bytes, *cursor, bytes_len, spec.structure)?;
1967 *cursor = checked_add(*cursor, bytes_len, spec.structure)?;
1968 table.chunks_exact(spec.entry_len).map(spec.parse).collect()
1969}
1970
1971fn parse_u32_array(bytes: &[u8], structure: &'static str) -> Result<Vec<u32>, FormatError> {
1972 let mut out = Vec::with_capacity(bytes.len() / 4);
1973 for chunk in bytes.chunks_exact(4) {
1974 out.push(read_u32(chunk, 0, structure)?);
1975 }
1976 Ok(out)
1977}
1978
1979fn string_slice<'a>(
1980 string_pool: &'a [u8],
1981 offset: u64,
1982 length: u64,
1983 structure: &'static str,
1984) -> Result<&'a [u8], FormatError> {
1985 let start = to_usize(offset, structure)?;
1986 let len = to_usize(length, structure)?;
1987 slice(string_pool, start, len, structure)
1988}
1989
1990fn shard_entry_sort_key(entry: &ShardEntry) -> ([u8; 8], [u8; 8], u64) {
1991 (
1992 entry.first_path_hash,
1993 entry.last_path_hash,
1994 entry.shard_index,
1995 )
1996}
1997
1998fn directory_hint_shard_sort_key(entry: &DirectoryHintShardEntry) -> ([u8; 8], [u8; 8], u64) {
1999 (
2000 entry.first_dir_hash,
2001 entry.last_dir_hash,
2002 entry.hint_shard_index,
2003 )
2004}
2005
2006fn table_offset(len: usize, cursor: usize) -> u32 {
2007 if len == 0 {
2008 0
2009 } else {
2010 cursor as u32
2011 }
2012}
2013
2014fn expect_magic(
2015 structure: &'static str,
2016 expected: [u8; 4],
2017 actual: [u8; 4],
2018) -> Result<(), FormatError> {
2019 if actual != expected {
2020 return Err(FormatError::BadMagic { structure });
2021 }
2022 Ok(())
2023}
2024
2025fn expect_zero(structure: &'static str, bytes: &[u8]) -> Result<(), FormatError> {
2026 if bytes.iter().any(|byte| *byte != 0) {
2027 return Err(FormatError::NonZeroReserved { structure });
2028 }
2029 Ok(())
2030}
2031
2032fn expect_offset(
2033 structure: &'static str,
2034 name: &'static str,
2035 actual: u64,
2036 expected: usize,
2037) -> Result<(), FormatError> {
2038 if actual != expected as u64 {
2039 return Err(FormatError::InvalidMetadata {
2040 structure,
2041 reason: name,
2042 });
2043 }
2044 Ok(())
2045}
2046
2047fn slice<'a>(
2048 bytes: &'a [u8],
2049 offset: usize,
2050 len: usize,
2051 structure: &'static str,
2052) -> Result<&'a [u8], FormatError> {
2053 let end = checked_add(offset, len, structure)?;
2054 bytes.get(offset..end).ok_or(FormatError::InvalidMetadata {
2055 structure,
2056 reason: "range is out of bounds",
2057 })
2058}
2059
2060fn read_array<const N: usize>(
2061 bytes: &[u8],
2062 offset: usize,
2063 structure: &'static str,
2064) -> Result<[u8; N], FormatError> {
2065 let mut out = [0u8; N];
2066 out.copy_from_slice(slice(bytes, offset, N, structure)?);
2067 Ok(out)
2068}
2069
2070fn read_u32(bytes: &[u8], offset: usize, structure: &'static str) -> Result<u32, FormatError> {
2071 let raw = read_array::<4>(bytes, offset, structure)?;
2072 Ok(u32::from_le_bytes(raw))
2073}
2074
2075fn read_u64(bytes: &[u8], offset: usize, structure: &'static str) -> Result<u64, FormatError> {
2076 let raw = read_array::<8>(bytes, offset, structure)?;
2077 Ok(u64::from_le_bytes(raw))
2078}
2079
2080fn write_u32(bytes: &mut [u8], offset: usize, value: u32) {
2081 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
2082}
2083
2084fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
2085 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
2086}
2087
2088fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
2089 lhs.checked_add(rhs)
2090 .ok_or(FormatError::MetadataArithmeticOverflow { structure })
2091}
2092
2093fn checked_mul(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
2094 lhs.checked_mul(rhs)
2095 .ok_or(FormatError::MetadataArithmeticOverflow { structure })
2096}
2097
2098fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
2099 usize::try_from(value).map_err(|_| FormatError::MetadataArithmeticOverflow { structure })
2100}
2101
2102fn invalid<T>(structure: &'static str, reason: &'static str) -> Result<T, FormatError> {
2103 Err(FormatError::InvalidMetadata { structure, reason })
2104}
2105
2106#[cfg(test)]
2107mod tests {
2108 use super::*;
2109
2110 #[test]
2111 fn default_reader_caps_match_v41() {
2112 let limits = MetadataLimits::default();
2113 assert_eq!(limits.max_shard_count, 1_000_000);
2114 assert_eq!(limits.max_directory_hint_shards, 1_000_000);
2115 assert_eq!(limits.max_files_per_index_shard, 1_000_000);
2116 assert_eq!(limits.max_entries_per_directory_hint_shard, 1_000_000);
2117 assert_eq!(limits.max_hash_collision_shard_scan, 16);
2118 }
2119
2120 #[test]
2121 fn index_root_rejects_shard_extent_above_crypto_header_class_limits() {
2122 let path_hash = hash_prefix(b"a.txt");
2123 let root = IndexRoot {
2124 header: IndexRootHeader {
2125 file_count: 1,
2126 ..IndexRootHeader::empty()
2127 },
2128 shards: vec![ShardEntry {
2129 shard_index: 0,
2130 first_block_index: 1,
2131 data_block_count: 1,
2132 parity_block_count: 2,
2133 encrypted_size: 4096,
2134 decompressed_size: 64,
2135 file_count: 1,
2136 first_path_hash: path_hash,
2137 last_path_hash: path_hash,
2138 }],
2139 directory_hint_shards: Vec::new(),
2140 };
2141 let limits = MetadataLimits {
2142 max_index_parity_shards: 1,
2143 ..MetadataLimits::default()
2144 };
2145
2146 assert_eq!(
2147 IndexRoot::parse(&root.to_bytes(), false, limits).unwrap_err(),
2148 FormatError::InvalidMetadata {
2149 structure: "ShardEntry",
2150 reason: "parity_block_count exceeds class maximum",
2151 }
2152 );
2153 }
2154
2155 #[test]
2156 fn metadata_fec_extent_rejects_reed_solomon_total_overflow() {
2157 assert_eq!(
2158 validate_fec_class_extent("EnvelopeEntry", 65_535, 1, u16::MAX, u16::MAX).unwrap_err(),
2159 FormatError::InvalidMetadata {
2160 structure: "EnvelopeEntry",
2161 reason: "data_block_count + parity_block_count exceeds ReedSolomonGF16 limit",
2162 }
2163 );
2164 }
2165
2166 #[test]
2167 fn parses_valid_empty_index_root() {
2168 let root = IndexRoot {
2169 header: IndexRootHeader::empty(),
2170 shards: Vec::new(),
2171 directory_hint_shards: Vec::new(),
2172 };
2173
2174 let bytes = root.to_bytes();
2175 let parsed = IndexRoot::parse(&bytes, false, MetadataLimits::default()).unwrap();
2176
2177 assert_eq!(parsed.header.file_count, 0);
2178 assert!(parsed.shards.is_empty());
2179 assert!(parsed.directory_hint_shards.is_empty());
2180 }
2181
2182 #[test]
2183 fn index_root_rejects_nonzero_offsets_for_absent_counted_tables() {
2184 let mut root = IndexRoot {
2185 header: IndexRootHeader::empty(),
2186 shards: Vec::new(),
2187 directory_hint_shards: Vec::new(),
2188 };
2189
2190 let mut bytes = root.to_bytes();
2191 write_u64(&mut bytes, 88, INDEX_ROOT_LEN as u64);
2192 assert_eq!(
2193 IndexRoot::parse(&bytes, false, MetadataLimits::default()).unwrap_err(),
2194 FormatError::InvalidMetadata {
2195 structure: "IndexRoot",
2196 reason: "absent shard table has non-zero offset",
2197 }
2198 );
2199
2200 root.header.file_count = 1;
2201 root.shards.push(ShardEntry {
2202 shard_index: 0,
2203 first_block_index: 1,
2204 data_block_count: 1,
2205 parity_block_count: 0,
2206 encrypted_size: 4096,
2207 decompressed_size: 128,
2208 file_count: 1,
2209 first_path_hash: hash_prefix(b"a.txt"),
2210 last_path_hash: hash_prefix(b"a.txt"),
2211 });
2212 let mut bytes = root.to_bytes();
2213 write_u64(&mut bytes, 96, (INDEX_ROOT_LEN + SHARD_ENTRY_LEN) as u64);
2214 assert_eq!(
2215 IndexRoot::parse(&bytes, false, MetadataLimits::default()).unwrap_err(),
2216 FormatError::InvalidMetadata {
2217 structure: "IndexRoot",
2218 reason: "absent directory hint shard table has non-zero offset",
2219 }
2220 );
2221 }
2222
2223 #[test]
2224 fn index_root_rejects_has_dictionary_with_zero_dictionary_fields() {
2225 let root = IndexRoot {
2226 header: IndexRootHeader::empty(),
2227 shards: Vec::new(),
2228 directory_hint_shards: Vec::new(),
2229 };
2230
2231 assert_eq!(
2232 IndexRoot::parse(&root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
2233 FormatError::InvalidMetadata {
2234 structure: "IndexRoot",
2235 reason: "dictionary data block count is zero while has_dictionary is true",
2236 }
2237 );
2238 }
2239
2240 #[test]
2241 fn index_root_rejects_empty_archive_with_dictionary_extent() {
2242 let root = IndexRoot {
2243 header: IndexRootHeader {
2244 dictionary_first_block: 1,
2245 dictionary_data_block_count: 1,
2246 dictionary_encrypted_size: 4096,
2247 dictionary_decompressed_size: 16,
2248 ..IndexRootHeader::empty()
2249 },
2250 shards: Vec::new(),
2251 directory_hint_shards: Vec::new(),
2252 };
2253
2254 assert_eq!(
2255 IndexRoot::parse(&root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
2256 FormatError::InvalidMetadata {
2257 structure: "IndexRoot",
2258 reason: "empty archive cannot use dictionary",
2259 }
2260 );
2261 }
2262
2263 #[test]
2264 fn encrypted_object_extents_reject_zero_data_or_size_for_all_metadata_rows() {
2265 assert_eq!(
2266 validate_encrypted_extent("ManifestFooter.IndexRoot", 0, 4096, 4096).unwrap_err(),
2267 FormatError::InvalidMetadata {
2268 structure: "ManifestFooter.IndexRoot",
2269 reason: "encrypted object has zero data blocks or size",
2270 }
2271 );
2272 assert_eq!(
2273 validate_encrypted_extent("EnvelopeEntry", 1, 0, 4096).unwrap_err(),
2274 FormatError::InvalidMetadata {
2275 structure: "EnvelopeEntry",
2276 reason: "encrypted object has zero data blocks or size",
2277 }
2278 );
2279
2280 let path_hash = hash_prefix(b"a.txt");
2281 let mut root = IndexRoot {
2282 header: IndexRootHeader {
2283 file_count: 1,
2284 ..IndexRootHeader::empty()
2285 },
2286 shards: vec![ShardEntry {
2287 shard_index: 0,
2288 first_block_index: 1,
2289 data_block_count: 0,
2290 parity_block_count: 0,
2291 encrypted_size: 4096,
2292 decompressed_size: 128,
2293 file_count: 1,
2294 first_path_hash: path_hash,
2295 last_path_hash: path_hash,
2296 }],
2297 directory_hint_shards: Vec::new(),
2298 };
2299 assert_eq!(
2300 IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
2301 FormatError::InvalidMetadata {
2302 structure: "ShardEntry",
2303 reason: "encrypted object has zero data blocks or size",
2304 }
2305 );
2306
2307 root.shards[0].data_block_count = 1;
2308 root.shards[0].encrypted_size = 4096;
2309 root.directory_hint_shards.push(DirectoryHintShardEntry {
2310 hint_shard_index: 0,
2311 first_dir_hash: hash_prefix(b""),
2312 last_dir_hash: hash_prefix(b""),
2313 first_block_index: 2,
2314 data_block_count: 1,
2315 parity_block_count: 0,
2316 encrypted_size: 0,
2317 decompressed_size: 72,
2318 entry_count: 1,
2319 });
2320 assert_eq!(
2321 IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
2322 FormatError::InvalidMetadata {
2323 structure: "DirectoryHintShardEntry",
2324 reason: "encrypted object has zero data blocks or size",
2325 }
2326 );
2327
2328 let mut dict_root = IndexRoot {
2329 header: IndexRootHeader {
2330 file_count: 1,
2331 dictionary_first_block: 10,
2332 dictionary_data_block_count: 0,
2333 dictionary_parity_block_count: 0,
2334 dictionary_encrypted_size: 4096,
2335 dictionary_decompressed_size: 32,
2336 ..IndexRootHeader::empty()
2337 },
2338 shards: vec![ShardEntry {
2339 shard_index: 0,
2340 first_block_index: 1,
2341 data_block_count: 1,
2342 parity_block_count: 0,
2343 encrypted_size: 4096,
2344 decompressed_size: 128,
2345 file_count: 1,
2346 first_path_hash: path_hash,
2347 last_path_hash: path_hash,
2348 }],
2349 directory_hint_shards: Vec::new(),
2350 };
2351 assert_eq!(
2352 IndexRoot::parse(&dict_root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
2353 FormatError::InvalidMetadata {
2354 structure: "IndexRoot",
2355 reason: "dictionary data block count is zero while has_dictionary is true",
2356 }
2357 );
2358 dict_root.header.dictionary_data_block_count = 1;
2359 dict_root.header.dictionary_encrypted_size = 0;
2360 assert_eq!(
2361 IndexRoot::parse(&dict_root.to_bytes(), true, MetadataLimits::default()).unwrap_err(),
2362 FormatError::InvalidMetadata {
2363 structure: "IndexRoot",
2364 reason: "required dictionary field is zero",
2365 }
2366 );
2367 }
2368
2369 #[test]
2370 fn index_root_rejects_dictionary_fields_when_crypto_header_has_no_dictionary() {
2371 let mut root = IndexRoot {
2372 header: IndexRootHeader::empty(),
2373 shards: Vec::new(),
2374 directory_hint_shards: Vec::new(),
2375 };
2376 root.header.dictionary_first_block = 1;
2377 root.header.dictionary_data_block_count = 1;
2378 root.header.dictionary_encrypted_size = 4096;
2379 root.header.dictionary_decompressed_size = 16;
2380
2381 assert_eq!(
2382 IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
2383 FormatError::InvalidMetadata {
2384 structure: "IndexRoot",
2385 reason: "dictionary fields are non-zero while has_dictionary is false",
2386 }
2387 );
2388 }
2389
2390 #[test]
2391 fn rejects_directory_hint_rows_sorted_by_old_v36_key_only() {
2392 let h = [0x10; 8];
2393 let z = [0x20; 8];
2394 let root = IndexRoot {
2395 header: IndexRootHeader {
2396 file_count: 1,
2397 ..IndexRootHeader::empty()
2398 },
2399 shards: vec![ShardEntry {
2400 shard_index: 0,
2401 first_block_index: 0,
2402 data_block_count: 1,
2403 parity_block_count: 1,
2404 encrypted_size: 4096,
2405 decompressed_size: 64,
2406 file_count: 1,
2407 first_path_hash: h,
2408 last_path_hash: z,
2409 }],
2410 directory_hint_shards: vec![
2411 DirectoryHintShardEntry {
2412 hint_shard_index: 0,
2413 first_dir_hash: h,
2414 last_dir_hash: z,
2415 first_block_index: 10,
2416 data_block_count: 1,
2417 parity_block_count: 1,
2418 encrypted_size: 4096,
2419 decompressed_size: 72,
2420 entry_count: 1,
2421 },
2422 DirectoryHintShardEntry {
2423 hint_shard_index: 1,
2424 first_dir_hash: h,
2425 last_dir_hash: h,
2426 first_block_index: 12,
2427 data_block_count: 1,
2428 parity_block_count: 1,
2429 encrypted_size: 4096,
2430 decompressed_size: 72,
2431 entry_count: 1,
2432 },
2433 ],
2434 };
2435
2436 assert_eq!(
2437 IndexRoot::parse(&root.to_bytes(), false, MetadataLimits::default()).unwrap_err(),
2438 FormatError::InvalidMetadata {
2439 structure: "IndexRoot",
2440 reason: "DirectoryHintShardEntry rows are not sorted"
2441 }
2442 );
2443 }
2444
2445 #[test]
2446 fn directory_hint_shard_count_cap_is_independent_from_index_shard_cap() {
2447 let path_hash = hash_prefix(b"a.txt");
2448 let dir_hash = hash_prefix(b"");
2449 let root = IndexRoot {
2450 header: IndexRootHeader {
2451 file_count: 1,
2452 ..IndexRootHeader::empty()
2453 },
2454 shards: vec![ShardEntry {
2455 shard_index: 0,
2456 first_block_index: 1,
2457 data_block_count: 1,
2458 parity_block_count: 0,
2459 encrypted_size: 4096,
2460 decompressed_size: 128,
2461 file_count: 1,
2462 first_path_hash: path_hash,
2463 last_path_hash: path_hash,
2464 }],
2465 directory_hint_shards: vec![DirectoryHintShardEntry {
2466 hint_shard_index: 0,
2467 first_dir_hash: dir_hash,
2468 last_dir_hash: dir_hash,
2469 first_block_index: 2,
2470 data_block_count: 1,
2471 parity_block_count: 0,
2472 encrypted_size: 4096,
2473 decompressed_size: 72,
2474 entry_count: 1,
2475 }],
2476 };
2477 let limits = MetadataLimits {
2478 max_shard_count: 1,
2479 max_directory_hint_shards: 1,
2480 ..MetadataLimits::default()
2481 };
2482 IndexRoot::parse(&root.to_bytes(), false, limits).unwrap();
2483
2484 let rejecting_limits = MetadataLimits {
2485 max_directory_hint_shards: 0,
2486 ..limits
2487 };
2488 assert_eq!(
2489 IndexRoot::parse(&root.to_bytes(), false, rejecting_limits).unwrap_err(),
2490 FormatError::InvalidMetadata {
2491 structure: "IndexRoot",
2492 reason: "directory hint shard count exceeds resource cap",
2493 }
2494 );
2495 }
2496
2497 #[test]
2498 fn directory_hint_paths_obey_configured_max_path_length() {
2499 let path = b"toolong".to_vec();
2500 let table = DirectoryHintTable {
2501 header: DirectoryHintTableHeader {
2502 version: 1,
2503 hint_shard_index: 0,
2504 entry_count: 0,
2505 entry_table_offset: 0,
2506 shard_list_offset: 0,
2507 string_pool_offset: 0,
2508 string_pool_size: 0,
2509 },
2510 entries: vec![DirectoryHintEntry {
2511 dir_hash: hash_prefix(&path),
2512 path_offset: 0,
2513 path_length: path.len() as u32,
2514 shard_list_start_index: 0,
2515 shard_count: 1,
2516 }],
2517 shard_row_indexes: vec![0],
2518 string_pool: path.clone(),
2519 entry_paths: vec![path.clone()],
2520 };
2521 let bytes = table.to_bytes();
2522 let locating = DirectoryHintShardEntry {
2523 hint_shard_index: 0,
2524 first_dir_hash: hash_prefix(&path),
2525 last_dir_hash: hash_prefix(&path),
2526 first_block_index: 0,
2527 data_block_count: 1,
2528 parity_block_count: 0,
2529 encrypted_size: 4096,
2530 decompressed_size: bytes.len() as u32,
2531 entry_count: 1,
2532 };
2533 let limits = MetadataLimits {
2534 max_path_length: 3,
2535 ..MetadataLimits::default()
2536 };
2537
2538 assert_eq!(
2539 DirectoryHintTable::parse(&bytes, &locating, 1, limits).unwrap_err(),
2540 FormatError::UnsafeArchivePath
2541 );
2542 }
2543
2544 #[test]
2545 fn directory_hint_table_rejects_wrong_hint_shard_identity() {
2546 let path = b"dir".to_vec();
2547 let table = DirectoryHintTable {
2548 header: DirectoryHintTableHeader {
2549 version: 1,
2550 hint_shard_index: 5,
2551 entry_count: 0,
2552 entry_table_offset: 0,
2553 shard_list_offset: 0,
2554 string_pool_offset: 0,
2555 string_pool_size: 0,
2556 },
2557 entries: vec![DirectoryHintEntry {
2558 dir_hash: hash_prefix(&path),
2559 path_offset: 0,
2560 path_length: path.len() as u32,
2561 shard_list_start_index: 0,
2562 shard_count: 1,
2563 }],
2564 shard_row_indexes: vec![0],
2565 string_pool: path.clone(),
2566 entry_paths: vec![path.clone()],
2567 };
2568 let bytes = table.to_bytes();
2569 let locating = DirectoryHintShardEntry {
2570 hint_shard_index: 6,
2571 first_dir_hash: hash_prefix(&path),
2572 last_dir_hash: hash_prefix(&path),
2573 first_block_index: 0,
2574 data_block_count: 1,
2575 parity_block_count: 0,
2576 encrypted_size: 4096,
2577 decompressed_size: bytes.len() as u32,
2578 entry_count: 1,
2579 };
2580
2581 assert_eq!(
2582 DirectoryHintTable::parse(&bytes, &locating, 1, MetadataLimits::default()).unwrap_err(),
2583 FormatError::InvalidMetadata {
2584 structure: "DirectoryHintTable",
2585 reason: "hint shard index does not match locating DirectoryHintShardEntry",
2586 }
2587 );
2588 }
2589
2590 #[test]
2591 fn directory_hint_table_rejects_empty_shard_lists() {
2592 let path = b"dir".to_vec();
2593 let table = DirectoryHintTable {
2594 header: DirectoryHintTableHeader {
2595 version: 1,
2596 hint_shard_index: 0,
2597 entry_count: 0,
2598 entry_table_offset: 0,
2599 shard_list_offset: 0,
2600 string_pool_offset: 0,
2601 string_pool_size: 0,
2602 },
2603 entries: vec![DirectoryHintEntry {
2604 dir_hash: hash_prefix(&path),
2605 path_offset: 0,
2606 path_length: path.len() as u32,
2607 shard_list_start_index: 0,
2608 shard_count: 0,
2609 }],
2610 shard_row_indexes: Vec::new(),
2611 string_pool: path.clone(),
2612 entry_paths: vec![path.clone()],
2613 };
2614 let bytes = table.to_bytes();
2615 let locating = DirectoryHintShardEntry {
2616 hint_shard_index: 0,
2617 first_dir_hash: hash_prefix(&path),
2618 last_dir_hash: hash_prefix(&path),
2619 first_block_index: 0,
2620 data_block_count: 1,
2621 parity_block_count: 0,
2622 encrypted_size: 4096,
2623 decompressed_size: bytes.len() as u32,
2624 entry_count: 1,
2625 };
2626
2627 assert_eq!(
2628 DirectoryHintTable::parse(&bytes, &locating, 1, MetadataLimits::default()).unwrap_err(),
2629 FormatError::InvalidMetadata {
2630 structure: "DirectoryHintEntry",
2631 reason: "shard count is zero",
2632 }
2633 );
2634 }
2635
2636 #[test]
2637 fn index_shard_rejects_unsupported_version_and_zero_count_pointer_offsets() {
2638 let path = b"file.txt";
2639 let path_hash = hash_prefix(path);
2640 let file = FileEntry {
2641 path_hash,
2642 path_offset: 0,
2643 path_length: path.len() as u32,
2644 first_frame_index: 0,
2645 frame_count: 1,
2646 offset_in_first_frame_plaintext: 0,
2647 tar_member_group_size: 1536,
2648 file_data_size: 0,
2649 flags: EXTENDED_METADATA_V1,
2650 };
2651 let frame = FrameEntry {
2652 frame_index: 0,
2653 envelope_index: 0,
2654 offset_in_envelope: 0,
2655 compressed_size: 128,
2656 decompressed_size: 1536,
2657 flags: 0,
2658 tar_stream_offset: 0,
2659 };
2660 let envelope = EnvelopeEntry {
2661 envelope_index: 0,
2662 first_block_index: 0,
2663 data_block_count: 1,
2664 parity_block_count: 0,
2665 encrypted_size: 4096,
2666 plaintext_size: 128,
2667 first_frame_index: 0,
2668 frame_count: 1,
2669 };
2670 let shard = IndexShard {
2671 header: IndexShardHeader {
2672 version: 1,
2673 shard_index: 7,
2674 file_count: 0,
2675 frame_count: 0,
2676 envelope_count: 0,
2677 file_table_offset: 0,
2678 frame_table_offset: 0,
2679 envelope_table_offset: 0,
2680 string_pool_offset: 0,
2681 string_pool_size: 0,
2682 },
2683 files: vec![file],
2684 frames: vec![frame],
2685 envelopes: vec![envelope],
2686 string_pool: path.to_vec(),
2687 file_paths: Vec::new(),
2688 file_tar_member_group_starts: Vec::new(),
2689 };
2690 let locating = ShardEntry {
2691 shard_index: 7,
2692 first_block_index: 10,
2693 data_block_count: 1,
2694 parity_block_count: 0,
2695 encrypted_size: 4096,
2696 decompressed_size: shard.to_bytes().len() as u32,
2697 file_count: 1,
2698 first_path_hash: path_hash,
2699 last_path_hash: path_hash,
2700 };
2701
2702 let mut unsupported_version = shard.to_bytes();
2703 write_u32(&mut unsupported_version, 4, 3);
2704 assert_eq!(
2705 IndexShard::parse(&unsupported_version, &locating, MetadataLimits::default())
2706 .unwrap_err(),
2707 FormatError::InvalidMetadata {
2708 structure: "IndexShard",
2709 reason: "unsupported version",
2710 }
2711 );
2712
2713 let mut nonzero_zero_frame_table = shard.to_bytes();
2714 write_u32(&mut nonzero_zero_frame_table, 20, 0);
2715 write_u32(
2716 &mut nonzero_zero_frame_table,
2717 32,
2718 INDEX_SHARD_HEADER_LEN as u32,
2719 );
2720 assert_eq!(
2721 IndexShard::parse(
2722 &nonzero_zero_frame_table,
2723 &locating,
2724 MetadataLimits::default()
2725 )
2726 .unwrap_err(),
2727 FormatError::InvalidMetadata {
2728 structure: "IndexShard",
2729 reason: "absent counted table has non-zero offset",
2730 }
2731 );
2732
2733 let mut nonzero_zero_envelope_table = shard.to_bytes();
2734 write_u32(&mut nonzero_zero_envelope_table, 24, 0);
2735 write_u32(
2736 &mut nonzero_zero_envelope_table,
2737 36,
2738 (INDEX_SHARD_HEADER_LEN + FILE_ENTRY_LEN + FRAME_ENTRY_LEN) as u32,
2739 );
2740 assert_eq!(
2741 IndexShard::parse(
2742 &nonzero_zero_envelope_table,
2743 &locating,
2744 MetadataLimits::default()
2745 )
2746 .unwrap_err(),
2747 FormatError::InvalidMetadata {
2748 structure: "IndexShard",
2749 reason: "absent counted table has non-zero offset",
2750 }
2751 );
2752 }
2753
2754 #[test]
2755 fn directory_hint_table_rejects_zero_count_nonzero_offsets() {
2756 let path = b"dir".to_vec();
2757 let table = DirectoryHintTable {
2758 header: DirectoryHintTableHeader {
2759 version: 1,
2760 hint_shard_index: 5,
2761 entry_count: 0,
2762 entry_table_offset: 0,
2763 shard_list_offset: 0,
2764 string_pool_offset: 0,
2765 string_pool_size: 0,
2766 },
2767 entries: vec![DirectoryHintEntry {
2768 dir_hash: hash_prefix(&path),
2769 path_offset: 0,
2770 path_length: path.len() as u32,
2771 shard_list_start_index: 0,
2772 shard_count: 1,
2773 }],
2774 shard_row_indexes: vec![0],
2775 string_pool: path.clone(),
2776 entry_paths: vec![path.clone()],
2777 };
2778 let locating = DirectoryHintShardEntry {
2779 hint_shard_index: 5,
2780 first_dir_hash: hash_prefix(&path),
2781 last_dir_hash: hash_prefix(&path),
2782 first_block_index: 0,
2783 data_block_count: 1,
2784 parity_block_count: 0,
2785 encrypted_size: 4096,
2786 decompressed_size: table.to_bytes().len() as u32,
2787 entry_count: 1,
2788 };
2789 let mut bytes = table.to_bytes();
2790 let bytes_len = bytes.len() as u64;
2791 write_u64(&mut bytes, 48, 0);
2792 write_u64(&mut bytes, 40, bytes_len);
2793
2794 assert_eq!(
2795 DirectoryHintTable::parse(&bytes, &locating, 1, MetadataLimits::default()).unwrap_err(),
2796 FormatError::InvalidMetadata {
2797 structure: "DirectoryHintTable",
2798 reason: "absent string pool has non-zero offset",
2799 }
2800 );
2801 }
2802
2803 #[test]
2804 fn index_shard_rejects_non_exact_local_frame_and_envelope_tables() {
2805 let path = b"exact-local.txt";
2806 let path_hash = hash_prefix(path);
2807 let file = FileEntry {
2808 path_hash,
2809 path_offset: 0,
2810 path_length: path.len() as u32,
2811 first_frame_index: 0,
2812 frame_count: 1,
2813 offset_in_first_frame_plaintext: 0,
2814 tar_member_group_size: 1536,
2815 file_data_size: 0,
2816 flags: EXTENDED_METADATA_V1,
2817 };
2818 let frame = FrameEntry {
2819 frame_index: 0,
2820 envelope_index: 0,
2821 offset_in_envelope: 0,
2822 compressed_size: 128,
2823 decompressed_size: 1536,
2824 flags: 0,
2825 tar_stream_offset: 0,
2826 };
2827 let envelope = EnvelopeEntry {
2828 envelope_index: 0,
2829 first_block_index: 10,
2830 data_block_count: 1,
2831 parity_block_count: 0,
2832 encrypted_size: 4096,
2833 plaintext_size: 128,
2834 first_frame_index: 0,
2835 frame_count: 1,
2836 };
2837 let shard = IndexShard {
2838 header: IndexShardHeader {
2839 version: 1,
2840 shard_index: 3,
2841 file_count: 0,
2842 frame_count: 0,
2843 envelope_count: 0,
2844 file_table_offset: 0,
2845 frame_table_offset: 0,
2846 envelope_table_offset: 0,
2847 string_pool_offset: 0,
2848 string_pool_size: 0,
2849 },
2850 files: vec![file.clone()],
2851 frames: vec![frame.clone()],
2852 envelopes: vec![envelope.clone()],
2853 string_pool: path.to_vec(),
2854 file_paths: Vec::new(),
2855 file_tar_member_group_starts: Vec::new(),
2856 };
2857 let locating = ShardEntry {
2858 shard_index: 3,
2859 first_block_index: 20,
2860 data_block_count: 1,
2861 parity_block_count: 0,
2862 encrypted_size: 4096,
2863 decompressed_size: shard.to_bytes().len() as u32,
2864 file_count: 1,
2865 first_path_hash: path_hash,
2866 last_path_hash: path_hash,
2867 };
2868 IndexShard::parse(&shard.to_bytes(), &locating, MetadataLimits::default()).unwrap();
2869
2870 let parse_with = |frames: Vec<FrameEntry>, envelopes: Vec<EnvelopeEntry>| {
2871 let mut mutated = shard.clone();
2872 mutated.frames = frames;
2873 mutated.envelopes = envelopes;
2874 let bytes = mutated.to_bytes();
2875 let locating = ShardEntry {
2876 decompressed_size: bytes.len() as u32,
2877 ..locating.clone()
2878 };
2879 IndexShard::parse(&bytes, &locating, MetadataLimits::default()).unwrap_err()
2880 };
2881
2882 let mut missing_frame = frame.clone();
2883 missing_frame.frame_index = 1;
2884 assert_eq!(
2885 parse_with(vec![missing_frame], vec![envelope.clone()]),
2886 FormatError::InvalidMetadata {
2887 structure: "FileEntry",
2888 reason: "referenced FrameEntry is missing",
2889 }
2890 );
2891
2892 let mut unreferenced_frame = frame.clone();
2893 unreferenced_frame.frame_index = 9;
2894 unreferenced_frame.tar_stream_offset = 3072;
2895 assert_eq!(
2896 parse_with(
2897 vec![frame.clone(), unreferenced_frame],
2898 vec![envelope.clone()]
2899 ),
2900 FormatError::InvalidMetadata {
2901 structure: "IndexShard",
2902 reason: "FrameEntry table is not the exact set referenced by FileEntry rows",
2903 }
2904 );
2905
2906 assert_eq!(
2907 parse_with(vec![frame.clone(), frame.clone()], vec![envelope.clone()]),
2908 FormatError::InvalidMetadata {
2909 structure: "IndexShard",
2910 reason: "FrameEntry rows are not sorted and unique",
2911 }
2912 );
2913
2914 let mut missing_envelope = envelope.clone();
2915 missing_envelope.envelope_index = 1;
2916 assert_eq!(
2917 parse_with(vec![frame.clone()], vec![missing_envelope]),
2918 FormatError::InvalidMetadata {
2919 structure: "FrameEntry",
2920 reason: "referenced EnvelopeEntry is missing",
2921 }
2922 );
2923
2924 let mut unreferenced_envelope = envelope.clone();
2925 unreferenced_envelope.envelope_index = 9;
2926 unreferenced_envelope.first_block_index = 11;
2927 unreferenced_envelope.first_frame_index = 9;
2928 assert_eq!(
2929 parse_with(
2930 vec![frame.clone()],
2931 vec![envelope.clone(), unreferenced_envelope]
2932 ),
2933 FormatError::InvalidMetadata {
2934 structure: "IndexShard",
2935 reason: "EnvelopeEntry table is not the exact set referenced by FrameEntry rows",
2936 }
2937 );
2938
2939 assert_eq!(
2940 parse_with(vec![frame], vec![envelope.clone(), envelope]),
2941 FormatError::InvalidMetadata {
2942 structure: "IndexShard",
2943 reason: "EnvelopeEntry rows are not sorted and unique",
2944 }
2945 );
2946 }
2947
2948 #[test]
2949 fn metadata_parsers_reject_malformed_buffer_corpus() {
2950 let limits = MetadataLimits::default();
2951 let path = b"file.txt";
2952 let path_hash = hash_prefix(path);
2953 let shard_entry = ShardEntry {
2954 shard_index: 0,
2955 first_block_index: 1,
2956 data_block_count: 1,
2957 parity_block_count: 0,
2958 encrypted_size: 4096,
2959 decompressed_size: 0,
2960 file_count: 1,
2961 first_path_hash: path_hash,
2962 last_path_hash: path_hash,
2963 };
2964
2965 let root = IndexRoot {
2966 header: IndexRootHeader {
2967 file_count: 1,
2968 frame_count: 1,
2969 envelope_count: 1,
2970 payload_block_count: 1,
2971 tar_total_size: 512,
2972 ..IndexRootHeader::empty()
2973 },
2974 shards: vec![ShardEntry {
2975 decompressed_size: 256,
2976 ..shard_entry.clone()
2977 }],
2978 directory_hint_shards: Vec::new(),
2979 };
2980 let root_bytes = root.to_bytes();
2981 IndexRoot::parse(&root_bytes, false, limits).unwrap();
2982
2983 assert_eq!(
2984 IndexRoot::parse(&root_bytes[..INDEX_ROOT_LEN - 1], false, limits).unwrap_err(),
2985 FormatError::InvalidMetadata {
2986 structure: "IndexRoot",
2987 reason: "plaintext is shorter than fixed header",
2988 }
2989 );
2990 let mut bad_root = root_bytes.clone();
2991 bad_root[0] ^= 1;
2992 assert_eq!(
2993 IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
2994 FormatError::BadMagic {
2995 structure: "IndexRoot"
2996 }
2997 );
2998 let mut bad_root = root_bytes.clone();
2999 write_u32(&mut bad_root, 4, 2);
3000 assert_eq!(
3001 IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
3002 FormatError::InvalidMetadata {
3003 structure: "IndexRoot",
3004 reason: "unsupported version",
3005 }
3006 );
3007 let mut bad_root = root_bytes.clone();
3008 bad_root[128] = 1;
3009 assert_eq!(
3010 IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
3011 FormatError::NonZeroReserved {
3012 structure: "IndexRoot"
3013 }
3014 );
3015 let mut bad_root = root_bytes.clone();
3016 write_u64(&mut bad_root, 88, (INDEX_ROOT_LEN + 1) as u64);
3017 assert_eq!(
3018 IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
3019 FormatError::InvalidMetadata {
3020 structure: "IndexRoot",
3021 reason: "shard table",
3022 }
3023 );
3024 assert_eq!(
3025 IndexRoot::parse(&root_bytes[..root_bytes.len() - 1], false, limits).unwrap_err(),
3026 FormatError::InvalidMetadata {
3027 structure: "IndexRoot",
3028 reason: "range is out of bounds",
3029 }
3030 );
3031 let mut bad_root = root_bytes.clone();
3032 bad_root.push(0);
3033 assert_eq!(
3034 IndexRoot::parse(&bad_root, false, limits).unwrap_err(),
3035 FormatError::InvalidMetadata {
3036 structure: "IndexRoot",
3037 reason: "plaintext length does not match canonical cursor",
3038 }
3039 );
3040
3041 let file = FileEntry {
3042 path_hash,
3043 path_offset: 0,
3044 path_length: path.len() as u32,
3045 first_frame_index: 0,
3046 frame_count: 1,
3047 offset_in_first_frame_plaintext: 0,
3048 tar_member_group_size: 1536,
3049 file_data_size: 0,
3050 flags: EXTENDED_METADATA_V1,
3051 };
3052 let frame = FrameEntry {
3053 frame_index: 0,
3054 envelope_index: 0,
3055 offset_in_envelope: 0,
3056 compressed_size: 128,
3057 decompressed_size: 1536,
3058 flags: 0x0000_0003,
3059 tar_stream_offset: 0,
3060 };
3061 let envelope = EnvelopeEntry {
3062 envelope_index: 0,
3063 first_block_index: 1,
3064 data_block_count: 1,
3065 parity_block_count: 0,
3066 encrypted_size: 4096,
3067 plaintext_size: 128,
3068 first_frame_index: 0,
3069 frame_count: 1,
3070 };
3071 let shard = IndexShard {
3072 header: IndexShardHeader {
3073 version: 1,
3074 shard_index: 0,
3075 file_count: 0,
3076 frame_count: 0,
3077 envelope_count: 0,
3078 file_table_offset: 0,
3079 frame_table_offset: 0,
3080 envelope_table_offset: 0,
3081 string_pool_offset: 0,
3082 string_pool_size: 0,
3083 },
3084 files: vec![file],
3085 frames: vec![frame],
3086 envelopes: vec![envelope],
3087 string_pool: path.to_vec(),
3088 file_paths: Vec::new(),
3089 file_tar_member_group_starts: Vec::new(),
3090 };
3091 let shard_bytes = shard.to_bytes();
3092 let locating = ShardEntry {
3093 decompressed_size: shard_bytes.len() as u32,
3094 ..shard_entry
3095 };
3096 IndexShard::parse(&shard_bytes, &locating, limits).unwrap();
3097
3098 assert_eq!(
3099 IndexShard::parse(
3100 &shard_bytes[..INDEX_SHARD_HEADER_LEN - 1],
3101 &locating,
3102 limits
3103 )
3104 .unwrap_err(),
3105 FormatError::InvalidMetadata {
3106 structure: "IndexShard",
3107 reason: "plaintext is shorter than fixed header",
3108 }
3109 );
3110 let mut bad_shard = shard_bytes.clone();
3111 bad_shard[0] ^= 1;
3112 assert_eq!(
3113 IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
3114 FormatError::BadMagic {
3115 structure: "IndexShard"
3116 }
3117 );
3118 let mut bad_shard = shard_bytes.clone();
3119 bad_shard[48] = 1;
3120 assert_eq!(
3121 IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
3122 FormatError::NonZeroReserved {
3123 structure: "IndexShard"
3124 }
3125 );
3126 let mut bad_shard = shard_bytes.clone();
3127 write_u32(&mut bad_shard, 28, INDEX_SHARD_HEADER_LEN as u32 + 1);
3128 assert_eq!(
3129 IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
3130 FormatError::InvalidMetadata {
3131 structure: "IndexShard",
3132 reason: "file table",
3133 }
3134 );
3135 assert_eq!(
3136 IndexShard::parse(&shard_bytes[..shard_bytes.len() - 1], &locating, limits)
3137 .unwrap_err(),
3138 FormatError::InvalidMetadata {
3139 structure: "IndexShard",
3140 reason: "range is out of bounds",
3141 }
3142 );
3143 let mut bad_shard = shard_bytes.clone();
3144 bad_shard.push(0);
3145 assert_eq!(
3146 IndexShard::parse(&bad_shard, &locating, limits).unwrap_err(),
3147 FormatError::InvalidMetadata {
3148 structure: "IndexShard",
3149 reason: "plaintext length does not match canonical cursor",
3150 }
3151 );
3152
3153 let dir_path = b"dir".to_vec();
3154 let dir_hash = hash_prefix(&dir_path);
3155 let table = DirectoryHintTable {
3156 header: DirectoryHintTableHeader {
3157 version: 1,
3158 hint_shard_index: 0,
3159 entry_count: 0,
3160 entry_table_offset: 0,
3161 shard_list_offset: 0,
3162 string_pool_offset: 0,
3163 string_pool_size: 0,
3164 },
3165 entries: vec![DirectoryHintEntry {
3166 dir_hash,
3167 path_offset: 0,
3168 path_length: dir_path.len() as u32,
3169 shard_list_start_index: 0,
3170 shard_count: 1,
3171 }],
3172 shard_row_indexes: vec![0],
3173 string_pool: dir_path.clone(),
3174 entry_paths: Vec::new(),
3175 };
3176 let table_bytes = table.to_bytes();
3177 let locating_hint = DirectoryHintShardEntry {
3178 hint_shard_index: 0,
3179 first_dir_hash: dir_hash,
3180 last_dir_hash: dir_hash,
3181 first_block_index: 2,
3182 data_block_count: 1,
3183 parity_block_count: 0,
3184 encrypted_size: 4096,
3185 decompressed_size: table_bytes.len() as u32,
3186 entry_count: 1,
3187 };
3188 DirectoryHintTable::parse(&table_bytes, &locating_hint, 1, limits).unwrap();
3189
3190 assert_eq!(
3191 DirectoryHintTable::parse(
3192 &table_bytes[..DIRECTORY_HINT_TABLE_LEN - 1],
3193 &locating_hint,
3194 1,
3195 limits,
3196 )
3197 .unwrap_err(),
3198 FormatError::InvalidMetadata {
3199 structure: "DirectoryHintTable",
3200 reason: "plaintext is shorter than fixed header",
3201 }
3202 );
3203 let mut bad_table = table_bytes.clone();
3204 bad_table[0] ^= 1;
3205 assert_eq!(
3206 DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
3207 FormatError::BadMagic {
3208 structure: "DirectoryHintTable"
3209 }
3210 );
3211 let mut bad_table = table_bytes.clone();
3212 bad_table[56] = 1;
3213 assert_eq!(
3214 DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
3215 FormatError::NonZeroReserved {
3216 structure: "DirectoryHintTable"
3217 }
3218 );
3219 let mut bad_table = table_bytes.clone();
3220 write_u64(&mut bad_table, 24, DIRECTORY_HINT_TABLE_LEN as u64 + 1);
3221 assert_eq!(
3222 DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
3223 FormatError::InvalidMetadata {
3224 structure: "DirectoryHintTable",
3225 reason: "entry table",
3226 }
3227 );
3228 assert_eq!(
3229 DirectoryHintTable::parse(
3230 &table_bytes[..table_bytes.len() - 1],
3231 &locating_hint,
3232 1,
3233 limits
3234 )
3235 .unwrap_err(),
3236 FormatError::InvalidMetadata {
3237 structure: "DirectoryHintTable",
3238 reason: "range is out of bounds",
3239 }
3240 );
3241 let mut bad_table = table_bytes.clone();
3242 bad_table.push(0);
3243 assert_eq!(
3244 DirectoryHintTable::parse(&bad_table, &locating_hint, 1, limits).unwrap_err(),
3245 FormatError::InvalidMetadata {
3246 structure: "DirectoryHintTable",
3247 reason: "plaintext length does not match canonical cursor",
3248 }
3249 );
3250 }
3251
3252 #[test]
3253 fn candidate_path_lookup_uses_supplied_collision_cap() {
3254 let path = b"same-prefix.txt";
3255 let hash = hash_prefix(path);
3256 let root = IndexRoot {
3257 header: IndexRootHeader::empty(),
3258 shards: (0..3)
3259 .map(|idx| ShardEntry {
3260 shard_index: idx,
3261 first_block_index: idx,
3262 data_block_count: 1,
3263 parity_block_count: 1,
3264 encrypted_size: 4096,
3265 decompressed_size: 256,
3266 file_count: 1,
3267 first_path_hash: hash,
3268 last_path_hash: hash,
3269 })
3270 .collect(),
3271 directory_hint_shards: Vec::new(),
3272 };
3273
3274 let mut limits = MetadataLimits {
3275 max_hash_collision_shard_scan: 0,
3276 ..MetadataLimits::default()
3277 };
3278 assert_eq!(
3279 root.candidate_shards_for_path(path, limits).unwrap_err(),
3280 FormatError::HashPrefixCollisionRunExceeded
3281 );
3282
3283 limits.max_hash_collision_shard_scan = 2;
3284 assert_eq!(
3285 root.candidate_shards_for_path(path, limits).unwrap(),
3286 vec![0, 1, 2]
3287 );
3288 }
3289
3290 #[test]
3291 fn parses_single_shard_and_finds_final_file_entry() {
3292 let path = b"file.txt";
3293 let path_hash = hash_prefix(path);
3294 let file = FileEntry {
3295 path_hash,
3296 path_offset: 0,
3297 path_length: path.len() as u32,
3298 first_frame_index: 0,
3299 frame_count: 1,
3300 offset_in_first_frame_plaintext: 0,
3301 tar_member_group_size: 1536,
3302 file_data_size: 0,
3303 flags: EXTENDED_METADATA_V1,
3304 };
3305 let frame = FrameEntry {
3306 frame_index: 0,
3307 envelope_index: 0,
3308 offset_in_envelope: 0,
3309 compressed_size: 128,
3310 decompressed_size: 1536,
3311 flags: 0,
3312 tar_stream_offset: 0,
3313 };
3314 let envelope = EnvelopeEntry {
3315 envelope_index: 0,
3316 first_block_index: 0,
3317 data_block_count: 1,
3318 parity_block_count: 1,
3319 encrypted_size: 4096,
3320 plaintext_size: 128,
3321 first_frame_index: 0,
3322 frame_count: 1,
3323 };
3324 let shard = IndexShard {
3325 header: IndexShardHeader {
3326 version: 1,
3327 shard_index: 7,
3328 file_count: 0,
3329 frame_count: 0,
3330 envelope_count: 0,
3331 file_table_offset: 0,
3332 frame_table_offset: 0,
3333 envelope_table_offset: 0,
3334 string_pool_offset: 0,
3335 string_pool_size: 0,
3336 },
3337 files: vec![file],
3338 frames: vec![frame],
3339 envelopes: vec![envelope],
3340 string_pool: path.to_vec(),
3341 file_paths: Vec::new(),
3342 file_tar_member_group_starts: Vec::new(),
3343 };
3344 let locating = ShardEntry {
3345 shard_index: 7,
3346 first_block_index: 10,
3347 data_block_count: 1,
3348 parity_block_count: 1,
3349 encrypted_size: 4096,
3350 decompressed_size: shard.to_bytes().len() as u32,
3351 file_count: 1,
3352 first_path_hash: path_hash,
3353 last_path_hash: path_hash,
3354 };
3355
3356 let parsed =
3357 IndexShard::parse(&shard.to_bytes(), &locating, MetadataLimits::default()).unwrap();
3358
3359 assert_eq!(parsed.lookup_file_index(path), Some(0));
3360 assert_eq!(parsed.file_path(0), Some(path.as_slice()));
3361 }
3362}