Skip to main content

serial_codec/sms/
gsm7.rs

1//! GSM 7-bit default alphabet, extension table and national language tables
2//! (3GPP TS 23.038 §6.2.1 and Annex A), plus septet packing (§6.1.2.1).
3//!
4//! Septets are handled unpacked (one `u8` per septet, values 0..=0x7F) by
5//! [`encode`]/[`decode`] and bit-packed by [`pack`]/[`unpack`].
6//!
7//! National tables covered: Turkish (locking + single shift), Spanish (single shift;
8//! 23.038 defines no Spanish locking table), Portuguese (locking + single shift).
9// ponytail: Indic tables (NLI 4–13: Bengali … Urdu) are not included; add them from
10// 23.038 Annex A.2.4+/A.3.4+ when a caller needs them. Unknown NLIs decode with the
11// default table (see `Language::from_nli`).
12
13use serde::{Deserialize, Serialize};
14
15/// Escape to the single shift (extension) table.
16pub const ESC: u8 = 0x1B;
17
18/// National language identifier (23.038 §6.2.1.2.4), limited to the tables this
19/// module carries.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Language {
23    #[default]
24    Default,
25    Turkish,
26    Spanish,
27    Portuguese,
28}
29
30impl Language {
31    /// Map a UDH national language identifier (IEI 0x24/0x25 value). `None` for
32    /// identifiers whose tables are not carried here.
33    pub fn from_nli(nli: u8) -> Option<Self> {
34        match nli {
35            0 => Some(Self::Default),
36            1 => Some(Self::Turkish),
37            2 => Some(Self::Spanish),
38            3 => Some(Self::Portuguese),
39            _ => None,
40        }
41    }
42
43    pub fn nli(self) -> u8 {
44        match self {
45            Self::Default => 0,
46            Self::Turkish => 1,
47            Self::Spanish => 2,
48            Self::Portuguese => 3,
49        }
50    }
51
52    /// Whether 23.038 defines a locking shift table for this language.
53    pub fn has_locking_table(self) -> bool {
54        !matches!(self, Self::Spanish)
55    }
56
57    fn locking(self) -> &'static [char; 128] {
58        match self {
59            Self::Turkish => &TURKISH,
60            Self::Portuguese => &PORTUGUESE,
61            Self::Default | Self::Spanish => &DEFAULT,
62        }
63    }
64
65    fn single(self) -> &'static [(u8, char)] {
66        match self {
67            Self::Default => DEFAULT_EXT,
68            Self::Turkish => TURKISH_EXT,
69            Self::Spanish => SPANISH_EXT,
70            Self::Portuguese => PORTUGUESE_EXT,
71        }
72    }
73}
74
75/// Locking shift table plus single shift table used for one message.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
77pub struct Tables {
78    pub locking: Language,
79    pub single: Language,
80}
81
82impl Tables {
83    pub const DEFAULT: Tables = Tables {
84        locking: Language::Default,
85        single: Language::Default,
86    };
87
88    pub fn new(locking: Language, single: Language) -> Self {
89        Tables { locking, single }
90    }
91
92    /// Number of UDH octets the national language IEs add (3 per non-default table).
93    pub fn udh_ie_len(self) -> usize {
94        3 * (usize::from(self.locking != Language::Default)
95            + usize::from(self.single != Language::Default))
96    }
97}
98
99// 0x1B is ESC; the slot holds U+00A0 only so the array is complete. It is never
100// produced by `decode` and never matched by `encode`.
101const DEFAULT: [char; 128] = [
102    '@', '£', '$', '¥', 'è', 'é', 'ù', 'ì', 'ò', 'Ç', '\n', 'Ø', 'ø', '\r', 'Å', 'å', //
103    'Δ', '_', 'Φ', 'Γ', 'Λ', 'Ω', 'Π', 'Ψ', 'Σ', 'Θ', 'Ξ', '\u{A0}', 'Æ', 'æ', 'ß', 'É', //
104    ' ', '!', '"', '#', '¤', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', //
105    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', //
106    '¡', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', //
107    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ä', 'Ö', 'Ñ', 'Ü', '§', //
108    '¿', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', //
109    'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à', //
110];
111
112// 23.038 A.3.1 Turkish locking shift table.
113const TURKISH: [char; 128] = [
114    '@', '£', '$', '¥', '€', 'é', 'ù', 'ı', 'ò', 'Ç', '\n', 'Ğ', 'ğ', '\r', 'Å', 'å', //
115    'Δ', '_', 'Φ', 'Γ', 'Λ', 'Ω', 'Π', 'Ψ', 'Σ', 'Θ', 'Ξ', '\u{A0}', 'Ş', 'ş', 'ß', 'É', //
116    ' ', '!', '"', '#', '¤', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', //
117    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', //
118    'İ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', //
119    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ä', 'Ö', 'Ñ', 'Ü', '§', //
120    'ç', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', //
121    'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à', //
122];
123
124// 23.038 A.3.3 Portuguese locking shift table.
125const PORTUGUESE: [char; 128] = [
126    '@', '£', '$', '¥', 'ê', 'é', 'ú', 'í', 'ó', 'ç', '\n', 'Ô', 'ô', '\r', 'Á', 'á', //
127    'Δ', '_', 'ª', 'Ç', 'À', '∞', '^', '\\', '€', 'Ó', '|', '\u{A0}', 'Â', 'â', 'Ê', 'É', //
128    ' ', '!', '"', '#', 'º', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', //
129    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', //
130    'Í', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', //
131    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ã', 'Õ', 'Ú', 'Ü', '§', //
132    '~', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', //
133    'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ã', 'õ', '`', 'ü', 'à', //
134];
135
136// 23.038 §6.2.1.1 default extension table.
137const DEFAULT_EXT: &[(u8, char)] = &[
138    (0x0A, '\u{C}'),
139    (0x14, '^'),
140    (0x28, '{'),
141    (0x29, '}'),
142    (0x2F, '\\'),
143    (0x3C, '['),
144    (0x3D, '~'),
145    (0x3E, ']'),
146    (0x40, '|'),
147    (0x65, '€'),
148];
149
150// 23.038 A.2.1 Turkish single shift table.
151const TURKISH_EXT: &[(u8, char)] = &[
152    (0x0A, '\u{C}'),
153    (0x14, '^'),
154    (0x28, '{'),
155    (0x29, '}'),
156    (0x2F, '\\'),
157    (0x3C, '['),
158    (0x3D, '~'),
159    (0x3E, ']'),
160    (0x40, '|'),
161    (0x47, 'Ğ'),
162    (0x49, 'İ'),
163    (0x53, 'Ş'),
164    (0x63, 'ç'),
165    (0x65, '€'),
166    (0x67, 'ğ'),
167    (0x69, 'ı'),
168    (0x73, 'ş'),
169];
170
171// 23.038 A.2.2 Spanish single shift table.
172const SPANISH_EXT: &[(u8, char)] = &[
173    (0x09, 'ç'),
174    (0x0A, '\u{C}'),
175    (0x14, '^'),
176    (0x28, '{'),
177    (0x29, '}'),
178    (0x2F, '\\'),
179    (0x3C, '['),
180    (0x3D, '~'),
181    (0x3E, ']'),
182    (0x40, '|'),
183    (0x41, 'Á'),
184    (0x49, 'Í'),
185    (0x4F, 'Ó'),
186    (0x55, 'Ú'),
187    (0x61, 'á'),
188    (0x65, '€'),
189    (0x69, 'í'),
190    (0x6F, 'ó'),
191    (0x75, 'ú'),
192];
193
194// 23.038 A.2.3 Portuguese single shift table.
195const PORTUGUESE_EXT: &[(u8, char)] = &[
196    (0x05, 'ê'),
197    (0x09, 'ç'),
198    (0x0A, '\u{C}'),
199    (0x0B, 'Ô'),
200    (0x0C, 'ô'),
201    (0x0E, 'Á'),
202    (0x0F, 'á'),
203    (0x12, 'Φ'),
204    (0x13, 'Γ'),
205    (0x14, '^'),
206    (0x15, 'Ω'),
207    (0x16, 'Π'),
208    (0x17, 'Ψ'),
209    (0x18, 'Σ'),
210    (0x19, 'Θ'),
211    (0x1F, 'Ê'),
212    (0x28, '{'),
213    (0x29, '}'),
214    (0x2F, '\\'),
215    (0x3C, '['),
216    (0x3D, '~'),
217    (0x3E, ']'),
218    (0x40, '|'),
219    (0x41, 'À'),
220    (0x49, 'Í'),
221    (0x4F, 'Ó'),
222    (0x55, 'Ú'),
223    (0x5B, 'Ã'),
224    (0x5C, 'Õ'),
225    (0x61, 'Â'),
226    (0x65, '€'),
227    (0x69, 'í'),
228    (0x6F, 'ó'),
229    (0x75, 'ú'),
230    (0x7B, 'ã'),
231    (0x7C, 'õ'),
232    (0x7F, 'â'),
233];
234
235/// Septets for one character: `[code]` or `[ESC, code]`. `None` if the character is in
236/// neither table.
237pub fn encode_char(c: char, tables: Tables) -> Option<([u8; 2], usize)> {
238    if let Some(i) = tables
239        .locking
240        .locking()
241        .iter()
242        .position(|&t| t == c)
243        .filter(|&i| i != usize::from(ESC))
244    {
245        return Some(([i as u8, 0], 1));
246    }
247    tables
248        .single
249        .single()
250        .iter()
251        .find(|(_, t)| *t == c)
252        .map(|&(code, _)| ([ESC, code], 2))
253}
254
255/// Text → unpacked septets. `Err(c)` names the first character with no mapping.
256pub fn encode(text: &str, tables: Tables) -> Result<Vec<u8>, char> {
257    let mut out = Vec::with_capacity(text.len());
258    for c in text.chars() {
259        let (s, n) = encode_char(c, tables).ok_or(c)?;
260        out.extend_from_slice(&s[..n]);
261    }
262    Ok(out)
263}
264
265/// Septet count of `text`, or `None` if it is not encodable with `tables`.
266pub fn septet_len(text: &str, tables: Tables) -> Option<usize> {
267    text.chars()
268        .map(|c| encode_char(c, tables).map(|(_, n)| n))
269        .sum()
270}
271
272pub fn is_encodable(text: &str, tables: Tables) -> bool {
273    septet_len(text, tables).is_some()
274}
275
276/// Cheapest table pair able to encode `text`: the default tables if they work,
277/// otherwise the combination of the allowed `locking` and `single` shift tables (the
278/// default table is always allowed) with the fewest septets plus UDH cost (3 octets per
279/// national IE). `None` if nothing fits.
280pub fn choose_tables(text: &str, locking: &[Language], single: &[Language]) -> Option<Tables> {
281    if is_encodable(text, Tables::DEFAULT) {
282        return Some(Tables::DEFAULT);
283    }
284    let with_default = |l: &[Language]| {
285        let mut v = vec![Language::Default];
286        v.extend(l.iter().copied().filter(|l| *l != Language::Default));
287        v
288    };
289    let (locking, single) = (with_default(locking), with_default(single));
290    let mut best: Option<(usize, Tables)> = None;
291    for &l in locking.iter().filter(|l| l.has_locking_table()) {
292        for &s in &single {
293            let t = Tables::new(l, s);
294            if let Some(n) = septet_len(text, t) {
295                let cost = n + (t.udh_ie_len() * 8).div_ceil(7);
296                if best.is_none_or(|(c, _)| cost < c) {
297                    best = Some((cost, t));
298                }
299            }
300        }
301    }
302    best.map(|(_, t)| t)
303}
304
305/// Unpacked septets → text (23.038 §6.2.1.1). Unknown escape sequences show the
306/// locking-table character at that index; `ESC ESC` and a trailing lone `ESC` show a
307/// space. Bit 7 of every input byte is ignored.
308pub fn decode(septets: &[u8], tables: Tables) -> String {
309    let locking = tables.locking.locking();
310    let single = tables.single.single();
311    let mut out = String::with_capacity(septets.len());
312    let mut it = septets.iter().map(|b| b & 0x7F);
313    while let Some(s) = it.next() {
314        if s != ESC {
315            out.push(locking[usize::from(s)]);
316            continue;
317        }
318        match it.next() {
319            None | Some(ESC) => out.push(' '),
320            Some(n) => out.push(
321                single
322                    .iter()
323                    .find(|(code, _)| *code == n)
324                    .map_or(locking[usize::from(n)], |&(_, c)| c),
325            ),
326        }
327    }
328    out
329}
330
331/// Pack septets into octets, LSB first, after `fill_bits` zero bits (0..=7; larger
332/// values are taken modulo 8). Used with `fill_bits` > 0 after a UDH (23.040 §9.2.3.24).
333pub fn pack(septets: &[u8], fill_bits: u8) -> Vec<u8> {
334    let fill = usize::from(fill_bits % 8);
335    let total_bits = fill + septets.len() * 7;
336    let mut out = vec![0u8; total_bits.div_ceil(8)];
337    for (i, &s) in septets.iter().enumerate() {
338        let bit = fill + i * 7;
339        let v = u16::from(s & 0x7F) << (bit % 8);
340        out[bit / 8] |= v as u8;
341        if v > 0xFF {
342            out[bit / 8 + 1] |= (v >> 8) as u8;
343        }
344    }
345    out
346}
347
348/// Unpack up to `count` septets starting after `fill_bits` bits. Returns fewer when the
349/// input runs out; never panics.
350pub fn unpack(octets: &[u8], fill_bits: u8, count: usize) -> Vec<u8> {
351    let fill = usize::from(fill_bits % 8);
352    let avail = (octets.len() * 8).saturating_sub(fill) / 7;
353    (0..count.min(avail))
354        .map(|i| {
355            let bit = fill + i * 7;
356            let lo = u16::from(octets[bit / 8]);
357            let hi = octets.get(bit / 8 + 1).copied().map_or(0, u16::from);
358            (((hi << 8 | lo) >> (bit % 8)) & 0x7F) as u8
359        })
360        .collect()
361}
362
363/// Number of septets that fit in `octets` after `fill_bits` bits.
364pub fn septets_in(octets: usize, fill_bits: u8) -> usize {
365    (octets * 8).saturating_sub(usize::from(fill_bits % 8)) / 7
366}