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;