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}