Skip to main content

protocol_droid/adapters/primary/stdio/
source.rs

1//! Reading whole frames off a peer's stream.
2//!
3//! # The two `Ok(None)`s
4//!
5//! Two different `None`s meet in this file and they mean opposites.
6//!
7//! [`FrameDecoder::next_frame`] answers `Ok(None)` for **"not ready — the
8//! terminator has not arrived"**. A pipe splits wherever it likes, so that
9//! answer is the normal case: most reads return a partial line. It is not an
10//! end condition, and a caller that treated it as one would drop every frame
11//! that did not happen to land on a read boundary.
12//!
13//! [`FrameSource::next_frame`] answers `Ok(None)` for **"the peer closed"**, and
14//! nothing else. It is the only signal the loop above has that the peer is
15//! gone, so it must never be produced by a decoder that simply has nothing yet.
16//!
17//! Forwarding the decoder's answer straight up is therefore the one mistake
18//! this module exists to not make, and it is a quiet one: a transport built
19//! that way works perfectly against a peer that frames its writes to match the
20//! read buffer, and silently truncates the conversation against one that does
21//! not. So [`StdioSource::next_frame`] loops internally — it reads more bytes,
22//! checks the decoder again, and only resolves when a frame is complete, the
23//! stream has ended, or a read has failed. It never returns the decoder's
24//! `None`.
25//!
26//! # A failure is latched
27//!
28//! Once a read fails, every later call answers
29//! [`SourceError::Transport`] without touching the stream. The alternative —
30//! retrying, and letting a later read return `Ok(0)` — would let a broken
31//! connection quietly present as a clean close, and a caller that distinguishes
32//! a peer that left from a peer that died would be told the wrong one. There is
33//! no recovery path here because a pipe that failed mid-read is not
34//! resynchronisable: the bytes already consumed are gone.
35//!
36//! # What the loop guarantees at end-of-stream
37//!
38//! End-of-stream is only ever reported once the decoder has nothing complete in
39//! it, because the check for a complete frame runs before every read. A frame
40//! the peer wrote and terminated is therefore never lost to a close that
41//! arrived in the same breath. A frame the peer wrote and did *not* terminate is
42//! dropped, which is the only defensible reading: there is no length on a
43//! newline to wait for, and reporting the partial bytes as a message would hand
44//! the next layer something the peer never sent.
45
46use core::future::Future;
47
48use tokio::io::{AsyncRead, AsyncReadExt};
49use tokio::sync::Mutex;
50
51use crate::application::jsonrpc::{FrameSource, SourceError};
52use crate::domain::{FrameDecoder, MaxFrameSize};
53
54use super::StdioConfig;
55
56/// The mutable half of a [`StdioSource`], behind one lock.
57///
58/// Stream and decoder share a single lock rather than one each because they
59/// are one resource seen from two angles: a read that returns bytes and a
60/// decoder that consumes them are a single step, and two locks would let a
61/// concurrent caller interleave between them. It also settles the question of
62/// which state is authoritative — there is exactly one.
63#[derive(Debug)]
64struct Inner<R> {
65    stream: R,
66    decoder: FrameDecoder,
67    /// Reused across reads so a long-lived session allocates its buffer once.
68    buf: Vec<u8>,
69    /// `read` returned zero: the peer is gone and this stays true.
70    ended: bool,
71    /// A read failed. Latched, so a failure never presents as a close.
72    failed: bool,
73}
74
75/// A [`FrameSource`] over any `AsyncRead`.
76///
77/// # Examples
78///
79/// A frame split across two reads is reassembled, because the partial
80/// `Ok(None)` from the decoder is a reason to read again rather than an answer:
81///
82/// ```
83/// use std::pin::Pin;
84/// use std::task::{Context, Poll};
85///
86/// use protocol_droid::adapters::primary::stdio::{StdioConfig, StdioError, StdioSource};
87/// use protocol_droid::application::jsonrpc::FrameSource;
88/// use protocol_droid::domain::MaxFrameSize;
89/// use tokio::io::{AsyncRead, ReadBuf};
90///
91/// /// Hands over at most three bytes per read, so a frame cannot arrive whole.
92/// struct Trickle {
93///     data: Vec<u8>,
94///     served: usize,
95/// }
96///
97/// impl AsyncRead for Trickle {
98///     fn poll_read(
99///         mut self: Pin<&mut Self>,
100///         _cx: &mut Context<'_>,
101///         buf: &mut ReadBuf<'_>,
102///     ) -> Poll<std::io::Result<()>> {
103///         let end = (self.served + 3).min(self.data.len());
104///         let Some(chunk) = self.data.get(self.served..end) else {
105///             return Poll::Ready(Ok(())); // end of stream
106///         };
107///         buf.put_slice(chunk);
108///         self.served = end;
109///         Poll::Ready(Ok(()))
110///     }
111/// }
112///
113/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
114/// let stream = Trickle { data: b"{\"jsonrpc\":\"2.0\"}\n".to_vec(), served: 0 };
115/// let source = StdioSource::new(stream, StdioConfig::new(MaxFrameSize::new(1024)?));
116///
117/// assert_eq!(source.next_frame().await?, Some(r#"{"jsonrpc":"2.0"}"#.to_owned()));
118/// # Ok(())
119/// # }
120/// # fn _unused(e: StdioError) {}
121/// ```
122#[derive(Debug)]
123pub struct StdioSource<R> {
124    inner: Mutex<Inner<R>>,
125    config: StdioConfig,
126}
127
128impl<R> StdioSource<R> {
129    /// Wraps a stream, decoding frames at the configured ceiling.
130    #[must_use]
131    pub fn new(stream: R, config: StdioConfig) -> Self {
132        let buf = vec![0; config.read_chunk().get()];
133        Self {
134            inner: Mutex::new(Inner {
135                stream,
136                decoder: FrameDecoder::new(config.limit()),
137                buf,
138                ended: false,
139                failed: false,
140            }),
141            config,
142        }
143    }
144
145    /// The settings this source was built with.
146    #[must_use]
147    pub const fn config(&self) -> StdioConfig {
148        self.config
149    }
150
151    /// The ceiling this source refuses frames above.
152    #[must_use]
153    pub const fn limit(&self) -> MaxFrameSize {
154        self.config.limit()
155    }
156}
157
158impl<R> StdioSource<R>
159where
160    R: AsyncRead + Unpin,
161{
162    /// Reads the peer's next frame, waiting for as many reads as that takes.
163    ///
164    /// This is the read loop, and the reason this type exists. Every exit from
165    /// it is a decision, and the decoder's `None` is not one of them.
166    ///
167    /// # Errors
168    ///
169    /// [`SourceError::Transport`] when a read fails, which is latched, and
170    /// [`SourceError::Framing`] when the peer's bytes are not a frame — most
171    /// often [`crate::domain::FramingError::FrameTooLarge`], or bytes that are
172    /// not UTF-8.
173    ///
174    /// `Ok(None)` is the peer closing and only that.
175    ///
176    /// The lock is held for the whole loop, which is the point of holding one:
177    /// two callers must not interleave a read with the decoder that consumes
178    /// what it returned. Every exit from the loop is still holding it, so the
179    /// guard has no earlier last use to be tightened to.
180    #[expect(
181        clippy::significant_drop_tightening,
182        reason = "the guard is the mutual exclusion for the entire read loop, and every path out of that loop is still holding it"
183    )]
184    pub async fn next_frame(&self) -> Result<Option<String>, SourceError> {
185        let mut guard = self.inner.lock().await;
186        // Destructured so the stream and the read buffer are separate mutable
187        // borrows rather than two claims on the same guard.
188        let Inner {
189            stream,
190            decoder,
191            buf,
192            ended,
193            failed,
194        } = &mut *guard;
195
196        // Latched before anything else: a source that has already failed must
197        // not go back to the stream and risk a later `Ok(0)` reading as a
198        // clean close.
199        if *failed {
200            return Err(SourceError::Transport);
201        }
202
203        loop {
204            // The decoder's `None` means "no terminator yet". Checking here, at
205            // the top of every pass, is also what makes the end-of-stream path
206            // below safe: the stream is only ever read once the decoder has been
207            // asked and had nothing.
208            if let Some(frame) = decoder.next_frame()? {
209                return Ok(Some(frame.to_owned()));
210            }
211
212            // Already saw the close, and the decoder had nothing, so the peer
213            // is gone and there is nothing left to wait for.
214            if *ended {
215                return Ok(None);
216            }
217
218            let Ok(read) = stream.read(buf.as_mut_slice()).await else {
219                *failed = true;
220                return Err(SourceError::Transport);
221            };
222
223            if read == 0 {
224                *ended = true;
225                return Ok(None);
226            }
227
228            let Some(bytes) = buf.get(..read) else {
229                // `read` is bounded by the slice it was given, so this is
230                // unreachable. Latching the failure anyway costs nothing and
231                // keeps the alternative — indexing, or a silently dropped
232                // frame — off the table.
233                *failed = true;
234                return Err(SourceError::Transport);
235            };
236            decoder.push(bytes)?;
237        }
238    }
239}
240
241impl<R> FrameSource for StdioSource<R>
242where
243    R: AsyncRead + Unpin + Send,
244{
245    /// Waits for the peer's next frame.
246    ///
247    /// The port's `Ok(None)` is the peer closing, so this never returns the
248    /// decoder's "no terminator yet" — see the module docs for why the two
249    /// `None`s are opposites and what inverting them looks like from the
250    /// outside.
251    ///
252    /// A framing error is *not* latched: the decoder refused the bytes and kept
253    /// its own buffer, so the next call is refused again by the same check
254    /// rather than by a flag, and the stream stays open for the caller to shut
255    /// down.
256    ///
257    /// # Errors
258    ///
259    /// As [`StdioSource::next_frame`].
260    fn next_frame(&self) -> impl Future<Output = Result<Option<String>, SourceError>> + Send {
261        Self::next_frame(self)
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use core::pin::Pin;
268    use core::sync::atomic::{AtomicUsize, Ordering};
269    use core::task::{Context, Poll};
270    use std::io;
271    use std::sync::Arc;
272
273    use assert_matches::assert_matches;
274
275    use tokio::io::{AsyncRead, ReadBuf};
276
277    use super::{StdioConfig, StdioSource};
278    use crate::application::jsonrpc::{DynFrameSource, SourceError};
279    use crate::domain::{FrameDecoder, FramingError, MaxFrameSize, MaxFrameSizeError};
280
281    /// The `Failure` an `?` in a test can produce, so a fallible call whose
282    /// error type is not the one under test still propagates instead of
283    /// unwrapping. Without it every test below would need an `unwrap` at the
284    /// boundary, which the project rules forbid in tests too.
285    #[derive(Debug)]
286    #[expect(dead_code, reason = "the payloads are read by the derived Debug")]
287    enum Failure {
288        /// A source refused to produce the frame the test expected.
289        Source(SourceError),
290        /// A limit the test tried to build was refused.
291        Limit(MaxFrameSizeError),
292    }
293
294    impl From<SourceError> for Failure {
295        fn from(error: SourceError) -> Self {
296            Self::Source(error)
297        }
298    }
299
300    impl From<MaxFrameSizeError> for Failure {
301        fn from(error: MaxFrameSizeError) -> Self {
302            Self::Limit(error)
303        }
304    }
305
306    /// A reader that hands over at most `per_read` bytes at a time, can be made
307    /// to fail on demand, and counts how many reads it served.
308    ///
309    /// A real pipe is the one thing a test cannot ask to split a particular
310    /// way, and "split across two reads" is the whole property under test. This
311    /// is the deterministic stand-in, and it is also how the read-failure case
312    /// is produced without a socket that someone has to break.
313    struct Trickle {
314        data: Vec<u8>,
315        served: usize,
316        per_read: usize,
317        /// Fail once this many bytes have been handed over.
318        fail_after: Option<usize>,
319        reads: Arc<AtomicUsize>,
320    }
321
322    impl Trickle {
323        fn new(data: &[u8], per_read: usize) -> (Self, Arc<AtomicUsize>) {
324            let reads = Arc::new(AtomicUsize::new(0));
325            (
326                Self {
327                    data: data.to_vec(),
328                    served: 0,
329                    per_read,
330                    fail_after: None,
331                    reads: Arc::clone(&reads),
332                },
333                reads,
334            )
335        }
336
337        fn failing(data: &[u8], per_read: usize) -> Self {
338            Self {
339                fail_after: Some(0),
340                ..Self::new(data, per_read).0
341            }
342        }
343    }
344
345    impl AsyncRead for Trickle {
346        fn poll_read(
347            self: Pin<&mut Self>,
348            _cx: &mut Context<'_>,
349            buf: &mut ReadBuf<'_>,
350        ) -> Poll<io::Result<()>> {
351            let this = self.get_mut();
352            this.reads.fetch_add(1, Ordering::Relaxed);
353
354            if this.fail_after.is_some_and(|limit| this.served >= limit) {
355                return Poll::Ready(Err(io::Error::other("injected read failure")));
356            }
357
358            let remaining = this.data.len().saturating_sub(this.served);
359            if remaining == 0 {
360                // End of stream: leave the buffer empty so `read` answers zero.
361                return Poll::Ready(Ok(()));
362            }
363
364            let end = this
365                .served
366                .saturating_add(this.per_read)
367                .min(this.data.len());
368            let Some(chunk) = this.data.get(this.served..end) else {
369                return Poll::Ready(Ok(()));
370            };
371            buf.put_slice(chunk);
372            this.served = end;
373            Poll::Ready(Ok(()))
374        }
375    }
376
377    /// A source over a [`Trickle`], plus the read counter for asserting that a
378    /// frame really did span more than one read.
379    fn trickling(
380        wire: &[u8],
381        per_read: usize,
382        limit: u32,
383    ) -> Result<(StdioSource<Trickle>, Arc<AtomicUsize>), Failure> {
384        let (stream, reads) = Trickle::new(wire, per_read);
385        let config = StdioConfig::new(MaxFrameSize::new(limit)?).with_read_chunk(per_read);
386        Ok((StdioSource::new(stream, config), reads))
387    }
388
389    /// The property the module exists for: a frame that arrives in pieces is
390    /// reassembled rather than truncated, because the decoder's "no terminator
391    /// yet" is not forwarded up as an answer.
392    #[tokio::test]
393    async fn a_frame_split_across_several_reads_reassembles() -> Result<(), Failure> {
394        let document = r#"{"jsonrpc":"2.0","id":1,"result":{}}"#;
395        let mut wire = document.as_bytes().to_vec();
396        wire.push(b'\n');
397
398        // Four bytes at a time, so a 38-byte frame cannot arrive whole.
399        let (source, reads) = trickling(&wire, 4, 1024)?;
400
401        assert_eq!(source.next_frame().await?, Some(document.to_owned()));
402        assert!(
403            reads.load(Ordering::Relaxed) > 1,
404            "the frame must have spanned more than one read for this test to mean anything"
405        );
406        Ok(())
407    }
408
409    /// Two frames in one read, then a close. The close must not cost the second
410    /// frame, which is the case that a "read, then check" loop gets wrong when
411    /// the end-of-stream check runs before the decoder check.
412    #[tokio::test]
413    async fn a_frame_buffered_before_the_close_is_still_delivered() -> Result<(), Failure> {
414        let (source, _) = trickling(b"one\ntwo\n", 8, 1024)?;
415
416        assert_eq!(source.next_frame().await?, Some("one".to_owned()));
417        assert_eq!(source.next_frame().await?, Some("two".to_owned()));
418        assert_eq!(source.next_frame().await?, None);
419        Ok(())
420    }
421
422    /// End of stream is `Ok(None)`, and it stays `Ok(None)` on every later call
423    /// rather than turning into an error or a second close of a different kind.
424    #[tokio::test]
425    async fn a_clean_end_of_stream_is_a_close_and_stays_one() -> Result<(), Failure> {
426        let (source, _) = trickling(b"{\"jsonrpc\":\"2.0\"}\n", 64, 1024)?;
427
428        assert!(source.next_frame().await?.is_some());
429        assert_eq!(source.next_frame().await?, None);
430        assert_eq!(source.next_frame().await?, None);
431        Ok(())
432    }
433
434    /// A read failure is [`SourceError::Transport`], not a close. The two are
435    /// opposites and the caller above is entitled to the difference.
436    #[tokio::test]
437    async fn a_read_failure_is_a_transport_error() -> Result<(), Failure> {
438        let config = StdioConfig::new(MaxFrameSize::new(1024)?);
439        let source = StdioSource::new(Trickle::failing(b"ignored\n", 8), config);
440
441        assert_matches!(source.next_frame().await, Err(SourceError::Transport));
442        Ok(())
443    }
444
445    /// The latch. A source that has failed keeps reporting the failure without
446    /// touching the stream again, so a later `Ok(0)` can never present a dead
447    /// connection as a peer that left politely.
448    #[tokio::test]
449    async fn a_failed_read_stays_failed_and_never_becomes_a_clean_close() -> Result<(), Failure> {
450        let config = StdioConfig::new(MaxFrameSize::new(1024)?);
451        let source = StdioSource::new(Trickle::failing(b"ignored\n", 8), config);
452
453        for _ in 0..3 {
454            assert_matches!(source.next_frame().await, Err(SourceError::Transport));
455        }
456        Ok(())
457    }
458
459    /// The ceiling applies to what a peer sends, reported as the framing error
460    /// the domain already defines rather than as a transport failure.
461    #[tokio::test]
462    async fn an_oversized_inbound_frame_is_refused() -> Result<(), Failure> {
463        // A 64-byte ceiling against a 200-byte line.
464        let (source, _) = trickling(&[b'x'; 200], 16, 64)?;
465
466        assert_matches!(
467            source.next_frame().await,
468            Err(SourceError::Framing(FramingError::FrameTooLarge { .. }))
469        );
470        Ok(())
471    }
472
473    /// A line the peer never terminated is not a frame, and reporting it as one
474    /// would hand the next layer a message the peer did not send.
475    #[tokio::test]
476    async fn an_unterminated_trailing_line_is_not_a_frame() -> Result<(), Failure> {
477        let (source, _) = trickling(b"{\"jsonrpc\":\"2.0\"}", 64, 1024)?;
478
479        assert_eq!(source.next_frame().await?, None);
480        Ok(())
481    }
482
483    /// Bytes that are not UTF-8 are refused rather than replaced, so the
484    /// corruption surfaces as an encoding error instead of as a routing one.
485    #[tokio::test]
486    async fn non_utf8_bytes_are_refused() -> Result<(), Failure> {
487        let (source, _) = trickling(&[0xFF, 0xFE, b'\n'], 8, 1024)?;
488
489        assert_matches!(
490            source.next_frame().await,
491            Err(SourceError::Framing(FramingError::NotUtf8 { .. }))
492        );
493        Ok(())
494    }
495
496    /// The blanket impl in the port module covers the object-safe spelling, so
497    /// a caller holding `Arc<dyn DynFrameSource>` gets a source without this
498    /// adapter writing a second impl that would collide with it.
499    #[tokio::test]
500    async fn the_source_is_usable_behind_a_trait_object() -> Result<(), Failure> {
501        let (source, _) = trickling(b"behind-a-box\n", 16, 1024)?;
502        let boxed: Arc<dyn DynFrameSource> = Arc::new(source);
503
504        assert_eq!(boxed.next_frame().await?, Some("behind-a-box".to_owned()));
505        Ok(())
506    }
507
508    /// The decoder the loop drives is the real one, and a source that has read
509    /// nothing holds no bytes — the two facts together are what "a `None` here
510    /// means not-ready" is allowed to assume.
511    #[test]
512    fn the_framing_is_the_domain_decoder_not_a_local_copy() {
513        let mut decoder =
514            FrameDecoder::new(MaxFrameSize::new(1024).unwrap_or(MaxFrameSize::DEFAULT));
515        assert!(decoder.is_empty());
516        assert_matches!(decoder.next_frame(), Ok(None));
517    }
518}