1use iris_abi::{Reader, Tag};
27
28use crate::container::FileHeader;
29use crate::digest::Digest;
30use crate::error::{Error, Result};
31use crate::layout::{
32 DecoderLocation, FORMAT_MAJOR, HEADER_SIZE, MAGIC, MIN_SIZE, TRAILER_SIZE, tag,
33};
34use crate::meta::{Dataset, DecoderRef, Schema, Section};
35
36#[derive(Clone, Copy, PartialEq, Eq, Debug)]
42pub struct Placement {
43 file_len: u64,
44 footer_at: u64,
45 footer_len: u32,
46 root: Digest,
47}
48
49impl Placement {
50 pub const TRAILER_LEN: usize = TRAILER_SIZE;
52
53 pub fn trailer_at(file_len: u64) -> Result<u64> {
60 if file_len < MIN_SIZE as u64 {
61 return Err(Error::Truncated {
62 what: "the container",
63 needed: MIN_SIZE as u64,
64 available: file_len,
65 });
66 }
67 Ok(file_len - TRAILER_SIZE as u64)
68 }
69
70 pub fn read(trailer: &[u8], file_len: u64) -> Result<Self> {
80 let trailer_at = Self::trailer_at(file_len)?;
81 let t: &[u8; TRAILER_SIZE] = trailer.first_chunk().ok_or(Error::Truncated {
84 what: "the trailer",
85 needed: TRAILER_SIZE as u64,
86 available: trailer.len() as u64,
87 })?;
88
89 if t[48..56] != MAGIC {
90 return Err(Error::Truncated {
91 what: "the container, which does not end with the magic",
92 needed: file_len,
93 available: file_len,
94 });
95 }
96
97 let footer_at = u64::from_le_bytes([t[0], t[1], t[2], t[3], t[4], t[5], t[6], t[7]]);
98 let footer_len = u32::from_le_bytes([t[8], t[9], t[10], t[11]]);
99 let reserved = u32::from_le_bytes([t[12], t[13], t[14], t[15]]);
100 if reserved != 0 {
101 return Err(Error::Reserved { what: "trailer" });
102 }
103 let root: [u8; 32] = core::array::from_fn(|i| t[16 + i]);
104
105 let footer_end = footer_at
109 .checked_add(u64::from(footer_len))
110 .ok_or(Error::Truncated {
111 what: "the footer",
112 needed: u64::MAX,
113 available: file_len,
114 })?;
115 if footer_at < HEADER_SIZE as u64 || footer_end > trailer_at {
116 return Err(Error::Truncated {
117 what: "the footer",
118 needed: footer_end,
119 available: trailer_at,
120 });
121 }
122
123 Ok(Self {
124 file_len,
125 footer_at,
126 footer_len,
127 root: Digest(root),
128 })
129 }
130
131 #[must_use]
133 pub const fn file_len(&self) -> u64 {
134 self.file_len
135 }
136
137 #[must_use]
139 pub const fn footer_at(&self) -> u64 {
140 self.footer_at
141 }
142
143 #[must_use]
148 pub const fn footer_len(&self) -> usize {
149 self.footer_len as usize
150 }
151
152 #[must_use]
154 pub const fn root_digest(&self) -> Digest {
155 self.root
156 }
157}
158
159#[derive(Clone, Debug)]
165pub struct Directory<'a> {
166 header: FileHeader,
167 raw_header: [u8; HEADER_SIZE],
168 footer: &'a [u8],
169 placement: Placement,
170 dataset: Dataset,
171 schema: Option<Schema<'a>>,
172 decoder: Option<DecoderRef<'a>>,
173 sections: Vec<Section>,
174}
175
176impl<'a> Directory<'a> {
177 pub fn parse(header: &[u8], footer: &'a [u8], placement: Placement) -> Result<Self> {
189 let directory = Self::parse_without_root_digest(header, footer, placement)?;
190 let actual = directory.compute_root();
191 if actual != placement.root {
192 return Err(Error::DigestMismatch {
193 what: "footer".to_owned(),
194 expected: placement.root.to_string(),
195 actual: actual.to_string(),
196 });
197 }
198 Ok(directory)
199 }
200
201 pub fn parse_without_root_digest(
211 header: &[u8],
212 footer: &'a [u8],
213 placement: Placement,
214 ) -> Result<Self> {
215 let raw_header: [u8; HEADER_SIZE] = *header.first_chunk().ok_or(Error::Truncated {
216 what: "the header",
217 needed: HEADER_SIZE as u64,
218 available: header.len() as u64,
219 })?;
220
221 if raw_header[..MAGIC.len()] != MAGIC {
222 return Err(Error::NotAContainer {
223 found: raw_header[..MAGIC.len()].to_vec(),
224 expected: MAGIC.to_vec(),
225 });
226 }
227
228 if footer.len() != placement.footer_len() {
232 return Err(Error::Truncated {
233 what: "the footer",
234 needed: placement.footer_len() as u64,
235 available: footer.len() as u64,
236 });
237 }
238
239 let mut directory = Self {
240 header: parse_header(&raw_header)?,
241 raw_header,
242 footer,
243 placement,
244 dataset: Dataset {
245 rows: 0,
246 name: String::new(),
247 },
248 schema: None,
249 decoder: None,
250 sections: Vec::new(),
251 };
252 directory.parse_footer(footer)?;
253 directory.check_sections()?;
254 Ok(directory)
255 }
256
257 fn parse_footer(&mut self, footer: &'a [u8]) -> Result<()> {
258 let mut seen_dataset = false;
259 let mut p = Reader::new(footer);
260 while !p.is_empty() {
261 let (header, mut body) = p.record()?;
262 match header.tag {
263 tag::DATASET => {
264 expect_version(header.tag, header.version, Dataset::VERSION)?;
265 if seen_dataset {
266 return Err(Error::RepeatedRecord(tag::DATASET));
267 }
268 self.dataset = Dataset::decode(&mut body)?;
269 seen_dataset = true;
270 }
271 tag::SCHEMA => {
272 expect_version(header.tag, header.version, Schema::VERSION)?;
273 if self.schema.is_some() {
274 return Err(Error::RepeatedRecord(tag::SCHEMA));
275 }
276 self.schema = Some(Schema::decode(&mut body)?);
277 }
278 tag::DECODER => {
279 expect_version(header.tag, header.version, DecoderRef::VERSION)?;
280 if self.decoder.is_some() {
281 return Err(Error::RepeatedRecord(tag::DECODER));
282 }
283 self.decoder = Some(DecoderRef::decode(&mut body)?);
284 }
285 tag::SECTION => {
286 expect_version(header.tag, header.version, Section::VERSION)?;
287 self.sections.push(Section::decode(&mut body)?);
288 }
289 _ => {}
293 }
294 }
295 if !seen_dataset {
296 return Err(Error::MissingRecord(tag::DATASET));
297 }
298 Ok(())
299 }
300
301 fn check_sections(&self) -> Result<()> {
302 let file_len = self.placement.file_len();
303 for (i, section) in self.sections.iter().enumerate() {
304 let end = section.end().ok_or(Error::SectionOutOfBounds {
305 id: section.id,
306 offset: section.offset,
307 end: u64::MAX,
308 file_len,
309 })?;
310 if section.offset < HEADER_SIZE as u64 || end > self.placement.footer_at() {
311 return Err(Error::SectionOutOfBounds {
312 id: section.id,
313 offset: section.offset,
314 end,
315 file_len,
316 });
317 }
318 if self.sections[..i].iter().any(|s| s.id == section.id) {
319 return Err(Error::DuplicateSection { id: section.id });
320 }
321 }
322 Ok(())
323 }
324
325 fn compute_root(&self) -> Digest {
326 let mut hasher = blake3::Hasher::new();
327 hasher.update(&self.raw_header);
328 hasher.update(self.footer);
329 Digest(*hasher.finalize().as_bytes())
330 }
331
332 #[must_use]
334 pub const fn header(&self) -> FileHeader {
335 self.header
336 }
337
338 #[must_use]
340 pub const fn placement(&self) -> Placement {
341 self.placement
342 }
343
344 #[must_use]
346 pub const fn root_digest(&self) -> Digest {
347 self.placement.root_digest()
348 }
349
350 #[must_use]
352 pub const fn dataset(&self) -> &Dataset {
353 &self.dataset
354 }
355
356 #[must_use]
358 pub const fn schema(&self) -> Option<&Schema<'a>> {
359 self.schema.as_ref()
360 }
361
362 #[must_use]
364 pub const fn decoder(&self) -> Option<&DecoderRef<'a>> {
365 self.decoder.as_ref()
366 }
367
368 #[must_use]
370 pub fn sections(&self) -> &[Section] {
371 &self.sections
372 }
373
374 #[must_use]
376 pub fn section(&self, id: u32) -> Option<&Section> {
377 self.sections.iter().find(|s| s.id == id)
378 }
379
380 #[must_use]
386 pub fn decoder_section(&self) -> Option<&Section> {
387 let decoder = self.decoder.as_ref()?;
388 let DecoderLocation::Embedded { section } = decoder.location else {
389 return None;
390 };
391 self.section(section)
392 }
393}
394
395fn parse_header(header: &[u8; HEADER_SIZE]) -> Result<FileHeader> {
396 let major = u16::from_le_bytes([header[8], header[9]]);
397 let minor = u16::from_le_bytes([header[10], header[11]]);
398 if major != FORMAT_MAJOR {
399 return Err(Error::UnsupportedFormat {
400 major,
401 minor,
402 supported_major: FORMAT_MAJOR,
403 });
404 }
405 let flags = u32::from_le_bytes([header[12], header[13], header[14], header[15]]);
406 if flags != 0 {
407 return Err(Error::Reserved { what: "header" });
408 }
409 Ok(FileHeader { major, minor })
410}
411
412fn expect_version(tag: Tag, found: u16, supported: u16) -> Result<()> {
413 if found > supported {
414 return Err(Error::UnsupportedRecord {
415 tag,
416 version: found,
417 });
418 }
419 Ok(())
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn a_file_shorter_than_the_smallest_container_has_no_trailer_to_read() {
428 assert!(matches!(
429 Placement::trailer_at(MIN_SIZE as u64 - 1),
430 Err(Error::Truncated { .. })
431 ));
432 assert_eq!(
433 Placement::trailer_at(MIN_SIZE as u64).expect("the smallest container has a trailer"),
434 (MIN_SIZE - TRAILER_SIZE) as u64
435 );
436 }
437
438 fn trailer(footer_at: u64, footer_len: u32) -> [u8; TRAILER_SIZE] {
439 let mut t = [0u8; TRAILER_SIZE];
440 t[0..8].copy_from_slice(&footer_at.to_le_bytes());
441 t[8..12].copy_from_slice(&footer_len.to_le_bytes());
442 t[48..56].copy_from_slice(&MAGIC);
443 t
444 }
445
446 #[test]
447 fn a_trailer_that_does_not_end_with_the_magic_is_not_a_container() {
448 let mut t = trailer(HEADER_SIZE as u64, 8);
449 t[55] = 0;
450 assert!(matches!(
451 Placement::read(&t, 1024),
452 Err(Error::Truncated { .. })
453 ));
454 }
455
456 #[test]
457 fn a_footer_that_overlaps_the_header_or_the_trailer_is_refused() {
458 assert!(matches!(
460 Placement::read(&trailer(HEADER_SIZE as u64 - 1, 8), 1024),
461 Err(Error::Truncated { .. })
462 ));
463 assert!(matches!(
465 Placement::read(&trailer(1024 - TRAILER_SIZE as u64, 8), 1024),
466 Err(Error::Truncated { .. })
467 ));
468 assert!(matches!(
470 Placement::read(&trailer(u64::MAX, 8), 1024),
471 Err(Error::Truncated { .. })
472 ));
473 }
474
475 #[test]
476 fn a_reserved_field_that_is_not_zero_is_refused() {
477 let mut t = trailer(HEADER_SIZE as u64, 8);
478 t[12] = 1;
479 assert!(matches!(
480 Placement::read(&t, 1024),
481 Err(Error::Reserved { what: "trailer" })
482 ));
483 }
484
485 #[test]
486 fn a_footer_of_the_wrong_length_is_refused_rather_than_parsed() {
487 let placement = Placement::read(&trailer(HEADER_SIZE as u64, 8), 1024)
488 .expect("the placement is well formed");
489 let mut header = [0u8; HEADER_SIZE];
490 header[..MAGIC.len()].copy_from_slice(&MAGIC);
491 header[8..10].copy_from_slice(&FORMAT_MAJOR.to_le_bytes());
492 assert!(matches!(
493 Directory::parse_without_root_digest(&header, &[0u8; 4], placement),
494 Err(Error::Truncated {
495 what: "the footer",
496 ..
497 })
498 ));
499 }
500}