Skip to main content

iso8583_core/
encoding.rs

1//! Encoding and decoding utilities for ISO 8583 messages
2//!
3//! Supports multiple encoding formats:
4//! - ASCII: Standard text encoding
5//! - BCD (Binary Coded Decimal): Compact numeric encoding
6//! - EBCDIC: IBM mainframe encoding (less common)
7
8use crate::error::{ISO8583Error, Result};
9
10/// Encoding format for ISO 8583 messages
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Encoding {
13    /// ASCII encoding
14    ASCII,
15    /// Binary Coded Decimal
16    BCD,
17    /// EBCDIC (IBM mainframe)
18    EBCDIC,
19}
20
21/// Encode numeric string to BCD
22///
23/// Each pair of digits is encoded into one byte.
24/// If the string has odd length, a leading zero is added.
25///
26/// Example: "1234" -> [0x12, 0x34]
27/// Example: "123" -> [0x01, 0x23]
28pub fn encode_bcd(s: &str) -> Result<Vec<u8>> {
29    if !s.chars().all(|c| c.is_ascii_digit()) {
30        return Err(ISO8583Error::EncodingError(format!(
31            "BCD encoding requires numeric input, got: {}",
32            s
33        )));
34    }
35
36    let mut padded = s.to_string();
37    if padded.len() % 2 != 0 {
38        padded.insert(0, '0');
39    }
40
41    let mut result = Vec::with_capacity(padded.len() / 2);
42
43    for chunk in padded.as_bytes().chunks(2) {
44        let high = (chunk[0] - b'0') << 4;
45        let low = chunk[1] - b'0';
46        result.push(high | low);
47    }
48
49    Ok(result)
50}
51
52/// Decode BCD to numeric string
53pub fn decode_bcd(bytes: &[u8], length: usize) -> Result<String> {
54    let mut result = String::with_capacity(length);
55
56    for &byte in bytes {
57        let high = (byte >> 4) & 0x0F;
58        let low = byte & 0x0F;
59
60        if high > 9 || low > 9 {
61            return Err(ISO8583Error::EncodingError(format!(
62                "Invalid BCD byte: 0x{:02X}",
63                byte
64            )));
65        }
66
67        result.push((b'0' + high) as char);
68        result.push((b'0' + low) as char);
69
70        if result.len() >= length {
71            break;
72        }
73    }
74
75    // Remove leading zeros if needed
76    result.truncate(length);
77
78    Ok(result)
79}
80
81/// Encode string to ASCII bytes
82pub fn encode_ascii(s: &str) -> Vec<u8> {
83    s.as_bytes().to_vec()
84}
85
86/// Decode ASCII bytes to string
87pub fn decode_ascii(bytes: &[u8]) -> Result<String> {
88    std::str::from_utf8(bytes)
89        .map(|s| s.to_string())
90        .map_err(|e| ISO8583Error::EncodingError(format!("Invalid ASCII: {}", e)))
91}
92
93/// EBCDIC to ASCII conversion table (simplified)
94const EBCDIC_TO_ASCII: &[u8; 256] = &[
95    0x00, 0x01, 0x02, 0x03, 0x9C, 0x09, 0x86, 0x7F, // 0x00-0x07
96    0x97, 0x8D, 0x8E, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // 0x08-0x0F
97    0x10, 0x11, 0x12, 0x13, 0x9D, 0x85, 0x08, 0x87, // 0x10-0x17
98    0x18, 0x19, 0x92, 0x8F, 0x1C, 0x1D, 0x1E, 0x1F, // 0x18-0x1F
99    0x80, 0x81, 0x82, 0x83, 0x84, 0x0A, 0x17, 0x1B, // 0x20-0x27
100    0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x05, 0x06, 0x07, // 0x28-0x2F
101    0x90, 0x91, 0x16, 0x93, 0x94, 0x95, 0x96, 0x04, // 0x30-0x37
102    0x98, 0x99, 0x9A, 0x9B, 0x14, 0x15, 0x9E, 0x1A, // 0x38-0x3F
103    0x20, 0xA0, 0xE2, 0xE4, 0xE0, 0xE1, 0xE3, 0xE5, // 0x40-0x47 (space, special chars)
104    0xE7, 0xF1, 0xA2, 0x2E, 0x3C, 0x28, 0x2B, 0x7C, // 0x48-0x4F
105    0x26, 0xE9, 0xEA, 0xEB, 0xE8, 0xED, 0xEE, 0xEF, // 0x50-0x57
106    0xEC, 0xDF, 0x21, 0x24, 0x2A, 0x29, 0x3B, 0xAC, // 0x58-0x5F
107    0x2D, 0x2F, 0xC2, 0xC4, 0xC0, 0xC1, 0xC3, 0xC5, // 0x60-0x67
108    0xC7, 0xD1, 0xA6, 0x2C, 0x25, 0x5F, 0x3E, 0x3F, // 0x68-0x6F
109    0xF8, 0xC9, 0xCA, 0xCB, 0xC8, 0xCD, 0xCE, 0xCF, // 0x70-0x77
110    0xCC, 0x60, 0x3A, 0x23, 0x40, 0x27, 0x3D, 0x22, // 0x78-0x7F
111    0xD8, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // 0x80-0x87 (a-g)
112    0x68, 0x69, 0xAB, 0xBB, 0xF0, 0xFD, 0xFE, 0xB1, // 0x88-0x8F
113    0xB0, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, // 0x90-0x97 (j-p)
114    0x71, 0x72, 0xAA, 0xBA, 0xE6, 0xB8, 0xC6, 0xA4, // 0x98-0x9F
115    0xB5, 0x7E, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, // 0xA0-0xA7 (s-x)
116    0x79, 0x7A, 0xA1, 0xBF, 0xD0, 0xDD, 0xDE, 0xAE, // 0xA8-0xAF
117    0x5E, 0xA3, 0xA5, 0xB7, 0xA9, 0xA7, 0xB6, 0xBC, // 0xB0-0xB7
118    0xBD, 0xBE, 0x5B, 0x5D, 0xAF, 0xA8, 0xB4, 0xD7, // 0xB8-0xBF
119    0x7B, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // 0xC0-0xC7 (A-G)
120    0x48, 0x49, 0xAD, 0xF4, 0xF6, 0xF2, 0xF3, 0xF5, // 0xC8-0xCF
121    0x7D, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, // 0xD0-0xD7 (J-P)
122    0x51, 0x52, 0xB9, 0xFB, 0xFC, 0xF9, 0xFA, 0xFF, // 0xD8-0xDF
123    0x5C, 0xF7, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, // 0xE0-0xE7 (S-X)
124    0x59, 0x5A, 0xB2, 0xD4, 0xD6, 0xD2, 0xD3, 0xD5, // 0xE8-0xEF
125    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0xF0-0xF7 (0-7)
126    0x38, 0x39, 0xB3, 0xDB, 0xDC, 0xD9, 0xDA, 0x9F, // 0xF8-0xFF (8-9)
127];
128
129/// Encode string to EBCDIC bytes
130pub fn encode_ebcdic(s: &str) -> Result<Vec<u8>> {
131    let mut result = Vec::with_capacity(s.len());
132
133    for byte in s.as_bytes() {
134        // Find ASCII byte in conversion table
135        let ebcdic = EBCDIC_TO_ASCII
136            .iter()
137            .position(|&b| b == *byte)
138            .ok_or_else(|| {
139                ISO8583Error::EncodingError(format!("Cannot encode byte to EBCDIC: 0x{:02X}", byte))
140            })?;
141
142        result.push(ebcdic as u8);
143    }
144
145    Ok(result)
146}
147
148/// Decode EBCDIC bytes to string
149pub fn decode_ebcdic(bytes: &[u8]) -> Result<String> {
150    let ascii_bytes: Vec<u8> = bytes.iter().map(|&b| EBCDIC_TO_ASCII[b as usize]).collect();
151
152    decode_ascii(&ascii_bytes)
153}
154
155/// Encode length indicator (for LLVAR and LLLVAR fields)
156pub fn encode_length(length: usize, digits: usize, encoding: Encoding) -> Result<Vec<u8>> {
157    let length_str = format!("{:0width$}", length, width = digits);
158
159    match encoding {
160        Encoding::ASCII => Ok(encode_ascii(&length_str)),
161        Encoding::BCD => encode_bcd(&length_str),
162        Encoding::EBCDIC => encode_ebcdic(&length_str),
163    }
164}
165
166/// Decode length indicator
167pub fn decode_length(bytes: &[u8], digits: usize, encoding: Encoding) -> Result<usize> {
168    let length_str = match encoding {
169        Encoding::ASCII => decode_ascii(bytes)?,
170        Encoding::BCD => decode_bcd(bytes, digits)?,
171        Encoding::EBCDIC => decode_ebcdic(bytes)?,
172    };
173
174    length_str
175        .parse()
176        .map_err(|e| ISO8583Error::EncodingError(format!("Invalid length value: {}", e)))
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_bcd_encoding() {
185        let encoded = encode_bcd("1234").unwrap();
186        assert_eq!(encoded, vec![0x12, 0x34]);
187
188        let encoded = encode_bcd("123").unwrap();
189        assert_eq!(encoded, vec![0x01, 0x23]);
190    }
191
192    #[test]
193    fn test_bcd_decoding() {
194        let decoded = decode_bcd(&[0x12, 0x34], 4).unwrap();
195        assert_eq!(decoded, "1234");
196
197        let decoded = decode_bcd(&[0x01, 0x23], 3).unwrap();
198        assert_eq!(decoded, "012");
199    }
200
201    #[test]
202    fn test_ascii_encoding() {
203        let encoded = encode_ascii("Hello");
204        assert_eq!(encoded, b"Hello");
205    }
206
207    #[test]
208    fn test_ascii_decoding() {
209        let decoded = decode_ascii(b"Hello").unwrap();
210        assert_eq!(decoded, "Hello");
211    }
212
213    #[test]
214    fn test_length_encoding_ascii() {
215        let encoded = encode_length(12, 2, Encoding::ASCII).unwrap();
216        assert_eq!(encoded, b"12");
217
218        let encoded = encode_length(123, 3, Encoding::ASCII).unwrap();
219        assert_eq!(encoded, b"123");
220    }
221
222    #[test]
223    fn test_length_decoding_ascii() {
224        let decoded = decode_length(b"12", 2, Encoding::ASCII).unwrap();
225        assert_eq!(decoded, 12);
226
227        let decoded = decode_length(b"123", 3, Encoding::ASCII).unwrap();
228        assert_eq!(decoded, 123);
229    }
230
231    #[test]
232    fn test_length_encoding_bcd() {
233        let encoded = encode_length(12, 2, Encoding::BCD).unwrap();
234        assert_eq!(encoded, vec![0x12]);
235    }
236
237    #[test]
238    fn test_invalid_bcd_input() {
239        assert!(encode_bcd("12A4").is_err());
240        assert!(encode_bcd("ABCD").is_err());
241    }
242
243    #[test]
244    fn test_ebcdic_numbers() {
245        // Test numbers 0-9
246        let encoded = encode_ebcdic("0123456789").unwrap();
247        let decoded = decode_ebcdic(&encoded).unwrap();
248        assert_eq!(decoded, "0123456789");
249    }
250
251    #[test]
252    fn test_ebcdic_letters() {
253        // Test letters
254        let encoded = encode_ebcdic("ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap();
255        let decoded = decode_ebcdic(&encoded).unwrap();
256        assert_eq!(decoded, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
257    }
258}