use crate::{
byte_to_symbol, checksum_bytes, symbol_to_byte, Error, Result, ALPHABET_SIZE, PROTOCOL_VERSION,
PUA_BASE,
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MappingId(pub String);
impl MappingId {
pub fn static_v1() -> Self {
Self(format!("static-v{PROTOCOL_VERSION}"))
}
pub fn from_seed(seed: &[u8]) -> Self {
let c = checksum_bytes(seed);
Self(format!("session-v{PROTOCOL_VERSION}-{c:08x}"))
}
}
impl fmt::Display for MappingId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mapping {
encode_table: [u8; ALPHABET_SIZE],
decode_table: [u8; ALPHABET_SIZE],
id: MappingId,
}
impl Mapping {
pub fn identity() -> Self {
let mut encode_table = [0u8; ALPHABET_SIZE];
let mut decode_table = [0u8; ALPHABET_SIZE];
for i in 0..ALPHABET_SIZE {
encode_table[i] = i as u8;
decode_table[i] = i as u8;
}
Self {
encode_table,
decode_table,
id: MappingId::static_v1(),
}
}
pub fn from_encode_table(encode_table: [u8; ALPHABET_SIZE], id: MappingId) -> Result<Self> {
let mut decode_table = [0u8; ALPHABET_SIZE];
let mut seen = [false; ALPHABET_SIZE];
for (byte, &sym) in encode_table.iter().enumerate() {
if seen[sym as usize] {
return Err(Error::MappingCollision(format!(
"symbol offset {sym} assigned twice"
)));
}
seen[sym as usize] = true;
decode_table[sym as usize] = byte as u8;
}
if seen.iter().any(|&s| !s) {
return Err(Error::InvalidMapping(
"encode table is not a complete permutation".into(),
));
}
Ok(Self {
encode_table,
decode_table,
id,
})
}
pub fn from_seed(seed: &[u8]) -> Result<Self> {
let id = MappingId::from_seed(seed);
let mut state = mix_seed(seed);
let mut encode_table = [0u8; ALPHABET_SIZE];
for (i, slot) in encode_table.iter_mut().enumerate() {
*slot = i as u8;
}
for i in (1..ALPHABET_SIZE).rev() {
state = splitmix64(state);
let j = (state as usize) % (i + 1);
encode_table.swap(i, j);
}
Self::from_encode_table(encode_table, id)
}
pub fn id(&self) -> &MappingId {
&self.id
}
#[inline]
pub fn encode_byte(&self, byte: u8) -> char {
let offset = self.encode_table[byte as usize];
char::from_u32(PUA_BASE + u32::from(offset)).expect("PUA symbol")
}
pub fn decode_char(&self, ch: char) -> Result<u8> {
let offset = symbol_to_byte(ch).ok_or(Error::InvalidCodepoint(ch as u32))? as usize;
Ok(self.decode_table[offset])
}
pub fn encode_str(&self, text: &str) -> String {
let bytes = text.as_bytes();
let mut out = String::with_capacity(bytes.len() * 3); for &b in bytes {
out.push(self.encode_byte(b));
}
out
}
pub fn decode_str(&self, encoded: &str) -> Result<String> {
let mut bytes = Vec::with_capacity(encoded.chars().count());
for ch in encoded.chars() {
bytes.push(self.decode_char(ch)?);
}
String::from_utf8(bytes).map_err(Error::from)
}
pub fn encode_table(&self) -> &[u8; ALPHABET_SIZE] {
&self.encode_table
}
}
pub type StaticMapping = Mapping;
fn mix_seed(seed: &[u8]) -> u64 {
let mut state = 0x243f_6a88_85a3_08d3_u64; if seed.is_empty() {
return splitmix64(state);
}
for chunk in seed.chunks(8) {
let mut buf = [0u8; 8];
buf[..chunk.len()].copy_from_slice(chunk);
state ^= u64::from_le_bytes(buf);
state = splitmix64(state);
}
state
}
#[inline]
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = x;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
#[inline]
#[must_use]
pub fn identity_symbol(byte: u8) -> char {
byte_to_symbol(byte)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_roundtrip() {
let m = Mapping::identity();
let text = "Hello, 世界 👋";
assert_eq!(m.decode_str(&m.encode_str(text)).unwrap(), text);
}
#[test]
fn seed_deterministic() {
let a = Mapping::from_seed(b"agent-alpha").unwrap();
let b = Mapping::from_seed(b"agent-alpha").unwrap();
assert_eq!(a, b);
assert_ne!(a.encode_table(), Mapping::identity().encode_table());
}
#[test]
fn seed_roundtrip() {
let m = Mapping::from_seed(b"session-001").unwrap();
let text = "deploy ready ✓";
assert_eq!(m.decode_str(&m.encode_str(text)).unwrap(), text);
}
}