#![forbid(unsafe_code)]
#![warn(missing_docs)]
use bytes::{Bytes, BytesMut, BufMut};
pub const VT: u8 = 0x0B;
pub const FS: u8 = 0x1C;
pub const CR: u8 = 0x0D;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MllpError {
MissingStartByte,
MissingEndSequence,
EmptyPayload,
Incomplete,
}
impl std::fmt::Display for MllpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingStartByte => write!(f, "MLLP frame missing VT start byte (0x0B)"),
Self::MissingEndSequence => write!(f, "MLLP frame missing FS+CR end sequence (0x1C 0x0D)"),
Self::EmptyPayload => write!(f, "MLLP frame contains no HL7 payload"),
Self::Incomplete => write!(f, "Buffer does not contain a complete MLLP frame"),
}
}
}
impl std::error::Error for MllpError {}
pub struct MllpFrame;
impl MllpFrame {
pub fn encode(payload: &[u8]) -> Bytes {
let mut buf = BytesMut::with_capacity(payload.len() + 3);
buf.put_u8(VT);
buf.put_slice(payload);
buf.put_u8(FS);
buf.put_u8(CR);
buf.freeze()
}
pub fn decode(buf: &[u8]) -> Result<&[u8], MllpError> {
if buf.len() < 4 {
return Err(MllpError::Incomplete);
}
if buf[0] != VT {
return Err(MllpError::MissingStartByte);
}
let end = buf.len();
if buf[end - 2] != FS || buf[end - 1] != CR {
return Err(MllpError::MissingEndSequence);
}
let payload = &buf[1..end - 2];
if payload.is_empty() {
return Err(MllpError::EmptyPayload);
}
Ok(payload)
}
pub fn find_frame_end(buf: &[u8]) -> Option<usize> {
if buf.is_empty() || buf[0] != VT {
return None;
}
for i in 1..buf.len().saturating_sub(1) {
if buf[i] == FS && buf[i + 1] == CR {
return Some(i + 2);
}
}
None
}
pub fn build_ack(message_control_id: &str, accepting: bool) -> String {
let code = if accepting { "AA" } else { "AE" };
format!(
"MSH|^~\\&|||||{}||ACK|{}|P|2.3.1\rMSA|{}|{}",
chrono_now_str(),
message_control_id,
code,
message_control_id,
)
}
}
fn chrono_now_str() -> String {
"20250101000000".to_string()
}
pub trait MllpTransport {
type Error: std::error::Error;
fn read_frame(&mut self) -> Result<Vec<u8>, Self::Error>;
fn write_frame(&mut self, frame: &[u8]) -> Result<(), Self::Error>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
let payload = b"MSH|^~\\&|SendApp|SendFac|RecApp|RecFac|20240101120000||ORU^R01|12345|P|2.3.1";
let framed = MllpFrame::encode(payload);
let decoded = MllpFrame::decode(&framed).unwrap();
assert_eq!(decoded, payload);
}
#[test]
fn missing_start_byte() {
let bad = b"no_vt_here\x1C\x0D";
assert_eq!(MllpFrame::decode(bad), Err(MllpError::MissingStartByte));
}
#[test]
fn missing_end_sequence() {
let bad = b"\x0Bpayload_no_end";
assert_eq!(MllpFrame::decode(bad), Err(MllpError::MissingEndSequence));
}
#[test]
fn find_frame_end_complete() {
let payload = b"MSH|test";
let framed = MllpFrame::encode(payload);
assert_eq!(MllpFrame::find_frame_end(&framed), Some(framed.len()));
}
#[test]
fn find_frame_end_incomplete() {
let partial = b"\x0Bincomplete_data";
assert_eq!(MllpFrame::find_frame_end(partial), None);
}
}