Skip to main content

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{}{}", &timestamp, 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{}{}", &timestamp, 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}