1use crate::{PDUError, Result};
2use chrono::{DateTime, TimeZone, Timelike, Utc};
3use lazy_static::lazy_static;
4use std::collections::HashMap;
5
6fn swap_nibbles(data: &str) -> String {
8 let mut res = String::with_capacity(data.len());
9 let mut chars = data.chars();
10 while let (Some(c2), Some(c1)) = (chars.next(), chars.next()) {
11 res.push(c1);
12 res.push(c2);
13 }
14 res
15}
16
17pub struct Date;
19
20impl Date {
21 pub fn decode(data: &str) -> Result<DateTime<Utc>> {
23 let swapped = swap_nibbles(data);
24 if swapped.len() != 14 {
25 return Err(PDUError::EndOfPdu);
26 }
27
28 let year = 2000 + swapped[0..2].parse::<u8>().unwrap_or(0) as i32;
29 let month = swapped[2..4].parse::<u8>().unwrap_or(0) as u32;
30 let day = swapped[4..6].parse::<u8>().unwrap_or(0) as u32;
31 let hour = swapped[6..8].parse::<u8>().unwrap_or(0) as u32;
32 let minute = swapped[8..10].parse::<u8>().unwrap_or(0) as u32;
33 let second = swapped[10..12].parse::<u8>().unwrap_or(0) as u32;
34 let tz_data = u8::from_str_radix(&swapped[12..14], 16).unwrap_or(0);
35
36 let tz_multiplier = if tz_data & 0x80 != 0 { -1 } else { 1 };
37 let tz_offset_abs = format!("{:x}", tz_data & 0x7f).parse::<i64>().unwrap_or(0);
39 let tz_delta_minutes = 15 * tz_multiplier * tz_offset_abs;
40
41 let tz_offset = chrono::FixedOffset::east_opt(tz_delta_minutes as i32 * 60)
43 .ok_or_else(|| PDUError::InvalidToa("Invalid Date Time Offset".to_string()))?;
44
45 let local_date = tz_offset
46 .with_ymd_and_hms(year, month, day, hour, minute, second)
47 .single()
48 .ok_or_else(|| PDUError::InvalidToa("Invalid Date components".to_string()))?
49 .with_nanosecond(0)
50 .unwrap();
51
52 Ok(local_date.with_timezone(&Utc))
53 }
54
55 pub fn encode(date: &DateTime<Utc>) -> String {
57 let result = date.format("%y%m%d%H%M%S").to_string();
59
60 let tz_delta_seconds: f64 = 0.0;
62 let tz_delta_quarters = (tz_delta_seconds.abs() / 60.0 / 15.0).round() as i32;
64 let tz_delta_gsm =
66 i32::from_str_radix(&tz_delta_quarters.to_string(), 16).unwrap_or(0) as u8;
67
68 let tz_delta_gsm = if tz_delta_seconds < 0.0 {
69 tz_delta_gsm | 0x80 } else {
71 tz_delta_gsm
72 };
73
74 let hex_with_tz = format!("{}{:02x}", result, tz_delta_gsm);
75 swap_nibbles(&hex_with_tz)
76 }
77}
78
79pub struct Number;
81
82impl Number {
83 pub fn decode(data: &str) -> Result<String> {
85 let mut data = swap_nibbles(data);
86 if data.ends_with('F') || data.ends_with('f') {
87 data.pop();
88 }
89 Ok(data)
90 }
91
92 pub fn encode(data: &str) -> String {
94 let mut data = data.to_string();
95 if !data.len().is_multiple_of(2) {
96 data.push('F');
97 }
98 swap_nibbles(&data)
99 }
100}
101
102pub struct TypeOfAddress;
104
105lazy_static! {
106 static ref TON: HashMap<u8, &'static str> = {
107 let mut m = HashMap::new();
108 m.insert(0b000, "unknown");
109 m.insert(0b001, "international");
110 m.insert(0b010, "national");
111 m.insert(0b011, "specific");
112 m.insert(0b100, "subscriber");
113 m.insert(0b101, "alphanumeric");
114 m.insert(0b110, "abbreviated");
115 m.insert(0b111, "extended");
116 m
117 };
118 static ref TON_INV: HashMap<&'static str, u8> = TON.iter().map(|(k, v)| (*v, *k)).collect();
119 static ref NPI: HashMap<u8, &'static str> = {
120 let mut m = HashMap::new();
121 m.insert(0b0000, "unknown");
122 m.insert(0b0001, "isdn");
123 m.insert(0b0011, "data");
124 m.insert(0b0100, "telex");
125 m.insert(0b0101, "specific1");
126 m.insert(0b0110, "specific2");
127 m.insert(0b1000, "national");
128 m.insert(0b1001, "private");
129 m.insert(0b1010, "ermes");
130 m.insert(0b1111, "extended");
131 m
132 };
133 static ref NPI_INV: HashMap<&'static str, u8> = NPI.iter().map(|(k, v)| (*v, *k)).collect();
134}
135
136#[derive(Debug, PartialEq)]
137pub struct Toa {
138 pub ton: String,
139 pub npi: String,
140}
141
142impl TypeOfAddress {
143 pub fn decode(data: &str) -> Result<Toa> {
145 let octet = u8::from_str_radix(data, 16)
146 .map_err(|_| PDUError::InvalidHex(hex::FromHexError::InvalidStringLength))?;
147
148 if octet & 0x80 == 0 {
149 return Err(PDUError::InvalidToaExtension);
150 }
151
152 let ton_bits = (octet & 0x70) >> 4;
153 let npi_bits = octet & 0x0F;
154
155 let ton = TON.get(&ton_bits).ok_or(PDUError::InvalidTon)?.to_string();
156 let npi = NPI.get(&npi_bits).ok_or(PDUError::InvalidNpi)?.to_string();
157
158 Ok(Toa { ton, npi })
159 }
160
161 pub fn encode(data: &Toa) -> Result<String> {
163 let ton_bits = TON_INV
164 .get(data.ton.as_str())
165 .ok_or_else(|| PDUError::InvalidToa("Invalid TON".to_string()))?;
166 let npi_bits = NPI_INV
167 .get(data.npi.as_str())
168 .ok_or_else(|| PDUError::InvalidToa("Invalid NPI".to_string()))?;
169
170 let octet: u8 = 0x80 | (ton_bits << 4) | npi_bits;
171 Ok(format!("{:02X}", octet))
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use chrono::{TimeZone, Utc};
179
180 #[test]
182 fn test_swap_nibbles() {
183 assert_eq!(swap_nibbles("0123"), "1032");
184 assert_eq!(swap_nibbles("123456"), "214365");
185 }
186
187 #[test]
189 fn test_date_decode() -> Result<()> {
190 let decoded = Date::decode("70402132522400")?;
191 assert_eq!(
192 decoded,
193 Utc.with_ymd_and_hms(2007, 4, 12, 23, 25, 42).unwrap()
194 );
195 Ok(())
196 }
197
198 #[test]
199 fn test_date_decode_positive_offset() -> Result<()> {
200 let decoded = Date::decode("70402132522423")?;
201 assert_eq!(
202 decoded,
203 Utc.with_ymd_and_hms(2007, 4, 12, 15, 25, 42).unwrap()
204 );
205 Ok(())
206 }
207
208 #[test]
209 fn test_date_decode_negative_offset() -> Result<()> {
210 let decoded = Date::decode("3130523210658A")?;
211 assert_eq!(
212 decoded,
213 Utc.with_ymd_and_hms(2013, 3, 26, 6, 1, 56).unwrap()
214 );
215 Ok(())
216 }
217
218 #[test]
219 fn test_date_encode() {
220 let dt_utc = Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap();
221 assert_eq!(Date::encode(&dt_utc), "81101000000000"); }
231
232 #[test]
234 fn test_number_decode() -> Result<()> {
235 assert_eq!(Number::decode("5155214365F7")?, "15551234567");
236 assert_eq!(Number::decode("1032547698")?, "0123456789");
237 Ok(())
238 }
239
240 #[test]
241 fn test_number_encode() {
242 assert_eq!(Number::encode("15551234567"), "5155214365F7");
243 assert_eq!(Number::encode("0123456789"), "1032547698");
244 }
245
246 #[test]
247 fn test_number_empty() {
248 assert_eq!(Number::encode(""), Number::decode("").unwrap(), "");
249 }
250
251 #[test]
253 fn test_toa_decode() -> Result<()> {
254 assert_eq!(
255 TypeOfAddress::decode("91")?,
256 Toa {
257 ton: "international".to_string(),
258 npi: "isdn".to_string()
259 }
260 );
261 Ok(())
262 }
263
264 #[test]
265 fn test_toa_encode() -> Result<()> {
266 let toa = Toa {
267 ton: "international".to_string(),
268 npi: "isdn".to_string(),
269 };
270 assert_eq!(TypeOfAddress::encode(&toa)?, "91");
271 Ok(())
272 }
273
274 #[test]
275 fn test_toa_unknown() -> Result<()> {
276 assert_eq!(
277 TypeOfAddress::decode("80")?,
278 Toa {
279 ton: "unknown".to_string(),
280 npi: "unknown".to_string()
281 }
282 );
283 assert_eq!(TypeOfAddress::encode(&TypeOfAddress::decode("80")?)?, "80");
284 Ok(())
285 }
286
287 #[test]
288 fn test_toa_decode_invalid_extension() {
289 assert!(matches!(
290 TypeOfAddress::decode("00"),
291 Err(PDUError::InvalidToaExtension)
292 ));
293 }
294
295 #[test]
297 fn test_toa_decode_invalid_npi() {
298 assert!(matches!(
299 TypeOfAddress::decode("82"),
300 Err(PDUError::InvalidNpi)
301 ));
302 }
303
304 #[test]
305 fn test_toa_encode_invalid_npi() {
306 let toa = Toa {
307 npi: "strange".to_string(),
308 ton: "international".to_string(),
309 };
310 assert!(matches!(
311 TypeOfAddress::encode(&toa),
312 Err(PDUError::InvalidToa(_))
313 ));
314 }
315
316 #[test]
317 fn test_toa_encode_invalid_ton() {
318 let toa = Toa {
319 npi: "isdn".to_string(),
320 ton: "strange".to_string(),
321 };
322 assert!(matches!(
323 TypeOfAddress::encode(&toa),
324 Err(PDUError::InvalidToa(_))
325 ));
326 }
327}