use crate::sms::gsm7::{self, Tables};
use crate::sms::{hex_decode, ucs2};
use serde::{Deserialize, Serialize};
const CR: u8 = 0x0D;
pub fn pack_septets(septets: &[u8], fill_bits: u8) -> Vec<u8> {
let fill = usize::from(fill_bits % 8);
let mut s = septets.to_vec();
let bits = fill + 7 * s.len();
if bits % 8 == 1 || (bits % 8 == 0 && s.last() == Some(&CR)) {
s.push(CR);
}
gsm7::pack(&s, fill_bits)
}
pub fn unpack_septets(octets: &[u8], fill_bits: u8) -> Vec<u8> {
let fill = usize::from(fill_bits % 8);
let n = gsm7::septets_in(octets.len(), fill_bits);
let mut s = gsm7::unpack(octets, fill_bits, n);
let exact = fill + 7 * n == 8 * octets.len();
let boundary_before_last = n > 0 && (fill + 7 * (n - 1)) % 8 == 0;
let doubled = s.len() >= 2 && s[s.len() - 2] == CR && boundary_before_last;
if s.last() == Some(&CR) && (exact || doubled) {
s.pop();
}
s
}
pub fn encode(text: &str) -> Result<Vec<u8>, char> {
Ok(pack_septets(&gsm7::encode(text, Tables::DEFAULT)?, 0))
}
pub fn decode_gsm7(octets: &[u8]) -> String {
gsm7::decode(&unpack_septets(octets, 0), Tables::DEFAULT)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CbsDcs {
pub raw: u8,
pub alphabet: crate::sms::pdu::Alphabet,
pub compressed: bool,
pub language_prefix: bool,
pub reserved: bool,
}
impl CbsDcs {
pub fn language(&self) -> Option<&'static str> {
const G0: [&str; 15] = [
"de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu",
"pl",
];
const G2: [&str; 5] = ["cs", "he", "ar", "ru", "is"];
let table: &[&str] = match self.raw >> 4 {
0 => &G0,
2 => &G2,
_ => return None,
};
table.get(usize::from(self.raw & 0x0F)).copied()
}
pub fn decode(raw: u8) -> Self {
use crate::sms::pdu::Alphabet::*;
let mut d = CbsDcs {
raw,
alphabet: Gsm7,
compressed: false,
language_prefix: false,
reserved: false,
};
let alpha = |bits: u8| match bits & 3 {
0 => (Gsm7, false),
1 => (Bit8, false),
2 => (Ucs2, false),
_ => (Gsm7, true),
};
match raw >> 4 {
0x0 => {}
0x1 => match raw {
0x10 => d.language_prefix = true,
0x11 => (d.alphabet, d.language_prefix) = (Ucs2, true),
_ => d.reserved = true,
},
0x2 => d.reserved = raw > 0x24,
0x4..=0x7 => {
d.compressed = raw & 0x20 != 0;
(d.alphabet, d.reserved) = alpha(raw >> 2);
}
0x9 => (d.alphabet, d.reserved) = alpha(raw >> 2),
0xF => {
if raw & 0x04 != 0 {
d.alphabet = Bit8;
}
}
_ => d.reserved = true,
}
d
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UssdText {
pub text: String,
pub language: Option<String>,
}
pub fn decode(payload: &[u8], dcs: u8) -> Option<UssdText> {
use crate::sms::pdu::Alphabet::*;
let d = CbsDcs::decode(dcs);
if d.compressed {
return None;
}
let (language, body): (Option<String>, &[u8]) = match (d.language_prefix, d.alphabet) {
(true, Ucs2) => {
let lang = payload.get(..2)?;
let l = gsm7::decode(&gsm7::unpack(lang, 0, 2), Tables::DEFAULT);
(Some(l), &payload[2..])
}
_ => (None, payload),
};
let mut text = match d.alphabet {
Gsm7 => decode_gsm7(body),
Ucs2 => ucs2::decode(body),
Bit8 => body.iter().map(|&b| char::from(b)).collect(),
};
let mut language = language.or_else(|| d.language().map(String::from));
if d.language_prefix && d.alphabet == Gsm7 {
let mut it = text.chars();
let lang: String = it.by_ref().take(2).collect();
if it.next() == Some('\r') {
language = Some(lang);
text = it.collect();
}
}
Some(UssdText { text, language })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CusdStatus {
Done,
ActionRequired,
Terminated,
OtherClient,
NotSupported,
Timeout,
Other(u8),
}
impl CusdStatus {
pub fn from_code(m: u8) -> Self {
match m {
0 => Self::Done,
1 => Self::ActionRequired,
2 => Self::Terminated,
3 => Self::OtherClient,
4 => Self::NotSupported,
5 => Self::Timeout,
n => Self::Other(n),
}
}
pub fn session_active(self) -> bool {
self == Self::ActionRequired
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cusd {
pub status: CusdStatus,
pub raw: Option<String>,
pub dcs: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invalid_dcs: Option<String>,
}
pub fn parse_cusd(line: &str) -> Option<Cusd> {
let line = line.trim();
let rest = line
.get(..6)
.filter(|p| p.eq_ignore_ascii_case("+CUSD:"))
.map(|_| line[6..].trim_start())?;
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
let status = CusdStatus::from_code(rest[..digits].parse().ok()?);
let mut cusd = Cusd {
status,
raw: None,
dcs: None,
invalid_dcs: None,
};
let tail = rest[digits..].trim();
if strip_ok(tail).is_empty() {
return Some(cusd);
}
let tail = tail.strip_prefix(',')?.trim_start();
let (raw, after) = if let Some(q) = tail.strip_prefix('"') {
match q.rfind('"') {
Some(end) => (unescape(&q[..end]), q[end + 1..].trim()),
None => (unescape(q), ""),
}
} else {
let tail = strip_ok(tail);
match tail.rfind(',') {
Some(i) if is_number(tail[i + 1..].trim()) => {
(tail[..i].trim().to_string(), &tail[i..])
}
_ => (tail.to_string(), ""),
}
};
cusd.raw = Some(raw);
match strip_ok(after).strip_prefix(',').map(str::trim) {
None | Some("") => {}
Some(d) => match d.parse::<u8>() {
Ok(v) => cusd.dcs = Some(v),
Err(_) => cusd.invalid_dcs = Some(d.to_string()),
},
}
Some(cusd)
}
fn is_number(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
fn strip_ok(s: &str) -> &str {
let t = s.trim_end();
if let Some(head) = t.strip_suffix("OK").or_else(|| t.strip_suffix("ok")) {
let h = head.trim_end();
if h.is_empty() || h.ends_with('"') || h.ends_with(|c: char| c.is_ascii_digit()) {
return h;
}
}
t
}
fn unescape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
match (c, it.peek()) {
('\\', Some(&n @ ('"' | '\\'))) => {
out.push(n);
it.next();
}
_ => out.push(c),
}
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Cscs {
#[default]
Gsm,
Ira,
Ucs2,
Hex,
}
impl Cusd {
pub fn text(&self) -> Option<String> {
self.text_as(Cscs::Gsm)
}
pub fn text_as(&self, cscs: Cscs) -> Option<String> {
use crate::sms::pdu::Alphabet::*;
let raw = self.raw.as_deref()?;
let d = CbsDcs::decode(self.dcs.unwrap_or(0));
if d.compressed {
return None;
}
match (d.alphabet, cscs) {
(Ucs2 | Bit8, _) => match hex_decode(raw) {
Some(b) => decode(&b, d.raw).map(|t| t.text),
None => Some(raw.to_string()),
},
(Gsm7, Cscs::Gsm | Cscs::Ira) => Some(raw.to_string()),
(Gsm7, Cscs::Ucs2) => hex_decode(raw).map(|b| ucs2::decode(&b)),
(Gsm7, Cscs::Hex) => hex_decode(raw).map(|b| gsm7::decode(&b, Tables::DEFAULT)),
}
}
pub fn text_packed_hex(&self) -> Option<String> {
let d = CbsDcs::decode(self.dcs.unwrap_or(0));
if d.alphabet != crate::sms::pdu::Alphabet::Gsm7 {
return self.text();
}
decode(&hex_decode(self.raw.as_deref()?)?, d.raw).map(|t| t.text)
}
}