use crate::error::{FinTSError, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DataElement {
Empty,
Text(String),
Binary(Vec<u8>),
}
impl DataElement {
pub fn as_str(&self) -> Option<&str> {
match self {
DataElement::Text(s) => Some(s),
_ => None,
}
}
pub fn as_text(&self) -> String {
match self {
DataElement::Text(s) => s.clone(),
DataElement::Empty => String::new(),
DataElement::Binary(b) => {
let (cow, _, _) = encoding_rs::WINDOWS_1252.decode(b);
cow.into_owned()
}
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
DataElement::Binary(b) => Some(b),
_ => None,
}
}
pub fn is_empty(&self) -> bool {
matches!(self, DataElement::Empty)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DEG(pub Vec<DataElement>);
impl DEG {
pub fn new() -> Self {
DEG(Vec::new())
}
pub fn get(&self, index: usize) -> &DataElement {
self.0.get(index).unwrap_or(&DataElement::Empty)
}
pub fn get_str(&self, index: usize) -> String {
self.get(index).as_text()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty() || self.0.iter().all(|de| de.is_empty())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RawSegment {
pub degs: Vec<DEG>,
}
impl RawSegment {
pub fn segment_type(&self) -> &str {
match self.degs.first().and_then(|deg| deg.0.first()) {
Some(DataElement::Text(s)) => s.as_str(),
_ => "",
}
}
pub fn segment_number(&self) -> u16 {
self.degs
.first()
.and_then(|deg| deg.0.get(1))
.and_then(|de| de.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
pub fn segment_version(&self) -> u16 {
self.degs
.first()
.and_then(|deg| deg.0.get(2))
.and_then(|de| de.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
pub fn segment_reference(&self) -> Option<u16> {
self.degs
.first()
.and_then(|deg| deg.0.get(3))
.and_then(|de| de.as_str())
.and_then(|s| s.parse().ok())
}
pub fn deg(&self, index: usize) -> &DEG {
static EMPTY: DEG = DEG(Vec::new());
self.degs.get(index).unwrap_or(&EMPTY)
}
pub fn deg_count(&self) -> usize {
self.degs.len()
}
}
pub fn parse_message(data: &[u8]) -> Result<Vec<RawSegment>> {
let mut segments = Vec::new();
let mut pos = 0;
let len = data.len();
while pos < len {
while pos < len && (data[pos] == b'\r' || data[pos] == b'\n' || data[pos] == b' ') {
pos += 1;
}
if pos >= len {
break;
}
let (segment, new_pos) = parse_segment(data, pos)?;
segments.push(segment);
pos = new_pos;
}
Ok(segments)
}
fn parse_segment(data: &[u8], start: usize) -> Result<(RawSegment, usize)> {
let mut degs: Vec<DEG> = Vec::new();
let mut current_deg = DEG::new();
let mut pos = start;
let len = data.len();
let mut need_new_de = true;
loop {
if pos >= len {
if !current_deg.0.is_empty() {
degs.push(current_deg);
}
break;
}
match data[pos] {
b'?' => {
pos += 1;
if pos >= len {
return Err(FinTSError::Parse(
"Unexpected end after escape character".into(),
));
}
let ch = data[pos];
let buf = [ch];
let (cow, _, _) = encoding_rs::WINDOWS_1252.decode(&buf);
if need_new_de {
current_deg.0.push(DataElement::Text(cow.into_owned()));
need_new_de = false;
} else {
append_text_to_de(&mut current_deg, &cow);
}
pos += 1;
}
b'@' => {
pos += 1;
let num_start = pos;
while pos < len && data[pos] != b'@' {
pos += 1;
}
if pos >= len {
return Err(FinTSError::Parse(
"Unterminated binary length prefix".into(),
));
}
let len_str = std::str::from_utf8(&data[num_start..pos])
.map_err(|e| FinTSError::Parse(format!("Invalid binary length: {}", e)))?;
let bin_len: usize = len_str.parse().map_err(|e| {
FinTSError::Parse(format!("Invalid binary length number: {}", e))
})?;
pos += 1;
if pos + bin_len > data.len() {
return Err(FinTSError::Parse(format!(
"Binary data extends past end of message: need {} bytes at pos {}, only {} available",
bin_len, pos, data.len() - pos
)));
}
let binary_data = data[pos..pos + bin_len].to_vec();
current_deg.0.push(DataElement::Binary(binary_data));
need_new_de = false;
pos += bin_len;
}
b':' => {
if current_deg.0.is_empty() {
current_deg.0.push(DataElement::Empty);
}
pos += 1;
need_new_de = true;
if pos < len && (data[pos] == b':' || data[pos] == b'+' || data[pos] == b'\'') {
current_deg.0.push(DataElement::Empty);
}
}
b'+' => {
if current_deg.0.is_empty() {
current_deg.0.push(DataElement::Empty);
}
degs.push(current_deg);
current_deg = DEG::new();
need_new_de = true;
pos += 1;
}
b'\'' => {
if current_deg.0.is_empty() {
current_deg.0.push(DataElement::Empty);
}
degs.push(current_deg);
pos += 1;
return Ok((RawSegment { degs }, pos));
}
_ => {
let char_start = pos;
while pos < len
&& data[pos] != b'?'
&& data[pos] != b'@'
&& data[pos] != b':'
&& data[pos] != b'+'
&& data[pos] != b'\''
{
pos += 1;
}
let (cow, _, _) = encoding_rs::WINDOWS_1252.decode(&data[char_start..pos]);
if need_new_de {
current_deg.0.push(DataElement::Text(cow.into_owned()));
need_new_de = false;
} else {
append_text_to_de(&mut current_deg, &cow);
}
}
}
}
Ok((RawSegment { degs }, pos))
}
fn append_text_to_de(deg: &mut DEG, text: &str) {
if let Some(DataElement::Text(ref mut s)) = deg.0.last_mut() {
s.push_str(text);
} else {
deg.0.push(DataElement::Text(text.to_string()));
}
}
pub fn parse_inner_segments(data: &[u8]) -> Result<Vec<RawSegment>> {
parse_message(data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_segment() {
let data = b"HNHBS:5:1+2'";
let segments = parse_message(data).unwrap();
assert_eq!(segments.len(), 1);
let seg = &segments[0];
assert_eq!(seg.segment_type(), "HNHBS");
assert_eq!(seg.segment_number(), 5);
assert_eq!(seg.segment_version(), 1);
assert_eq!(seg.deg(1).get_str(0), "2");
}
#[test]
fn test_parse_multiple_segments() {
let data = b"HIRMG:3:2+0010::Nachricht entgegengenommen.'HNHBS:4:1+1'";
let segments = parse_message(data).unwrap();
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].segment_type(), "HIRMG");
assert_eq!(segments[1].segment_type(), "HNHBS");
}
#[test]
fn test_parse_empty_des() {
let data = b"HKTAN:5:7+4+HKIDN+++++++'";
let segments = parse_message(data).unwrap();
let seg = &segments[0];
assert_eq!(seg.segment_type(), "HKTAN");
assert_eq!(seg.deg(1).get_str(0), "4");
assert_eq!(seg.deg(2).get_str(0), "HKIDN");
}
#[test]
fn test_parse_deg_with_colons() {
let data = b"HNSHK:2:4+PIN:1+999'";
let segments = parse_message(data).unwrap();
let seg = &segments[0];
assert_eq!(seg.deg(1).get_str(0), "PIN");
assert_eq!(seg.deg(1).get_str(1), "1");
assert_eq!(seg.deg(2).get_str(0), "999");
}
#[test]
fn test_parse_escaped_characters() {
let data = b"TEST:1:1+Hello?+World?:Test?'End'";
let segments = parse_message(data).unwrap();
let seg = &segments[0];
assert_eq!(seg.deg(1).get_str(0), "Hello+World:Test'End");
}
#[test]
fn test_parse_binary_data() {
let data = b"TEST:1:1+@5@HELLO+next'";
let segments = parse_message(data).unwrap();
let seg = &segments[0];
assert_eq!(seg.deg(1).get(0).as_bytes().unwrap(), b"HELLO");
assert_eq!(seg.deg(2).get_str(0), "next");
}
#[test]
fn test_segment_header_with_reference() {
let data = b"HIRMS:4:2:3+0010::Nachricht entgegengenommen.'";
let segments = parse_message(data).unwrap();
let seg = &segments[0];
assert_eq!(seg.segment_type(), "HIRMS");
assert_eq!(seg.segment_number(), 4);
assert_eq!(seg.segment_version(), 2);
assert_eq!(seg.segment_reference(), Some(3));
}
}