haste_hl7v2/mllp_async.rs
1//! Async MLLP framing, for a listener that serves many senders at once.
2//!
3//! [`crate::mllp::MllpFormatter::read_frame`] reads a byte at a time from a
4//! blocking [`std::io::Read`], which is fine for a client sending one message
5//! and wrong for a listener: it holds a thread per connection and cannot be
6//! cancelled, so one sender that opens a socket and stops writing occupies that
7//! thread until the process restarts.
8//!
9//! This reader is cancel-safe (a timeout around it drops cleanly), buffers
10//! rather than syscalling per byte, and refuses a frame past
11//! [`MAX_FRAME_BYTES`] instead of growing until the host runs out of memory.
12
13use haste_fhir_model::r4::generated::terminology::IssueType;
14use haste_fhir_operation_error::OperationOutcomeError;
15use tokio::io::{AsyncRead, AsyncReadExt};
16
17const START_BLOCK: u8 = 0x0B;
18const END_BLOCK: u8 = 0x1C;
19const CARRIAGE_RETURN: u8 = 0x0D;
20
21/// Largest message this listener accepts, in bytes.
22///
23/// A sender that never writes `<EB>` would otherwise grow the buffer forever.
24/// 16 MiB is far above any real HL7 v2 message a large one with embedded
25/// documents runs to a few hundred KiB so this rejects only a broken or
26/// hostile sender.
27pub const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
28
29/// Why a frame read ended.
30#[derive(Debug)]
31pub enum FrameEnd {
32 /// A complete frame; the payload with framing bytes removed.
33 Frame(Vec<u8>),
34 /// The sender closed cleanly between frames. Not an error: a sender that
35 /// sends one message and hangs up is behaving correctly.
36 Closed,
37}
38
39/// Reads one MLLP frame from `reader`.
40///
41/// Returns [`FrameEnd::Closed`] when the stream ends *between* frames, and an
42/// error when it ends *within* one, since a truncated message must not be
43/// mistaken for a complete one.
44///
45/// # Errors
46///
47/// Returns an [`OperationOutcomeError`] when the stream does not begin with
48/// `<SB>`, the frame exceeds [`MAX_FRAME_BYTES`], the stream ends mid-frame, or
49/// the underlying read fails.
50pub async fn read_frame<R>(reader: &mut R) -> Result<FrameEnd, OperationOutcomeError>
51where
52 R: AsyncRead + Unpin,
53{
54 // Leading bytes before `<SB>` are a framing error, not something to skip:
55 // silently resynchronising would accept a corrupted stream as valid.
56 let first = match reader.read_u8().await {
57 Ok(byte) => byte,
58 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(FrameEnd::Closed),
59 Err(e) => return Err(read_error(&e)),
60 };
61
62 if first != START_BLOCK {
63 return Err(OperationOutcomeError::error(
64 IssueType::invalid(),
65 format!("Expected an MLLP frame to start with <SB> (0x0B), got 0x{first:02X}"),
66 ));
67 }
68
69 let mut payload = Vec::with_capacity(4096);
70 // `<EB>` is only a terminator when `<CR>` follows it, and a payload may
71 // legitimately contain 0x1C, so the previous byte has to be remembered.
72 let mut pending_end_block = false;
73
74 loop {
75 let byte = match reader.read_u8().await {
76 Ok(byte) => byte,
77 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
78 return Err(OperationOutcomeError::error(
79 IssueType::incomplete(),
80 format!(
81 "Stream ended after {} byte(s) without <EB><CR>; \
82 a truncated message is not acknowledged",
83 payload.len()
84 ),
85 ));
86 }
87 Err(e) => return Err(read_error(&e)),
88 };
89
90 if pending_end_block {
91 if byte == CARRIAGE_RETURN {
92 return Ok(FrameEnd::Frame(payload));
93 }
94 // The 0x1C was payload after all.
95 payload.push(END_BLOCK);
96 pending_end_block = false;
97 }
98
99 if byte == END_BLOCK {
100 pending_end_block = true;
101 continue;
102 }
103
104 payload.push(byte);
105
106 if payload.len() > MAX_FRAME_BYTES {
107 return Err(OperationOutcomeError::error(
108 IssueType::too_costly(),
109 format!("MLLP frame exceeded the {MAX_FRAME_BYTES} byte limit"),
110 ));
111 }
112 }
113}
114
115fn read_error(error: &std::io::Error) -> OperationOutcomeError {
116 OperationOutcomeError::error(
117 IssueType::exception(),
118 format!("Failed to read from the MLLP stream: {error}"),
119 )
120}
121
122/// Wraps `payload` in `<SB>`/`<EB><CR>`.
123#[must_use]
124pub fn encode_frame(payload: &[u8]) -> Vec<u8> {
125 let mut framed = Vec::with_capacity(payload.len() + 3);
126 framed.push(START_BLOCK);
127 framed.extend_from_slice(payload);
128 framed.push(END_BLOCK);
129 framed.push(CARRIAGE_RETURN);
130 framed
131}