1use crate::container::header::HEADER_LEN;
32use crate::container::record::{RECORD_OVERHEAD, RecordTag};
33use crate::error::{Error, Result};
34use crate::limits::Limits;
35
36pub const SEEK_DIRECTORY_VERSION: u8 = 1;
38
39pub const SECTION_LOCATORS: u8 = 0x01;
41pub const SECTION_CLASS_INDEX: u8 = 0x02;
43pub const SECTION_CHANNEL_LENGTHS: u8 = 0x04;
45
46pub const SEEK_DIRECTORY_KNOWN_SECTION_FLAGS: u8 =
48 SECTION_LOCATORS | SECTION_CLASS_INDEX | SECTION_CHANNEL_LENGTHS;
49
50pub const SEEK_DIRECTORY_ALL_SECTIONS: u8 = SEEK_DIRECTORY_KNOWN_SECTION_FLAGS;
52
53pub const DIRECTORY_ENTRY_LEN: usize = 1 + 8 + 4;
55pub const CLASS_ENTRY_LEN: usize = 1 + 4 + 4;
57
58pub const CLASS_INDEX_TAGS: [RecordTag; 7] = [
63 RecordTag::Model,
64 RecordTag::EntropyChannel,
65 RecordTag::Object,
66 RecordTag::Graph,
67 RecordTag::ObservationIndex,
68 RecordTag::Integrity,
69 RecordTag::Trailer,
70];
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct DirectoryEntry {
76 pub tag: u8,
78 pub offset: u64,
80 pub payload_len: u32,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct ClassEntry {
88 pub tag: u8,
90 pub first: u32,
92 pub count: u32,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
101pub struct SeekDirectory {
102 pub section_flags: u8,
104 pub entries: Vec<DirectoryEntry>,
106 pub classes: Vec<ClassEntry>,
108 pub channel_lengths: Vec<u64>,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct RecordSite {
117 pub tag: u8,
119 pub offset: u64,
121 pub payload_len: u32,
123}
124
125impl SeekDirectory {
126 pub fn encode(&self) -> Result<Vec<u8>> {
132 if self.section_flags & !SEEK_DIRECTORY_KNOWN_SECTION_FLAGS != 0 {
133 return Err(Error::invalid_container(
134 "seek directory has unknown section flags",
135 ));
136 }
137 if self.section_flags & SECTION_LOCATORS == 0 && !self.entries.is_empty() {
138 return Err(Error::invalid_container(
139 "seek directory carries locators without the locator section flag",
140 ));
141 }
142 if self.section_flags & SECTION_CLASS_INDEX == 0 && !self.classes.is_empty() {
143 return Err(Error::invalid_container(
144 "seek directory carries classes without the class-index section flag",
145 ));
146 }
147 if self.section_flags & SECTION_CHANNEL_LENGTHS == 0 && !self.channel_lengths.is_empty() {
148 return Err(Error::invalid_container(
149 "seek directory carries channel lengths without the channel-lengths section flag",
150 ));
151 }
152
153 let mut out = Vec::new();
154 out.push(SEEK_DIRECTORY_VERSION);
155 out.push(self.section_flags);
156
157 if self.section_flags & SECTION_LOCATORS != 0 {
158 let count = u32::try_from(self.entries.len())
159 .map_err(|_| Error::resource_limit("seek directory entry_count exceeds u32"))?;
160 out.extend_from_slice(&count.to_le_bytes());
161 for e in &self.entries {
162 out.push(e.tag);
163 out.extend_from_slice(&e.offset.to_le_bytes());
164 out.extend_from_slice(&e.payload_len.to_le_bytes());
165 }
166 }
167 if self.section_flags & SECTION_CLASS_INDEX != 0 {
168 let count = u8::try_from(self.classes.len())
169 .map_err(|_| Error::resource_limit("seek directory class_count exceeds u8"))?;
170 out.push(count);
171 for c in &self.classes {
172 out.push(c.tag);
173 out.extend_from_slice(&c.first.to_le_bytes());
174 out.extend_from_slice(&c.count.to_le_bytes());
175 }
176 }
177 if self.section_flags & SECTION_CHANNEL_LENGTHS != 0 {
178 let count = u32::try_from(self.channel_lengths.len())
179 .map_err(|_| Error::resource_limit("seek directory channel_count exceeds u32"))?;
180 out.extend_from_slice(&count.to_le_bytes());
181 for &len in &self.channel_lengths {
182 out.extend_from_slice(&len.to_le_bytes());
183 }
184 }
185 Ok(out)
186 }
187
188 pub fn decode(bytes: &[u8], limits: Limits) -> Result<SeekDirectory> {
194 if bytes.len() as u64 > u64::from(limits.max_directory_bytes) {
195 return Err(Error::resource_limit(format!(
196 "seek directory payload {} exceeds limit {}",
197 bytes.len(),
198 limits.max_directory_bytes
199 )));
200 }
201 if bytes.len() < 2 {
202 return Err(Error::invalid_container("truncated SEEK_DIRECTORY header"));
203 }
204 let version = bytes[0];
205 if version != SEEK_DIRECTORY_VERSION {
206 return Err(Error::unsupported_version(format!(
207 "seek directory version {version} is not supported"
208 )));
209 }
210 let section_flags = bytes[1];
211 if section_flags & !SEEK_DIRECTORY_KNOWN_SECTION_FLAGS != 0 {
212 return Err(Error::invalid_container(
213 "SEEK_DIRECTORY has unknown section flags",
214 ));
215 }
216
217 let mut p = 2usize;
218
219 let mut entries = Vec::new();
220 if section_flags & SECTION_LOCATORS != 0 {
221 let count = read_u32(bytes, &mut p)?;
222 if count > limits.max_record_count {
223 return Err(Error::resource_limit(format!(
224 "seek directory entry_count {count} exceeds limit {}",
225 limits.max_record_count
226 )));
227 }
228 let count = count as usize;
229 require(bytes, p, count, DIRECTORY_ENTRY_LEN)?;
230 entries.reserve(count);
231 for _ in 0..count {
232 entries.push(DirectoryEntry {
233 tag: read_u8(bytes, &mut p)?,
234 offset: read_u64(bytes, &mut p)?,
235 payload_len: read_u32(bytes, &mut p)?,
236 });
237 }
238 }
239
240 let mut classes = Vec::new();
241 if section_flags & SECTION_CLASS_INDEX != 0 {
242 let count = read_u8(bytes, &mut p)? as usize;
243 require(bytes, p, count, CLASS_ENTRY_LEN)?;
244 classes.reserve(count);
245 for _ in 0..count {
246 classes.push(ClassEntry {
247 tag: read_u8(bytes, &mut p)?,
248 first: read_u32(bytes, &mut p)?,
249 count: read_u32(bytes, &mut p)?,
250 });
251 }
252 }
253
254 let mut channel_lengths = Vec::new();
255 if section_flags & SECTION_CHANNEL_LENGTHS != 0 {
256 let count = read_u32(bytes, &mut p)?;
257 if count > limits.max_channel_count {
258 return Err(Error::resource_limit(format!(
259 "seek directory channel_count {count} exceeds limit {}",
260 limits.max_channel_count
261 )));
262 }
263 let count = count as usize;
264 require(bytes, p, count, 8)?;
265 channel_lengths.reserve(count);
266 for _ in 0..count {
267 channel_lengths.push(read_u64(bytes, &mut p)?);
268 }
269 }
270
271 if p != bytes.len() {
272 return Err(Error::invalid_container(format!(
273 "SEEK_DIRECTORY has {} trailing bytes",
274 bytes.len() - p
275 )));
276 }
277
278 Ok(SeekDirectory {
279 section_flags,
280 entries,
281 classes,
282 channel_lengths,
283 })
284 }
285
286 pub fn validate(&self, records: &[RecordSite], file_len: u64, limits: Limits) -> Result<()> {
293 self.validate_structural(file_len, limits)?;
294
295 if self.section_flags & SECTION_LOCATORS == 0 {
296 return Err(Error::invalid_container(
297 "seek directory omits the locator section",
298 ));
299 }
300 if self.entries.len() != records.len() {
301 return Err(Error::invalid_container(format!(
302 "seek directory lists {} records but {} were framed",
303 self.entries.len(),
304 records.len()
305 )));
306 }
307 for (i, (entry, site)) in self.entries.iter().zip(records).enumerate() {
308 if entry.tag != site.tag
309 || entry.offset != site.offset
310 || entry.payload_len != site.payload_len
311 {
312 return Err(Error::invalid_container(format!(
313 "seek directory locator {i} disagrees with the record framing"
314 )));
315 }
316 }
317 Ok(())
318 }
319
320 pub fn validate_structural(&self, file_len: u64, limits: Limits) -> Result<()> {
325 if self.section_flags & !SEEK_DIRECTORY_KNOWN_SECTION_FLAGS != 0 {
326 return Err(Error::invalid_container(
327 "seek directory has unknown section flags",
328 ));
329 }
330 if self.entries.len() > limits.max_record_count as usize {
331 return Err(Error::resource_limit(
332 "seek directory entry_count exceeds the record-count limit",
333 ));
334 }
335 if self.channel_lengths.len() > limits.max_channel_count as usize {
336 return Err(Error::resource_limit(
337 "seek directory channel_count exceeds the channel-count limit",
338 ));
339 }
340
341 if self.section_flags & SECTION_LOCATORS != 0 {
342 if self.entries.is_empty() {
343 return Err(Error::invalid_container("seek directory has no locators"));
344 }
345 let first = self.entries[0];
347 if first.tag != RecordTag::Directory as u8 {
348 return Err(Error::invalid_container(
349 "seek directory locator 0 is not the DIRECTORY record",
350 ));
351 }
352 if first.offset != HEADER_LEN as u64 {
353 return Err(Error::invalid_container(
354 "seek directory locator 0 is not at the end of the header",
355 ));
356 }
357
358 let mut prev_end: Option<u64> = None;
361 for (i, e) in self.entries.iter().enumerate() {
362 let end = e
363 .offset
364 .checked_add(RECORD_OVERHEAD as u64)
365 .and_then(|v| v.checked_add(u64::from(e.payload_len)))
366 .ok_or_else(|| {
367 Error::invalid_container(format!(
368 "seek directory locator {i} end overflows"
369 ))
370 })?;
371 if let Some(pe) = prev_end
372 && e.offset != pe
373 {
374 return Err(Error::invalid_container(format!(
375 "seek directory locator {i} is not contiguous with the previous record"
376 )));
377 }
378 prev_end = Some(end);
379 }
380 let last = self.entries.last().expect("non-empty");
381 if last.tag != RecordTag::Trailer as u8 {
382 return Err(Error::invalid_container(
383 "seek directory does not end at the TRAILER record",
384 ));
385 }
386 match prev_end {
387 Some(end) if end == file_len => {}
388 Some(end) => {
389 return Err(Error::invalid_container(format!(
390 "seek directory ends at offset {end} but the file is {file_len} bytes"
391 )));
392 }
393 None => unreachable!("entries is non-empty"),
394 }
395
396 for tag in [
398 RecordTag::Graph,
399 RecordTag::ObservationIndex,
400 RecordTag::Integrity,
401 RecordTag::Trailer,
402 ] {
403 let n = self.entries.iter().filter(|e| e.tag == tag as u8).count();
404 if n > 1 {
405 return Err(Error::invalid_container(format!(
406 "seek directory lists the singleton {} {n} times",
407 tag.name()
408 )));
409 }
410 }
411 }
412
413 for (i, c) in self.classes.iter().enumerate() {
415 if c.count == 0 {
416 return Err(Error::invalid_container(format!(
417 "seek directory class entry {i} has a zero count"
418 )));
419 }
420 if self.classes[..i].iter().any(|o| o.tag == c.tag) {
421 return Err(Error::invalid_container(format!(
422 "seek directory has duplicate class entry for tag {:#04x}",
423 c.tag
424 )));
425 }
426 let first = c.first as usize;
427 let count = c.count as usize;
428 let end = first.checked_add(count).ok_or_else(|| {
429 Error::invalid_container(format!("seek directory class entry {i} range overflows"))
430 })?;
431 if end > self.entries.len() {
432 return Err(Error::invalid_container(format!(
433 "seek directory class entry {i} range {first}..{end} exceeds {} locators",
434 self.entries.len()
435 )));
436 }
437 if self.entries[first..end].iter().any(|e| e.tag != c.tag) {
438 return Err(Error::invalid_container(format!(
439 "seek directory class entry {i} spans a foreign tag"
440 )));
441 }
442 let scan_first = self.entries.iter().position(|e| e.tag == c.tag);
443 let scan_count = self.entries.iter().filter(|e| e.tag == c.tag).count();
444 if scan_first != Some(first) || scan_count != count {
445 return Err(Error::invalid_container(format!(
446 "seek directory class entry {i} disagrees with a scan of the locators"
447 )));
448 }
449 }
450
451 if self.section_flags & SECTION_CHANNEL_LENGTHS != 0 {
452 let channels = self
453 .entries
454 .iter()
455 .filter(|e| e.tag == RecordTag::EntropyChannel as u8)
456 .count();
457 if channels != self.channel_lengths.len() {
458 return Err(Error::invalid_container(format!(
459 "seek directory lists {} channel lengths for {channels} channel records",
460 self.channel_lengths.len()
461 )));
462 }
463 }
464
465 Ok(())
466 }
467}
468
469pub fn class_index(entries: &[DirectoryEntry]) -> Vec<ClassEntry> {
472 let mut classes = Vec::new();
473 for tag in CLASS_INDEX_TAGS {
474 let count = entries.iter().filter(|e| e.tag == tag as u8).count();
475 if count == 0 {
476 continue;
477 }
478 let first = entries
479 .iter()
480 .position(|e| e.tag == tag as u8)
481 .expect("count > 0 implies a first occurrence");
482 classes.push(ClassEntry {
483 tag: tag as u8,
484 first: first as u32,
485 count: count as u32,
486 });
487 }
488 classes
489}
490
491fn require(bytes: &[u8], p: usize, count: usize, entry_len: usize) -> Result<()> {
493 let need = count
494 .checked_mul(entry_len)
495 .ok_or_else(|| Error::invalid_container("SEEK_DIRECTORY section length overflow"))?;
496 let available = bytes
497 .len()
498 .checked_sub(p)
499 .ok_or_else(|| Error::invalid_container("SEEK_DIRECTORY cursor past end of payload"))?;
500 if need > available {
501 return Err(Error::invalid_container(format!(
502 "SEEK_DIRECTORY section needs {need} bytes but only {available} remain"
503 )));
504 }
505 Ok(())
506}
507
508fn read_u8(bytes: &[u8], p: &mut usize) -> Result<u8> {
509 let v = *bytes
510 .get(*p)
511 .ok_or_else(|| Error::invalid_container("truncated SEEK_DIRECTORY payload"))?;
512 *p += 1;
513 Ok(v)
514}
515
516fn read_u32(bytes: &[u8], p: &mut usize) -> Result<u32> {
517 let end = p
518 .checked_add(4)
519 .ok_or_else(|| Error::invalid_container("SEEK_DIRECTORY cursor overflow"))?;
520 let slice = bytes
521 .get(*p..end)
522 .ok_or_else(|| Error::invalid_container("truncated SEEK_DIRECTORY payload"))?;
523 *p = end;
524 Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
525}
526
527fn read_u64(bytes: &[u8], p: &mut usize) -> Result<u64> {
528 let end = p
529 .checked_add(8)
530 .ok_or_else(|| Error::invalid_container("SEEK_DIRECTORY cursor overflow"))?;
531 let slice = bytes
532 .get(*p..end)
533 .ok_or_else(|| Error::invalid_container("truncated SEEK_DIRECTORY payload"))?;
534 *p = end;
535 Ok(u64::from_le_bytes([
536 slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
537 ]))
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 fn entry(tag: RecordTag, offset: u64, payload_len: u32) -> DirectoryEntry {
545 DirectoryEntry {
546 tag: tag as u8,
547 offset,
548 payload_len,
549 }
550 }
551
552 fn sample_dir() -> SeekDirectory {
555 let entries = vec![
556 entry(RecordTag::Directory, HEADER_LEN as u64, 30),
557 entry(RecordTag::Integrity, 106, 40),
558 entry(RecordTag::Trailer, 158, 20),
559 ];
560 let classes = class_index(&entries);
561 SeekDirectory {
562 section_flags: SEEK_DIRECTORY_ALL_SECTIONS,
563 entries,
564 classes,
565 channel_lengths: Vec::new(),
566 }
567 }
568
569 #[test]
570 fn roundtrip() {
571 let dir = sample_dir();
572 let bytes = dir.encode().unwrap();
573 let decoded = SeekDirectory::decode(&bytes, Limits::DEFAULT).unwrap();
574 assert_eq!(decoded, dir);
575 }
576
577 #[test]
578 fn encoded_length_is_the_documented_sum() {
579 let dir = sample_dir();
580 let bytes = dir.encode().unwrap();
581 let expected = 2
582 + 4
583 + DIRECTORY_ENTRY_LEN * dir.entries.len()
584 + 1
585 + CLASS_ENTRY_LEN * dir.classes.len()
586 + 4
587 + 8 * dir.channel_lengths.len();
588 assert_eq!(bytes.len(), expected);
589 }
590
591 #[test]
592 fn unknown_version_and_flags_fail_closed() {
593 let dir = sample_dir();
594 let mut bytes = dir.encode().unwrap();
595 bytes[0] = 2;
596 assert_eq!(
597 SeekDirectory::decode(&bytes, Limits::DEFAULT)
598 .unwrap_err()
599 .class(),
600 crate::ErrorClass::UnsupportedVersion
601 );
602
603 let mut bytes = dir.encode().unwrap();
604 bytes[1] |= 0x80;
605 assert_eq!(
606 SeekDirectory::decode(&bytes, Limits::DEFAULT)
607 .unwrap_err()
608 .class(),
609 crate::ErrorClass::InvalidContainer
610 );
611 }
612
613 #[test]
614 fn truncated_section_is_rejected() {
615 let dir = sample_dir();
616 let bytes = dir.encode().unwrap();
617 for cut in 0..bytes.len() {
618 if cut < 2 {
620 continue;
621 }
622 let e = SeekDirectory::decode(&bytes[..cut], Limits::DEFAULT);
623 assert!(e.is_err(), "cut {cut} was accepted");
624 }
625 }
626
627 #[test]
628 fn trailing_bytes_are_rejected() {
629 let dir = sample_dir();
630 let mut bytes = dir.encode().unwrap();
631 bytes.push(0);
632 assert_eq!(
633 SeekDirectory::decode(&bytes, Limits::DEFAULT)
634 .unwrap_err()
635 .class(),
636 crate::ErrorClass::InvalidContainer
637 );
638 }
639
640 #[test]
641 fn over_limit_size_is_declined_before_allocation() {
642 let dir = sample_dir();
643 let bytes = dir.encode().unwrap();
644 let tiny = Limits {
645 max_directory_bytes: 4,
646 ..Limits::DEFAULT
647 };
648 assert_eq!(
649 SeekDirectory::decode(&bytes, tiny).unwrap_err().class(),
650 crate::ErrorClass::ResourceLimit
651 );
652 }
653
654 #[test]
655 fn structural_validation_accepts_a_contiguous_directory() {
656 let dir = sample_dir();
657 dir.validate_structural(190, Limits::DEFAULT).unwrap();
659 }
660
661 #[test]
662 fn structural_validation_rejects_bad_geometry() {
663 let mut dir = sample_dir();
665 dir.entries[2].offset += 1;
666 assert_eq!(
667 dir.validate_structural(190, Limits::DEFAULT)
668 .unwrap_err()
669 .class(),
670 crate::ErrorClass::InvalidContainer
671 );
672
673 let dir = sample_dir();
675 assert_eq!(
676 dir.validate_structural(191, Limits::DEFAULT)
677 .unwrap_err()
678 .class(),
679 crate::ErrorClass::InvalidContainer
680 );
681
682 let mut dir = sample_dir();
684 dir.classes[0].count += 1;
685 assert_eq!(
686 dir.validate_structural(190, Limits::DEFAULT)
687 .unwrap_err()
688 .class(),
689 crate::ErrorClass::InvalidContainer
690 );
691 }
692
693 #[test]
694 fn validate_cross_checks_the_record_framing() {
695 let dir = sample_dir();
696 let sites = vec![
697 RecordSite {
698 tag: RecordTag::Directory as u8,
699 offset: HEADER_LEN as u64,
700 payload_len: 30,
701 },
702 RecordSite {
703 tag: RecordTag::Integrity as u8,
704 offset: 106,
705 payload_len: 40,
706 },
707 RecordSite {
708 tag: RecordTag::Trailer as u8,
709 offset: 158,
710 payload_len: 20,
711 },
712 ];
713 dir.validate(&sites, 190, Limits::DEFAULT).unwrap();
714
715 let mut wrong = sites.clone();
717 wrong[1].payload_len = 41;
718 assert_eq!(
719 dir.validate(&wrong, 190, Limits::DEFAULT)
720 .unwrap_err()
721 .class(),
722 crate::ErrorClass::InvalidContainer
723 );
724 }
725}