use alloc::vec::Vec;
use super::Codex;
use crate::Result;
use crate::error::Error;
use crate::fragment::util::fisher_yates;
use crate::memory::LockedBytes;
pub struct StaticCodex {
table: LockedBytes,
}
impl core::fmt::Debug for StaticCodex {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StaticCodex")
.field("table", &"<redacted>")
.finish()
}
}
impl StaticCodex {
pub fn from_swaps(swaps: &[(u8, u8)]) -> Result<Self> {
let mut table = [0u8; 256];
for (i, slot) in table.iter_mut().enumerate() {
#[allow(clippy::cast_possible_truncation)]
{
*slot = i as u8;
}
}
let mut used = [false; 256];
for &(a, b) in swaps {
if a == b {
used[a as usize] = true;
continue;
}
if used[a as usize] || used[b as usize] {
return Err(Error::Codex(alloc::string::ToString::to_string(
"byte appears in more than one swap pair",
)));
}
table[a as usize] = b;
table[b as usize] = a;
used[a as usize] = true;
used[b as usize] = true;
}
Ok(Self {
table: LockedBytes::from_slice(&table),
})
}
pub fn random_involution() -> Result<Self> {
let mut table = [0u8; 256];
for (i, slot) in table.iter_mut().enumerate() {
#[allow(clippy::cast_possible_truncation)]
{
*slot = i as u8;
}
}
let mut perm: Vec<u8> = (0u8..=255).collect();
fisher_yates(&mut perm)?;
let mut i = 0usize;
while i + 1 < perm.len() {
let a = perm[i];
let b = perm[i + 1];
table[a as usize] = b;
table[b as usize] = a;
i += 2;
}
Ok(Self {
table: LockedBytes::from_slice(&table),
})
}
}
impl Codex for StaticCodex {
#[inline]
fn encode(&self, byte: u8) -> u8 {
self.table.as_bytes()[byte as usize]
}
#[inline]
fn decode(&self, byte: u8) -> u8 {
self.table.as_bytes()[byte as usize]
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
mod tests {
use super::*;
#[test]
fn identity_when_no_swaps() {
let codex = StaticCodex::from_swaps(&[]).unwrap();
for byte in 0u8..=255 {
assert_eq!(codex.encode(byte), byte);
assert_eq!(codex.decode(byte), byte);
}
}
#[test]
fn explicit_swap_pair() {
let codex = StaticCodex::from_swaps(&[(0x42, 0x99)]).unwrap();
assert_eq!(codex.encode(0x42), 0x99);
assert_eq!(codex.encode(0x99), 0x42);
assert_eq!(codex.decode(0x99), 0x42);
assert_eq!(codex.decode(0x42), 0x99);
for byte in 0u8..=255 {
if byte != 0x42 && byte != 0x99 {
assert_eq!(codex.encode(byte), byte);
}
}
}
#[test]
fn rejects_byte_in_two_swap_pairs() {
let err = StaticCodex::from_swaps(&[(0x42, 0x99), (0x42, 0xab)]).unwrap_err();
assert!(matches!(err, Error::Codex(_)));
}
#[test]
fn rejects_byte_in_two_swap_pairs_either_side() {
let err = StaticCodex::from_swaps(&[(0x42, 0x99), (0xab, 0x99)]).unwrap_err();
assert!(matches!(err, Error::Codex(_)));
}
#[test]
fn self_swap_is_a_noop() {
let codex = StaticCodex::from_swaps(&[(0x42, 0x42)]).unwrap();
assert_eq!(codex.encode(0x42), 0x42);
}
#[test]
fn involution_holds_for_every_byte_from_swaps() {
let codex = StaticCodex::from_swaps(&[(0x00, 0xff), (0x10, 0xa1), (0x42, 0x88)]).unwrap();
for byte in 0u8..=255 {
assert_eq!(codex.decode(codex.encode(byte)), byte);
}
}
#[test]
fn random_involution_holds_for_every_byte() {
let codex = StaticCodex::random_involution().unwrap();
for byte in 0u8..=255 {
assert_eq!(codex.decode(codex.encode(byte)), byte);
}
}
#[test]
fn random_involution_has_no_fixed_points() {
let codex = StaticCodex::random_involution().unwrap();
for byte in 0u8..=255 {
assert_ne!(
codex.encode(byte),
byte,
"byte {byte:#04x} is a fixed point"
);
}
}
#[test]
fn random_involutions_differ_across_calls() {
let a = StaticCodex::random_involution().unwrap();
let b = StaticCodex::random_involution().unwrap();
assert_ne!(a.table.as_bytes(), b.table.as_bytes());
}
#[test]
fn debug_redacts_table() {
let codex = StaticCodex::from_swaps(&[(0x00, 0xff)]).unwrap();
let rendered = alloc::format!("{codex:?}");
assert!(rendered.contains("<redacted>"));
}
}