sms_pdu_decoder/
fields.rs

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