#![expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
clippy::missing_errors_doc,
reason = "pre-existing MLLP implementation debt moved from staged microcrate into hl7v2; cleanup is split from topology collapse"
)]
use crate::model::Error;
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum MllpError {
#[error("Invalid MLLP frame structure: {details}")]
InvalidFrame {
details: String,
},
#[error("Missing MLLP start block character (0x0B)")]
MissingStartBlock,
#[error("Missing MLLP end block sequence (0x1C 0x0D)")]
MissingEndBlock,
#[error("IO error: {0}")]
IoError(String),
#[error("Connection timeout")]
Timeout,
}
impl From<std::io::Error> for MllpError {
fn from(err: std::io::Error) -> Self {
MllpError::IoError(err.to_string())
}
}
pub const MLLP_START: u8 = 0x0B;
pub const MLLP_END_1: u8 = 0x1C;
pub const MLLP_END_2: u8 = 0x0D;
pub fn wrap_mllp(bytes: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(bytes.len() + 3);
buf.push(MLLP_START);
buf.extend_from_slice(bytes);
buf.push(MLLP_END_1);
buf.push(MLLP_END_2);
buf
}
pub fn unwrap_mllp(bytes: &[u8]) -> Result<&[u8], Error> {
if bytes.is_empty() || bytes[0] != MLLP_START {
return Err(Error::Framing(
"Missing MLLP start block character (0x0B)".to_string(),
));
}
let end_pos = find_mllp_end(bytes)?;
Ok(&bytes[1..end_pos])
}
pub fn unwrap_mllp_checked(bytes: &[u8]) -> Result<&[u8], MllpError> {
if bytes.is_empty() || bytes[0] != MLLP_START {
return Err(MllpError::MissingStartBlock);
}
let end_pos = find_mllp_end_checked(bytes)?;
Ok(&bytes[1..end_pos])
}
pub fn unwrap_mllp_owned(bytes: &[u8]) -> Result<Vec<u8>, Error> {
unwrap_mllp(bytes).map(<[u8]>::to_vec)
}
pub fn unwrap_mllp_owned_checked(bytes: &[u8]) -> Result<Vec<u8>, MllpError> {
unwrap_mllp_checked(bytes).map(<[u8]>::to_vec)
}
fn find_mllp_end(bytes: &[u8]) -> Result<usize, Error> {
for i in 0..bytes.len().saturating_sub(1) {
if bytes[i] == MLLP_END_1 && bytes[i + 1] == MLLP_END_2 {
return Ok(i);
}
}
Err(Error::Framing(
"Missing MLLP end block sequence (0x1C 0x0D)".to_string(),
))
}
fn find_mllp_end_checked(bytes: &[u8]) -> Result<usize, MllpError> {
for i in 0..bytes.len().saturating_sub(1) {
if bytes[i] == MLLP_END_1 && bytes[i + 1] == MLLP_END_2 {
return Ok(i);
}
}
Err(MllpError::MissingEndBlock)
}
pub fn is_mllp_framed(bytes: &[u8]) -> bool {
!bytes.is_empty() && bytes[0] == MLLP_START
}
pub fn find_complete_mllp_message(bytes: &[u8]) -> Option<usize> {
if bytes.is_empty() || bytes[0] != MLLP_START {
return None;
}
for i in 1..bytes.len().saturating_sub(1) {
if bytes[i] == MLLP_END_1 && bytes[i + 1] == MLLP_END_2 {
return Some(i + 2);
}
}
None
}
#[derive(Debug, Default)]
pub struct MllpFrameIterator {
buffer: Vec<u8>,
}
impl MllpFrameIterator {
pub fn new() -> Self {
Self { buffer: Vec::new() }
}
pub fn extend(&mut self, bytes: &[u8]) {
self.buffer.extend_from_slice(bytes);
}
pub fn next_frame(&mut self) -> Option<Vec<u8>> {
let total_len = find_complete_mllp_message(&self.buffer)?;
let frame: Vec<u8> = self.buffer.drain(..total_len).collect();
Some(frame)
}
pub fn next_message(&mut self) -> Option<Result<Vec<u8>, Error>> {
let frame = self.next_frame()?;
Some(unwrap_mllp_owned(&frame))
}
pub fn buffer_len(&self) -> usize {
self.buffer.len()
}
pub fn clear(&mut self) {
self.buffer.clear();
}
}
#[cfg(test)]
mod tests;