const MODEL_BYTES: &[u8] = include_bytes!("../../data/model-en.data");
const SIGNATURE: &[u8] = b"keybr.com";
const EXPECTED_ALPHABET_SIZE: usize = 27;
const EXPECTED_ORDER: usize = 4;
pub struct TransitionTable {
order: usize,
alphabet_size: usize,
data: Vec<u8>,
}
impl TransitionTable {
pub fn from_embedded() -> Self {
Self::from_bytes(MODEL_BYTES).expect("embedded model-en.data is malformed")
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
let mut cursor = 0usize;
if bytes.len() < SIGNATURE.len() {
return Err("file too short for signature".into());
}
if &bytes[..SIGNATURE.len()] != SIGNATURE {
return Err("missing keybr.com signature".into());
}
cursor += SIGNATURE.len();
if bytes.len() < cursor + 2 {
return Err("file too short for chain header".into());
}
let order = bytes[cursor] as usize;
cursor += 1;
let alphabet_size = bytes[cursor] as usize;
cursor += 1;
if order != EXPECTED_ORDER {
return Err(format!("unsupported order {} (expected 4)", order));
}
if alphabet_size != EXPECTED_ALPHABET_SIZE {
return Err(format!(
"unsupported alphabet size {} (expected 27)",
alphabet_size
));
}
let alphabet_bytes = alphabet_size * 2;
if bytes.len() < cursor + alphabet_bytes {
return Err("file too short for alphabet".into());
}
for i in 0..alphabet_size {
let hi = bytes[cursor + i * 2] as u16;
let lo = bytes[cursor + i * 2 + 1] as u16;
let code_point = (hi << 8) | lo;
let expected: u16 = if i == 0 {
0x0020
} else {
(b'a' + (i as u8) - 1) as u16
};
if code_point != expected {
return Err(format!(
"unexpected alphabet code point at slot {}: 0x{:04x} (expected 0x{:04x})",
i, code_point, expected
));
}
}
cursor += alphabet_bytes;
let segment_count = alphabet_size.pow((order - 1) as u32);
let dense_len = segment_count * alphabet_size;
let mut data = vec![0u8; dense_len];
for seg in 0..segment_count {
if cursor >= bytes.len() {
return Err(format!("file truncated at segment {}", seg));
}
let entry_count = bytes[cursor] as usize;
cursor += 1;
if entry_count > alphabet_size {
return Err(format!(
"segment {} has {} entries (max {})",
seg, entry_count, alphabet_size
));
}
if bytes.len() < cursor + entry_count * 2 {
return Err(format!("file truncated in segment {} body", seg));
}
let segment_start = seg * alphabet_size;
for _ in 0..entry_count {
let index = bytes[cursor] as usize;
let freq = bytes[cursor + 1];
cursor += 2;
if index >= alphabet_size {
return Err(format!(
"segment {} entry has alphabet index {} (max {})",
seg,
index,
alphabet_size - 1
));
}
if freq == 0 {
return Err(format!(
"segment {} contains zero-frequency entry (sparse format must omit)",
seg
));
}
data[segment_start + index] = freq;
}
}
if cursor != bytes.len() {
return Err(format!(
"trailing {} unused bytes after segments",
bytes.len() - cursor
));
}
Ok(Self {
order,
alphabet_size,
data,
})
}
#[inline]
#[allow(dead_code)]
pub fn order(&self) -> usize {
self.order
}
#[inline]
#[allow(dead_code)]
pub fn alphabet_size(&self) -> usize {
self.alphabet_size
}
#[inline]
pub fn char_to_idx(c: char) -> Option<usize> {
if c == ' ' {
Some(0)
} else if c.is_ascii_lowercase() {
Some((c as u8 - b'a' + 1) as usize)
} else {
None
}
}
#[inline]
pub fn idx_to_char(i: usize) -> char {
if i == 0 {
' '
} else if i <= 26 {
(b'a' + (i as u8) - 1) as char
} else {
panic!("alphabet index {} out of range", i)
}
}
#[inline]
pub fn segment(&self, history: &[usize]) -> &[u8] {
debug_assert_eq!(history.len(), self.order - 1);
let mut offset = 0usize;
for &c in history {
debug_assert!(c < self.alphabet_size);
offset = offset * self.alphabet_size + c;
}
let segment_start = offset * self.alphabet_size;
&self.data[segment_start..segment_start + self.alphabet_size]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_embedded_model() {
let table = TransitionTable::from_embedded();
assert_eq!(table.order(), 4);
assert_eq!(table.alphabet_size(), 27);
}
#[test]
fn char_idx_roundtrip() {
assert_eq!(TransitionTable::char_to_idx(' '), Some(0));
assert_eq!(TransitionTable::idx_to_char(0), ' ');
for c in 'a'..='z' {
let idx = TransitionTable::char_to_idx(c).unwrap();
assert_eq!(TransitionTable::idx_to_char(idx), c);
}
assert_eq!(TransitionTable::char_to_idx('A'), None);
assert_eq!(TransitionTable::char_to_idx('!'), None);
}
#[test]
fn every_segment_sums_to_255_or_0() {
let table = TransitionTable::from_embedded();
let n = table.alphabet_size;
let segments = n.pow((table.order - 1) as u32);
for seg in 0..segments {
let slice = &table.data[seg * n..(seg + 1) * n];
let sum: u32 = slice.iter().map(|&b| b as u32).sum();
assert!(
sum == 0 || sum == 255,
"segment {} sums to {} (expected 0 or 255)",
seg,
sum
);
}
}
#[test]
fn segment_lookup_matches_indexing_formula() {
let table = TransitionTable::from_embedded();
let history = [
TransitionTable::char_to_idx(' ').unwrap(),
TransitionTable::char_to_idx('t').unwrap(),
TransitionTable::char_to_idx('h').unwrap(),
];
let seg = table.segment(&history);
let sum: u32 = seg.iter().map(|&b| b as u32).sum();
assert_eq!(sum, 255, "_th segment should be a populated distribution");
let e_idx = TransitionTable::char_to_idx('e').unwrap();
let max_idx = seg
.iter()
.enumerate()
.max_by_key(|(_, &v)| v)
.map(|(i, _)| i)
.unwrap();
assert_eq!(
max_idx, e_idx,
"most likely follow-up to '_th' should be 'e' (got idx {})",
max_idx
);
}
}