Skip to main content

coven_format/
write_stream.rs

1//! Write headers describe independently framed, chunked audience streams (ยง15.6).
2
3use crate::chunks::{FrameDecoder, PlaintextChunks, CHUNK_SIZE};
4use crate::dismissal::WriteFrame;
5use crate::error::{require, Error, Rule};
6use crate::wire::{decode_frame_value, wire_struct};
7use crate::write::{WriteHeader, WriteRecord};
8use crate::{encode_frame, FRAME_PREFIX_LEN};
9use coven_merge::{Audience, RowId, WriteId};
10
11/// The plaintext stream boundaries of one audience part.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct PartHeader {
14    /// Every row in the stream belongs to this audience.
15    pub audience: Audience,
16    /// Number of change, execution and dismissal frames, without a per-write
17    /// collection bound.
18    pub record_count: u64,
19    /// Sum of the complete change and dismissal frames' lengths, including frame prefixes.
20    pub plaintext_length: u64,
21}
22wire_struct!(PartHeader, audience, record_count, plaintext_length);
23impl PartHeader {
24    pub(crate) fn validate(&self) -> Result<(), Error> {
25        require(self.record_count > 0, "write part rows", Rule::Required)?;
26        require(
27            self.record_count <= self.plaintext_length / FRAME_PREFIX_LEN as u64,
28            "write part length",
29            Rule::StreamLength,
30        )
31    }
32
33    /// Number of sealed chunks required by this part's plaintext length.
34    pub fn chunk_count(&self) -> u64 {
35        crate::chunks::chunk_count(self.plaintext_length)
36    }
37}
38
39/// The kind-1 header frame; changes and dismissals are separate kind-2 and kind-3 frames.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct WriteHeaderFrame {
42    /// Identity, last consumed slot, timestamp and schema.
43    pub header: WriteHeader,
44    /// Descriptors in strictly increasing audience order. Empty only for a migration.
45    pub parts: Vec<PartHeader>,
46}
47wire_struct!(WriteHeaderFrame, header, parts);
48impl WriteHeaderFrame {
49    /// Validate and encode the bounded header frame.
50    pub fn encode(&self) -> Result<Vec<u8>, Error> {
51        self.validate()?;
52        encode_frame(crate::kinds::WRITE_HEADER, self)
53    }
54
55    /// Decode exactly one header, refusing trailing bytes and invalid counts.
56    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
57        let header: Self =
58            decode_frame_value(bytes, crate::kinds::WRITE_HEADER, "write header kind")?;
59        header.validate()?;
60        Ok(header)
61    }
62
63    fn validate(&self) -> Result<(), Error> {
64        self.header.validate_parts(self.parts.iter().map(|part| &part.audience))?;
65        for part in &self.parts {
66            part.validate()?;
67        }
68        Ok(())
69    }
70}
71
72/// A borrowed write producer. Before emitting, it encodes one record at a time
73/// to measure each part's byte length and construct the header;
74/// emitting a part retains at most one record frame and one 64-KiB chunk.
75pub struct WriteEncoder<'a> {
76    record: &'a WriteRecord,
77    /// Each part's record frames, when the caller encoded them.
78    encoded: Option<&'a [Vec<Vec<u8>>]>,
79    header: WriteHeaderFrame,
80    frame: Vec<u8>,
81    length: u64,
82}
83impl<'a> WriteEncoder<'a> {
84    /// Validate the complete in-memory write and measure each part's stream.
85    pub fn new(record: &'a WriteRecord) -> Result<Self, Error> {
86        record.validate()?;
87        Self::measured(record, None)
88    }
89
90    /// `new` for a write whose records the caller encoded already, a part's
91    /// `WritePart::encode_records` each, in the parts' order: they are
92    /// emitted as given, validated and encoded once, and the bytes are those
93    /// `new` produces.
94    pub fn with_records(
95        record: &'a WriteRecord,
96        encoded: &'a [Vec<Vec<u8>>],
97    ) -> Result<Self, Error> {
98        require(
99            encoded.len() == record.parts.len()
100                && record
101                    .parts
102                    .iter()
103                    .zip(encoded)
104                    .all(|(part, frames)| frames.len() == part.records().count()),
105            "write encoded records",
106            Rule::Required,
107        )?;
108        record.validate_encoded()?;
109        Self::measured(record, Some(encoded))
110    }
111
112    fn measured(
113        record: &'a WriteRecord,
114        encoded: Option<&'a [Vec<Vec<u8>>]>,
115    ) -> Result<Self, Error> {
116        let mut parts = Vec::new();
117        for (index, part) in record.parts.iter().enumerate() {
118            let mut length = 0u64;
119            for frame in part_frames(part, encoded.map(|encoded| &encoded[index][..])) {
120                length = add_length(length, frame?.len() as u64)?;
121            }
122            parts.push(PartHeader {
123                audience: part.audience.clone(),
124                record_count: (part.rows.len() + part.executions.len() + part.dismissals.len())
125                    as u64,
126                plaintext_length: length,
127            });
128        }
129        let header = WriteHeaderFrame { header: record.header.clone(), parts };
130        let frame = header.encode()?;
131        let mut length = frame.len() as u64;
132        for part in &header.parts {
133            length = add_length(length, part.plaintext_length)?;
134        }
135        Ok(Self { record, encoded, header, frame, length })
136    }
137
138    /// Metadata that lets a receiver delimit each part without opening it.
139    pub fn header(&self) -> &WriteHeaderFrame {
140        &self.header
141    }
142
143    /// The header's complete kind-1 plaintext frame.
144    pub fn header_frame(&self) -> &[u8] {
145        &self.frame
146    }
147
148    /// The queued plaintext length, for the database's SQLite value limit.
149    pub fn plaintext_length(&self) -> u64 {
150        self.length
151    }
152
153    /// Encode the upload-queue value into an exactly sized buffer. The database
154    /// checks [`Self::plaintext_length`] against SQLite's limit before allocating
155    /// this buffer. A different buffer length is refused before writing bytes.
156    pub fn encode_plaintext(&self, output: &mut [u8]) -> Result<(), Error> {
157        require(
158            output.len() as u64 == self.length,
159            "write plaintext buffer length",
160            Rule::StreamLength,
161        )?;
162        let (header, mut remaining) = output.split_at_mut(self.frame.len());
163        header.copy_from_slice(&self.frame);
164        for index in 0..self.header.parts.len() {
165            for chunk in self.part_chunks(index)? {
166                let chunk = chunk?;
167                let (piece, rest) = remaining.split_at_mut(chunk.len());
168                piece.copy_from_slice(&chunk);
169                remaining = rest;
170            }
171        }
172        assert!(remaining.is_empty(), "encoded the measured write length");
173        Ok(())
174    }
175
176    /// Cut the selected audience's change and dismissal frames into canonical plaintext chunks.
177    pub fn part_chunks(
178        &self,
179        index: usize,
180    ) -> Result<PlaintextChunks<impl Iterator<Item = Result<Vec<u8>, Error>> + '_>, Error> {
181        let part = self
182            .record
183            .parts
184            .get(index)
185            .ok_or(Error::Invalid { field: "write part index", rule: Rule::Chunk })?;
186        Ok(PlaintextChunks::new(part_frames(part, self.encoded.map(|encoded| &encoded[index][..]))))
187    }
188}
189
190/// A part's frames: its records encoded now, or as `encoded` has them.
191fn part_frames<'a>(
192    part: &'a crate::write::WritePart,
193    encoded: Option<&'a [Vec<u8>]>,
194) -> Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + 'a> {
195    match encoded {
196        Some(encoded) => Box::new(part.frames_from(encoded)),
197        None => Box::new(part.frames()),
198    }
199}
200
201fn add_length(a: u64, b: u64) -> Result<u64, Error> {
202    a.checked_add(b)
203        .ok_or(Error::Invalid { field: "write plaintext length", rule: Rule::StreamLength })
204}
205
206/// Decode opened chunks of one part in order, retaining only its unfinished
207/// frame and previous record identity. The caller stages yielded records until the
208/// whole object and signature have been checked. `finish` checks the declared
209/// record count and byte length; frames crossing the part's end are refused
210/// before their allocation.
211pub struct PartDecoder {
212    header: PartHeader,
213    frames: FrameDecoder,
214    received: u64,
215    rows: u64,
216    previous: Option<(RowId, Option<(crate::dismissal::DismissalTarget, WriteId)>)>,
217}
218impl PartDecoder {
219    /// Begin the stream described by the authenticated write header.
220    pub fn new(header: PartHeader) -> Result<Self, Error> {
221        header.validate()?;
222        Ok(Self {
223            frames: FrameDecoder::new(Some(header.plaintext_length)),
224            header,
225            received: 0,
226            rows: 0,
227            previous: None,
228        })
229    }
230
231    /// Feed exactly the next chunk. Full chunks are 64 KiB, and the last chunk
232    /// has the header's remaining length. Discard the decoder after any error.
233    pub fn chunk(&mut self, mut bytes: &[u8]) -> Result<Vec<WriteFrame>, Error> {
234        let left = self.header.plaintext_length - self.received;
235        require(
236            left > 0 && bytes.len() as u64 == left.min(CHUNK_SIZE as u64),
237            "write part chunk length",
238            Rule::StreamLength,
239        )?;
240        self.received += bytes.len() as u64;
241        let mut rows = Vec::new();
242        while let Some(frame) = self.frames.next(&mut bytes)? {
243            require(
244                self.rows < self.header.record_count,
245                "write part row count",
246                Rule::StreamLength,
247            )?;
248            let row = WriteFrame::decode(&frame)?;
249            let key = row.key();
250            require(key.0.audience == self.header.audience, "write part audience", Rule::Audience)?;
251            require(
252                self.previous.as_ref().is_none_or(|previous| *previous < key),
253                "write part rows",
254                Rule::Order,
255            )?;
256            self.previous = Some(key);
257            self.rows += 1;
258            rows.push(row);
259            if bytes.is_empty() {
260                break;
261            }
262        }
263        Ok(rows)
264    }
265
266    /// Refuse an unfinished frame, missing bytes, or a mismatched record count.
267    pub fn finish(&self) -> Result<(), Error> {
268        self.frames.finish()?;
269        require(
270            self.received == self.header.plaintext_length && self.rows == self.header.record_count,
271            "write part row count/length",
272            Rule::StreamLength,
273        )
274    }
275}
276
277/// Decode a plaintext upload-queue value into the database's in-memory write.
278/// Stream consumers can instead use [`WriteHeaderFrame`] and [`PartDecoder`].
279#[cfg(any(test, feature = "test-utils"))]
280#[expect(
281    clippy::map_err_ignore,
282    reason = "a part longer than usize cannot be in these bytes: they are truncated"
283)]
284pub fn decode_plaintext(bytes: &[u8]) -> Result<WriteRecord, Error> {
285    let length = crate::frame_length(bytes)?;
286    let header = WriteHeaderFrame::decode(bytes.get(..length).ok_or(Error::Truncated)?)?;
287    let mut bytes = &bytes[length..];
288    let mut parts = Vec::new();
289    for part in header.parts {
290        let length = usize::try_from(part.plaintext_length).map_err(|_| Error::Truncated)?;
291        let stream = bytes.get(..length).ok_or(Error::Truncated)?;
292        bytes = &bytes[length..];
293        let audience = part.audience.clone();
294        let mut decoder = PartDecoder::new(part)?;
295        let mut rows = Vec::new();
296        let mut executions = Vec::new();
297        let mut dismissals = Vec::new();
298        for chunk in stream.chunks(CHUNK_SIZE) {
299            for frame in decoder.chunk(chunk)? {
300                match frame {
301                    WriteFrame::Change(row) => rows.push(row),
302                    WriteFrame::Execution(execution) => executions.push(execution),
303                    WriteFrame::Dismissal(dismissal) => dismissals.push(dismissal),
304                }
305            }
306        }
307        decoder.finish()?;
308        parts.push(crate::write::WritePart { audience, rows, executions, dismissals });
309    }
310    if !bytes.is_empty() {
311        return Err(Error::TrailingBytes);
312    }
313    let record = WriteRecord { header: header.header, parts };
314    record.validate()?;
315    Ok(record)
316}
317
318#[cfg(test)]
319#[path = "write_stream_tests.rs"]
320mod tests;