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