sms_pdu_decoder/
fields.rs

1use crate::codecs::{GSM, UCS2};
2use crate::elements::{Date, Number, Toa, TypeOfAddress};
3use crate::{PDUError, Result};
4
5use std::io::{Cursor, Read, Seek, SeekFrom};
6
7/// A custom reader that ensures all requested bytes are read or returns an error.
8struct PDUReader {
9    cursor: Cursor<Vec<u8>>,
10}
11
12impl PDUReader {
13    fn new(data: &str) -> Result<Self> {
14        if !data.len().is_multiple_of(2) {
15            return Err(PDUError::OddLength);
16        }
17        let bytes = hex::decode(data)?;
18        Ok(PDUReader {
19            cursor: Cursor::new(bytes),
20        })
21    }
22
23    /// Reads `len` hex octets (2 * len characters) from the stream and returns them as a String.
24    fn read_hex(&mut self, len: usize) -> Result<String> {
25        let mut buffer = vec![0; len];
26        if self.cursor.read_exact(&mut buffer).is_err() {
27            return Err(PDUError::EndOfPdu);
28        }
29        Ok(hex::encode_upper(buffer))
30    }
31
32    /// Reads up to `len` hex octets, returning whatever is available (for handling truncated PDUs)
33    fn read_hex_available(&mut self, len: usize) -> Result<String> {
34        let mut buffer = vec![0; len];
35        let bytes_read = self
36            .cursor
37            .read(&mut buffer)
38            .map_err(|_| PDUError::EndOfPdu)?;
39        buffer.truncate(bytes_read);
40        Ok(hex::encode_upper(buffer))
41    }
42
43    /// Reads a single octet (2 characters) from the stream and returns it as a u8.
44    fn read_octet(&mut self) -> Result<u8> {
45        let hex_str = self.read_hex(1)?;
46        Ok(u8::from_str_radix(&hex_str, 16).unwrap_or(0))
47    }
48
49    /// Returns the current position in bytes.
50    fn position(&self) -> u64 {
51        self.cursor.position()
52    }
53
54    /// Sets the current position in bytes.
55    fn seek(&mut self, pos: u64) -> Result<u64> {
56        self.cursor
57            .seek(SeekFrom::Start(pos))
58            .map_err(|_| PDUError::EndOfPdu)
59    }
60}
61
62// Helper struct for shared context between PDU fields
63#[derive(Debug, Default)]
64pub struct DecodeContext {
65    pub header: Option<PDUHeaderDecoded>,
66    pub dcs: Option<DcsDecoded>,
67}
68
69/// --- Address Field ---
70#[derive(Debug, PartialEq)]
71pub struct AddressDecoded {
72    pub length: u8,
73    pub toa: Toa,
74    pub number: String,
75}
76
77pub struct Address;
78
79impl Address {
80    /// Decodes an address from PDU.
81    pub fn decode(reader: &mut PDUReader) -> Result<AddressDecoded> {
82        let length = reader.read_octet()?;
83        let toa = TypeOfAddress::decode(&reader.read_hex(1)?)?;
84
85        // Number of octets to read for the number. +1 handles the case where length is odd (BCD padding)
86        let octets_to_read = (length as usize).div_ceil(2);
87        let encoded_number = reader.read_hex(octets_to_read)?;
88
89        let number = if toa.ton == "alphanumeric" {
90            GSM::decode(&encoded_number, false)?
91        } else {
92            Number::decode(&encoded_number)?
93        };
94
95        Ok(AddressDecoded {
96            length,
97            toa,
98            number,
99        })
100    }
101}
102
103/// --- SMSC Field ---
104#[derive(Debug, PartialEq)]
105pub struct SmscDecoded {
106    pub length: u8,
107    pub toa: Option<Toa>,
108    pub number: Option<String>,
109}
110
111pub struct SMSC;
112
113impl SMSC {
114    /// Decodes the SMS-C information PDU.
115    pub fn decode(reader: &mut PDUReader) -> Result<SmscDecoded> {
116        let length = reader.read_octet()?;
117        if length == 0 {
118            return Ok(SmscDecoded {
119                length: 0,
120                toa: None,
121                number: None,
122            });
123        }
124
125        let toa = TypeOfAddress::decode(&reader.read_hex(1)?)?;
126
127        let octets_to_read = length as usize - 1;
128        let encoded_number = reader.read_hex(octets_to_read)?;
129
130        let number = if toa.ton == "alphanumeric" {
131            GSM::decode(&encoded_number, false)?
132        } else {
133            Number::decode(&encoded_number)?
134        };
135
136        Ok(SmscDecoded {
137            length,
138            toa: Some(toa),
139            number: Some(number),
140        })
141    }
142}
143
144/// --- PDU Header (Incoming/Deliver) ---
145#[derive(Debug, PartialEq, Default, Clone)]
146pub struct PDUHeaderDecoded {
147    pub rp: bool,
148    pub udhi: bool,
149    pub sri: bool,
150    pub lp: bool,
151    pub mms: bool,
152    pub mti: String,
153}
154
155pub struct PDUHeader;
156
157impl PDUHeader {
158    const MTI: [(u8, &'static str); 3] = [
159        (0b00, "deliver"),
160        (0b01, "submit-report"),
161        (0b10, "status-report"),
162    ];
163
164    /// Decodes an incoming PDU header.
165    pub fn decode(reader: &mut PDUReader) -> Result<PDUHeaderDecoded> {
166        let octet = reader.read_octet()?;
167
168        let mti_bits = octet & 0x03;
169        let mti = Self::MTI
170            .iter()
171            .find(|(k, _)| *k == mti_bits)
172            .map(|(_, v)| v.to_string())
173            .ok_or(PDUError::InvalidMti)?;
174
175        Ok(PDUHeaderDecoded {
176            rp: octet & 0x80 != 0,   // Reply Path
177            udhi: octet & 0x40 != 0, // User Data Header Indicator
178            sri: octet & 0x20 != 0,  // Status Report Indication
179            lp: octet & 0x08 != 0,   // Loop Prevention (bit 3 skipped)
180            mms: octet & 0x04 != 0,  // More Messages to Send
181            mti,
182        })
183    }
184}
185
186/// --- Outgoing PDU Header (Submit) ---
187#[derive(Debug, PartialEq, Default, Clone)]
188pub struct OutgoingPDUHeaderDecoded {
189    pub rp: bool,
190    pub udhi: bool,
191    pub srr: bool,
192    pub vpf: u8,
193    pub rd: bool,
194    pub mti: String,
195}
196
197pub struct OutgoingPDUHeader;
198
199impl OutgoingPDUHeader {
200    const MTI: [(u8, &'static str); 3] = [(0b00, "deliver"), (0b01, "submit"), (0b10, "status")];
201
202    /// Decodes an outgoing PDU header.
203    pub fn decode(reader: &mut PDUReader) -> Result<OutgoingPDUHeaderDecoded> {
204        let octet = reader.read_octet()?;
205
206        let mti_bits = octet & 0x03;
207        let mti = Self::MTI
208            .iter()
209            .find(|(k, _)| *k == mti_bits)
210            .map(|(_, v)| v.to_string())
211            .ok_or(PDUError::InvalidMti)?;
212
213        Ok(OutgoingPDUHeaderDecoded {
214            rp: octet & 0x80 != 0,    // Reply Path
215            udhi: octet & 0x40 != 0,  // User Data Header Indicator
216            srr: octet & 0x20 != 0,   // Status Report Request
217            vpf: (octet & 0x18) >> 3, // Validity Period Format
218            rd: octet & 0x04 != 0,    // Reject Duplicates
219            mti,
220        })
221    }
222}
223
224/// --- Data Coding Scheme (DCS) ---
225#[derive(Debug, PartialEq, Default, Clone)]
226pub struct DcsDecoded {
227    pub encoding: String,
228}
229
230pub struct DCS;
231
232impl DCS {
233    pub fn decode(reader: &mut PDUReader) -> Result<DcsDecoded> {
234        let dcs = reader.read_octet()?;
235        let coding = (dcs & 0b1100) >> 2;
236        let encoding = match coding {
237            1 => "binary".to_string(),
238            2 => "ucs2".to_string(),
239            _ => "gsm".to_string(),
240        };
241        Ok(DcsDecoded { encoding })
242    }
243}
244
245/// --- Information Element ---
246#[derive(Debug, PartialEq)]
247pub struct InformationElementDecoded {
248    pub iei: u8,
249    pub length: u8,
250    pub data: serde_json::Value,
251}
252
253pub struct InformationElement;
254
255impl InformationElement {
256    fn concatenated_sms(data: &str, length_bits: u8) -> serde_json::Value {
257        // Data is always 3 bytes (6 hex chars): reference, parts_count, part_number
258        if data.len() < 6 {
259            return serde_json::Value::String(data.to_string());
260        }
261
262        let bytes = hex::decode(data).unwrap_or_default();
263        if bytes.len() != 3 {
264            return serde_json::Value::String(data.to_string());
265        }
266
267        let reference = if length_bits == 16 {
268            (bytes[0] as u16) << 8 | (bytes[1] as u16)
269        } else {
270            bytes[0] as u16
271        };
272
273        let (parts_count, part_number) = if length_bits == 16 {
274            (bytes[2], bytes[3]) // This branch is theoretically wrong for a 3-byte IE, but follows Python's logic
275        } else {
276            (bytes[1], bytes[2])
277        };
278
279        serde_json::json!({
280            "reference": reference,
281            "parts_count": parts_count,
282            "part_number": part_number,
283        })
284    }
285
286    pub fn decode(reader: &mut PDUReader) -> Result<InformationElementDecoded> {
287        let iei = reader.read_octet()?;
288        let length = reader.read_octet()?;
289        let data_hex = reader.read_hex(length as usize)?;
290
291        let processed_data = match iei {
292            0x00 => Self::concatenated_sms(&data_hex, 8),
293            0x08 => Self::concatenated_sms(&data_hex, 16),
294            _ => serde_json::Value::String(data_hex),
295        };
296
297        Ok(InformationElementDecoded {
298            iei,
299            length,
300            data: processed_data,
301        })
302    }
303}
304
305/// --- User Data Header ---
306#[derive(Debug, PartialEq, Default)]
307pub struct UserDataHeaderDecoded {
308    pub length: u8,
309    pub elements: Vec<InformationElementDecoded>,
310}
311
312pub struct UserDataHeader;
313
314impl UserDataHeader {
315    pub fn decode(reader: &mut PDUReader) -> Result<UserDataHeaderDecoded> {
316        let length = reader.read_octet()?;
317        let final_position = reader.position() + length as u64;
318        let mut elements = Vec::new();
319        while reader.position() < final_position {
320            elements.push(InformationElement::decode(reader)?);
321        }
322        Ok(UserDataHeaderDecoded { length, elements })
323    }
324}
325
326/// --- User Data ---
327#[derive(Debug, PartialEq, Default)]
328pub struct UserDataDecoded {
329    pub header: Option<UserDataHeaderDecoded>,
330    pub data: String,
331    pub warning: Option<String>,
332}
333
334pub struct UserData;
335
336impl UserData {
337    pub fn decode(reader: &mut PDUReader, ctx: &DecodeContext) -> Result<UserDataDecoded> {
338        let length_octets = reader.read_octet()?;
339        let pdu_start = reader.position();
340        let mut header: Option<UserDataHeaderDecoded> = None;
341        let mut header_length_octets: u8 = 0;
342
343        if ctx.header.as_ref().map(|h| h.udhi).unwrap_or(false) {
344            header = Some(UserDataHeader::decode(reader)?);
345            header_length_octets = header.as_ref().unwrap().length + 1;
346        }
347
348        let data_length_octets = length_octets.saturating_sub(header_length_octets);
349        let encoding = ctx
350            .dcs
351            .as_ref()
352            .map(|d| d.encoding.as_str())
353            .unwrap_or("gsm");
354        let mut warning: Option<String> = None;
355
356        let user_data = match encoding {
357            "binary" => {
358                let hex_data = reader.read_hex(data_length_octets as usize)?;
359                hex::decode(hex_data)
360                    .map_err(|_| PDUError::InvalidHex(hex::FromHexError::InvalidStringLength))?
361                    .iter()
362                    .map(|b| format!("{:02X}", b))
363                    .collect::<String>()
364            }
365            "gsm" => {
366                reader.seek(pdu_start)?;
367                let header_length_bits = header_length_octets as usize * 8;
368                let data_length_septets = length_octets as usize;
369
370                // The number of octets needed to store all septets
371                let data_length_bytes = (data_length_septets * 7).div_ceil(8);
372                let data_hex = reader.read_hex(data_length_bytes)?;
373
374                let decoded_msg = GSM::decode(&data_hex, false)?;
375
376                // Calculate where the header septets end
377                let header_length_septets = header_length_bits.div_ceil(7);
378
379                if decoded_msg.len() > header_length_septets {
380                    decoded_msg[header_length_septets..].to_string()
381                } else {
382                    decoded_msg.to_string() // Should be empty or the remainder
383                }
384            }
385            "ucs2" => {
386                let expected_hex_len = 2 * data_length_octets as usize;
387                let hex_data = reader.read_hex_available(data_length_octets as usize)?;
388
389                let is_truncated = hex_data.len() < expected_hex_len;
390
391                if is_truncated {
392                    warning = Some(PDUError::UserDataTruncated.to_string());
393                    let trunc_len = hex_data.len() - (hex_data.len() % 4);
394                    UCS2::decode(&hex_data[..trunc_len])? + "…"
395                } else {
396                    UCS2::decode(&hex_data)?
397                }
398            }
399            _ => return Err(PDUError::NonRecognizedEncoding(encoding.to_string())),
400        };
401
402        Ok(UserDataDecoded {
403            header,
404            data: user_data,
405            warning,
406        })
407    }
408}
409
410/// --- SMS Deliver (Incoming) ---
411#[derive(Debug, PartialEq)]
412pub struct SmsDeliverDecoded {
413    pub smsc: SmscDecoded,
414    pub header: PDUHeaderDecoded,
415    pub sender: AddressDecoded,
416    pub pid: u8,
417    pub dcs: DcsDecoded,
418    pub scts: chrono::DateTime<chrono::Utc>,
419    pub user_data: UserDataDecoded,
420}
421
422pub struct SMSDeliver;
423
424impl SMSDeliver {
425    /// Decodes an SMS-DELIVER TP-DU.
426    pub fn decode(data: &str) -> Result<SmsDeliverDecoded> {
427        let mut reader = PDUReader::new(data)?;
428        let smsc = SMSC::decode(&mut reader)?;
429        let header = PDUHeader::decode(&mut reader)?;
430        let sender = Address::decode(&mut reader)?;
431        let pid = reader.read_octet()?;
432        let dcs = DCS::decode(&mut reader)?;
433        let scts_hex = reader.read_hex(7)?;
434        let scts = Date::decode(&scts_hex)?;
435
436        let ctx = DecodeContext {
437            header: Some(header.clone()),
438            dcs: Some(dcs.clone()),
439        };
440        let user_data = UserData::decode(&mut reader, &ctx)?;
441
442        Ok(SmsDeliverDecoded {
443            smsc,
444            header,
445            sender,
446            pid,
447            dcs,
448            scts,
449            user_data,
450        })
451    }
452}
453
454/// --- SMS Submit (Outgoing) ---
455#[derive(Debug, PartialEq)]
456pub struct SmsSubmitDecoded {
457    pub smsc: SmscDecoded,
458    pub header: OutgoingPDUHeaderDecoded,
459    pub message_ref: u8,
460    pub recipient: AddressDecoded,
461    pub pid: u8,
462    pub dcs: DcsDecoded,
463    pub vp: Option<u8>,
464    pub validity_minutes: Option<u16>,
465    pub validity_hours: Option<u16>,
466    pub validity_days: Option<u8>,
467    pub validity_weeks: Option<u8>,
468    pub vp_date: Option<chrono::DateTime<chrono::Utc>>,
469    pub user_data: UserDataDecoded,
470}
471
472pub struct SMSSubmit;
473
474impl SMSSubmit {
475    /// Decodes an SMS-SUBMIT TP-DU.
476    pub fn decode(data: &str) -> Result<SmsSubmitDecoded> {
477        let mut reader = PDUReader::new(data)?;
478        let smsc = SMSC::decode(&mut reader)?;
479        let header = OutgoingPDUHeader::decode(&mut reader)?;
480        let message_ref = reader.read_octet()?;
481        let recipient = Address::decode(&mut reader)?;
482        let pid = reader.read_octet()?;
483        let dcs = DCS::decode(&mut reader)?;
484
485        let mut decoded = SmsSubmitDecoded {
486            smsc,
487            header: header.clone(),
488            message_ref,
489            recipient,
490            pid,
491            dcs: dcs.clone(),
492            vp: None,
493            validity_minutes: None,
494            validity_hours: None,
495            validity_days: None,
496            validity_weeks: None,
497            vp_date: None,
498            user_data: UserDataDecoded::default(),
499        };
500
501        if header.vpf == 0 {
502            // No Validity Period
503        } else if header.vpf == 2 {
504            // Relative Format
505            let vp = reader.read_octet()?;
506            decoded.vp = Some(vp);
507            if vp <= 143 {
508                decoded.validity_minutes = Some(vp as u16 * 5);
509            } else if vp <= 167 {
510                decoded.validity_hours = Some(12 + (vp - 143) as u16 / 2);
511            } else if vp <= 196 {
512                decoded.validity_days = Some(vp - 166);
513            } else {
514                decoded.validity_weeks = Some(vp - 192);
515            }
516        } else if header.vpf == 3 {
517            // Absolute Format
518            let vp_hex = reader.read_hex(7)?;
519            decoded.vp_date = Some(Date::decode(&vp_hex)?);
520        } else {
521            // VPF = 1, Enhanced Format - Skip
522            reader.read_hex(7)?;
523        }
524
525        let user_data_ctx = DecodeContext {
526            header: Some(PDUHeaderDecoded {
527                udhi: decoded.header.udhi,
528                ..Default::default()
529            }),
530            dcs: Some(decoded.dcs.clone()),
531        };
532        decoded.user_data = UserData::decode(&mut reader, &user_data_ctx)?;
533
534        Ok(decoded)
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_decode_truncated_ucs2() -> Result<()> {
544        let pdu = "0891683110304105F1240D91683167414052F700081270115183942344597D70E6597D70E651CF80A551CF80A55C";
545
546        let decoded_data = SMSDeliver::decode(pdu)?;
547
548        // Check the decoded data (U+597D '好', U+70烦 '烦', U+51CF '减', U+80A5 '肥')
549        // The last 4 hex characters '5C' are the start of a character, which is truncated.
550        // The truncated PDU should be '好烦好烦减肥减肥…'
551        assert_eq!(decoded_data.user_data.data, "好烦好烦减肥减肥…");
552
553        // Check for the presence of a warning
554        assert!(decoded_data.user_data.warning.is_some());
555
556        Ok(())
557    }
558}