Skip to main content

iris_format/
container.rs

1//! Reading a container.
2//!
3//! This is the untrusted path. Everything in this file is written on the assumption that the bytes
4//! were produced by somebody who wants them to be trouble. The rules it follows are short enough to
5//! check by reading:
6//!
7//! - no indexing that is not bounds checked first, and no `unsafe` at all
8//! - no arithmetic on a length from the file that is not `checked_` or `saturating_`
9//! - nothing is allocated in proportion to a number read out of the file, only in proportion to
10//!   bytes that are actually there
11//!
12//! The third rule is the one that gets skipped in hand written parsers, and it is the one that
13//! turns a sixty byte file into an out of memory kill. There is no count field anywhere in the
14//! footer for exactly this reason: the number of sections is however many section records the
15//! footer actually contains, so a liar has to pay for every one.
16
17use iris_abi::{Reader, Tag};
18
19use crate::digest::Digest;
20use crate::error::{Error, Result};
21use crate::layout::{
22    DecoderLocation, FORMAT_MAJOR, HEADER_SIZE, MAGIC, MIN_SIZE, TRAILER_SIZE, tag,
23};
24use crate::meta::{Dataset, DecoderRef, Schema, Section};
25
26/// The fixed fields at the front of a container.
27#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
28pub struct FileHeader {
29    /// The format major version.
30    pub major: u16,
31    /// The format minor version.
32    pub minor: u16,
33}
34
35/// A parsed container, borrowing the bytes it was parsed from.
36///
37/// Parsing does not copy the payload and does not read it. A container of a hundred gigabytes
38/// parses in the time it takes to hash a footer, and the sections are read when somebody asks for
39/// them.
40#[derive(Clone, Debug)]
41pub struct Container<'a> {
42    bytes: &'a [u8],
43    header: FileHeader,
44    footer_range: (usize, usize),
45    root: Digest,
46    dataset: Dataset,
47    schema: Option<Schema<'a>>,
48    decoder: Option<DecoderRef<'a>>,
49    sections: Vec<Section>,
50}
51
52impl<'a> Container<'a> {
53    /// Parses a container and checks that the footer is the footer that was written.
54    ///
55    /// The root digest covers the header and the footer, so a container that parses has metadata
56    /// nobody has edited since it was written. Section contents are not read here. Call
57    /// [`Container::verify`] for that, which is the expensive one and is a separate decision.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`Error`] describing the first thing that was wrong. It never panics, whatever the
62    /// input is, and there is a fuzz target that exists to keep that true.
63    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
64        let container = Self::parse_without_root_digest(bytes)?;
65        let actual = container.compute_root();
66        if actual != container.root {
67            return Err(Error::DigestMismatch {
68                what: "footer".to_owned(),
69                expected: container.root.to_string(),
70                actual: actual.to_string(),
71            });
72        }
73        Ok(container)
74    }
75
76    /// Parses a container without checking the root digest.
77    ///
78    /// This exists for the fuzzer. Checking the digest first would mean essentially every generated
79    /// input is rejected in the trailer, and the parser behind it would never be reached, which is
80    /// the part that needs the fuzzing. It is public because the fuzz target lives outside this
81    /// crate, and it is named at length so that nobody reaches for it by accident.
82    ///
83    /// # Errors
84    ///
85    /// The same as [`Container::parse`], minus the digest mismatch.
86    pub fn parse_without_root_digest(bytes: &'a [u8]) -> Result<Self> {
87        if bytes.len() < MIN_SIZE {
88            let head = &bytes[..bytes.len().min(MAGIC.len())];
89            if !MAGIC.starts_with(head) {
90                return Err(Error::NotAContainer {
91                    found: head.to_vec(),
92                    expected: MAGIC.to_vec(),
93                });
94            }
95            return Err(Error::Truncated {
96                what: "the container",
97                needed: MIN_SIZE as u64,
98                available: bytes.len() as u64,
99            });
100        }
101
102        if bytes[..MAGIC.len()] != MAGIC {
103            return Err(Error::NotAContainer {
104                found: bytes[..MAGIC.len()].to_vec(),
105                expected: MAGIC.to_vec(),
106            });
107        }
108
109        let header = Self::parse_header(bytes)?;
110        let (footer_offset, footer_len, root) = Self::parse_trailer(bytes)?;
111
112        // The footer has to sit between the header and the trailer. Both ends are checked, because
113        // a footer that starts inside the header would let a writer describe its own magic as
114        // records, and a footer that runs into the trailer would let it describe its own digest.
115        let trailer_at = bytes.len() - TRAILER_SIZE;
116        let footer_end = footer_offset
117            .checked_add(footer_len)
118            .ok_or(Error::Truncated {
119                what: "the footer",
120                needed: u64::MAX,
121                available: bytes.len() as u64,
122            })?;
123        if footer_offset < HEADER_SIZE as u64 || footer_end > trailer_at as u64 {
124            return Err(Error::Truncated {
125                what: "the footer",
126                needed: footer_end,
127                available: trailer_at as u64,
128            });
129        }
130        let start = usize::try_from(footer_offset).map_err(|_| Error::TooLarge {
131            what: "the footer offset",
132            needed: footer_offset,
133        })?;
134        let end = usize::try_from(footer_end).map_err(|_| Error::TooLarge {
135            what: "the footer",
136            needed: footer_end,
137        })?;
138
139        let mut container = Self {
140            bytes,
141            header,
142            footer_range: (start, end),
143            root,
144            dataset: Dataset {
145                rows: 0,
146                name: String::new(),
147            },
148            schema: None,
149            decoder: None,
150            sections: Vec::new(),
151        };
152        container.parse_footer(&bytes[start..end])?;
153        container.check_sections(footer_offset)?;
154        Ok(container)
155    }
156
157    fn parse_header(bytes: &[u8]) -> Result<FileHeader> {
158        let major = u16::from_le_bytes([bytes[8], bytes[9]]);
159        let minor = u16::from_le_bytes([bytes[10], bytes[11]]);
160        if major != FORMAT_MAJOR {
161            return Err(Error::UnsupportedFormat {
162                major,
163                minor,
164                supported_major: FORMAT_MAJOR,
165            });
166        }
167        let flags = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
168        if flags != 0 {
169            return Err(Error::Reserved { what: "header" });
170        }
171        Ok(FileHeader { major, minor })
172    }
173
174    fn parse_trailer(bytes: &[u8]) -> Result<(u64, u64, Digest)> {
175        let at = bytes.len() - TRAILER_SIZE;
176        let t = &bytes[at..];
177        if t[48..56] != MAGIC {
178            return Err(Error::Truncated {
179                what: "the container, which does not end with the magic",
180                needed: bytes.len() as u64,
181                available: bytes.len() as u64,
182            });
183        }
184        let footer_offset = u64::from_le_bytes(t[0..8].try_into().expect("eight bytes"));
185        let footer_len = u64::from(u32::from_le_bytes(t[8..12].try_into().expect("four bytes")));
186        let reserved = u32::from_le_bytes(t[12..16].try_into().expect("four bytes"));
187        if reserved != 0 {
188            return Err(Error::Reserved { what: "trailer" });
189        }
190        let mut root = [0u8; 32];
191        root.copy_from_slice(&t[16..48]);
192        Ok((footer_offset, footer_len, Digest(root)))
193    }
194
195    fn parse_footer(&mut self, footer: &'a [u8]) -> Result<()> {
196        let mut seen_dataset = false;
197        let mut p = Reader::new(footer);
198        while !p.is_empty() {
199            let (header, mut body) = p.record()?;
200            match header.tag {
201                tag::DATASET => {
202                    Self::expect_version(header.tag, header.version, Dataset::VERSION)?;
203                    if seen_dataset {
204                        return Err(Error::RepeatedRecord(tag::DATASET));
205                    }
206                    self.dataset = Dataset::decode(&mut body)?;
207                    seen_dataset = true;
208                }
209                tag::SCHEMA => {
210                    Self::expect_version(header.tag, header.version, Schema::VERSION)?;
211                    if self.schema.is_some() {
212                        return Err(Error::RepeatedRecord(tag::SCHEMA));
213                    }
214                    self.schema = Some(Schema::decode(&mut body)?);
215                }
216                tag::DECODER => {
217                    Self::expect_version(header.tag, header.version, DecoderRef::VERSION)?;
218                    if self.decoder.is_some() {
219                        return Err(Error::RepeatedRecord(tag::DECODER));
220                    }
221                    self.decoder = Some(DecoderRef::decode(&mut body)?);
222                }
223                tag::SECTION => {
224                    Self::expect_version(header.tag, header.version, Section::VERSION)?;
225                    self.sections.push(Section::decode(&mut body)?);
226                }
227                // An unknown record is stepped over, which is the whole reason the footer is framed
228                // this way. A tool that only wants the section table should not be stopped by a
229                // record a newer writer added.
230                _ => {}
231            }
232        }
233        if !seen_dataset {
234            return Err(Error::MissingRecord(tag::DATASET));
235        }
236        Ok(())
237    }
238
239    fn expect_version(tag: Tag, found: u16, supported: u16) -> Result<()> {
240        if found > supported {
241            return Err(Error::UnsupportedRecord {
242                tag,
243                version: found,
244            });
245        }
246        Ok(())
247    }
248
249    fn check_sections(&self, footer_offset: u64) -> Result<()> {
250        for (i, section) in self.sections.iter().enumerate() {
251            let end = section.end().ok_or(Error::SectionOutOfBounds {
252                id: section.id,
253                offset: section.offset,
254                end: u64::MAX,
255                file_len: self.bytes.len() as u64,
256            })?;
257            if section.offset < HEADER_SIZE as u64 || end > footer_offset {
258                return Err(Error::SectionOutOfBounds {
259                    id: section.id,
260                    offset: section.offset,
261                    end,
262                    file_len: self.bytes.len() as u64,
263                });
264            }
265            if self.sections[..i].iter().any(|s| s.id == section.id) {
266                return Err(Error::DuplicateSection { id: section.id });
267            }
268        }
269        Ok(())
270    }
271
272    fn compute_root(&self) -> Digest {
273        let mut hasher = blake3::Hasher::new();
274        hasher.update(&self.bytes[..HEADER_SIZE]);
275        hasher.update(&self.bytes[self.footer_range.0..self.footer_range.1]);
276        Digest(*hasher.finalize().as_bytes())
277    }
278
279    /// The format version this container was written at.
280    #[must_use]
281    pub const fn header(&self) -> FileHeader {
282        self.header
283    }
284
285    /// The digest that covers the header and the footer.
286    #[must_use]
287    pub const fn root_digest(&self) -> Digest {
288        self.root
289    }
290
291    /// What the dataset is.
292    #[must_use]
293    pub const fn dataset(&self) -> &Dataset {
294        &self.dataset
295    }
296
297    /// The schema, if there is one.
298    #[must_use]
299    pub const fn schema(&self) -> Option<&Schema<'a>> {
300        self.schema.as_ref()
301    }
302
303    /// The decoder reference, if there is one.
304    #[must_use]
305    pub const fn decoder(&self) -> Option<&DecoderRef<'a>> {
306        self.decoder.as_ref()
307    }
308
309    /// Every section, in the order the footer listed them.
310    #[must_use]
311    pub fn sections(&self) -> &[Section] {
312        &self.sections
313    }
314
315    /// The section with this id.
316    #[must_use]
317    pub fn section(&self, id: u32) -> Option<&Section> {
318        self.sections.iter().find(|s| s.id == id)
319    }
320
321    /// The bytes of a section.
322    ///
323    /// The bounds were checked during parsing, so this cannot be out of range for a section that
324    /// came from this container. It takes a `&Section` rather than an id so that the only way to
325    /// call it is with one that did.
326    #[must_use]
327    pub fn section_bytes(&self, section: &Section) -> &'a [u8] {
328        let start = usize::try_from(section.offset).unwrap_or(usize::MAX);
329        let end = usize::try_from(section.end().unwrap_or(u64::MAX)).unwrap_or(usize::MAX);
330        self.bytes.get(start..end).unwrap_or(&[])
331    }
332
333    /// The bytes of the embedded decoder module, if the decoder is embedded and the section it
334    /// names exists.
335    #[must_use]
336    pub fn decoder_bytes(&self) -> Option<&'a [u8]> {
337        let decoder = self.decoder.as_ref()?;
338        let DecoderLocation::Embedded { section } = decoder.location else {
339            return None;
340        };
341        self.section(section).map(|s| self.section_bytes(s))
342    }
343
344    /// Hashes every section and checks it against the footer.
345    ///
346    /// This reads the whole file, so it is a decision rather than something that happens on every
347    /// open. The honest place for it is once when a dataset arrives and then never again.
348    ///
349    /// # Errors
350    ///
351    /// Returns [`Error::DigestMismatch`] naming the first section whose bytes do not hash to what
352    /// the footer says they should.
353    pub fn verify(&self) -> Result<()> {
354        for section in &self.sections {
355            let actual = Digest::of(self.section_bytes(section));
356            if actual != section.digest {
357                return Err(Error::DigestMismatch {
358                    what: format!("section {}", section.id),
359                    expected: section.digest.to_string(),
360                    actual: actual.to_string(),
361                });
362            }
363        }
364        Ok(())
365    }
366}