Skip to main content

serial_codec/sms/pdu/
mod.rs

1//! 3GPP TS 23.040 TPDUs in TS 27.005 PDU-mode form (SMSC address + TPDU).
2//!
3//! Decoding is strict about lengths (a TP-UDL that points past the data is an error,
4//! so is an odd UCS2 payload) and lenient about values the spec tells receivers to
5//! tolerate: reserved DCS groups decode as GSM 7-bit (23.038 §4), TOA bit 7 is not
6//! checked, trailing octets after the user data are ignored, an SCTS that is not a
7//! valid date decodes as `None` (the raw octets stay available).
8
9use super::gsm7::{self, Language, Tables};
10use super::{hex_decode, ucs2};
11use serde::{Deserialize, Serialize};
12use std::fmt;
13
14mod encode;
15pub use encode::*;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum PduError {
20    /// Input is not an even-length hex string.
21    InvalidHex,
22    /// Data ended inside the named field.
23    Truncated(&'static str),
24    /// The named field holds a value the spec does not allow.
25    Invalid(&'static str),
26    /// TP-MTI 0b11 (reserved, 23.040 §9.2.3.1).
27    ReservedMessageType,
28    /// Encoding: character not representable in the chosen alphabet.
29    Unencodable(char),
30    /// Encoding: the message needs more than 255 concatenated parts.
31    TooManyParts,
32    /// Encoding: the message needs concatenation but
33    /// [`SubmitRequest::concat_ref`] is `None`.
34    MissingConcatRef,
35}
36
37impl fmt::Display for PduError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::InvalidHex => f.write_str("PDU is not a valid hex string"),
41            Self::Truncated(field) => write!(f, "PDU truncated in {field}"),
42            Self::Invalid(field) => write!(f, "invalid {field}"),
43            Self::ReservedMessageType => f.write_str("reserved TP-MTI"),
44            Self::Unencodable(c) => write!(f, "character {c:?} is not encodable"),
45            Self::TooManyParts => f.write_str("message needs more than 255 parts"),
46            Self::MissingConcatRef => {
47                f.write_str("message needs concatenation: set a concatenation reference")
48            }
49        }
50    }
51}
52
53impl std::error::Error for PduError {}
54
55type Result<T> = std::result::Result<T, PduError>;
56
57// ---------------------------------------------------------------- addresses
58
59/// Type of number, TOA bits 6–4 (23.040 §9.1.2.5).
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum Ton {
63    Unknown,
64    International,
65    National,
66    NetworkSpecific,
67    Subscriber,
68    Alphanumeric,
69    Abbreviated,
70    Reserved,
71}
72
73impl Ton {
74    pub fn from_toa(toa: u8) -> Self {
75        match (toa >> 4) & 7 {
76            0 => Self::Unknown,
77            1 => Self::International,
78            2 => Self::National,
79            3 => Self::NetworkSpecific,
80            4 => Self::Subscriber,
81            5 => Self::Alphanumeric,
82            6 => Self::Abbreviated,
83            _ => Self::Reserved,
84        }
85    }
86}
87
88/// TOA for international numbers, ISDN/E.164 plan.
89pub const TOA_INTERNATIONAL: u8 = 0x91;
90/// TOA for unknown type, ISDN/E.164 plan (what most phones send without `+`).
91pub const TOA_UNKNOWN: u8 = 0x81;
92/// TOA for national numbers, ISDN/E.164 plan.
93pub const TOA_NATIONAL: u8 = 0xA1;
94/// TOA for alphanumeric (GSM 7-bit packed) addresses.
95pub const TOA_ALPHANUMERIC: u8 = 0xD0;
96
97/// An address field: type of address octet and its value — digits (`0-9 * # a b c`)
98/// or, for TON 5, the decoded text. The value never carries a `+`; [`Display`] adds
99/// one for international numbers.
100///
101/// [`Display`]: fmt::Display
102#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
103pub struct Address {
104    pub toa: u8,
105    pub value: String,
106}
107
108impl Address {
109    pub fn ton(&self) -> Ton {
110        Ton::from_toa(self.toa)
111    }
112
113    /// Numbering plan identification, TOA bits 3–0.
114    pub fn npi(&self) -> u8 {
115        self.toa & 0x0F
116    }
117}
118
119impl fmt::Display for Address {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        if self.ton() == Ton::International && !self.value.is_empty() {
122            f.write_str("+")?;
123        }
124        f.write_str(&self.value)
125    }
126}
127
128const SEMI_OCTET: &[u8; 15] = b"0123456789*#abc";
129
130/// Swapped semi-octets → digits; stops at the first `F` filler. `digits` caps the count.
131pub fn decode_semi_octets(bytes: &[u8], digits: usize) -> String {
132    bytes
133        .iter()
134        .flat_map(|b| [b & 0x0F, b >> 4])
135        .take(digits)
136        .take_while(|&n| n != 0x0F)
137        .map(|n| char::from(SEMI_OCTET[usize::from(n)]))
138        .collect()
139}
140
141/// Digits (`0-9 * # a b c`, case-insensitive) → swapped semi-octets, `F`-padded.
142/// `Err(c)` names the first character that has no semi-octet value.
143pub fn encode_semi_octets(digits: &str) -> std::result::Result<Vec<u8>, char> {
144    let nibbles = digits
145        .chars()
146        .map(|c| {
147            SEMI_OCTET
148                .iter()
149                .position(|&d| char::from(d) == c.to_ascii_lowercase())
150                .map(|n| n as u8)
151                .ok_or(c)
152        })
153        .collect::<std::result::Result<Vec<u8>, char>>()?;
154    Ok(nibbles
155        .chunks(2)
156        .map(|p| p[0] | (p.get(1).copied().unwrap_or(0x0F) << 4))
157        .collect())
158}
159
160fn decode_address_value(toa: u8, body: &[u8], semi_octets: usize) -> String {
161    if Ton::from_toa(toa) == Ton::Alphanumeric {
162        let septets = gsm7::unpack(body, 0, semi_octets * 4 / 7);
163        gsm7::decode(&septets, Tables::DEFAULT)
164    } else {
165        decode_semi_octets(body, semi_octets)
166    }
167}
168
169/// TP address field (OA/DA/RA): `len(digits) TOA value`. Returns the address and the
170/// octets consumed.
171pub fn decode_address(bytes: &[u8]) -> Result<(Address, usize)> {
172    let mut r = Reader::new(bytes);
173    let digits = usize::from(r.u8("address length")?);
174    if digits > 20 {
175        return Err(PduError::Invalid("address length"));
176    }
177    let toa = r.u8("address type")?;
178    let body = r.take(digits.div_ceil(2), "address")?;
179    let value = decode_address_value(toa, body, digits);
180    Ok((Address { toa, value }, r.pos))
181}
182
183/// SMSC address field (`len(octets incl. TOA) TOA value`); length 0 → `None`.
184pub fn decode_smsc(bytes: &[u8]) -> Result<(Option<Address>, usize)> {
185    let mut r = Reader::new(bytes);
186    let len = usize::from(r.u8("SMSC length")?);
187    if len == 0 {
188        return Ok((None, 1));
189    }
190    if len > 11 {
191        return Err(PduError::Invalid("SMSC length"));
192    }
193    let toa = r.u8("SMSC type")?;
194    let body = r.take(len - 1, "SMSC address")?;
195    let value = decode_address_value(toa, body, body.len() * 2);
196    Ok((Some(Address { toa, value }), r.pos))
197}
198
199// ---------------------------------------------------------------- timestamps
200
201/// TP-SCTS / TP-DT / absolute TP-VP (23.040 §9.2.3.11): local time plus the offset to
202/// UTC in signed quarter hours. Two-digit years map to 1970–2069.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
204pub struct Timestamp {
205    pub year: u16,
206    pub month: u8,
207    pub day: u8,
208    pub hour: u8,
209    pub minute: u8,
210    pub second: u8,
211    pub tz_quarters: i8,
212}
213
214fn bcd(b: u8) -> Option<u8> {
215    let (tens, units) = (b & 0x0F, b >> 4);
216    (tens <= 9 && units <= 9).then_some(tens * 10 + units)
217}
218
219fn to_bcd(v: u8) -> u8 {
220    (v % 10) << 4 | (v / 10)
221}
222
223fn days_in_month(year: u16, month: u8) -> u8 {
224    match month {
225        2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29,
226        2 => 28,
227        4 | 6 | 9 | 11 => 30,
228        _ => 31,
229    }
230}
231
232impl Timestamp {
233    /// Decode 7 swapped-BCD octets; `None` if any field is out of range.
234    pub fn decode(b: &[u8; 7]) -> Option<Self> {
235        let yy = bcd(b[0])?;
236        let year = if yy >= 70 { 1900 } else { 2000 } + u16::from(yy);
237        let (month, day, hour, minute, second) =
238            (bcd(b[1])?, bcd(b[2])?, bcd(b[3])?, bcd(b[4])?, bcd(b[5])?);
239        let tz_quarters = decode_tz(b[6])?;
240        let ok = (1..=12).contains(&month)
241            && day >= 1
242            && day <= days_in_month(year, month)
243            && hour < 24
244            && minute < 60
245            && second < 60;
246        ok.then_some(Timestamp {
247            year,
248            month,
249            day,
250            hour,
251            minute,
252            second,
253            tz_quarters,
254        })
255    }
256
257    /// Encode; `None` if the year is outside 1970–2069, the offset beyond ±79
258    /// quarters, or a field out of range.
259    pub fn encode(&self) -> Option<[u8; 7]> {
260        if !(1970..=2069).contains(&self.year) || self.tz_quarters.unsigned_abs() > 79 {
261            return None;
262        }
263        let out = [
264            to_bcd((self.year % 100) as u8),
265            to_bcd(self.month),
266            to_bcd(self.day),
267            to_bcd(self.hour),
268            to_bcd(self.minute),
269            to_bcd(self.second),
270            to_bcd(self.tz_quarters.unsigned_abs()) | if self.tz_quarters < 0 { 0x08 } else { 0 },
271        ];
272        // Round trip rejects out-of-range fields (month 13, Feb 30, …).
273        (Self::decode(&out) == Some(*self)).then_some(out)
274    }
275
276    pub fn offset_minutes(&self) -> i32 {
277        i32::from(self.tz_quarters) * 15
278    }
279
280    /// Seconds since 1970-01-01T00:00:00Z.
281    pub fn unix_utc(&self) -> i64 {
282        // Howard Hinnant's days_from_civil.
283        let (m, d) = (i64::from(self.month), i64::from(self.day));
284        let y = i64::from(self.year) - i64::from(m <= 2);
285        let era = y.div_euclid(400);
286        let yoe = y - era * 400;
287        let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1;
288        let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
289        let days = era * 146_097 + doe - 719_468;
290        days * 86_400
291            + i64::from(self.hour) * 3600
292            + i64::from(self.minute) * 60
293            + i64::from(self.second)
294            - i64::from(self.offset_minutes()) * 60
295    }
296}
297
298/// The time zone octet alone: signed quarter hours, `None` for a non-BCD digit.
299pub fn decode_tz(b: u8) -> Option<i8> {
300    let q = bcd(b & 0xF7)? as i8;
301    Some(if b & 0x08 != 0 { -q } else { q })
302}
303
304impl fmt::Display for Timestamp {
305    /// ISO 8601 with offset, e.g. `2011-01-01T12:34:56+01:00`.
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        let off = self.offset_minutes();
308        write!(
309            f,
310            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
311            self.year,
312            self.month,
313            self.day,
314            self.hour,
315            self.minute,
316            self.second,
317            if off < 0 { '-' } else { '+' },
318            off.unsigned_abs() / 60,
319            off.unsigned_abs() % 60
320        )
321    }
322}
323
324// ---------------------------------------------------------------- validity period
325
326/// TP-VP (23.040 §9.2.3.12).
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
328pub enum Validity {
329    /// Relative format octet; see [`relative_minutes`] / [`relative_from_minutes`].
330    Relative(u8),
331    Absolute(Timestamp),
332    /// Enhanced format, raw 7 octets (bits 2–0 of the first octet give the sub-format).
333    Enhanced([u8; 7]),
334}
335
336impl Validity {
337    /// TP-VPF value for the first octet.
338    pub fn vpf(&self) -> u8 {
339        match self {
340            Self::Relative(_) => 2,
341            Self::Absolute(_) => 3,
342            Self::Enhanced(_) => 1,
343        }
344    }
345}
346
347/// Minutes encoded by a relative TP-VP octet.
348pub fn relative_minutes(vp: u8) -> u32 {
349    let v = u32::from(vp);
350    match vp {
351        0..=143 => (v + 1) * 5,
352        144..=167 => 720 + (v - 143) * 30,
353        168..=196 => (v - 166) * 1440,
354        _ => (v - 192) * 10_080,
355    }
356}
357
358/// Smallest relative TP-VP octet covering `minutes` (rounded up); `None` beyond the
359/// 63-week maximum.
360pub fn relative_from_minutes(minutes: u32) -> Option<u8> {
361    let v = match minutes {
362        0..=720 => minutes.div_ceil(5).max(1) - 1,
363        721..=1440 => 143 + (minutes - 720).div_ceil(30),
364        1441..=43_200 => 166 + minutes.div_ceil(1440),
365        _ => 192 + minutes.div_ceil(10_080),
366    };
367    u8::try_from(v).ok()
368}
369
370// ---------------------------------------------------------------- DCS
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
373#[serde(rename_all = "lowercase")]
374pub enum Alphabet {
375    Gsm7,
376    Bit8,
377    Ucs2,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
381#[serde(rename_all = "lowercase")]
382pub enum MwiKind {
383    Voicemail,
384    Fax,
385    Email,
386    Other,
387}
388
389/// Message waiting indication from DCS groups 1100/1101/1110 (23.038 §4).
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391pub struct Mwi {
392    pub active: bool,
393    pub kind: MwiKind,
394    /// `false` for the "discard message" group (1100).
395    pub store: bool,
396}
397
398/// TP-DCS for SMS (23.038 §4).
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
400pub struct Dcs {
401    pub raw: u8,
402    pub alphabet: Alphabet,
403    /// Message class 0–3, when the DCS carries one.
404    pub class: Option<u8>,
405    pub compressed: bool,
406    /// Coding group 01xx: delete after reading.
407    pub auto_delete: bool,
408    pub mwi: Option<Mwi>,
409    /// Reserved coding group or alphabet; decoded as GSM 7-bit as 23.038 §4 requires.
410    pub reserved: bool,
411}
412
413impl Dcs {
414    pub fn decode(raw: u8) -> Self {
415        let mut d = Dcs {
416            raw,
417            alphabet: Alphabet::Gsm7,
418            class: None,
419            compressed: false,
420            auto_delete: false,
421            mwi: None,
422            reserved: false,
423        };
424        match raw >> 4 {
425            0x0..=0x7 => {
426                d.auto_delete = raw & 0x40 != 0;
427                d.compressed = raw & 0x20 != 0;
428                d.class = (raw & 0x10 != 0).then_some(raw & 3);
429                match (raw >> 2) & 3 {
430                    0 => {}
431                    1 => d.alphabet = Alphabet::Bit8,
432                    2 => d.alphabet = Alphabet::Ucs2,
433                    _ => d.reserved = true,
434                }
435            }
436            0xC..=0xE => {
437                d.alphabet = if raw >> 4 == 0xE {
438                    Alphabet::Ucs2
439                } else {
440                    Alphabet::Gsm7
441                };
442                d.mwi = Some(Mwi {
443                    active: raw & 0x08 != 0,
444                    kind: [
445                        MwiKind::Voicemail,
446                        MwiKind::Fax,
447                        MwiKind::Email,
448                        MwiKind::Other,
449                    ][usize::from(raw & 3)],
450                    store: raw >> 4 != 0xC,
451                });
452            }
453            0xF => {
454                if raw & 0x04 != 0 {
455                    d.alphabet = Alphabet::Bit8;
456                }
457                d.class = Some(raw & 3);
458            }
459            _ => d.reserved = true,
460        }
461        d
462    }
463
464    /// DCS octet for an outgoing message: general data coding group, uncompressed.
465    pub fn encode(alphabet: Alphabet, class: Option<u8>) -> u8 {
466        let a = match alphabet {
467            Alphabet::Gsm7 => 0x00,
468            Alphabet::Bit8 => 0x04,
469            Alphabet::Ucs2 => 0x08,
470        };
471        a | class.map_or(0, |c| 0x10 | (c & 3))
472    }
473}
474
475// ---------------------------------------------------------------- UDH
476
477/// One information element.
478#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
479pub struct Ie {
480    pub id: u8,
481    pub data: Vec<u8>,
482}
483
484/// User data header (23.040 §9.2.3.24).
485#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
486pub struct Udh {
487    pub ies: Vec<Ie>,
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
491pub struct Concat {
492    pub reference: u16,
493    pub total: u8,
494    pub seq: u8,
495    /// IEI 0x08 (16-bit reference) rather than 0x00.
496    pub ref_16bit: bool,
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
500pub struct Ports {
501    pub dest: u16,
502    pub orig: u16,
503    /// IEI 0x05 (16-bit ports) rather than 0x04.
504    pub wide: bool,
505}
506
507pub const IEI_CONCAT_8: u8 = 0x00;
508pub const IEI_PORTS_8: u8 = 0x04;
509pub const IEI_PORTS_16: u8 = 0x05;
510pub const IEI_CONCAT_16: u8 = 0x08;
511pub const IEI_SINGLE_SHIFT: u8 = 0x24;
512pub const IEI_LOCKING_SHIFT: u8 = 0x25;
513
514impl Udh {
515    /// Parse the IEs (the octets after UDHL).
516    pub fn parse(mut bytes: &[u8]) -> Result<Self> {
517        let mut ies = Vec::new();
518        while let [id, len, rest @ ..] = bytes {
519            let len = usize::from(*len);
520            let data = rest.get(..len).ok_or(PduError::Truncated("UDH element"))?;
521            ies.push(Ie {
522                id: *id,
523                data: data.to_vec(),
524            });
525            bytes = &rest[len..];
526        }
527        if bytes.is_empty() {
528            Ok(Udh { ies })
529        } else {
530            Err(PduError::Truncated("UDH element"))
531        }
532    }
533
534    /// UDHL followed by the IEs. `Invalid("UDH length")` when an IE holds more than 255
535    /// octets or the header more than 255 (neither length fits its octet; a UDH that
536    /// fits a TPDU is at most 139 octets anyway).
537    pub fn encode(&self) -> Result<Vec<u8>> {
538        let mut out = vec![0];
539        for ie in &self.ies {
540            let len = u8::try_from(ie.data.len()).map_err(|_| PduError::Invalid("UDH length"))?;
541            out.push(ie.id);
542            out.push(len);
543            out.extend_from_slice(&ie.data);
544        }
545        out[0] = u8::try_from(out.len() - 1).map_err(|_| PduError::Invalid("UDH length"))?;
546        Ok(out)
547    }
548
549    fn ie(&self, id: u8) -> Option<&[u8]> {
550        self.ies
551            .iter()
552            .rev()
553            .find(|ie| ie.id == id)
554            .map(|ie| ie.data.as_slice())
555    }
556
557    /// Concatenation info; `None` without the IE or when total/seq are inconsistent.
558    pub fn concat(&self) -> Option<Concat> {
559        let c = match (self.ie(IEI_CONCAT_8), self.ie(IEI_CONCAT_16)) {
560            (_, Some(&[hi, lo, total, seq])) => Concat {
561                reference: u16::from_be_bytes([hi, lo]),
562                total,
563                seq,
564                ref_16bit: true,
565            },
566            (Some(&[r, total, seq]), _) => Concat {
567                reference: u16::from(r),
568                total,
569                seq,
570                ref_16bit: false,
571            },
572            _ => return None,
573        };
574        (c.total > 0 && c.seq > 0 && c.seq <= c.total).then_some(c)
575    }
576
577    pub fn ports(&self) -> Option<Ports> {
578        match (self.ie(IEI_PORTS_8), self.ie(IEI_PORTS_16)) {
579            (_, Some(&[a, b, c, d])) => Some(Ports {
580                dest: u16::from_be_bytes([a, b]),
581                orig: u16::from_be_bytes([c, d]),
582                wide: true,
583            }),
584            (Some(&[dest, orig]), _) => Some(Ports {
585                dest: u16::from(dest),
586                orig: u16::from(orig),
587                wide: false,
588            }),
589            _ => None,
590        }
591    }
592
593    /// National language identifiers (single shift, locking shift) as sent.
594    pub fn national_languages(&self) -> (Option<u8>, Option<u8>) {
595        let one = |id| self.ie(id).and_then(|d| d.first().copied());
596        (one(IEI_SINGLE_SHIFT), one(IEI_LOCKING_SHIFT))
597    }
598
599    /// GSM 7-bit tables selected by the header; identifiers without a table here fall
600    /// back to the default table.
601    pub fn tables(&self) -> Tables {
602        let (single, locking) = self.national_languages();
603        let lang = |n: Option<u8>| n.and_then(Language::from_nli).unwrap_or_default();
604        let locking = lang(locking);
605        Tables::new(
606            if locking.has_locking_table() {
607                locking
608            } else {
609                Language::Default
610            },
611            lang(single),
612        )
613    }
614}
615
616// ---------------------------------------------------------------- user data
617
618/// TP-UD after UDH parsing.
619#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
620pub struct UserData {
621    pub udh: Option<Udh>,
622    /// Unpacked septets for GSM 7-bit, octets otherwise (UDH excluded).
623    pub payload: Vec<u8>,
624    /// Decoded text; `None` for 8-bit and compressed data.
625    pub text: Option<String>,
626}
627
628/// Decode TP-UD given TP-UDL, TP-UDHI and the DCS. `ud` may be longer than needed.
629pub fn decode_user_data(udl: u8, ud: &[u8], udhi: bool, dcs: &Dcs) -> Result<UserData> {
630    let udl = usize::from(udl);
631    let septets = dcs.alphabet == Alphabet::Gsm7 && !dcs.compressed;
632    let (need, max) = if septets {
633        ((udl * 7).div_ceil(8), 160)
634    } else {
635        (udl, 140)
636    };
637    if udl > max {
638        return Err(PduError::Invalid("TP-UDL"));
639    }
640    let ud = ud.get(..need).ok_or(PduError::Truncated("user data"))?;
641    let (udh, hdr) = if udhi {
642        let udhl = usize::from(*ud.first().ok_or(PduError::Invalid("TP-UDHL"))?);
643        let ies = ud.get(1..=udhl).ok_or(PduError::Invalid("TP-UDHL"))?;
644        (Some(Udh::parse(ies)?), udhl + 1)
645    } else {
646        (None, 0)
647    };
648    let tables = udh.as_ref().map_or(Tables::DEFAULT, Udh::tables);
649    let (payload, text) = if septets {
650        let hdr_septets = (hdr * 8).div_ceil(7);
651        let count = udl
652            .checked_sub(hdr_septets)
653            .ok_or(PduError::Invalid("TP-UDHL"))?;
654        let fill = (hdr_septets * 7 - hdr * 8) as u8;
655        let s = gsm7::unpack(&ud[hdr..], fill, count);
656        let text = gsm7::decode(&s, tables);
657        (s, Some(text))
658    } else {
659        let p = ud[hdr..].to_vec();
660        let text = match (dcs.alphabet, dcs.compressed) {
661            (Alphabet::Ucs2, false) if p.len() % 2 == 1 => {
662                return Err(PduError::Invalid("UCS2 length"))
663            }
664            (Alphabet::Ucs2, false) => Some(ucs2::decode(&p)),
665            _ => None,
666        };
667        (p, text)
668    };
669    Ok(UserData { udh, payload, text })
670}
671
672// ---------------------------------------------------------------- TPDUs
673
674#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
675pub struct Deliver {
676    /// TP-MMS: set = no more messages waiting.
677    pub mms: bool,
678    /// TP-LP loop prevention.
679    pub lp: bool,
680    /// TP-SRI: a status report will be returned to the sender.
681    pub sri: bool,
682    pub udhi: bool,
683    pub rp: bool,
684    pub originator: Address,
685    pub pid: u8,
686    pub dcs: Dcs,
687    /// `None` when the octets are not a valid date; see `scts_raw`.
688    pub scts: Option<Timestamp>,
689    pub scts_raw: [u8; 7],
690    pub user_data: UserData,
691}
692
693#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
694pub struct Submit {
695    /// TP-RD reject duplicates.
696    pub rd: bool,
697    /// TP-SRR status report request.
698    pub srr: bool,
699    pub udhi: bool,
700    pub rp: bool,
701    pub mr: u8,
702    pub destination: Address,
703    pub pid: u8,
704    pub dcs: Dcs,
705    pub validity: Option<Validity>,
706    pub user_data: UserData,
707}
708
709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
710pub struct StatusReport {
711    pub mms: bool,
712    pub lp: bool,
713    /// TP-SRQ: report of an SMS-COMMAND rather than an SMS-SUBMIT.
714    pub srq: bool,
715    pub udhi: bool,
716    pub mr: u8,
717    pub recipient: Address,
718    pub scts: Option<Timestamp>,
719    pub scts_raw: [u8; 7],
720    pub discharge: Option<Timestamp>,
721    pub discharge_raw: [u8; 7],
722    /// TP-ST (23.040 §9.2.3.15): 0x00–0x1F delivered, 0x20–0x3F still trying,
723    /// 0x40– failed.
724    pub status: u8,
725    /// TP-PI, when present.
726    pub pi: Option<u8>,
727    pub pid: Option<u8>,
728    pub dcs: Option<Dcs>,
729    pub user_data: Option<UserData>,
730}
731
732#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
733pub enum Tpdu {
734    Deliver(Deliver),
735    Submit(Submit),
736    StatusReport(StatusReport),
737}
738
739impl Tpdu {
740    pub fn user_data(&self) -> Option<&UserData> {
741        match self {
742            Self::Deliver(d) => Some(&d.user_data),
743            Self::Submit(s) => Some(&s.user_data),
744            Self::StatusReport(r) => r.user_data.as_ref(),
745        }
746    }
747
748    pub fn text(&self) -> Option<&str> {
749        self.user_data()?.text.as_deref()
750    }
751
752    /// Originator (DELIVER), destination (SUBMIT) or recipient (STATUS-REPORT).
753    pub fn address(&self) -> &Address {
754        match self {
755            Self::Deliver(d) => &d.originator,
756            Self::Submit(s) => &s.destination,
757            Self::StatusReport(r) => &r.recipient,
758        }
759    }
760
761    pub fn dcs(&self) -> Option<&Dcs> {
762        match self {
763            Self::Deliver(d) => Some(&d.dcs),
764            Self::Submit(s) => Some(&s.dcs),
765            Self::StatusReport(r) => r.dcs.as_ref(),
766        }
767    }
768}
769
770/// A 27.005 PDU: optional SMSC address followed by the TPDU.
771#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
772pub struct Pdu {
773    pub smsc: Option<Address>,
774    pub tpdu: Tpdu,
775}
776
777/// Decode a hex PDU as printed by `+CMGR`/`+CMGL`/`+CMT`/`+CDS` (SMSC prefix included).
778pub fn decode_hex(hex: &str) -> Result<Pdu> {
779    decode(&hex_decode(hex).ok_or(PduError::InvalidHex)?)
780}
781
782/// Decode PDU octets with the SMSC prefix. TP-MTI 00 is read as SMS-DELIVER, 01 as
783/// SMS-SUBMIT, 10 as SMS-STATUS-REPORT — the directions a modem hands to the host.
784pub fn decode(bytes: &[u8]) -> Result<Pdu> {
785    let (smsc, n) = decode_smsc(bytes)?;
786    Ok(Pdu {
787        smsc,
788        tpdu: decode_tpdu(&bytes[n..])?,
789    })
790}
791
792/// Decode a TPDU without the SMSC prefix.
793pub fn decode_tpdu(bytes: &[u8]) -> Result<Tpdu> {
794    let mut r = Reader::new(bytes);
795    let fo = r.u8("first octet")?;
796    let bit = |n: u8| fo & (1 << n) != 0;
797    match fo & 3 {
798        0 => {
799            let originator = r.address()?;
800            let pid = r.u8("TP-PID")?;
801            let dcs = Dcs::decode(r.u8("TP-DCS")?);
802            let scts_raw = r.array("TP-SCTS")?;
803            let user_data = r.user_data(bit(6), &dcs)?;
804            Ok(Tpdu::Deliver(Deliver {
805                mms: bit(2),
806                lp: bit(3),
807                sri: bit(5),
808                udhi: bit(6),
809                rp: bit(7),
810                originator,
811                pid,
812                dcs,
813                scts: Timestamp::decode(&scts_raw),
814                scts_raw,
815                user_data,
816            }))
817        }
818        1 => {
819            let mr = r.u8("TP-MR")?;
820            let destination = r.address()?;
821            let pid = r.u8("TP-PID")?;
822            let dcs = Dcs::decode(r.u8("TP-DCS")?);
823            let validity = match (fo >> 3) & 3 {
824                0 => None,
825                2 => Some(Validity::Relative(r.u8("TP-VP")?)),
826                3 => Some(Validity::Absolute(
827                    Timestamp::decode(&r.array("TP-VP")?).ok_or(PduError::Invalid("TP-VP"))?,
828                )),
829                _ => Some(Validity::Enhanced(r.array("TP-VP")?)),
830            };
831            let user_data = r.user_data(bit(6), &dcs)?;
832            Ok(Tpdu::Submit(Submit {
833                rd: bit(2),
834                srr: bit(5),
835                udhi: bit(6),
836                rp: bit(7),
837                mr,
838                destination,
839                pid,
840                dcs,
841                validity,
842                user_data,
843            }))
844        }
845        2 => {
846            let mr = r.u8("TP-MR")?;
847            let recipient = r.address()?;
848            let scts_raw = r.array("TP-SCTS")?;
849            let discharge_raw = r.array("TP-DT")?;
850            let status = r.u8("TP-ST")?;
851            let mut rep = StatusReport {
852                mms: bit(2),
853                lp: bit(3),
854                srq: bit(5),
855                udhi: bit(6),
856                mr,
857                recipient,
858                scts: Timestamp::decode(&scts_raw),
859                scts_raw,
860                discharge: Timestamp::decode(&discharge_raw),
861                discharge_raw,
862                status,
863                pi: None,
864                pid: None,
865                dcs: None,
866                user_data: None,
867            };
868            // TP-PI and what it announces are optional (23.040 §9.2.2.3) and a receiver
869            // ignores what it does not understand (§9.2.3.27): a truncated or malformed
870            // tail leaves the fields it could not read as `None`, the report stands.
871            let mut tail = || -> Result<()> {
872                let pi = r.u8("TP-PI")?;
873                rep.pi = Some(pi);
874                let mut ext = pi;
875                while ext & 0x80 != 0 {
876                    ext = r.u8("TP-PI")?;
877                }
878                if pi & 1 != 0 {
879                    rep.pid = Some(r.u8("TP-PID")?);
880                }
881                if pi & 2 != 0 {
882                    rep.dcs = Some(Dcs::decode(r.u8("TP-DCS")?));
883                }
884                if pi & 4 != 0 {
885                    let dcs = rep.dcs.unwrap_or(Dcs::decode(0));
886                    rep.user_data = Some(r.user_data(bit(6), &dcs)?);
887                }
888                Ok(())
889            };
890            let _ = tail();
891            Ok(Tpdu::StatusReport(rep))
892        }
893        _ => Err(PduError::ReservedMessageType),
894    }
895}
896
897struct Reader<'a> {
898    b: &'a [u8],
899    pos: usize,
900}
901
902impl<'a> Reader<'a> {
903    fn new(b: &'a [u8]) -> Self {
904        Reader { b, pos: 0 }
905    }
906
907    fn u8(&mut self, field: &'static str) -> Result<u8> {
908        Ok(self.take(1, field)?[0])
909    }
910
911    fn take(&mut self, n: usize, field: &'static str) -> Result<&'a [u8]> {
912        let s = self
913            .b
914            .get(self.pos..self.pos + n)
915            .ok_or(PduError::Truncated(field))?;
916        self.pos += n;
917        Ok(s)
918    }
919
920    fn array(&mut self, field: &'static str) -> Result<[u8; 7]> {
921        let mut a = [0; 7];
922        a.copy_from_slice(self.take(7, field)?);
923        Ok(a)
924    }
925
926    fn address(&mut self) -> Result<Address> {
927        let (a, n) = decode_address(&self.b[self.pos..])?;
928        self.pos += n;
929        Ok(a)
930    }
931
932    fn user_data(&mut self, udhi: bool, dcs: &Dcs) -> Result<UserData> {
933        let udl = self.u8("TP-UDL")?;
934        decode_user_data(udl, &self.b[self.pos..], udhi, dcs)
935    }
936}