Skip to main content

lace_sans_io/iltp/
output.rs

1//! The outgoing half of one carrier codec: the accepted output batch, produced
2//! progressively into one bounded chunk.
3//!
4//! Prefaces and control lines are rendered one line at a time and records are
5//! read from their canonical bytes after their label line, followed by their
6//! framing LF. Everything after a
7//! raw preface passes through
8//! the direction's 065 encoder into a chunk of at most the codec's chunk size,
9//! grown only as far as the batch can use. An `identity` direction hands a
10//! record that does not fit the current chunk to the writer in place, at most
11//! one chunk of it at a time, instead of copying it. The writer takes the current bytes, reports how many it
12//! wrote, and asks again; partial writes keep the same chunk and offset.
13//!
14//! A record without contiguous bytes (a file range) is read in pieces of at
15//! most one chunk: the cursor hands its owner a [`RecordRead`], the owner
16//! performs it, possibly on a blocking worker, and returns it before asking
17//! for more bytes.
18
19use super::control::{LineCursor, next_line};
20use super::encoded_stream::{Incoming, Outgoing, install};
21use super::framing::{IltpError, IltpItem, write_preface};
22use super::stream_encoding::{ENCODE_MIN_OUTPUT, EncodeMode, EncodeProgress};
23use crate::records::Record;
24
25/// Chunk room beyond a batch's plaintext that encoder framing, flushes and
26/// end marks may use.
27const ENCODING_SLACK: usize = 64;
28
29pub(super) struct Output {
30    chunk_bytes: usize,
31    /// Zero-filled chunk storage, grown lazily up to `chunk_bytes`.
32    chunk: Vec<u8>,
33    filled: usize,
34    /// Bytes the writer took from the chunk, or from the in-place record.
35    taken: usize,
36    /// An `identity` record handed to the writer without copying.
37    in_place: Option<Record>,
38    /// The current preface or control line.
39    line: Vec<u8>,
40    /// Bytes read from a file-range record and the count encoded so far.
41    read: Vec<u8>,
42    read_used: usize,
43    read_state: ReadState,
44    batch: Option<Batch>,
45}
46
47enum ReadState {
48    Idle,
49    /// Prepared and not yet handed to the owner.
50    Requested(RecordRead),
51    /// With the owner.
52    Outstanding,
53}
54
55/// The next bytes of accepted output; see [`IltpCodec::output_bytes`](super::IltpCodec::output_bytes).
56#[derive(Debug)]
57pub enum OutputBytes<'a> {
58    /// Bytes the writer must take next.
59    Write(&'a [u8]),
60    /// Record bytes the owner must read and return with
61    /// [`IltpCodec::complete_read`](super::IltpCodec::complete_read) before
62    /// more output exists.
63    Read(RecordRead),
64    /// The accepted output is complete, or none is accepted.
65    Complete,
66}
67
68/// A read of `buffer.len()` canonical bytes of `record` at `offset`.
69#[derive(Debug)]
70pub struct RecordRead {
71    record: Record,
72    offset: usize,
73    buffer: Vec<u8>,
74}
75
76impl RecordRead {
77    /// Fill the buffer from the record's source. This is blocking file IO.
78    pub fn run(mut self) -> std::io::Result<Self> {
79        let mut filled = 0;
80        while filled < self.buffer.len() {
81            match self
82                .record
83                .read_at(self.offset + filled, &mut self.buffer[filled..])?
84            {
85                0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
86                count => filled += count,
87            }
88        }
89        Ok(self)
90    }
91}
92
93struct Batch {
94    items: Vec<IltpItem>,
95    /// Canonical plaintext bytes of all items, which bound useful chunk size.
96    plaintext: usize,
97    step: Step,
98    /// Offset into the current line or record.
99    offset: usize,
100    /// Encoded items follow the last preface, so the batch ends with a flush.
101    flush: bool,
102}
103
104#[derive(Debug, Clone, Copy)]
105enum Step {
106    /// Start item `index`; past the last item, finish the batch.
107    Item(usize),
108    /// End the previous logical session's encoded stream before preface `index`.
109    EndBeforePreface(usize),
110    /// The raw bytes of preface `index`, held in `line`.
111    Preface(usize),
112    /// The current line of control item `index`, held in `line`.
113    Line(usize, LineCursor),
114    Record(usize),
115    /// The LF after record `index`.
116    RecordEnd(usize),
117    Flush,
118    End,
119    Done,
120}
121
122impl Output {
123    pub(super) fn new(chunk_bytes: usize) -> Self {
124        Self {
125            chunk_bytes,
126            chunk: Vec::new(),
127            filled: 0,
128            taken: 0,
129            in_place: None,
130            line: Vec::new(),
131            read: Vec::new(),
132            read_used: 0,
133            read_state: ReadState::Idle,
134            batch: None,
135        }
136    }
137
138    /// Bytes of chunk storage allocated so far.
139    #[cfg(test)]
140    pub(super) fn chunk_len(&self) -> usize {
141        self.chunk.len()
142    }
143
144    pub(super) fn is_pending(&self) -> bool {
145        self.batch.is_some()
146    }
147
148    /// Accept validated items holding `plaintext` canonical bytes.
149    pub(super) fn accept(&mut self, items: Vec<IltpItem>, plaintext: usize) {
150        self.start(items, plaintext, Step::Item(0));
151    }
152
153    /// Accept the end of the outgoing encoded stream.
154    pub(super) fn accept_end(&mut self) {
155        self.start(Vec::new(), 0, Step::End);
156    }
157
158    fn start(&mut self, items: Vec<IltpItem>, plaintext: usize, step: Step) {
159        debug_assert!(self.batch.is_none());
160        self.batch = Some(Batch {
161            items,
162            plaintext,
163            step,
164            offset: 0,
165            flush: false,
166        });
167    }
168
169    pub(super) fn discard(&mut self) {
170        self.batch = None;
171        self.in_place = None;
172        self.filled = 0;
173        self.taken = 0;
174        self.read.clear();
175        self.read_used = 0;
176        self.read_state = ReadState::Idle;
177    }
178
179    /// The next bytes the writer must take, producing them when the previous
180    /// ones are taken, or the record read that must precede them.
181    pub(super) fn bytes(
182        &mut self,
183        outgoing: &mut Outgoing,
184        incoming: &mut Incoming,
185    ) -> Result<OutputBytes<'_>, IltpError> {
186        if matches!(self.read_state, ReadState::Outstanding) {
187            return Err(IltpError::Local("ILTP output read is outstanding"));
188        }
189        if self.in_place.is_none()
190            && self.taken == self.filled
191            && matches!(self.read_state, ReadState::Idle)
192        {
193            self.fill(outgoing, incoming)?;
194        }
195        if let Some(record) = &self.in_place {
196            let bytes = record
197                .contiguous_bytes()
198                .ok_or(IltpError::Local("in-place record is not contiguous"))?;
199            let rest = &bytes[self.taken..];
200            return Ok(OutputBytes::Write(
201                &rest[..rest.len().min(self.chunk_bytes)],
202            ));
203        }
204        if self.taken < self.filled {
205            return Ok(OutputBytes::Write(&self.chunk[self.taken..self.filled]));
206        }
207        Ok(
208            match std::mem::replace(&mut self.read_state, ReadState::Outstanding) {
209                ReadState::Requested(read) => OutputBytes::Read(read),
210                _ => {
211                    self.read_state = ReadState::Idle;
212                    OutputBytes::Complete
213                }
214            },
215        )
216    }
217
218    /// Accept the owner's completed [`RecordRead`].
219    pub(super) fn complete_read(&mut self, read: RecordRead) -> Result<(), IltpError> {
220        let expected = match &self.batch {
221            Some(Batch {
222                step: Step::Record(_),
223                offset,
224                ..
225            }) => *offset,
226            // Output discarded while the read ran.
227            None => return Ok(()),
228            _ => return Err(IltpError::Local("ILTP output read without its record")),
229        };
230        if !matches!(self.read_state, ReadState::Outstanding) || read.offset != expected {
231            return Err(IltpError::Local(
232                "ILTP output read does not match its request",
233            ));
234        }
235        self.read = read.buffer;
236        self.read_used = 0;
237        self.read_state = ReadState::Idle;
238        Ok(())
239    }
240
241    /// Accept `count` written bytes of the current [`bytes`](Self::bytes).
242    pub(super) fn advance(&mut self, count: usize) {
243        self.taken += count;
244        match &self.in_place {
245            Some(record) if self.taken >= record.canonical_len() => {
246                self.in_place = None;
247                self.taken = 0;
248                self.filled = 0;
249            }
250            Some(_) => {}
251            None => debug_assert!(self.taken <= self.filled),
252        }
253        self.finish_when_taken();
254    }
255
256    fn finish_when_taken(&mut self) {
257        if self.in_place.is_none()
258            && self.taken == self.filled
259            && matches!(&self.batch, Some(batch) if matches!(batch.step, Step::Done))
260        {
261            self.batch = None;
262        }
263    }
264
265    /// Produce the next chunk, or hand out the next record in place.
266    fn fill(&mut self, outgoing: &mut Outgoing, incoming: &mut Incoming) -> Result<(), IltpError> {
267        self.filled = 0;
268        self.taken = 0;
269        let Self {
270            chunk_bytes,
271            chunk,
272            filled,
273            taken,
274            in_place,
275            line,
276            read,
277            read_used,
278            read_state,
279            batch,
280        } = self;
281        let Some(batch) = batch else {
282            return Ok(());
283        };
284        let useful = batch
285            .plaintext
286            .saturating_add(ENCODING_SLACK)
287            .min(*chunk_bytes);
288        if chunk.len() < useful {
289            chunk.resize(useful, 0);
290        }
291        loop {
292            match batch.step {
293                Step::Item(index) => {
294                    batch.offset = 0;
295                    batch.step = match batch.items.get(index) {
296                        None if batch.flush => Step::Flush,
297                        None => Step::Done,
298                        Some(IltpItem::SessionPreface(preface)) => {
299                            line.clear();
300                            write_preface(preface, line);
301                            Step::EndBeforePreface(index)
302                        }
303                        Some(item) => {
304                            batch.flush = true;
305                            let mut cursor = LineCursor::default();
306                            next_line(item, &mut cursor, line);
307                            Step::Line(index, cursor)
308                        }
309                    };
310                }
311                Step::EndBeforePreface(index) => {
312                    match encode_step(outgoing, chunk, filled, &[], EncodeMode::End)? {
313                        Some(progress) if progress.done => batch.step = Step::Preface(index),
314                        Some(_) => {}
315                        None => break,
316                    }
317                }
318                Step::Preface(index) => {
319                    let count = copy(&line[batch.offset..], &mut chunk[*filled..]);
320                    if count == 0 {
321                        break;
322                    }
323                    *filled += count;
324                    batch.offset += count;
325                    if batch.offset == line.len() {
326                        let IltpItem::SessionPreface(preface) = &batch.items[index] else {
327                            unreachable!("preface step on another item");
328                        };
329                        *outgoing = Outgoing::Pending(preface.offers.clone());
330                        if let Incoming::Pending(peer) = incoming {
331                            let peer = peer.clone();
332                            install(outgoing, incoming, &preface.offers, &peer, *chunk_bytes)?;
333                        }
334                        batch.flush = false;
335                        batch.step = Step::Item(index + 1);
336                    }
337                }
338                Step::Line(index, mut cursor) => {
339                    let input = &line[batch.offset..];
340                    match encode_step(outgoing, chunk, filled, input, EncodeMode::Write)? {
341                        Some(progress) => batch.offset += progress.consumed,
342                        None => break,
343                    }
344                    if batch.offset == line.len() {
345                        batch.offset = 0;
346                        let item = &batch.items[index];
347                        batch.step = if next_line(item, &mut cursor, line) {
348                            Step::Line(index, cursor)
349                        } else if matches!(item, IltpItem::Record(..)) {
350                            Step::Record(index)
351                        } else {
352                            Step::Item(index + 1)
353                        };
354                    }
355                }
356                Step::Record(index) => {
357                    let IltpItem::Record(_, record) = &batch.items[index] else {
358                        unreachable!("record step on another item");
359                    };
360                    let Some(bytes) = record.contiguous_bytes() else {
361                        let rest = record.canonical_len() - batch.offset;
362                        if rest == 0 {
363                            batch.offset = 0;
364                            batch.step = Step::RecordEnd(index);
365                        } else if *read_used < read.len() {
366                            let input = &read[*read_used..];
367                            match encode_step(outgoing, chunk, filled, input, EncodeMode::Write)? {
368                                Some(progress) => {
369                                    batch.offset += progress.consumed;
370                                    *read_used += progress.consumed;
371                                }
372                                None => break,
373                            }
374                        } else {
375                            let mut buffer = std::mem::take(read);
376                            buffer.clear();
377                            buffer.resize(rest.min(*chunk_bytes), 0);
378                            *read_used = 0;
379                            *read_state = ReadState::Requested(RecordRead {
380                                record: record.clone(),
381                                offset: batch.offset,
382                                buffer,
383                            });
384                            break;
385                        }
386                        continue;
387                    };
388                    let rest = &bytes[batch.offset..];
389                    if rest.is_empty() {
390                        batch.offset = 0;
391                        batch.step = Step::RecordEnd(index);
392                    } else if outgoing.is_identity() && rest.len() > chunk.len() - *filled {
393                        if *filled > 0 {
394                            break;
395                        }
396                        *taken = batch.offset;
397                        *in_place = Some(record.clone());
398                        batch.offset = 0;
399                        batch.step = Step::RecordEnd(index);
400                        return Ok(());
401                    } else {
402                        match encode_step(outgoing, chunk, filled, rest, EncodeMode::Write)? {
403                            Some(progress) => batch.offset += progress.consumed,
404                            None => break,
405                        }
406                    }
407                }
408                Step::RecordEnd(index) => {
409                    match encode_step(outgoing, chunk, filled, b"\n", EncodeMode::Write)? {
410                        Some(progress) if progress.consumed == 1 => {
411                            batch.step = Step::Item(index + 1);
412                        }
413                        Some(_) => {}
414                        None => break,
415                    }
416                }
417                Step::Flush | Step::End => {
418                    let mode = match batch.step {
419                        Step::Flush => EncodeMode::Flush,
420                        _ => EncodeMode::End,
421                    };
422                    match encode_step(outgoing, chunk, filled, &[], mode)? {
423                        Some(progress) if progress.done => batch.step = Step::Done,
424                        Some(_) => {}
425                        None => break,
426                    }
427                }
428                Step::Done => break,
429            }
430        }
431        self.finish_when_taken();
432        Ok(())
433    }
434}
435
436fn copy(input: &[u8], room: &mut [u8]) -> usize {
437    let count = input.len().min(room.len());
438    room[..count].copy_from_slice(&input[..count]);
439    count
440}
441
442/// One encoder step into the chunk's free room. `None` when the room is too
443/// small, so the filled bytes go to the writer first.
444fn encode_step(
445    outgoing: &mut Outgoing,
446    chunk: &mut [u8],
447    filled: &mut usize,
448    input: &[u8],
449    mode: EncodeMode,
450) -> Result<Option<EncodeProgress>, IltpError> {
451    let room = &mut chunk[*filled..];
452    if room.len() < ENCODE_MIN_OUTPUT {
453        return Ok(None);
454    }
455    let progress = outgoing.encode(input, mode, room)?;
456    *filled += progress.produced;
457    if !progress.done && progress.consumed == 0 && progress.produced == 0 {
458        return if *filled > 0 {
459            Ok(None)
460        } else {
461            Err(IltpError::Local("ILTP stream encoder made no progress"))
462        };
463    }
464    Ok(Some(progress))
465}