use super::{
Alphabet, Dcs, Ie, PduError, Result, Udh, Validity, IEI_CONCAT_16, IEI_CONCAT_8,
IEI_LOCKING_SHIFT, IEI_SINGLE_SHIFT, TOA_ALPHANUMERIC, TOA_INTERNATIONAL, TOA_UNKNOWN,
};
use crate::sms::gsm7::{self, Language, Tables};
use crate::sms::{hex_encode, ucs2};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Body {
Text(String),
Binary(Vec<u8>),
}
impl Default for Body {
fn default() -> Self {
Body::Text(String::new())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SubmitRequest {
pub smsc: Option<String>,
pub smsc_toa: Option<u8>,
pub destination: String,
pub destination_toa: Option<u8>,
pub body: Body,
pub alphabet: Option<Alphabet>,
pub locking_shift: Vec<Language>,
pub single_shift: Vec<Language>,
pub class: Option<u8>,
pub pid: u8,
pub message_ref: u8,
pub status_report: bool,
pub reject_duplicates: bool,
pub reply_path: bool,
pub validity: Option<Validity>,
pub concat_ref: Option<u16>,
pub concat_16bit: bool,
}
impl SubmitRequest {
pub fn new(destination: impl Into<String>, text: impl Into<String>) -> Self {
SubmitRequest {
destination: destination.into(),
body: Body::Text(text.into()),
..Default::default()
}
}
}
pub fn encode_address(value: &str, toa: Option<u8>) -> Result<Vec<u8>> {
let digits = value.strip_prefix('+');
if digits.unwrap_or(value).is_empty() {
return Err(PduError::Invalid("address"));
}
let toa = toa.unwrap_or(match digits {
Some(_) => TOA_INTERNATIONAL,
None if !value.is_empty()
&& value
.bytes()
.all(|b| b.is_ascii_digit() || b == b'*' || b == b'#') =>
{
TOA_UNKNOWN
}
None => TOA_ALPHANUMERIC,
});
if super::Ton::from_toa(toa) == super::Ton::Alphanumeric {
let septets = gsm7::encode(value, Tables::DEFAULT).map_err(PduError::Unencodable)?;
let semi = (septets.len() * 7).div_ceil(4);
if semi > 20 {
return Err(PduError::Invalid("address length"));
}
let mut out = vec![semi as u8, toa];
out.extend(gsm7::pack(&septets, 0));
return Ok(out);
}
let digits = digits.unwrap_or(value);
if digits.chars().count() > 20 {
return Err(PduError::Invalid("address length"));
}
let body = super::encode_semi_octets(digits).map_err(PduError::Unencodable)?;
let mut out = vec![digits.len() as u8, toa];
out.extend(body);
Ok(out)
}
pub fn encode_smsc(value: Option<&str>, toa: Option<u8>) -> Result<Vec<u8>> {
let Some(v) = value.filter(|v| !v.is_empty()) else {
return Ok(vec![0]);
};
let digits = v.strip_prefix('+');
let toa = toa.unwrap_or(if digits.is_some() {
TOA_INTERNATIONAL
} else {
TOA_UNKNOWN
});
let digits = digits.unwrap_or(v);
if digits.chars().count() > 20 {
return Err(PduError::Invalid("SMSC length"));
}
let body = super::encode_semi_octets(digits).map_err(PduError::Unencodable)?;
let mut out = vec![body.len() as u8 + 1, toa];
out.extend(body);
Ok(out)
}
struct Units {
alphabet: Alphabet,
tables: Tables,
units: Vec<Vec<u8>>,
}
fn units(req: &SubmitRequest) -> Result<Units> {
let text = match &req.body {
Body::Binary(b) => {
if req.alphabet.is_some_and(|a| a != Alphabet::Bit8) {
return Err(PduError::Invalid("alphabet for binary body"));
}
return Ok(Units {
alphabet: Alphabet::Bit8,
tables: Tables::DEFAULT,
units: b.iter().map(|&x| vec![x]).collect(),
});
}
Body::Text(t) => t,
};
let gsm = |tables: Tables| -> Result<Units> {
let units = text
.chars()
.map(|c| {
gsm7::encode_char(c, tables)
.map(|(s, n)| s[..n].to_vec())
.ok_or(PduError::Unencodable(c))
})
.collect::<Result<_>>()?;
Ok(Units {
alphabet: Alphabet::Gsm7,
tables,
units,
})
};
let ucs = || Units {
alphabet: Alphabet::Ucs2,
tables: Tables::DEFAULT,
units: text
.chars()
.map(|c| ucs2::encode(c.encode_utf8(&mut [0; 4])))
.collect(),
};
match req.alphabet {
Some(Alphabet::Gsm7) => {
let tables = gsm7::choose_tables(text, &req.locking_shift, &req.single_shift)
.ok_or_else(|| first_unencodable(text))?;
gsm(tables)
}
Some(Alphabet::Ucs2) => Ok(ucs()),
Some(Alphabet::Bit8) => Err(PduError::Invalid("alphabet for text body")),
None => Ok(
match gsm7::choose_tables(text, &req.locking_shift, &req.single_shift) {
Some(t) => gsm(t)?,
None => ucs(),
},
),
}
}
fn first_unencodable(text: &str) -> PduError {
gsm7::encode(text, Tables::DEFAULT)
.err()
.map_or(PduError::Invalid("text"), PduError::Unencodable)
}
fn capacity(alphabet: Alphabet, udh_len: usize) -> usize {
match alphabet {
Alphabet::Gsm7 => 160 - (udh_len * 8).div_ceil(7),
_ => 140 - udh_len,
}
}
pub fn encode_submit(req: &SubmitRequest) -> Result<Vec<(usize, String)>> {
let u = units(req)?;
let smsc = encode_smsc(req.smsc.as_deref(), req.smsc_toa)?;
let da = encode_address(&req.destination, req.destination_toa)?;
let mut national = Vec::new();
if u.tables.single != Language::Default {
national.push(Ie {
id: IEI_SINGLE_SHIFT,
data: vec![u.tables.single.nli()],
});
}
if u.tables.locking != Language::Default {
national.push(Ie {
id: IEI_LOCKING_SHIFT,
data: vec![u.tables.locking.nli()],
});
}
let national_len = national.iter().map(|ie| 2 + ie.data.len()).sum::<usize>();
let total: usize = u.units.iter().map(Vec::len).sum();
let single_udh = if national.is_empty() {
0
} else {
1 + national_len
};
let concat_ref = req.concat_ref.unwrap_or(0);
let wide = req.concat_16bit || concat_ref > 255;
let parts: Vec<&[Vec<u8>]> = if total <= capacity(u.alphabet, single_udh) {
vec![&u.units[..]]
} else {
let cap = capacity(u.alphabet, 1 + national_len + if wide { 6 } else { 5 });
let mut parts = Vec::new();
let (mut start, mut used) = (0, 0);
for (i, unit) in u.units.iter().enumerate() {
if used + unit.len() > cap {
parts.push(&u.units[start..i]);
(start, used) = (i, 0);
}
used += unit.len();
}
parts.push(&u.units[start..]);
parts
};
if parts.len() > 255 {
return Err(PduError::TooManyParts);
}
if parts.len() > 1 && req.concat_ref.is_none() {
return Err(PduError::MissingConcatRef);
}
let (vpf, vp) = match &req.validity {
None => (0, Vec::new()),
Some(v @ Validity::Relative(r)) => (v.vpf(), vec![*r]),
Some(v @ Validity::Absolute(t)) => (
v.vpf(),
t.encode().ok_or(PduError::Invalid("TP-VP"))?.to_vec(),
),
Some(v @ Validity::Enhanced(b)) => (v.vpf(), b.to_vec()),
};
let dcs = Dcs::encode(u.alphabet, req.class);
let n = parts.len();
let mut out = Vec::with_capacity(n);
for (i, part) in parts.iter().enumerate() {
let mut ies = Vec::new();
if n > 1 {
let (seq, total) = ((i + 1) as u8, n as u8);
ies.push(if wide {
let r = concat_ref.to_be_bytes();
Ie {
id: IEI_CONCAT_16,
data: vec![r[0], r[1], total, seq],
}
} else {
Ie {
id: IEI_CONCAT_8,
data: vec![concat_ref as u8, total, seq],
}
});
}
ies.extend(national.iter().cloned());
let udh = (!ies.is_empty())
.then(|| Udh { ies }.encode())
.transpose()?;
let udh_len = udh.as_ref().map_or(0, Vec::len);
let body: Vec<u8> = part.concat();
let (udl, packed) = match u.alphabet {
Alphabet::Gsm7 => {
let hdr_septets = (udh_len * 8).div_ceil(7);
let fill = (hdr_septets * 7 - udh_len * 8) as u8;
(hdr_septets + body.len(), gsm7::pack(&body, fill))
}
_ => (udh_len + body.len(), body),
};
let fo = 0x01
| u8::from(req.reject_duplicates) << 2
| vpf << 3
| u8::from(req.status_report) << 5
| u8::from(udh.is_some()) << 6
| u8::from(req.reply_path) << 7;
let mut tpdu = vec![fo, req.message_ref];
tpdu.extend_from_slice(&da);
tpdu.extend_from_slice(&[req.pid, dcs]);
tpdu.extend_from_slice(&vp);
tpdu.push(udl as u8);
tpdu.extend(udh.unwrap_or_default());
tpdu.extend(packed);
let mut pdu = smsc.clone();
pdu.extend_from_slice(&tpdu);
out.push((tpdu.len(), hex_encode(&pdu)));
}
Ok(out)
}