use crate::error::FinTSError;
use crate::parser::{parse_message, DataElement};
pub enum VerbosityLevel {
Minimal,
Segments,
Full,
}
pub struct DecodedSegment {
pub segment_type: String,
pub segment_number: u16,
pub segment_version: u16,
pub segment_reference: Option<u16>,
pub degs: Vec<Vec<String>>,
}
pub struct DecodedMessage {
pub segments: Vec<DecodedSegment>,
pub global_codes: Vec<(String, String)>,
pub segment_codes: Vec<(String, String)>,
pub raw_bytes: usize,
}
pub fn decode_message(data: &[u8]) -> Result<DecodedMessage, FinTSError> {
let raw_segments = parse_message(data)?;
let raw_bytes = data.len();
let mut segments: Vec<DecodedSegment> = Vec::new();
let mut global_codes: Vec<(String, String)> = Vec::new();
let mut segment_codes: Vec<(String, String)> = Vec::new();
for raw in &raw_segments {
let seg_type = raw.segment_type().to_string();
let seg_num = raw.segment_number();
let seg_ver = raw.segment_version();
let seg_ref = raw.segment_reference();
let degs: Vec<Vec<String>> = raw
.degs
.iter()
.skip(1)
.map(|deg| {
deg.0
.iter()
.map(|de| de_to_display_string(de, &seg_type, VerbosityLevel::Full))
.collect()
})
.collect();
match seg_type.as_str() {
"HIRMG" => {
for deg in raw.degs.iter().skip(1) {
let code = deg.get_str(0);
let text = deg.get_str(2);
if !code.is_empty() {
global_codes.push((code, text));
}
}
}
"HIRMS" => {
for deg in raw.degs.iter().skip(1) {
let code = deg.get_str(0);
let text = deg.get_str(2);
if !code.is_empty() {
segment_codes.push((code, text));
}
}
}
_ => {}
}
segments.push(DecodedSegment {
segment_type: seg_type,
segment_number: seg_num,
segment_version: seg_ver,
segment_reference: seg_ref,
degs,
});
}
Ok(DecodedMessage {
segments,
global_codes,
segment_codes,
raw_bytes,
})
}
pub fn format_decoded(msg: &DecodedMessage, verbosity: VerbosityLevel) -> String {
let mut out = String::new();
out.push_str(&format!("FinTS message ({} bytes)\n", msg.raw_bytes));
if !msg.global_codes.is_empty() {
out.push_str(" Global codes:\n");
for (code, text) in &msg.global_codes {
out.push_str(&format!(" {} — {}\n", code, text));
}
}
if !msg.segment_codes.is_empty() {
out.push_str(" Segment codes:\n");
for (code, text) in &msg.segment_codes {
out.push_str(&format!(" {} — {}\n", code, text));
}
}
out.push_str(" Segments:\n");
for seg in &msg.segments {
match verbosity {
VerbosityLevel::Minimal => {
out.push_str(&format!(" {}\n", seg.segment_type));
}
VerbosityLevel::Segments | VerbosityLevel::Full => {
let ref_part = seg
.segment_reference
.map(|r| format!(":{}", r))
.unwrap_or_default();
out.push_str(&format!(
" {}:{}:{}{}",
seg.segment_type, seg.segment_number, seg.segment_version, ref_part
));
for deg in &seg.degs {
let deg_str = deg.join(":");
out.push_str(&format!(" + {}", deg_str));
}
out.push('\n');
}
}
}
out
}
pub fn hex_dump(data: &[u8]) -> String {
let mut out = String::new();
for (chunk_idx, chunk) in data.chunks(16).enumerate() {
let offset = chunk_idx * 16;
out.push_str(&format!("{:08x} ", offset));
for (i, byte) in chunk.iter().enumerate() {
out.push_str(&format!("{:02x} ", byte));
if i == 7 {
out.push(' ');
}
}
if chunk.len() < 16 {
let missing = 16 - chunk.len();
for i in 0..missing {
out.push_str(" ");
if chunk.len() + i == 7 {
out.push(' ');
}
}
}
out.push_str(" |");
for byte in chunk {
if byte.is_ascii_graphic() || *byte == b' ' {
out.push(*byte as char);
} else {
out.push('.');
}
}
out.push_str("|\n");
}
out
}
pub fn format_wire_log(label: &str, data: &[u8], verbosity: VerbosityLevel) -> String {
let mut out = String::new();
out.push_str(&format!("=== {} ({} bytes) ===\n", label, data.len()));
if matches!(verbosity, VerbosityLevel::Full) {
out.push_str(&hex_dump(data));
out.push('\n');
}
match decode_message(data) {
Ok(msg) => out.push_str(&format_decoded(&msg, verbosity)),
Err(e) => out.push_str(&format!(" [parse error: {}]\n", e)),
}
out
}
fn de_to_display_string(de: &DataElement, seg_type: &str, verbosity: VerbosityLevel) -> String {
match de {
DataElement::Empty => String::new(),
DataElement::Text(s) => s.clone(),
DataElement::Binary(b) => {
if seg_type == "HNSHA" {
return "[REDACTED IN HNSHA]".to_string();
}
match verbosity {
VerbosityLevel::Minimal | VerbosityLevel::Segments => {
format!("[BINARY: {} bytes]", b.len())
}
VerbosityLevel::Full => {
let hex: String = b.iter().map(|byte| format!("{:02x}", byte)).collect();
format!("[BINARY: {}]", hex)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_simple_message() {
let data = b"HNHBS:5:1+2'";
let msg = decode_message(data).expect("decode should succeed");
assert_eq!(msg.segments.len(), 1);
assert_eq!(msg.segments[0].segment_type, "HNHBS");
assert_eq!(msg.segments[0].segment_number, 5);
assert_eq!(msg.raw_bytes, 12);
}
#[test]
fn test_decode_collects_global_codes() {
let data = b"HIRMG:3:2+0010::Nachricht entgegengenommen.'HNHBS:4:1+1'";
let msg = decode_message(data).expect("decode should succeed");
assert_eq!(msg.global_codes.len(), 1);
assert_eq!(msg.global_codes[0].0, "0010");
assert_eq!(msg.global_codes[0].1, "Nachricht entgegengenommen.");
}
#[test]
fn test_format_minimal_verbosity() {
let data = b"HNHBS:5:1+2'";
let msg = decode_message(data).expect("decode should succeed");
let formatted = format_decoded(&msg, VerbosityLevel::Minimal);
assert!(formatted.contains("HNHBS"));
assert!(!formatted.contains("HNHBS:5:1"));
}
#[test]
fn test_format_segments_verbosity() {
let data = b"HNHBS:5:1+2'";
let msg = decode_message(data).expect("decode should succeed");
let formatted = format_decoded(&msg, VerbosityLevel::Segments);
assert!(formatted.contains("HNHBS:5:1"));
assert!(formatted.contains("+ 2"));
}
#[test]
fn test_hex_dump_format() {
let data = b"HNHBS:5:1";
let dump = hex_dump(data);
assert!(dump.starts_with("00000000"));
assert!(dump.contains('|'));
assert!(dump.contains("HNHBS:5:1"));
}
#[test]
fn test_binary_redacted_in_hnsha() {
let binary = DataElement::Binary(b"secret_pin".to_vec());
let display = de_to_display_string(&binary, "HNSHA", VerbosityLevel::Full);
assert_eq!(display, "[REDACTED IN HNSHA]");
}
#[test]
fn test_binary_shown_at_full_verbosity() {
let binary = DataElement::Binary(vec![0xde, 0xad]);
let display = de_to_display_string(&binary, "HNVSD", VerbosityLevel::Full);
assert!(display.contains("dead"));
}
#[test]
fn test_binary_shown_as_size_at_segments_verbosity() {
let binary = DataElement::Binary(vec![0u8; 42]);
let display = de_to_display_string(&binary, "HNVSD", VerbosityLevel::Segments);
assert_eq!(display, "[BINARY: 42 bytes]");
}
#[test]
fn test_format_wire_log_contains_label() {
let data = b"HNHBS:5:1+2'";
let log = format_wire_log("OUTBOUND", data, VerbosityLevel::Minimal);
assert!(log.contains("OUTBOUND"));
assert!(log.contains("12 bytes"));
}
#[test]
fn test_decode_message_with_segment_reference() {
let data = b"HIRMS:4:2:3+0010::Nachricht entgegengenommen.'";
let msg = decode_message(data).expect("decode should succeed");
assert_eq!(msg.segments[0].segment_reference, Some(3));
assert_eq!(msg.segment_codes.len(), 1);
}
}