use super::gsm7::{self, Tables};
use super::pdu::{Alphabet, Tpdu};
use super::ucs2;
use std::collections::{BTreeMap, HashMap};
pub const DEFAULT_MAX_PENDING: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Kind {
Deliver,
Submit,
StatusReport,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Key {
pub kind: Kind,
pub address: String,
pub reference: u16,
pub ref_16bit: bool,
pub total: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fragment {
pub alphabet: Alphabet,
pub tables: Tables,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub key: Option<Key>,
pub alphabet: Alphabet,
pub payload: Vec<u8>,
pub text: Option<String>,
pub parts: usize,
}
struct Pending {
parts: BTreeMap<u8, Fragment>,
last_seen: u64,
tick: u64,
}
pub struct Reassembler {
pending: HashMap<Key, Pending>,
max_pending: usize,
tick: u64,
}
impl Default for Reassembler {
fn default() -> Self {
Self::with_max_pending(DEFAULT_MAX_PENDING)
}
}
impl Reassembler {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_pending(max: usize) -> Self {
Reassembler {
pending: HashMap::new(),
max_pending: max.max(1),
tick: 0,
}
}
pub fn max_pending(&self) -> usize {
self.max_pending
}
pub fn push(&mut self, key: Key, seq: u8, fragment: Fragment, now: u64) -> Option<Message> {
if seq == 0 || seq > key.total {
return None;
}
self.tick += 1;
if !self.pending.contains_key(&key) && self.pending.len() >= self.max_pending {
let oldest = self
.pending
.iter()
.min_by_key(|(_, p)| (p.last_seen, p.tick))
.map(|(k, _)| k.clone());
if let Some(k) = oldest {
self.pending.remove(&k);
}
}
let entry = self.pending.entry(key.clone()).or_insert_with(|| Pending {
parts: BTreeMap::new(),
last_seen: now,
tick: 0,
});
entry.parts.insert(seq, fragment);
entry.last_seen = entry.last_seen.max(now);
entry.tick = self.tick;
if entry.parts.len() < usize::from(key.total) {
return None;
}
let parts: Vec<Fragment> = self.pending.remove(&key)?.parts.into_values().collect();
Some(join(Some(key), parts))
}
pub fn push_tpdu(&mut self, tpdu: &Tpdu, now: u64) -> Option<Message> {
let ud = tpdu.user_data()?;
let dcs = tpdu.dcs().copied().unwrap_or(super::pdu::Dcs::decode(0));
let alphabet = if dcs.compressed {
Alphabet::Bit8
} else {
dcs.alphabet
};
let fragment = Fragment {
alphabet,
tables: ud.udh.as_ref().map_or(Tables::DEFAULT, |u| u.tables()),
payload: ud.payload.clone(),
};
let Some(c) = ud.udh.as_ref().and_then(|u| u.concat()) else {
return Some(join(None, vec![fragment]));
};
let kind = match tpdu {
Tpdu::Deliver(_) => Kind::Deliver,
Tpdu::Submit(_) => Kind::Submit,
Tpdu::StatusReport(_) => Kind::StatusReport,
};
let key = Key {
kind,
address: tpdu.address().to_string(),
reference: c.reference,
ref_16bit: c.ref_16bit,
total: c.total,
};
self.push(key, c.seq, fragment, now)
}
pub fn expire_before(&mut self, cutoff: u64) -> Vec<Key> {
let old: Vec<Key> = self
.pending
.iter()
.filter(|(_, p)| p.last_seen < cutoff)
.map(|(k, _)| k.clone())
.collect();
for k in &old {
self.pending.remove(k);
}
old
}
pub fn pending(&self) -> usize {
self.pending.len()
}
}
fn join(key: Option<Key>, parts: Vec<Fragment>) -> Message {
let first = parts[0].clone();
let uniform = parts
.iter()
.all(|p| p.alphabet == first.alphabet && p.tables == first.tables);
let payload: Vec<u8> = parts
.iter()
.flat_map(|p| p.payload.iter().copied())
.collect();
let decode = |alphabet, tables, bytes: &[u8]| match alphabet {
Alphabet::Gsm7 => Some(gsm7::decode(bytes, tables)),
Alphabet::Ucs2 => Some(ucs2::decode(bytes)),
Alphabet::Bit8 => None,
};
let text = if uniform {
decode(first.alphabet, first.tables, &payload)
} else {
parts
.iter()
.map(|p| decode(p.alphabet, p.tables, &p.payload))
.collect()
};
Message {
key,
alphabet: first.alphabet,
payload,
text,
parts: parts.len(),
}
}