Skip to main content

iris_format/
build.rs

1//! Writing a container.
2//!
3//! The builder holds its sections in memory and can either hand back the finished file or write it
4//! out as it goes. Writing it out is what makes a dataset larger than this host's address space
5//! possible to produce at all, and it is possible because the format puts its directory at the end:
6//! nothing written early depends on anything decided late.
7//!
8//! What is left is that the sections themselves are still held, so a four gigabyte section costs
9//! four gigabytes here. Fixing that means letting a caller push bytes into a section rather than
10//! hand one over whole, and the layout already allows it.
11
12use iris_abi::{CapabilitySet, Writer, wire};
13
14use crate::digest::Digest;
15use crate::error::{Error, Result};
16use crate::layout::{
17    DecoderLocation, FORMAT_MAJOR, FORMAT_MINOR, HEADER_SIZE, MAGIC, SchemaEncoding, SectionKind,
18    TRAILER_SIZE, tag,
19};
20use crate::meta::{Dataset, DecoderRef, Schema, Section};
21
22/// A decoder reference before it has been written, holding its own name.
23#[derive(Clone, Debug)]
24struct PendingDecoder {
25    abi_major: u16,
26    abi_minor: u16,
27    location: DecoderLocation,
28    digest: Digest,
29    required: CapabilitySet,
30    name: String,
31}
32
33/// Builds a container.
34///
35/// ```
36/// use iris_format::{Builder, Container, SectionKind};
37///
38/// let mut builder = Builder::new("readings", 3);
39/// let data = builder.section(SectionKind::Data, b"three rows go here".to_vec());
40/// let bytes = builder.build()?;
41///
42/// let container = Container::parse(&bytes)?;
43/// container.verify()?;
44/// assert_eq!(container.dataset().rows, 3);
45/// assert_eq!(
46///     container.section_bytes(container.section(data).unwrap()),
47///     b"three rows go here"
48/// );
49/// # Ok::<(), iris_format::Error>(())
50/// ```
51#[derive(Clone, Debug)]
52pub struct Builder {
53    dataset: Dataset,
54    schema: Option<(SchemaEncoding, Vec<u8>)>,
55    decoder: Option<PendingDecoder>,
56    sections: Vec<(u32, SectionKind, Vec<u8>)>,
57    next_id: u32,
58}
59
60impl Builder {
61    /// Starts a container for a dataset with this name and this many rows.
62    #[must_use]
63    pub fn new(name: impl Into<String>, rows: u64) -> Self {
64        Self {
65            dataset: Dataset {
66                rows,
67                name: name.into(),
68            },
69            schema: None,
70            decoder: None,
71            sections: Vec::new(),
72            next_id: 0,
73        }
74    }
75
76    /// Sets the schema.
77    pub fn schema(&mut self, encoding: SchemaEncoding, bytes: impl Into<Vec<u8>>) -> &mut Self {
78        self.schema = Some((encoding, bytes.into()));
79        self
80    }
81
82    /// Adds a section and returns the id the rest of the container refers to it by.
83    pub fn section(&mut self, kind: SectionKind, bytes: impl Into<Vec<u8>>) -> u32 {
84        let id = self.next_id;
85        self.next_id += 1;
86        self.sections.push((id, kind, bytes.into()));
87        id
88    }
89
90    /// Puts a decoder module in the container and points the dataset at it.
91    ///
92    /// Returns the id of the section the module went into, which is worth having for a tool that
93    /// wants to print the layout.
94    pub fn embed_decoder(
95        &mut self,
96        name: impl Into<String>,
97        abi: (u16, u16),
98        required: CapabilitySet,
99        module: impl Into<Vec<u8>>,
100    ) -> u32 {
101        let module = module.into();
102        let digest = Digest::of(&module);
103        let section = self.section(SectionKind::Decoder, module);
104        self.decoder = Some(PendingDecoder {
105            abi_major: abi.0,
106            abi_minor: abi.1,
107            location: DecoderLocation::Embedded { section },
108            digest,
109            required,
110            name: name.into(),
111        });
112        section
113    }
114
115    /// Points the dataset at a decoder that lives somewhere else, named by its digest.
116    pub fn external_decoder(
117        &mut self,
118        name: impl Into<String>,
119        abi: (u16, u16),
120        required: CapabilitySet,
121        digest: Digest,
122    ) -> &mut Self {
123        self.decoder = Some(PendingDecoder {
124            abi_major: abi.0,
125            abi_minor: abi.1,
126            location: DecoderLocation::External,
127            digest,
128            required,
129            name: name.into(),
130        });
131        self
132    }
133
134    /// Lays the container out and returns the bytes.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`crate::Error::Footer`] if a name or a schema is longer than the wire format can
139    /// describe, which takes four gigabytes of it.
140    pub fn build(&self) -> Result<Vec<u8>> {
141        let mut out = Vec::new();
142        self.build_into(&mut out)?;
143        Ok(out)
144    }
145
146    /// Lays the container out and writes it, returning how many bytes that was.
147    ///
148    /// The same file as [`Builder::build`], written rather than collected. It matters when the
149    /// sections are large: a container written this way costs the sections themselves and about a
150    /// kilobyte, where collecting it costs the sections twice over and briefly three times while a
151    /// buffer grows. A four gigabyte dataset is the difference between a machine that can write one
152    /// and a machine that cannot.
153    ///
154    /// The sections still have to be in memory, because this builder holds them. That is the next
155    /// thing to fix and the layout is already arranged for it: the directory is at the end, so
156    /// nothing written earlier depends on anything decided later.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`crate::Error::Io`] if the writer refused anything, and
161    /// [`crate::Error::Footer`] if a name or a schema is longer than the wire format can describe.
162    pub fn build_into(&self, out: impl std::io::Write) -> Result<u64> {
163        let mut out = Counted { inner: out, at: 0 };
164
165        out.write(&MAGIC)?;
166        out.write(&FORMAT_MAJOR.to_le_bytes())?;
167        out.write(&FORMAT_MINOR.to_le_bytes())?;
168        out.write(&0u32.to_le_bytes())?;
169        debug_assert_eq!(out.at, HEADER_SIZE as u64);
170
171        let mut placed = Vec::with_capacity(self.sections.len());
172        for (id, kind, bytes) in &self.sections {
173            out.pad()?;
174            placed.push(Section {
175                id: *id,
176                kind: *kind,
177                offset: out.at,
178                len: bytes.len() as u64,
179                digest: Digest::of(bytes),
180            });
181            out.write(bytes)?;
182        }
183
184        out.pad()?;
185        let footer_offset = out.at;
186        let footer = self.encode_footer(&placed)?;
187        out.write(&footer)?;
188
189        // The header is rebuilt here rather than kept, because it is sixteen bytes of constants and
190        // holding the bytes that were written would be one more thing that has to stay in step with
191        // what was written.
192        let mut hasher = blake3::Hasher::new();
193        hasher.update(&MAGIC);
194        hasher.update(&FORMAT_MAJOR.to_le_bytes());
195        hasher.update(&FORMAT_MINOR.to_le_bytes());
196        hasher.update(&0u32.to_le_bytes());
197        hasher.update(&footer);
198        let root = Digest(*hasher.finalize().as_bytes());
199
200        out.write(&footer_offset.to_le_bytes())?;
201        // The footer is metadata about a dataset, not the dataset. Four gigabytes of it would mean
202        // something has gone wrong that a wider length field would not fix.
203        let footer_len =
204            u32::try_from(footer.len()).map_err(|_| iris_abi::Error::LengthOverflow)?;
205        out.write(&footer_len.to_le_bytes())?;
206        out.write(&0u32.to_le_bytes())?;
207        out.write(root.as_bytes())?;
208        out.write(&MAGIC)?;
209        debug_assert_eq!(
210            out.at,
211            footer_offset + footer.len() as u64 + TRAILER_SIZE as u64
212        );
213
214        Ok(out.at)
215    }
216
217    /// Encodes the footer, growing the buffer until it fits.
218    ///
219    /// The `iris-abi` writer never grows its own buffer, which is what makes it usable from a guest
220    /// with no allocator. On the host side the cost of guessing wrong is one memcpy of a footer, so
221    /// guessing and retrying is simpler than computing the exact size twice and keeping the two
222    /// computations in agreement.
223    fn encode_footer(&self, sections: &[Section]) -> Result<Vec<u8>> {
224        let mut capacity = 1024 + sections.len() * 128;
225        loop {
226            let mut buf = vec![0u8; capacity];
227            match self.write_footer(&mut Writer::new(&mut buf), sections) {
228                Ok(written) => {
229                    buf.truncate(written);
230                    return Ok(buf);
231                }
232                Err(iris_abi::Error::BufferFull { .. }) => capacity *= 2,
233                Err(other) => return Err(other.into()),
234            }
235        }
236    }
237
238    fn write_footer(
239        &self,
240        w: &mut Writer<'_>,
241        sections: &[Section],
242    ) -> core::result::Result<usize, iris_abi::Error> {
243        w.record(tag::DATASET, Dataset::VERSION, |w| self.dataset.encode(w))?;
244        if let Some((encoding, bytes)) = &self.schema {
245            let schema = Schema {
246                encoding: *encoding,
247                bytes,
248            };
249            w.record(tag::SCHEMA, Schema::VERSION, |w| schema.encode(w))?;
250        }
251        if let Some(decoder) = &self.decoder {
252            let reference = DecoderRef {
253                abi_major: decoder.abi_major,
254                abi_minor: decoder.abi_minor,
255                location: decoder.location,
256                digest: decoder.digest,
257                required: decoder.required,
258                name: &decoder.name,
259            };
260            w.record(tag::DECODER, DecoderRef::VERSION, |w| reference.encode(w))?;
261        }
262        for section in sections {
263            w.record(tag::SECTION, Section::VERSION, |w| section.encode(w))?;
264        }
265        Ok(w.position())
266    }
267}
268
269/// A writer that remembers how far into the file it is.
270///
271/// Every offset in a container is an offset from the start of the file, and a writer does not know
272/// where it is. Counting here rather than asking the writer is what lets this work over a pipe, a
273/// file and a `Vec` without any of them having to answer the same question three different ways.
274struct Counted<W> {
275    inner: W,
276    at: u64,
277}
278
279impl<W: std::io::Write> Counted<W> {
280    fn write(&mut self, bytes: &[u8]) -> Result<()> {
281        self.inner.write_all(bytes).map_err(|err| Error::io(&err))?;
282        self.at += bytes.len() as u64;
283        Ok(())
284    }
285
286    /// Pads out to the next eight byte boundary, so that a section starts somewhere a decoder can
287    /// point a wider load at.
288    ///
289    /// A byte at a time, and at most seven of them. Doing it that way keeps the arithmetic in file
290    /// offsets, which are sixty four bits wide because a container is allowed to be longer than this
291    /// host can address, and seven one byte writes cost nothing next to the section behind them.
292    fn pad(&mut self) -> Result<()> {
293        while !self.at.is_multiple_of(ALIGN) {
294            self.write(&[0])?;
295        }
296        Ok(())
297    }
298}
299
300/// What every section starts on, as a file offset rather than as a length.
301const ALIGN: u64 = wire::ALIGN as u64;