hl7_mllp/lib.rs
1//! # hl7-mllp
2//!
3//! Transport-agnostic MLLP (Minimal Lower Layer Protocol) framing for HL7 v2 messages.
4//!
5//! MLLP is the standard transport envelope used by HL7 v2 over TCP/IP. This crate
6//! provides pure framing logic — encoding and decoding MLLP frames — without coupling
7//! to any specific async runtime, I/O library, or transport mechanism.
8//!
9//! ## What is MLLP?
10//!
11//! MLLP (Minimal Lower Layer Protocol) wraps HL7 v2 messages with simple byte delimiters
12//! for reliable streaming over TCP. It is defined in HL7 v2.5.1 Appendix C.
13//!
14//! ## MLLP Frame Format
15//!
16//! An MLLP frame wraps an HL7 message with 3 special bytes:
17//!
18//! ```text
19//! +--------+----------------------------+--------+--------+
20//! | VT | HL7 message bytes | FS | CR |
21//! | 0x0B | (variable) | 0x1C | 0x0D |
22//! +--------+----------------------------+--------+--------+
23//! ↑ ↑ ↑
24//! Start of Block End of Line
25//! (Vertical Tab) Block Terminator
26//! (File (Carriage
27//! Sep.) Return)
28//! ```
29//!
30//! - **VT (0x0B)**: Start of block marker. Every frame MUST begin with this byte.
31//! - **FS (0x1C)**: End of block marker. Every frame MUST end with FS followed by CR.
32//! - **CR (0x0D)**: Carriage return terminator. Required after FS.
33//!
34//! The payload between VT and FS-CR is the raw HL7 message (typically ER7-encoded).
35//!
36//! ## Design Philosophy
37//!
38//! This crate provides three main abstractions:
39//!
40//! - **[`MllpFrame`]**: Stateless encode/decode operations. Use for simple one-shot framing.
41//! - **[`MllpFramer`]**: Stateful streaming accumulator. Use for network I/O where data
42//! arrives in chunks.
43//! - **[`MllpTransport`]**: Trait for implementing transports (TCP, serial, etc.).
44//!
45//! All operations are:
46//! - **Zero-allocation where possible**: `decode()` returns a slice into the original buffer.
47//! - **No async/await**: Works with blocking or async code equally well.
48//! - **No I/O opinions**: You bring your own sockets/streams.
49//!
50//! ## Quick Start
51//!
52//! ### Encode a message for sending
53//!
54//! ```rust
55//! use hl7_mllp::MllpFrame;
56//!
57//! let raw_hl7 = b"MSH|^~\\&|SendApp|SendFac|20240101120000||ORU^R01|12345|P|2.5\r";
58//! let framed = MllpFrame::encode(raw_hl7);
59//! // framed now contains: VT + raw_hl7 + FS + CR
60//! // Send `framed` over your TCP socket...
61//! ```
62//!
63//! ### Decode a received frame
64//!
65//! ```rust
66//! use hl7_mllp::MllpFrame;
67//! use bytes::Bytes;
68//!
69//! // Received from TCP socket...
70//! let framed: Bytes = MllpFrame::encode(b"MSH|^~\\&|...");
71//!
72//! // decode() returns a slice into the original buffer (zero copy)
73//! let decoded = MllpFrame::decode(&framed).unwrap();
74//! assert_eq!(decoded, b"MSH|^~\\&|...");
75//! ```
76//!
77//! ### Streaming with MllpFramer
78//!
79//! ```rust
80//! use hl7_mllp::MllpFramer;
81//!
82//! let mut framer = MllpFramer::new();
83//!
84//! // Data arrives in chunks from TCP...
85//! framer.push(b"\x0BMSH|^~\\&|");
86//! framer.push(b"test message\x1C\x0D");
87//!
88//! // Extract complete frame when available
89//! if let Some(frame) = framer.next_frame() {
90//! // Process the complete frame
91//! println!("Received {} bytes", frame.len());
92//! }
93//! ```
94
95#![forbid(unsafe_code)]
96#![warn(missing_docs)]
97
98use bytes::{BufMut, Bytes, BytesMut};
99
100/// MLLP start-of-block character (VT, 0x0B).
101pub const VT: u8 = 0x0B;
102
103/// MLLP end-of-block character (FS, 0x1C).
104pub const FS: u8 = 0x1C;
105
106/// MLLP carriage return terminator (CR, 0x0D).
107pub const CR: u8 = 0x0D;
108
109/// Errors produced by MLLP framing operations.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum MllpError {
112 /// Input did not begin with the expected VT start byte.
113 MissingStartByte,
114 /// Input did not end with the expected FS+CR sequence.
115 MissingEndSequence,
116 /// The frame was empty (no HL7 payload between delimiters).
117 EmptyPayload,
118 /// The buffer was too short to contain a complete frame.
119 Incomplete,
120 /// Invalid frame format with detailed reason.
121 InvalidFrame {
122 /// Detailed explanation of why the frame is invalid.
123 reason: String,
124 },
125}
126
127impl std::fmt::Display for MllpError {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 Self::MissingStartByte => {
131 write!(
132 f,
133 "MLLP frame missing VT start byte (expected 0x0B at position 0)"
134 )
135 }
136 Self::MissingEndSequence => {
137 write!(
138 f,
139 "MLLP frame missing FS+CR end sequence (expected 0x1C 0x0D)"
140 )
141 }
142 Self::EmptyPayload => {
143 write!(f, "MLLP frame contains no HL7 payload between delimiters")
144 }
145 Self::Incomplete => {
146 write!(
147 f,
148 "Buffer too short for complete MLLP frame (need at least 4 bytes: VT + payload + FS + CR)"
149 )
150 }
151 Self::InvalidFrame { reason } => {
152 write!(f, "Invalid MLLP frame: {reason}")
153 }
154 }
155 }
156}
157
158impl std::error::Error for MllpError {}
159
160impl From<MllpError> for std::io::Error {
161 fn from(err: MllpError) -> Self {
162 std::io::Error::new(std::io::ErrorKind::InvalidData, err)
163 }
164}
165
166/// MLLP frame encoder and decoder.
167///
168/// This struct contains only associated functions — there is no state.
169/// It operates purely on byte slices and [`Bytes`].
170pub struct MllpFrame;
171
172impl MllpFrame {
173 /// Wrap a raw HL7 message payload in an MLLP frame.
174 ///
175 /// # Output Layout
176 ///
177 /// The returned [`Bytes`] contains exactly:
178 ///
179 /// | Byte(s) | Value | Description |
180 /// |---------|-------|-------------|
181 /// | 0 | 0x0B | VT (Vertical Tab) - start of block |
182 /// | 1..n | payload | Raw HL7 message bytes (n = payload.len()) |
183 /// | n+1 | 0x1C | FS (File Separator) - end of block |
184 /// | n+2 | 0x0D | CR (Carriage Return) - terminator |
185 ///
186 /// Total length: `payload.len() + 3` bytes.
187 ///
188 /// # Example
189 ///
190 /// ```rust
191 /// use hl7_mllp::{MllpFrame, VT, FS, CR};
192 ///
193 /// let payload = b"MSH|^~\\&|test";
194 /// let frame = MllpFrame::encode(payload);
195 ///
196 /// assert_eq!(frame[0], VT);
197 /// assert_eq!(&frame[1..14], payload);
198 /// assert_eq!(frame[14], FS);
199 /// assert_eq!(frame[15], CR);
200 /// ```
201 pub fn encode(payload: &[u8]) -> Bytes {
202 let mut buf = BytesMut::with_capacity(payload.len() + 3);
203 buf.put_u8(VT);
204 buf.put_slice(payload);
205 buf.put_u8(FS);
206 buf.put_u8(CR);
207 buf.freeze()
208 }
209
210 /// Extract the HL7 payload from an MLLP-framed buffer.
211 ///
212 /// # Zero-Copy Guarantee
213 ///
214 /// This method returns a slice `&[u8]` that points into the original `buf`.
215 /// No data is copied — this is O(1) regardless of payload size.
216 ///
217 /// The returned slice has the same lifetime as the input buffer. If you need
218 /// an owned copy, call `.to_vec()` on the result.
219 ///
220 /// # Validation
221 ///
222 /// This function validates:
223 /// - Buffer length ≥ 4 bytes (VT + at least 1 payload byte + FS + CR)
224 /// - First byte is VT (0x0B)
225 /// - Last two bytes are FS (0x1C) + CR (0x0D)
226 /// - Payload is not empty
227 ///
228 /// # Errors
229 ///
230 /// Returns [`MllpError`] variants:
231 /// - [`Incomplete`](MllpError::Incomplete) if buffer is too short
232 /// - [`MissingStartByte`](MllpError::MissingStartByte) if first byte is not VT
233 /// - [`MissingEndSequence`](MllpError::MissingEndSequence) if FS+CR not found at end
234 /// - [`EmptyPayload`](MllpError::EmptyPayload) if payload length is 0
235 ///
236 /// # Example
237 ///
238 /// ```rust
239 /// use hl7_mllp::MllpFrame;
240 ///
241 /// let frame = MllpFrame::encode(b"MSH|test");
242 /// let payload = MllpFrame::decode(&frame).unwrap();
243 ///
244 /// // payload is a slice into frame — zero copy
245 /// assert_eq!(payload, b"MSH|test");
246 /// ```
247 pub fn decode(buf: &[u8]) -> Result<&[u8], MllpError> {
248 if buf.len() < 4 {
249 return Err(MllpError::Incomplete);
250 }
251 if buf[0] != VT {
252 return Err(MllpError::MissingStartByte);
253 }
254 let end = buf.len();
255 if buf[end - 2] != FS || buf[end - 1] != CR {
256 return Err(MllpError::MissingEndSequence);
257 }
258 let payload = &buf[1..end - 2];
259 if payload.is_empty() {
260 return Err(MllpError::EmptyPayload);
261 }
262 Ok(payload)
263 }
264
265 /// Find the end of the first complete MLLP frame in a streaming buffer.
266 ///
267 /// Returns `Some(n)` where `n` is the byte length of the complete frame
268 /// (including delimiters), or `None` if the buffer does not yet contain
269 /// a complete frame. Useful for implementing streaming readers.
270 pub fn find_frame_end(buf: &[u8]) -> Option<usize> {
271 if buf.is_empty() || buf[0] != VT {
272 return None;
273 }
274 for i in 1..buf.len().saturating_sub(1) {
275 if buf[i] == FS && buf[i + 1] == CR {
276 return Some(i + 2);
277 }
278 }
279 None
280 }
281
282 /// Find all complete MLLP frames in a buffer.
283 ///
284 /// Returns a vector of (start, end) byte positions for each complete frame found.
285 /// Start position is the index of the VT byte, end position is the index after CR.
286 /// Partial frames at the end of the buffer are not included.
287 ///
288 /// # Example
289 /// ```
290 /// use hl7_mllp::MllpFrame;
291 ///
292 /// let frame1 = MllpFrame::encode(b"MSH|first");
293 /// let frame2 = MllpFrame::encode(b"MSH|second");
294 /// let combined = [&frame1[..], &frame2[..]].concat();
295 ///
296 /// let frames = MllpFrame::find_all_frames(&combined);
297 /// assert_eq!(frames, vec![(0, frame1.len()), (frame1.len(), frame1.len() + frame2.len())]);
298 /// ```
299 pub fn find_all_frames(buf: &[u8]) -> Vec<(usize, usize)> {
300 let mut frames = Vec::new();
301 let mut pos = 0;
302
303 while pos < buf.len() {
304 // Look for VT start byte
305 if buf[pos] != VT {
306 #[cfg(feature = "noncompliance")]
307 {
308 // Skip non-VT bytes at the start (tolerate extra bytes before VT)
309 if let Some(vt_pos) = buf[pos..].iter().position(|&b| b == VT) {
310 pos += vt_pos;
311 } else {
312 break;
313 }
314 }
315 #[cfg(not(feature = "noncompliance"))]
316 break;
317 }
318
319 // Need at least VT + 1 byte + FS + CR = 4 bytes minimum
320 if buf.len() - pos < 4 {
321 break;
322 }
323
324 // Search for FS+CR end sequence
325 let search_start = pos + 1;
326 let mut found_end = None;
327
328 for i in search_start..buf.len().saturating_sub(1) {
329 if buf[i] == FS && buf[i + 1] == CR {
330 found_end = Some(i + 2); // Position after CR
331 break;
332 }
333 }
334
335 #[cfg(feature = "noncompliance")]
336 if found_end.is_none() {
337 // Tolerate missing final CR - look for FS at end of remaining buffer
338 // Check: at least VT + 1 byte payload + FS from current position
339 let remaining = buf.len() - pos;
340 if remaining >= 3 && buf[buf.len() - 1] == FS {
341 // Ensure there's at least 1 byte of payload between VT and FS
342 if remaining >= 3 {
343 found_end = Some(buf.len());
344 }
345 }
346 }
347
348 if let Some(end) = found_end {
349 // Ensure payload is not empty (at least 1 byte between VT and FS)
350 if end - pos >= 4 {
351 frames.push((pos, end));
352 pos = end;
353 } else {
354 break;
355 }
356 } else {
357 break;
358 }
359 }
360
361 frames
362 }
363
364 /// Build a minimal HL7 ACK message payload (not MLLP-framed).
365 ///
366 /// `message_control_id` should be the message control ID from the original MSH-10.
367 ///
368 /// # Returns
369 /// Returns `Some(String)` with the ACK payload, or `None` if `message_control_id` is empty.
370 pub fn build_ack(message_control_id: &str, accepting: bool) -> Option<String> {
371 if message_control_id.is_empty() {
372 return None;
373 }
374 let code = if accepting { "AA" } else { "AE" };
375 let timestamp = chrono_now_str();
376 // Generate a unique ACK control ID (ACK + timestamp + original ID)
377 let ack_control_id = format!("ACK{}{}", ×tamp, message_control_id);
378 Some(format!(
379 "MSH|^~\\&|||||{}||ACK|{}|P|2.3.1\rMSA|{}|{}",
380 timestamp, ack_control_id, code, message_control_id,
381 ))
382 }
383
384 /// Build a minimal HL7 NACK message payload with error details (not MLLP-framed).
385 ///
386 /// `message_control_id` should be the message control ID from the original MSH-10.
387 /// `error_code` should be an HL7 error code (e.g., "101", "102").
388 /// `error_text` should be a human-readable error description.
389 ///
390 /// # Returns
391 /// Returns `Some(String)` with the NACK payload, or `None` if `message_control_id` is empty.
392 pub fn build_nack(
393 message_control_id: &str,
394 error_code: &str,
395 error_text: &str,
396 ) -> Option<String> {
397 if message_control_id.is_empty() {
398 return None;
399 }
400 let timestamp = chrono_now_str();
401 // Generate a unique NACK control ID (NACK + timestamp + original ID)
402 let nack_control_id = format!("NACK{}{}", ×tamp, message_control_id);
403 // Escape any pipe characters in error text to prevent breaking HL7 field structure
404 let escaped_text = error_text.replace('|', "\\F\\");
405 // Per HL7 spec: MSA-1 = AR, MSA-2 = original control ID, MSA-3 = error text
406 // Error code should be in separate ERR segment (not in MSA)
407 Some(format!(
408 "MSH|^~\\&|||||{}||ACK|{}|P|2.3.1\rMSA|AR|{}|{}: {} - {}",
409 timestamp, nack_control_id, message_control_id, error_code, error_code, escaped_text,
410 ))
411 }
412}
413
414fn chrono_now_str() -> String {
415 #[cfg(feature = "timestamps")]
416 {
417 use chrono::Local;
418 Local::now().format("%Y%m%d%H%M%S").to_string()
419 }
420 #[cfg(not(feature = "timestamps"))]
421 {
422 // Default placeholder timestamp — caller should provide real timestamp
423 "20250101000000".to_string()
424 }
425}
426
427/// Stateful streaming frame accumulator for MLLP protocol.
428///
429/// `MllpFramer` is designed for network I/O where data arrives in chunks.
430/// It maintains an internal [`BytesMut`] buffer and provides incremental
431/// frame extraction as complete MLLP frames become available.
432///
433/// # Streaming Usage Pattern
434///
435/// The typical streaming workflow:
436/// 1. Create a framer with `MllpFramer::new()`
437/// 2. In a loop, read bytes from your socket/stream
438/// 3. Push received bytes into the framer with `push()`
439/// 4. Repeatedly call `next_frame()` to extract all complete frames
440/// 5. Process each frame, then continue reading
441///
442/// # Handling Partial Frames
443///
444/// If `next_frame()` returns `None`, the buffer contains a partial frame
445/// (incomplete). Keep the framer alive and push more bytes — the partial
446/// data is preserved for the next call.
447///
448/// # Thread Safety
449///
450/// `MllpFramer` is not `Sync` — it cannot be shared between threads.
451/// It is `Clone` (cheap, since [`BytesMut`] uses ref-counting), so you
452/// can clone it if needed for single-threaded scenarios.
453///
454/// # Example: TCP Streaming
455///
456/// ```rust
457/// use hl7_mllp::MllpFramer;
458///
459/// let mut framer = MllpFramer::new();
460///
461/// // Simulate receiving data in chunks from TCP
462/// framer.push(b"\x0BMSH|^~\\&|"); // First chunk
463/// framer.push(b"partial data..."); // More data
464/// framer.push(b"\x1C\x0D\x0BMSH|second"); // Complete frame + partial
465///
466/// // Extract first complete frame
467/// let frame1 = framer.next_frame().unwrap();
468/// assert!(frame1.starts_with(&[0x0B])); // Starts with VT
469/// assert!(frame1.ends_with(&[0x1C, 0x0D])); // Ends with FS+CR
470///
471/// // No more complete frames yet
472/// assert!(framer.next_frame().is_none());
473///
474/// // Push remaining bytes to complete second frame
475/// framer.push(b"|more\x1C\x0D");
476/// let frame2 = framer.next_frame().unwrap();
477/// ```
478#[derive(Debug, Clone)]
479pub struct MllpFramer {
480 buffer: BytesMut,
481}
482
483impl MllpFramer {
484 /// Create a new empty framer.
485 pub fn new() -> Self {
486 Self {
487 buffer: BytesMut::new(),
488 }
489 }
490
491 /// Create a new framer with specified capacity.
492 pub fn with_capacity(capacity: usize) -> Self {
493 Self {
494 buffer: BytesMut::with_capacity(capacity),
495 }
496 }
497
498 /// Append bytes to the internal buffer.
499 pub fn push(&mut self, bytes: &[u8]) {
500 self.buffer.extend_from_slice(bytes);
501 }
502
503 /// Extract the next complete frame if available.
504 ///
505 /// Returns `Some(Vec<u8>)` with the complete frame (including delimiters),
506 /// or `None` if no complete frame is available yet.
507 ///
508 /// The returned frame is removed from the internal buffer.
509 pub fn next_frame(&mut self) -> Option<Vec<u8>> {
510 // Find the end of the first complete frame
511 let frame_end = MllpFrame::find_frame_end(&self.buffer)?;
512
513 // Extract the frame bytes
514 let frame = self.buffer.split_to(frame_end).to_vec();
515 Some(frame)
516 }
517
518 /// Returns true if the internal buffer is empty.
519 pub fn is_empty(&self) -> bool {
520 self.buffer.is_empty()
521 }
522
523 /// Returns the number of bytes in the internal buffer.
524 pub fn len(&self) -> usize {
525 self.buffer.len()
526 }
527
528 /// Clear the internal buffer.
529 pub fn clear(&mut self) {
530 self.buffer.clear();
531 }
532}
533
534impl Default for MllpFramer {
535 fn default() -> Self {
536 Self::new()
537 }
538}
539
540/// Trait for types that can act as an MLLP byte-stream transport.
541///
542/// Implement this trait for TCP streams, serial ports, in-memory buffers,
543/// or any other byte-stream source. This crate provides no concrete
544/// implementation — that is intentionally left to consumers.
545///
546/// # Implementation Contract
547///
548/// ## Thread Safety
549/// - The transport is **not required to be `Sync`**. Each transport instance
550/// should be used from a single thread, or synchronized externally.
551/// - The transport **should be `Send`** if you need to move it between threads.
552///
553/// ## Error Handling
554/// - `read_frame` should return an error only for I/O failures (broken socket,
555/// timeout, etc.), not for malformed MLLP frames.
556/// - Malformed frames should be handled by the caller after successful read.
557/// - `write_frame` should complete the write or return an error — partial
558/// writes are considered failures.
559///
560/// ## Frame Boundaries
561/// - `read_frame` must return **exactly one complete MLLP frame** per call.
562/// - It should accumulate bytes internally until `FS+CR` is found.
563/// - Consider using [`MllpFramer`] for the accumulation logic.
564///
565/// ## Blocking Behavior
566/// - `read_frame` may block until a complete frame is available.
567/// - Non-blocking transports should use an async runtime and return
568/// `WouldBlock` errors appropriately.
569///
570/// # Example Implementation
571///
572/// ```rust,ignore
573/// use hl7_mllp::{MllpTransport, MllpFramer, MllpFrame};
574/// use std::net::TcpStream;
575/// use std::io::{self, Read, Write};
576///
577/// pub struct TcpMllpTransport {
578/// stream: TcpStream,
579/// framer: MllpFramer,
580/// }
581///
582/// impl MllpTransport for TcpMllpTransport {
583/// type Error = io::Error;
584///
585/// fn read_frame(&mut self) -> Result<Vec<u8>, Self::Error> {
586/// let mut buf = [0u8; 1024];
587/// loop {
588/// // Try to extract a complete frame first
589/// if let Some(frame) = self.framer.next_frame() {
590/// return Ok(frame);
591/// }
592/// // Read more bytes from TCP
593/// let n = self.stream.read(&mut buf)?;
594/// if n == 0 {
595/// return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "connection closed"));
596/// }
597/// self.framer.push(&buf[..n]);
598/// }
599/// }
600///
601/// fn write_frame(&mut self, frame: &[u8]) -> Result<(), Self::Error> {
602/// self.stream.write_all(frame)
603/// }
604/// }
605/// ```
606pub trait MllpTransport {
607 /// The error type returned by this transport.
608 type Error: std::error::Error;
609
610 /// Read the next complete MLLP-framed message from the transport.
611 ///
612 /// Implementations are responsible for accumulating bytes until a
613 /// complete frame is available. Use [`MllpFrame::find_frame_end`]
614 /// or [`MllpFramer`] as the completion signal.
615 fn read_frame(&mut self) -> Result<Vec<u8>, Self::Error>;
616
617 /// Write an MLLP-framed message to the transport.
618 ///
619 /// The frame should be a complete MLLP frame (including VT and FS+CR).
620 /// Implementations must ensure the entire frame is written.
621 fn write_frame(&mut self, frame: &[u8]) -> Result<(), Self::Error>;
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn roundtrip() {
630 let payload =
631 b"MSH|^~\\&|SendApp|SendFac|RecApp|RecFac|20240101120000||ORU^R01|12345|P|2.3.1";
632 let framed = MllpFrame::encode(payload);
633 let decoded = MllpFrame::decode(&framed).unwrap();
634 assert_eq!(decoded, payload);
635 }
636
637 #[test]
638 fn missing_start_byte() {
639 let bad = b"no_vt_here\x1C\x0D";
640 assert_eq!(MllpFrame::decode(bad), Err(MllpError::MissingStartByte));
641 }
642
643 #[test]
644 fn missing_end_sequence() {
645 let bad = b"\x0Bpayload_no_end";
646 assert_eq!(MllpFrame::decode(bad), Err(MllpError::MissingEndSequence));
647 }
648
649 #[test]
650 fn find_frame_end_complete() {
651 let payload = b"MSH|test";
652 let framed = MllpFrame::encode(payload);
653 assert_eq!(MllpFrame::find_frame_end(&framed), Some(framed.len()));
654 }
655
656 #[test]
657 fn find_frame_end_incomplete() {
658 let partial = b"\x0Bincomplete_data";
659 assert_eq!(MllpFrame::find_frame_end(partial), None);
660 }
661
662 // T1.1 — Consecutive frames tests
663 #[test]
664 fn find_all_frames_two_back_to_back() {
665 let payload1 = b"MSH|first";
666 let payload2 = b"MSH|second";
667 let frame1 = MllpFrame::encode(payload1);
668 let frame2 = MllpFrame::encode(payload2);
669 let combined = [&frame1[..], &frame2[..]].concat();
670
671 let frames = MllpFrame::find_all_frames(&combined);
672 assert_eq!(frames.len(), 2);
673 assert_eq!(frames[0], (0, frame1.len()));
674 assert_eq!(frames[1], (frame1.len(), frame1.len() + frame2.len()));
675
676 // Verify decoded payloads
677 let decoded1 = MllpFrame::decode(&combined[frames[0].0..frames[0].1]).unwrap();
678 let decoded2 = MllpFrame::decode(&combined[frames[1].0..frames[1].1]).unwrap();
679 assert_eq!(decoded1, payload1);
680 assert_eq!(decoded2, payload2);
681 }
682
683 #[test]
684 fn find_all_frames_with_partial_third() {
685 let payload1 = b"MSH|first";
686 let payload2 = b"MSH|second";
687 let payload3 = b"MSH|partial_no_end";
688 let frame1 = MllpFrame::encode(payload1);
689 let frame2 = MllpFrame::encode(payload2);
690 let partial3 = [&[VT][..], payload3].concat();
691
692 let combined = [&frame1[..], &frame2[..], &partial3[..]].concat();
693
694 let frames = MllpFrame::find_all_frames(&combined);
695 assert_eq!(frames.len(), 2); // Only first two complete frames
696 assert_eq!(frames[0], (0, frame1.len()));
697 assert_eq!(frames[1], (frame1.len(), frame1.len() + frame2.len()));
698 }
699
700 #[test]
701 fn find_all_frames_empty_buffer() {
702 assert!(MllpFrame::find_all_frames(b"").is_empty());
703 }
704
705 #[test]
706 fn find_all_frames_no_frames() {
707 assert!(MllpFrame::find_all_frames(b"garbage_data_no_vt").is_empty());
708 }
709
710 #[test]
711 fn find_all_frames_empty_payload_rejected() {
712 // Empty payload (VT immediately followed by FS+CR) should be rejected
713 let empty_frame = [VT, FS, CR];
714 let frames = MllpFrame::find_all_frames(&empty_frame);
715 assert!(frames.is_empty(), "Empty payload frame should be rejected");
716 }
717
718 // T1.1 — Verify byte sequence against HL7 v2.5.1 Appendix C
719 #[test]
720 fn verify_mllp_byte_constants() {
721 // VT = 0x0B (Vertical Tab) - start of block
722 // FS = 0x1C (File Separator) - end of block
723 // CR = 0x0D (Carriage Return) - terminator
724 assert_eq!(VT, 0x0B, "VT must be 0x0B per HL7 v2.5.1 Appendix C");
725 assert_eq!(FS, 0x1C, "FS must be 0x1C per HL7 v2.5.1 Appendix C");
726 assert_eq!(CR, 0x0D, "CR must be 0x0D per HL7 v2.5.1 Appendix C");
727 }
728
729 #[test]
730 fn verify_single_byte_start_block() {
731 // MLLP uses single-byte VT start block, no multi-byte variants
732 let frame = MllpFrame::encode(b"test");
733 assert_eq!(frame[0], VT);
734 assert_eq!(frame.len(), 7); // VT (1) + 4 bytes + FS (1) + CR (1)
735 }
736
737 // Noncompliance feature tests
738 #[cfg(feature = "noncompliance")]
739 mod noncompliance_tests {
740 use super::*;
741
742 #[test]
743 fn tolerate_missing_final_cr() {
744 // Frame with VT + payload + FS (missing CR)
745 let payload = b"MSH|test";
746 let incomplete = [&[VT][..], payload, &[FS]].concat();
747
748 let frames = MllpFrame::find_all_frames(&incomplete);
749 assert_eq!(frames.len(), 1);
750 assert_eq!(frames[0], (0, incomplete.len()));
751 }
752
753 #[test]
754 fn tolerate_extra_bytes_before_vt() {
755 // Garbage bytes before valid frame
756 let payload = b"MSH|test";
757 let frame = MllpFrame::encode(payload);
758 let garbage_before = [b"garbage", &frame[..]].concat();
759
760 let frames = MllpFrame::find_all_frames(&garbage_before);
761 assert_eq!(frames.len(), 1);
762 // Frame should start after garbage (at position 7)
763 assert_eq!(frames[0].0, 7);
764 }
765
766 #[test]
767 fn noncompliance_empty_payload_rejected() {
768 // Even with noncompliance, empty payload should be rejected
769 let empty_frame = [VT, FS]; // VT + FS, no payload, no CR
770 let frames = MllpFrame::find_all_frames(&empty_frame);
771 assert!(
772 frames.is_empty(),
773 "Empty payload should be rejected even with noncompliance"
774 );
775 }
776
777 #[test]
778 fn strict_mode_rejects_missing_cr() {
779 // Without noncompliance feature, missing CR should result in no frames found
780 // This test is compiled only without the feature
781 let payload = b"MSH|test";
782 let incomplete = [&[VT][..], payload, &[FS]].concat();
783
784 // In strict mode, this should not find a complete frame
785 // (But we can't test this here since it's cfg-gated)
786 }
787 }
788
789 // T1.2 — ACK generation tests
790 #[test]
791 fn build_ack_validates_empty_control_id() {
792 assert!(MllpFrame::build_ack("", true).is_none());
793 assert!(MllpFrame::build_ack("", false).is_none());
794 }
795
796 #[test]
797 fn build_ack_creates_aa_for_accept() {
798 let ack = MllpFrame::build_ack("MSG001", true).unwrap();
799 // MSA-1 = AA, MSA-2 = original control ID
800 assert!(ack.contains("MSA|AA|MSG001"));
801 }
802
803 #[test]
804 fn build_ack_creates_ae_for_reject() {
805 let ack = MllpFrame::build_ack("MSG001", false).unwrap();
806 // MSA-1 = AE, MSA-2 = original control ID
807 assert!(ack.contains("MSA|AE|MSG001"));
808 }
809
810 #[test]
811 fn build_ack_has_unique_control_id() {
812 let ack = MllpFrame::build_ack("MSG001", true).unwrap();
813 // MSH-10 should contain ACK prefix + timestamp + original ID
814 assert!(ack.contains("||ACK|ACK"));
815 assert!(ack.contains("MSG001|P|2.3.1"));
816 }
817
818 #[test]
819 fn build_nack_validates_empty_control_id() {
820 assert!(MllpFrame::build_nack("", "101", "Error").is_none());
821 }
822
823 #[test]
824 fn build_nack_creates_ar_with_error_details() {
825 let nack = MllpFrame::build_nack("MSG001", "101", "Invalid message").unwrap();
826 // MSA-1 = AR, MSA-2 = original control ID, MSA-3 = error text with code
827 assert!(nack.contains("MSA|AR|MSG001|101: 101 - Invalid message"));
828 }
829
830 #[test]
831 fn build_nack_contains_ack_msh() {
832 let nack = MllpFrame::build_nack("MSG001", "102", "Parse error").unwrap();
833 // Should have MSH with ACK message type and unique control ID
834 assert!(nack.starts_with("MSH|^~\\&|||||"));
835 assert!(nack.contains("||ACK|NACK")); // NACK prefix for unique ID
836 }
837
838 #[test]
839 fn build_nack_escapes_pipe_in_error_text() {
840 let nack = MllpFrame::build_nack("MSG001", "101", "Error|with|pipes").unwrap();
841 // Pipe characters should be escaped as \F\
842 assert!(nack.contains("Error\\F\\with\\F\\pipes"));
843 }
844
845 // T1.2 — Round-trip ACK parse test
846 #[test]
847 fn ack_roundtrip_parse() {
848 use hl7_v2::Hl7Message;
849
850 let ack_str = MllpFrame::build_ack("MSG12345", true).unwrap();
851 let ack_bytes = ack_str.as_bytes();
852
853 // Parse the ACK using hl7-v2 crate
854 let parsed = Hl7Message::parse(ack_bytes);
855 assert!(
856 parsed.is_ok(),
857 "ACK should be valid HL7 that hl7-v2 can parse"
858 );
859
860 let msg = parsed.unwrap();
861 // Verify MSH segment exists
862 let msh = msg.segment("MSH");
863 assert!(msh.is_some(), "ACK should have MSH segment");
864
865 // Verify MSA segment exists
866 let msa = msg.segment("MSA");
867 assert!(msa.is_some(), "ACK should have MSA segment");
868
869 // Verify MSA-2 contains original control ID
870 let msa_seg = msa.unwrap();
871 let msa_2 = msa_seg.raw_fields().get(1);
872 assert_eq!(msa_2, Some(&"MSG12345"));
873 }
874
875 // T1.2 — Round-trip NACK parse test
876 #[test]
877 fn nack_roundtrip_parse() {
878 use hl7_v2::Hl7Message;
879
880 let nack_str = MllpFrame::build_nack("MSG999", "102", "Processing failed").unwrap();
881 let nack_bytes = nack_str.as_bytes();
882
883 // Parse the NACK using hl7-v2 crate
884 let parsed = Hl7Message::parse(nack_bytes);
885 assert!(
886 parsed.is_ok(),
887 "NACK should be valid HL7 that hl7-v2 can parse"
888 );
889
890 let msg = parsed.unwrap();
891 // Verify MSH segment exists
892 let msh = msg.segment("MSH");
893 assert!(msh.is_some(), "NACK should have MSH segment");
894
895 // Verify MSA segment exists
896 let msa = msg.segment("MSA");
897 assert!(msa.is_some(), "NACK should have MSA segment");
898
899 // Verify MSA-1 = AR (Application Reject)
900 let msa_seg = msa.unwrap();
901 let msa_1 = msa_seg.raw_fields().first();
902 assert_eq!(msa_1, Some(&"AR"));
903
904 // Verify MSA-2 contains original control ID
905 let msa_2 = msa_seg.raw_fields().get(1);
906 assert_eq!(msa_2, Some(&"MSG999"));
907 }
908
909 // T1.3 — Streaming support tests
910 #[test]
911 fn framer_push_single_bytes_and_recover_frame() {
912 let mut framer = MllpFramer::new();
913 let frame = MllpFrame::encode(b"MSH|test");
914
915 // Push bytes one at a time
916 for byte in &frame {
917 assert!(framer.next_frame().is_none());
918 framer.push(&[*byte]);
919 }
920
921 // Now we should have a complete frame
922 let recovered = framer.next_frame().unwrap();
923 assert_eq!(recovered, frame.to_vec());
924
925 // Framer should be empty now
926 assert!(framer.is_empty());
927 }
928
929 #[test]
930 fn framer_push_two_frames_at_once() {
931 let mut framer = MllpFramer::new();
932 let frame1 = MllpFrame::encode(b"MSH|first");
933 let frame2 = MllpFrame::encode(b"MSH|second");
934
935 // Push both frames in one call
936 let combined = [&frame1[..], &frame2[..]].concat();
937 framer.push(&combined);
938
939 // Should recover first frame
940 let recovered1 = framer.next_frame().unwrap();
941 assert_eq!(recovered1, frame1.to_vec());
942
943 // Should recover second frame
944 let recovered2 = framer.next_frame().unwrap();
945 assert_eq!(recovered2, frame2.to_vec());
946
947 // No more frames
948 assert!(framer.next_frame().is_none());
949 assert!(framer.is_empty());
950 }
951
952 #[test]
953 fn framer_is_empty_and_len() {
954 let mut framer = MllpFramer::new();
955 assert!(framer.is_empty());
956 assert_eq!(framer.len(), 0);
957
958 framer.push(b"\x0Btest");
959 assert!(!framer.is_empty());
960 assert_eq!(framer.len(), 5); // VT + "test" = 5 bytes
961
962 framer.clear();
963 assert!(framer.is_empty());
964 assert_eq!(framer.len(), 0);
965 }
966
967 #[test]
968 fn framer_with_capacity() {
969 let framer = MllpFramer::with_capacity(1024);
970 assert!(framer.is_empty());
971 }
972
973 #[test]
974 fn framer_default() {
975 let framer: MllpFramer = Default::default();
976 assert!(framer.is_empty());
977 }
978
979 #[test]
980 fn framer_partial_frame_no_complete() {
981 let mut framer = MllpFramer::new();
982 // Incomplete frame (no FS+CR)
983 framer.push(b"\x0Bpartial_data");
984
985 // Should not return a complete frame
986 assert!(framer.next_frame().is_none());
987 assert!(!framer.is_empty());
988 }
989
990 #[test]
991 fn framer_preserves_remaining_bytes() {
992 let mut framer = MllpFramer::new();
993 let frame1 = MllpFrame::encode(b"MSH|first");
994 let partial = b"\x0BMSH|partial_no_end";
995
996 // Push complete frame + partial frame
997 let combined = [&frame1[..], &partial[..]].concat();
998 framer.push(&combined);
999
1000 // Extract complete frame
1001 let recovered = framer.next_frame().unwrap();
1002 assert_eq!(recovered, frame1.to_vec());
1003
1004 // Partial frame should remain in buffer
1005 assert!(!framer.is_empty());
1006 assert_eq!(framer.len(), partial.len());
1007
1008 // No complete frame yet
1009 assert!(framer.next_frame().is_none());
1010 }
1011
1012 // T1.6 — Additional tests
1013 #[test]
1014 fn encode_decode_roundtrip_unicode() {
1015 // Test with Unicode payload (UTF-8 encoded)
1016 let unicode_payload = "MSH|^~\\&|Test|Facility|20240101120000||ORU^R01|12345|P|2.5\rPID|1||P001||Doe^John^José||19800101|M".as_bytes();
1017 let framed = MllpFrame::encode(unicode_payload);
1018 let decoded = MllpFrame::decode(&framed).unwrap();
1019 assert_eq!(decoded, unicode_payload);
1020 }
1021
1022 #[test]
1023 fn decode_minimum_length_valid_frame() {
1024 // Minimum valid frame: VT + 1 byte payload + FS + CR = 4 bytes
1025 let min_frame = [VT, b'X', FS, CR];
1026 let decoded = MllpFrame::decode(&min_frame).unwrap();
1027 assert_eq!(decoded, b"X");
1028 }
1029
1030 #[test]
1031 fn find_frame_end_exactly_one_frame() {
1032 // Buffer containing exactly one complete frame (no trailing bytes)
1033 let payload = b"MSH|exact";
1034 let frame = MllpFrame::encode(payload);
1035
1036 let end = MllpFrame::find_frame_end(&frame);
1037 assert_eq!(end, Some(frame.len()));
1038 }
1039
1040 #[test]
1041 fn find_frame_end_empty_buffer() {
1042 // Empty buffer should return None
1043 assert_eq!(MllpFrame::find_frame_end(b""), None);
1044 }
1045
1046 #[test]
1047 fn find_frame_end_no_vt() {
1048 // Buffer without VT start byte should return None
1049 assert_eq!(MllpFrame::find_frame_end(b"no_vt_here"), None);
1050 }
1051
1052 #[test]
1053 fn framer_push_pop_streaming() {
1054 // Test push/pop streaming pattern
1055 let mut framer = MllpFramer::new();
1056 let frames = vec![
1057 MllpFrame::encode(b"MSH|msg1"),
1058 MllpFrame::encode(b"MSH|msg2"),
1059 MllpFrame::encode(b"MSH|msg3"),
1060 ];
1061
1062 // Push all frames at once
1063 let combined: Vec<u8> = frames.iter().flat_map(|f| f.to_vec()).collect();
1064 framer.push(&combined);
1065
1066 // Pop frames one by one
1067 for (i, expected) in frames.iter().enumerate() {
1068 let actual = framer.next_frame().unwrap();
1069 assert_eq!(actual, expected.to_vec(), "Frame {} mismatch", i);
1070 }
1071
1072 // No more frames
1073 assert!(framer.next_frame().is_none());
1074 }
1075}