1use crate::sms::gsm7::{self, Tables};
5use crate::sms::{hex_decode, ucs2};
6use serde::{Deserialize, Serialize};
7
8const CR: u8 = 0x0D;
9
10pub fn pack_septets(septets: &[u8], fill_bits: u8) -> Vec<u8> {
14 let fill = usize::from(fill_bits % 8);
15 let mut s = septets.to_vec();
16 let bits = fill + 7 * s.len();
17 if bits % 8 == 1 || (bits % 8 == 0 && s.last() == Some(&CR)) {
19 s.push(CR);
20 }
21 gsm7::pack(&s, fill_bits)
22}
23
24pub fn unpack_septets(octets: &[u8], fill_bits: u8) -> Vec<u8> {
27 let fill = usize::from(fill_bits % 8);
28 let n = gsm7::septets_in(octets.len(), fill_bits);
29 let mut s = gsm7::unpack(octets, fill_bits, n);
30 let exact = fill + 7 * n == 8 * octets.len();
31 let boundary_before_last = n > 0 && (fill + 7 * (n - 1)) % 8 == 0;
32 let doubled = s.len() >= 2 && s[s.len() - 2] == CR && boundary_before_last;
33 if s.last() == Some(&CR) && (exact || doubled) {
34 s.pop();
35 }
36 s
37}
38
39pub fn encode(text: &str) -> Result<Vec<u8>, char> {
41 Ok(pack_septets(&gsm7::encode(text, Tables::DEFAULT)?, 0))
42}
43
44pub fn decode_gsm7(octets: &[u8]) -> String {
46 gsm7::decode(&unpack_septets(octets, 0), Tables::DEFAULT)
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
51pub struct CbsDcs {
52 pub raw: u8,
53 pub alphabet: crate::sms::pdu::Alphabet,
54 pub compressed: bool,
55 pub language_prefix: bool,
58 pub reserved: bool,
60}
61
62impl CbsDcs {
63 pub fn language(&self) -> Option<&'static str> {
67 const G0: [&str; 15] = [
68 "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu",
69 "pl",
70 ];
71 const G2: [&str; 5] = ["cs", "he", "ar", "ru", "is"];
72 let table: &[&str] = match self.raw >> 4 {
73 0 => &G0,
74 2 => &G2,
75 _ => return None,
76 };
77 table.get(usize::from(self.raw & 0x0F)).copied()
78 }
79
80 pub fn decode(raw: u8) -> Self {
81 use crate::sms::pdu::Alphabet::*;
82 let mut d = CbsDcs {
83 raw,
84 alphabet: Gsm7,
85 compressed: false,
86 language_prefix: false,
87 reserved: false,
88 };
89 let alpha = |bits: u8| match bits & 3 {
90 0 => (Gsm7, false),
91 1 => (Bit8, false),
92 2 => (Ucs2, false),
93 _ => (Gsm7, true),
94 };
95 match raw >> 4 {
96 0x0 => {}
97 0x1 => match raw {
98 0x10 => d.language_prefix = true,
99 0x11 => (d.alphabet, d.language_prefix) = (Ucs2, true),
100 _ => d.reserved = true,
101 },
102 0x2 => d.reserved = raw > 0x24,
103 0x4..=0x7 => {
104 d.compressed = raw & 0x20 != 0;
105 (d.alphabet, d.reserved) = alpha(raw >> 2);
106 }
107 0x9 => (d.alphabet, d.reserved) = alpha(raw >> 2),
108 0xF => {
109 if raw & 0x04 != 0 {
110 d.alphabet = Bit8;
111 }
112 }
113 _ => d.reserved = true,
114 }
115 d
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct UssdText {
122 pub text: String,
123 pub language: Option<String>,
126}
127
128pub fn decode(payload: &[u8], dcs: u8) -> Option<UssdText> {
131 use crate::sms::pdu::Alphabet::*;
132 let d = CbsDcs::decode(dcs);
133 if d.compressed {
134 return None;
135 }
136 let (language, body): (Option<String>, &[u8]) = match (d.language_prefix, d.alphabet) {
137 (true, Ucs2) => {
138 let lang = payload.get(..2)?;
139 let l = gsm7::decode(&gsm7::unpack(lang, 0, 2), Tables::DEFAULT);
140 (Some(l), &payload[2..])
141 }
142 _ => (None, payload),
143 };
144 let mut text = match d.alphabet {
145 Gsm7 => decode_gsm7(body),
146 Ucs2 => ucs2::decode(body),
147 Bit8 => body.iter().map(|&b| char::from(b)).collect(),
148 };
149 let mut language = language.or_else(|| d.language().map(String::from));
150 if d.language_prefix && d.alphabet == Gsm7 {
151 let mut it = text.chars();
152 let lang: String = it.by_ref().take(2).collect();
153 if it.next() == Some('\r') {
154 language = Some(lang);
155 text = it.collect();
156 }
157 }
158 Some(UssdText { text, language })
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum CusdStatus {
165 Done,
167 ActionRequired,
169 Terminated,
171 OtherClient,
173 NotSupported,
175 Timeout,
177 Other(u8),
178}
179
180impl CusdStatus {
181 pub fn from_code(m: u8) -> Self {
182 match m {
183 0 => Self::Done,
184 1 => Self::ActionRequired,
185 2 => Self::Terminated,
186 3 => Self::OtherClient,
187 4 => Self::NotSupported,
188 5 => Self::Timeout,
189 n => Self::Other(n),
190 }
191 }
192
193 pub fn session_active(self) -> bool {
194 self == Self::ActionRequired
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub struct Cusd {
201 pub status: CusdStatus,
202 pub raw: Option<String>,
205 pub dcs: Option<u8>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub invalid_dcs: Option<String>,
211}
212
213pub fn parse_cusd(line: &str) -> Option<Cusd> {
218 let line = line.trim();
219 let rest = line
220 .get(..6)
221 .filter(|p| p.eq_ignore_ascii_case("+CUSD:"))
222 .map(|_| line[6..].trim_start())?;
223 let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
224 let status = CusdStatus::from_code(rest[..digits].parse().ok()?);
225 let mut cusd = Cusd {
226 status,
227 raw: None,
228 dcs: None,
229 invalid_dcs: None,
230 };
231 let tail = rest[digits..].trim();
232 if strip_ok(tail).is_empty() {
233 return Some(cusd);
234 }
235 let tail = tail.strip_prefix(',')?.trim_start();
236 let (raw, after) = if let Some(q) = tail.strip_prefix('"') {
237 match q.rfind('"') {
238 Some(end) => (unescape(&q[..end]), q[end + 1..].trim()),
239 None => (unescape(q), ""),
240 }
241 } else {
242 let tail = strip_ok(tail);
244 match tail.rfind(',') {
245 Some(i) if is_number(tail[i + 1..].trim()) => {
246 (tail[..i].trim().to_string(), &tail[i..])
247 }
248 _ => (tail.to_string(), ""),
249 }
250 };
251 cusd.raw = Some(raw);
252 match strip_ok(after).strip_prefix(',').map(str::trim) {
253 None | Some("") => {}
254 Some(d) => match d.parse::<u8>() {
255 Ok(v) => cusd.dcs = Some(v),
256 Err(_) => cusd.invalid_dcs = Some(d.to_string()),
257 },
258 }
259 Some(cusd)
260}
261
262fn is_number(s: &str) -> bool {
263 !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
264}
265
266fn strip_ok(s: &str) -> &str {
268 let t = s.trim_end();
269 if let Some(head) = t.strip_suffix("OK").or_else(|| t.strip_suffix("ok")) {
270 let h = head.trim_end();
271 if h.is_empty() || h.ends_with('"') || h.ends_with(|c: char| c.is_ascii_digit()) {
272 return h;
273 }
274 }
275 t
276}
277
278fn unescape(s: &str) -> String {
279 let mut out = String::with_capacity(s.len());
280 let mut it = s.chars().peekable();
281 while let Some(c) = it.next() {
282 match (c, it.peek()) {
283 ('\\', Some(&n @ ('"' | '\\'))) => {
284 out.push(n);
285 it.next();
286 }
287 _ => out.push(c),
288 }
289 }
290 out
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
297#[serde(rename_all = "snake_case")]
298pub enum Cscs {
299 #[default]
301 Gsm,
302 Ira,
304 Ucs2,
306 Hex,
308}
309
310impl Cusd {
311 pub fn text(&self) -> Option<String> {
314 self.text_as(Cscs::Gsm)
315 }
316
317 pub fn text_as(&self, cscs: Cscs) -> Option<String> {
328 use crate::sms::pdu::Alphabet::*;
329 let raw = self.raw.as_deref()?;
330 let d = CbsDcs::decode(self.dcs.unwrap_or(0));
331 if d.compressed {
332 return None;
333 }
334 match (d.alphabet, cscs) {
335 (Ucs2 | Bit8, _) => match hex_decode(raw) {
336 Some(b) => decode(&b, d.raw).map(|t| t.text),
337 None => Some(raw.to_string()),
338 },
339 (Gsm7, Cscs::Gsm | Cscs::Ira) => Some(raw.to_string()),
340 (Gsm7, Cscs::Ucs2) => hex_decode(raw).map(|b| ucs2::decode(&b)),
341 (Gsm7, Cscs::Hex) => hex_decode(raw).map(|b| gsm7::decode(&b, Tables::DEFAULT)),
342 }
343 }
344
345 pub fn text_packed_hex(&self) -> Option<String> {
351 let d = CbsDcs::decode(self.dcs.unwrap_or(0));
352 if d.alphabet != crate::sms::pdu::Alphabet::Gsm7 {
353 return self.text();
354 }
355 decode(&hex_decode(self.raw.as_deref()?)?, d.raw).map(|t| t.text)
356 }
357}