Skip to main content

ic_host_process/framing/
mod.rs

1//! Caller-bounded newline framing without message schemas or process ownership.
2use std::{
3    fmt,
4    io::{self, BufRead, Read},
5};
6
7/// A refused frame; display never prints its potentially sensitive bytes.
8#[derive(Debug)]
9pub enum FrameReadError {
10    /// The stream ended before any frame bytes arrived.
11    EndOfStream,
12    /// The frame exceeded the caller's limit, including its newline.
13    TooLarge(Vec<u8>),
14    /// The stream ended with a partial frame and no newline.
15    Unterminated(Vec<u8>),
16    /// Reading, allocation, frame delivery or post-refusal admission failed.
17    Io(io::Error),
18}
19
20impl FrameReadError {
21    /// Borrow a refused prefix, retained up to the caller limit plus one byte.
22    #[must_use]
23    pub fn rejected_bytes(&self) -> Option<&[u8]> {
24        match self {
25            Self::TooLarge(bytes) | Self::Unterminated(bytes) => Some(bytes),
26            Self::EndOfStream | Self::Io(_) => None,
27        }
28    }
29}
30
31impl fmt::Display for FrameReadError {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::EndOfStream => formatter.write_str("frame stream closed"),
35            Self::TooLarge(_) => formatter.write_str("frame exceeds caller byte limit"),
36            Self::Unterminated(_) => formatter.write_str("frame is missing its newline"),
37            Self::Io(_) => formatter.write_str("frame read failed"),
38        }
39    }
40}
41
42impl std::error::Error for FrameReadError {
43    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
44        match self {
45            Self::Io(error) => Some(error),
46            _ => None,
47        }
48    }
49}
50
51/// Read one frame, retaining its newline and leaving later frames buffered.
52///
53/// `maximum` includes the newline. At most one excess byte is consumed to detect
54/// overflow; this bounds retained bytes, not allocator capacity or elapsed time.
55/// The caller owns parsing, deadlines, thread/process lifetime and diagnostics.
56/// A refusal does not resynchronize the channel; callers must stop it or choose
57/// their own recovery policy.
58/// # Errors
59/// Distinguishes clean EOF, missing newline, overflow and original I/O failures.
60pub fn read_line(reader: &mut impl BufRead, maximum: u64) -> Result<Vec<u8>, FrameReadError> {
61    let mut bytes = Vec::new();
62    reader
63        .take(maximum.saturating_add(1))
64        .read_until(b'\n', &mut bytes)
65        .map_err(FrameReadError::Io)?;
66    if bytes.is_empty() {
67        return Err(FrameReadError::EndOfStream);
68    }
69    if bytes.len() as u64 > maximum {
70        return Err(FrameReadError::TooLarge(bytes));
71    }
72    if !bytes.ends_with(b"\n") {
73        return Err(FrameReadError::Unterminated(bytes));
74    }
75    Ok(bytes)
76}
77
78/// Incremental newline framing for process-output callbacks.
79///
80/// The byte allowance includes the newline. Completed frames borrow reusable
81/// scratch until the callback returns. JSON, EOF acceptance, process lifetime
82/// and deadlines remain caller policy. After any failure, stop using the framer;
83/// no later input is admitted or implicitly drained.
84pub struct LineFramer {
85    maximum: u64,
86    pending: Option<Vec<u8>>,
87}
88
89impl LineFramer {
90    /// Create an empty framer with the caller's per-frame byte allowance.
91    #[must_use]
92    pub const fn new(maximum: u64) -> Self {
93        Self {
94            maximum,
95            pending: Some(Vec::new()),
96        }
97    }
98
99    /// Deliver each complete newline-terminated frame in order, retaining a tail.
100    ///
101    /// Oversized frames retain at most the allowance plus one refused byte.
102    /// On failure, the rest of this supplied chunk is abandoned; the caller must
103    /// stop the stream or choose its own recovery. Allocator capacity is not a
104    /// byte allowance, and already emitted frames are not rolled back.
105    ///
106    /// # Errors
107    /// Returns overflow, the original allocation/sink failure, or an invalid-input
108    /// I/O error when called again after refusal. Display never includes bytes.
109    pub fn feed(
110        &mut self,
111        chunk: &[u8],
112        mut emit: impl FnMut(&[u8]) -> io::Result<()>,
113    ) -> Result<(), FrameReadError> {
114        let mut pending = self.pending.take().ok_or_else(refused)?;
115        for fragment in chunk.split_inclusive(|byte| *byte == b'\n') {
116            let remaining = self.maximum.saturating_sub(pending.len() as u64);
117            if fragment.len() as u64 > remaining {
118                let prefix = usize::try_from(remaining)
119                    .map_err(|error| FrameReadError::Io(io::Error::other(error)))?
120                    + 1;
121                pending
122                    .try_reserve_exact(prefix)
123                    .map_err(|error| FrameReadError::Io(io::Error::other(error)))?;
124                pending.extend_from_slice(&fragment[..prefix]);
125                return Err(FrameReadError::TooLarge(pending));
126            }
127            pending
128                .try_reserve_exact(fragment.len())
129                .map_err(|error| FrameReadError::Io(io::Error::other(error)))?;
130            pending.extend_from_slice(fragment);
131            if pending.ends_with(b"\n") {
132                emit(&pending).map_err(FrameReadError::Io)?;
133                pending.clear();
134            }
135        }
136        self.pending = Some(pending);
137        Ok(())
138    }
139
140    /// Finish cleanly at a frame boundary, or return the owned unterminated tail.
141    ///
142    /// Callers may explicitly accept that tail when their protocol permits it.
143    ///
144    /// # Errors
145    /// Returns an unterminated frame, or invalid input after an earlier refusal.
146    pub fn finish(self) -> Result<(), FrameReadError> {
147        let pending = self.pending.ok_or_else(refused)?;
148        if pending.is_empty() {
149            Ok(())
150        } else {
151            Err(FrameReadError::Unterminated(pending))
152        }
153    }
154}
155
156fn refused() -> FrameReadError {
157    FrameReadError::Io(io::Error::new(
158        io::ErrorKind::InvalidInput,
159        "framer previously refused the stream",
160    ))
161}
162
163#[cfg(test)]
164mod tests;