1use crate::error::FinTSError;
6use crate::parser::{parse_message, DataElement};
7
8pub enum VerbosityLevel {
12 Minimal,
14 Segments,
16 Full,
18}
19
20pub struct DecodedSegment {
22 pub segment_type: String,
23 pub segment_number: u16,
24 pub segment_version: u16,
25 pub segment_reference: Option<u16>,
26 pub degs: Vec<Vec<String>>,
28}
29
30pub struct DecodedMessage {
32 pub segments: Vec<DecodedSegment>,
33 pub global_codes: Vec<(String, String)>,
35 pub segment_codes: Vec<(String, String)>,
37 pub raw_bytes: usize,
38}
39
40pub fn decode_message(data: &[u8]) -> Result<DecodedMessage, FinTSError> {
44 let raw_segments = parse_message(data)?;
45 let raw_bytes = data.len();
46
47 let mut segments: Vec<DecodedSegment> = Vec::new();
48 let mut global_codes: Vec<(String, String)> = Vec::new();
49 let mut segment_codes: Vec<(String, String)> = Vec::new();
50
51 for raw in &raw_segments {
52 let seg_type = raw.segment_type().to_string();
53 let seg_num = raw.segment_number();
54 let seg_ver = raw.segment_version();
55 let seg_ref = raw.segment_reference();
56
57 let degs: Vec<Vec<String>> = raw
59 .degs
60 .iter()
61 .skip(1)
62 .map(|deg| {
63 deg.0
64 .iter()
65 .map(|de| de_to_display_string(de, &seg_type, VerbosityLevel::Full))
66 .collect()
67 })
68 .collect();
69
70 match seg_type.as_str() {
72 "HIRMG" => {
73 for deg in raw.degs.iter().skip(1) {
74 let code = deg.get_str(0);
75 let text = deg.get_str(2);
76 if !code.is_empty() {
77 global_codes.push((code, text));
78 }
79 }
80 }
81 "HIRMS" => {
82 for deg in raw.degs.iter().skip(1) {
83 let code = deg.get_str(0);
84 let text = deg.get_str(2);
85 if !code.is_empty() {
86 segment_codes.push((code, text));
87 }
88 }
89 }
90 _ => {}
91 }
92
93 segments.push(DecodedSegment {
94 segment_type: seg_type,
95 segment_number: seg_num,
96 segment_version: seg_ver,
97 segment_reference: seg_ref,
98 degs,
99 });
100 }
101
102 Ok(DecodedMessage {
103 segments,
104 global_codes,
105 segment_codes,
106 raw_bytes,
107 })
108}
109
110pub fn format_decoded(msg: &DecodedMessage, verbosity: VerbosityLevel) -> String {
112 let mut out = String::new();
113
114 out.push_str(&format!("FinTS message ({} bytes)\n", msg.raw_bytes));
115
116 if !msg.global_codes.is_empty() {
117 out.push_str(" Global codes:\n");
118 for (code, text) in &msg.global_codes {
119 out.push_str(&format!(" {} — {}\n", code, text));
120 }
121 }
122 if !msg.segment_codes.is_empty() {
123 out.push_str(" Segment codes:\n");
124 for (code, text) in &msg.segment_codes {
125 out.push_str(&format!(" {} — {}\n", code, text));
126 }
127 }
128
129 out.push_str(" Segments:\n");
130 for seg in &msg.segments {
131 match verbosity {
132 VerbosityLevel::Minimal => {
133 out.push_str(&format!(" {}\n", seg.segment_type));
134 }
135 VerbosityLevel::Segments | VerbosityLevel::Full => {
136 let ref_part = seg
137 .segment_reference
138 .map(|r| format!(":{}", r))
139 .unwrap_or_default();
140 out.push_str(&format!(
141 " {}:{}:{}{}",
142 seg.segment_type, seg.segment_number, seg.segment_version, ref_part
143 ));
144
145 for deg in &seg.degs {
146 let deg_str = deg.join(":");
147 out.push_str(&format!(" + {}", deg_str));
148 }
149 out.push('\n');
150 }
151 }
152 }
153
154 out
155}
156
157pub fn hex_dump(data: &[u8]) -> String {
159 let mut out = String::new();
160 for (chunk_idx, chunk) in data.chunks(16).enumerate() {
161 let offset = chunk_idx * 16;
162 out.push_str(&format!("{:08x} ", offset));
164 for (i, byte) in chunk.iter().enumerate() {
166 out.push_str(&format!("{:02x} ", byte));
167 if i == 7 {
168 out.push(' ');
169 }
170 }
171 if chunk.len() < 16 {
173 let missing = 16 - chunk.len();
174 for i in 0..missing {
175 out.push_str(" ");
176 if chunk.len() + i == 7 {
177 out.push(' ');
178 }
179 }
180 }
181 out.push_str(" |");
182 for byte in chunk {
184 if byte.is_ascii_graphic() || *byte == b' ' {
185 out.push(*byte as char);
186 } else {
187 out.push('.');
188 }
189 }
190 out.push_str("|\n");
191 }
192 out
193}
194
195pub fn format_wire_log(label: &str, data: &[u8], verbosity: VerbosityLevel) -> String {
197 let mut out = String::new();
198 out.push_str(&format!("=== {} ({} bytes) ===\n", label, data.len()));
199
200 if matches!(verbosity, VerbosityLevel::Full) {
201 out.push_str(&hex_dump(data));
202 out.push('\n');
203 }
204
205 match decode_message(data) {
206 Ok(msg) => out.push_str(&format_decoded(&msg, verbosity)),
207 Err(e) => out.push_str(&format!(" [parse error: {}]\n", e)),
208 }
209
210 out
211}
212
213fn de_to_display_string(de: &DataElement, seg_type: &str, verbosity: VerbosityLevel) -> String {
218 match de {
219 DataElement::Empty => String::new(),
220 DataElement::Text(s) => s.clone(),
221 DataElement::Binary(b) => {
222 if seg_type == "HNSHA" {
223 return "[REDACTED IN HNSHA]".to_string();
224 }
225 match verbosity {
226 VerbosityLevel::Minimal | VerbosityLevel::Segments => {
227 format!("[BINARY: {} bytes]", b.len())
228 }
229 VerbosityLevel::Full => {
230 let hex: String = b.iter().map(|byte| format!("{:02x}", byte)).collect();
231 format!("[BINARY: {}]", hex)
232 }
233 }
234 }
235 }
236}
237
238#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_decode_simple_message() {
246 let data = b"HNHBS:5:1+2'";
247 let msg = decode_message(data).expect("decode should succeed");
248 assert_eq!(msg.segments.len(), 1);
249 assert_eq!(msg.segments[0].segment_type, "HNHBS");
250 assert_eq!(msg.segments[0].segment_number, 5);
251 assert_eq!(msg.raw_bytes, 12);
252 }
253
254 #[test]
255 fn test_decode_collects_global_codes() {
256 let data = b"HIRMG:3:2+0010::Nachricht entgegengenommen.'HNHBS:4:1+1'";
257 let msg = decode_message(data).expect("decode should succeed");
258 assert_eq!(msg.global_codes.len(), 1);
259 assert_eq!(msg.global_codes[0].0, "0010");
260 assert_eq!(msg.global_codes[0].1, "Nachricht entgegengenommen.");
261 }
262
263 #[test]
264 fn test_format_minimal_verbosity() {
265 let data = b"HNHBS:5:1+2'";
266 let msg = decode_message(data).expect("decode should succeed");
267 let formatted = format_decoded(&msg, VerbosityLevel::Minimal);
268 assert!(formatted.contains("HNHBS"));
269 assert!(!formatted.contains("HNHBS:5:1"));
271 }
272
273 #[test]
274 fn test_format_segments_verbosity() {
275 let data = b"HNHBS:5:1+2'";
276 let msg = decode_message(data).expect("decode should succeed");
277 let formatted = format_decoded(&msg, VerbosityLevel::Segments);
278 assert!(formatted.contains("HNHBS:5:1"));
279 assert!(formatted.contains("+ 2"));
280 }
281
282 #[test]
283 fn test_hex_dump_format() {
284 let data = b"HNHBS:5:1";
285 let dump = hex_dump(data);
286 assert!(dump.starts_with("00000000"));
288 assert!(dump.contains('|'));
290 assert!(dump.contains("HNHBS:5:1"));
291 }
292
293 #[test]
294 fn test_binary_redacted_in_hnsha() {
295 let binary = DataElement::Binary(b"secret_pin".to_vec());
296 let display = de_to_display_string(&binary, "HNSHA", VerbosityLevel::Full);
297 assert_eq!(display, "[REDACTED IN HNSHA]");
298 }
299
300 #[test]
301 fn test_binary_shown_at_full_verbosity() {
302 let binary = DataElement::Binary(vec![0xde, 0xad]);
303 let display = de_to_display_string(&binary, "HNVSD", VerbosityLevel::Full);
304 assert!(display.contains("dead"));
305 }
306
307 #[test]
308 fn test_binary_shown_as_size_at_segments_verbosity() {
309 let binary = DataElement::Binary(vec![0u8; 42]);
310 let display = de_to_display_string(&binary, "HNVSD", VerbosityLevel::Segments);
311 assert_eq!(display, "[BINARY: 42 bytes]");
312 }
313
314 #[test]
315 fn test_format_wire_log_contains_label() {
316 let data = b"HNHBS:5:1+2'";
317 let log = format_wire_log("OUTBOUND", data, VerbosityLevel::Minimal);
318 assert!(log.contains("OUTBOUND"));
319 assert!(log.contains("12 bytes"));
320 }
321
322 #[test]
323 fn test_decode_message_with_segment_reference() {
324 let data = b"HIRMS:4:2:3+0010::Nachricht entgegengenommen.'";
325 let msg = decode_message(data).expect("decode should succeed");
326 assert_eq!(msg.segments[0].segment_reference, Some(3));
327 assert_eq!(msg.segment_codes.len(), 1);
328 }
329}