Skip to main content

secretspec_ipc/
frame.rs

1use crate::ABSOLUTE_MAX_FRAME_BYTES;
2use crate::error::{Error, Result};
3use zeroize::Zeroizing;
4
5/// Incremental bounded NDJSON decoder. A frame is one non-empty UTF-8 JSON
6/// object followed by LF; the limit applies to JSON bytes, not the delimiter.
7pub struct FrameDecoder {
8    limit: usize,
9    payload: Zeroizing<Vec<u8>>,
10}
11
12// A partial frame is secret-bearing protocol data, so only its shape is shown.
13impl std::fmt::Debug for FrameDecoder {
14    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        formatter
16            .debug_struct("FrameDecoder")
17            .field("limit", &self.limit)
18            .field("buffered", &self.payload.len())
19            .finish()
20    }
21}
22
23impl FrameDecoder {
24    pub fn new(limit: usize) -> Result<Self> {
25        validate_limit(limit)?;
26        Ok(Self {
27            limit,
28            payload: Zeroizing::new(Vec::new()),
29        })
30    }
31
32    pub const fn limit(&self) -> usize {
33        self.limit
34    }
35
36    pub fn set_limit(&mut self, limit: usize) -> Result<()> {
37        validate_limit(limit)?;
38        if !self.payload.is_empty() {
39            return Err(Error::Protocol("cannot change a frame limit mid-frame"));
40        }
41        self.limit = limit;
42        Ok(())
43    }
44
45    pub fn push(&mut self, bytes: &[u8]) -> Result<Vec<Zeroizing<Vec<u8>>>> {
46        let mut frames = Vec::new();
47        for &byte in bytes {
48            if byte == b'\n' {
49                if self.payload.is_empty() {
50                    return Err(Error::Protocol("zero-length frame"));
51                }
52                std::str::from_utf8(&self.payload)
53                    .map_err(|_| Error::Protocol("frame payload is not valid UTF-8"))?;
54                frames.push(std::mem::replace(
55                    &mut self.payload,
56                    Zeroizing::new(Vec::new()),
57                ));
58            } else {
59                if self.payload.len() >= self.limit {
60                    return Err(Error::Protocol("frame exceeds the active limit"));
61                }
62                self.reserve_one();
63                self.payload.push(byte);
64            }
65        }
66        Ok(frames)
67    }
68
69    /// Grow into a fresh zeroizing allocation instead of letting `Vec`
70    /// reallocate, which would free the old copy of the payload unwiped.
71    fn reserve_one(&mut self) {
72        if self.payload.len() < self.payload.capacity() {
73            return;
74        }
75        let capacity = self
76            .payload
77            .capacity()
78            .saturating_mul(2)
79            .clamp(256.min(self.limit), self.limit);
80        let mut grown = Zeroizing::new(Vec::with_capacity(capacity));
81        grown.extend_from_slice(&self.payload);
82        self.payload = grown;
83    }
84
85    pub fn finish_eof(&self) -> Result<()> {
86        if self.payload.is_empty() {
87            Ok(())
88        } else {
89            Err(Error::Protocol("truncated frame"))
90        }
91    }
92}
93
94pub fn encode(payload: &[u8], limit: usize) -> Result<Vec<u8>> {
95    validate_payload(payload, limit)?;
96    let mut frame = Vec::with_capacity(payload.len() + 1);
97    frame.extend_from_slice(payload);
98    frame.push(b'\n');
99    Ok(frame)
100}
101
102fn validate_limit(limit: usize) -> Result<()> {
103    if limit == 0 || limit > ABSOLUTE_MAX_FRAME_BYTES {
104        Err(Error::Protocol("frame limit is outside the absolute bound"))
105    } else {
106        Ok(())
107    }
108}
109
110fn validate_payload(payload: &[u8], limit: usize) -> Result<()> {
111    validate_limit(limit)?;
112    if payload.is_empty() {
113        return Err(Error::Protocol("zero-length frame"));
114    }
115    if payload.len() > limit {
116        return Err(Error::Protocol("frame exceeds the active limit"));
117    }
118    if payload.contains(&b'\n') || payload.contains(&b'\r') {
119        return Err(Error::Protocol("frame payload must be single-line JSON"));
120    }
121    std::str::from_utf8(payload)
122        .map_err(|_| Error::Protocol("frame payload is not valid UTF-8"))?;
123    Ok(())
124}
125
126#[cfg(feature = "tokio")]
127pub(crate) struct AsyncFrameReader<R> {
128    reader: tokio::io::BufReader<R>,
129    // Kept across calls so `read_frame` is cancel safe: bytes of a partial
130    // frame are consumed from the buffer before its delimiter arrives, and a
131    // `select!` that drops the read future must not lose them.
132    decoder: Option<FrameDecoder>,
133}
134
135#[cfg(feature = "tokio")]
136impl<R> AsyncFrameReader<R>
137where
138    R: tokio::io::AsyncRead + Unpin,
139{
140    pub(crate) fn new(reader: R) -> Self {
141        Self {
142            reader: tokio::io::BufReader::new(reader),
143            decoder: None,
144        }
145    }
146
147    /// Cancel safe: dropping the returned future before it completes keeps any
148    /// partially read frame for the next call.
149    pub(crate) async fn read_frame(&mut self, limit: usize) -> Result<Option<Zeroizing<Vec<u8>>>> {
150        use tokio::io::AsyncBufReadExt;
151        match &mut self.decoder {
152            Some(decoder) if decoder.payload.is_empty() => decoder.set_limit(limit)?,
153            Some(_) => {}
154            None => self.decoder = Some(FrameDecoder::new(limit)?),
155        }
156        let decoder = self.decoder.as_mut().expect("decoder initialized above");
157        loop {
158            let (consumed, mut frames) = {
159                let available = self.reader.fill_buf().await?;
160                if available.is_empty() {
161                    decoder.finish_eof()?;
162                    return Ok(None);
163                }
164                let consumed = available
165                    .iter()
166                    .position(|byte| *byte == b'\n')
167                    .map_or(available.len(), |position| position + 1);
168                (consumed, decoder.push(&available[..consumed])?)
169            };
170            self.reader.consume(consumed);
171            if let Some(frame) = frames.pop() {
172                debug_assert!(frames.is_empty());
173                return Ok(Some(frame));
174            }
175        }
176    }
177}
178
179/// Read one frame from a one-shot or already-buffered stream.
180///
181/// Long-lived protocol loops use `AsyncFrameReader` so buffered bytes after
182/// the delimiter are retained for the next call. This compatibility helper
183/// intentionally avoids reading past the delimiter because it cannot retain
184/// state owned by a raw `AsyncRead` caller.
185#[cfg(feature = "tokio")]
186pub async fn read_frame<R>(reader: &mut R, limit: usize) -> Result<Option<Zeroizing<Vec<u8>>>>
187where
188    R: tokio::io::AsyncRead + Unpin,
189{
190    use tokio::io::AsyncReadExt;
191    let mut decoder = FrameDecoder::new(limit)?;
192    let mut byte = [0_u8; 1];
193    loop {
194        match reader.read(&mut byte).await? {
195            0 => {
196                decoder.finish_eof()?;
197                return Ok(None);
198            }
199            _ => {
200                let mut frames = decoder.push(&byte)?;
201                if let Some(frame) = frames.pop() {
202                    return Ok(Some(frame));
203                }
204            }
205        }
206    }
207}
208
209#[cfg(feature = "tokio")]
210pub async fn write_frame<W>(writer: &mut W, payload: &[u8], limit: usize) -> Result<()>
211where
212    W: tokio::io::AsyncWrite + Unpin,
213{
214    use tokio::io::AsyncWriteExt;
215    let frame = Zeroizing::new(encode(payload, limit)?);
216    writer.write_all(&frame).await?;
217    writer.flush().await?;
218    Ok(())
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    #[test]
225    fn accepts_every_chunk_boundary_and_multiple_frames() {
226        let all = [
227            encode(br#"{\"a\":1}"#, 1024).unwrap(),
228            encode(br#"{\"b\":2}"#, 1024).unwrap(),
229        ]
230        .concat();
231        let mut decoder = FrameDecoder::new(1024).unwrap();
232        let mut decoded = Vec::new();
233        for byte in all {
234            decoded.extend(decoder.push(&[byte]).unwrap());
235        }
236        decoder.finish_eof().unwrap();
237        assert_eq!(decoded.len(), 2);
238    }
239    #[test]
240    fn rejects_a_missing_delimiter_at_the_bound() {
241        let mut decoder = FrameDecoder::new(4).unwrap();
242        assert!(decoder.push(b"12345").is_err());
243    }
244
245    #[test]
246    fn decoder_debug_hides_the_buffered_payload() {
247        let mut decoder = FrameDecoder::new(1024).unwrap();
248        decoder.push(b"{\"value\":\"hunter2").unwrap();
249        let debug = format!("{decoder:?}");
250        assert!(!debug.contains("hunter2"), "{debug}");
251        assert!(debug.contains("buffered"), "{debug}");
252    }
253
254    #[test]
255    fn decoder_grows_to_the_limit_without_exceeding_it() {
256        let mut decoder = FrameDecoder::new(1000).unwrap();
257        let payload = vec![b'a'; 1000];
258        decoder.push(&payload).unwrap();
259        assert!(decoder.payload.capacity() <= 1000);
260        assert!(decoder.push(b"a").is_err());
261        let frames = decoder.push(b"").unwrap();
262        assert!(frames.is_empty());
263    }
264
265    #[cfg(feature = "tokio")]
266    #[tokio::test]
267    async fn async_reader_keeps_a_partial_frame_when_the_read_is_cancelled() {
268        use tokio::io::AsyncWriteExt;
269        let (mut writer, reader) = tokio::io::duplex(64);
270        let mut reader = AsyncFrameReader::new(reader);
271        writer.write_all(b"{\"a\":").await.unwrap();
272        // The biased read consumes the available half frame, then pends and
273        // loses the race, which drops its future mid-frame.
274        tokio::select! {
275            biased;
276            _ = reader.read_frame(1024) => panic!("half a frame must not complete"),
277            _ = std::future::ready(()) => {}
278        }
279        writer.write_all(b"1}\n").await.unwrap();
280        let frame = reader.read_frame(1024).await.unwrap().unwrap();
281        assert_eq!(&*frame, b"{\"a\":1}");
282    }
283
284    #[cfg(feature = "tokio")]
285    #[tokio::test]
286    async fn async_reader_reuses_one_buffered_chunk_across_frames() {
287        use std::pin::Pin;
288        use std::sync::Arc;
289        use std::sync::atomic::{AtomicUsize, Ordering};
290        use std::task::{Context, Poll};
291        use tokio::io::{AsyncRead, ReadBuf};
292
293        struct CountingReader {
294            bytes: Vec<u8>,
295            position: usize,
296            reads: Arc<AtomicUsize>,
297        }
298
299        impl AsyncRead for CountingReader {
300            fn poll_read(
301                mut self: Pin<&mut Self>,
302                _context: &mut Context<'_>,
303                buffer: &mut ReadBuf<'_>,
304            ) -> Poll<std::io::Result<()>> {
305                self.reads.fetch_add(1, Ordering::Relaxed);
306                let available = &self.bytes[self.position..];
307                let read = available.len().min(buffer.remaining());
308                buffer.put_slice(&available[..read]);
309                self.position += read;
310                Poll::Ready(Ok(()))
311            }
312        }
313
314        let reads = Arc::new(AtomicUsize::new(0));
315        let source = CountingReader {
316            bytes: b"{\"a\":1}\n{\"b\":2}\n".to_vec(),
317            position: 0,
318            reads: Arc::clone(&reads),
319        };
320        let mut reader = AsyncFrameReader::new(source);
321        let first = reader.read_frame(1024).await.unwrap().unwrap();
322        let second = reader.read_frame(1024).await.unwrap().unwrap();
323        assert_eq!(&*first, b"{\"a\":1}");
324        assert_eq!(&*second, b"{\"b\":2}");
325        assert_eq!(reads.load(Ordering::Relaxed), 1);
326    }
327}