use std::fmt::Debug;
use std::hash::Hash;
pub trait EncodingShape {
type State: Clone + Eq + Hash + Debug + 'static;
fn start() -> Self::State;
fn step(state: &Self::State, byte: u8) -> Option<Self::State>;
fn accepts(state: &Self::State) -> bool;
}
#[cfg(any(test, feature = "proptest"))]
fn shape_accepts<T: EncodingShape>(bytes: &[u8]) -> bool {
bytes
.iter()
.try_fold(T::start(), |state, b| T::step(&state, *b))
.is_some_and(|state| T::accepts(&state))
}
#[macro_export]
macro_rules! encoding_shape_as {
($ty:ty, $shape:ty) => {
impl $crate::EncodingShape for $ty {
type State = <$shape as $crate::EncodingShape>::State;
fn start() -> Self::State {
<$shape as $crate::EncodingShape>::start()
}
fn step(state: &Self::State, byte: u8) -> Option<Self::State> {
<$shape as $crate::EncodingShape>::step(state, byte)
}
fn accepts(state: &Self::State) -> bool {
<$shape as $crate::EncodingShape>::accepts(state)
}
}
};
}
impl<const N: usize> EncodingShape for [u8; N] {
type State = usize;
fn start() -> usize {
0
}
fn step(read: &usize, _: u8) -> Option<usize> {
(*read < N).then_some(read + 1)
}
fn accepts(read: &usize) -> bool {
*read == N
}
}
impl EncodingShape for bool {
type State = bool;
fn start() -> bool {
false
}
fn step(done: &bool, byte: u8) -> Option<bool> {
(!done && byte <= 1).then_some(true)
}
fn accepts(done: &bool) -> bool {
*done
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum SeqState<A, B> {
First(A),
Second(B),
}
fn seq_normalize<A: EncodingShape, B: EncodingShape>(
state: SeqState<A::State, B::State>,
) -> SeqState<A::State, B::State> {
match state {
SeqState::First(a) if A::accepts(&a) => SeqState::Second(B::start()),
other => other,
}
}
impl<A: EncodingShape, B: EncodingShape> EncodingShape for (A, B) {
type State = SeqState<A::State, B::State>;
fn start() -> Self::State {
seq_normalize::<A, B>(SeqState::First(A::start()))
}
fn step(state: &Self::State, byte: u8) -> Option<Self::State> {
Some(seq_normalize::<A, B>(match state {
SeqState::First(a) => SeqState::First(A::step(a, byte)?),
SeqState::Second(b) => SeqState::Second(B::step(b, byte)?),
}))
}
fn accepts(state: &Self::State) -> bool {
matches!(state, SeqState::Second(b) if B::accepts(b))
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum ScaleState {
Start,
Done,
TwoByte,
FourByte(u8, bool),
NByte { left: u8, n: u8 },
}
fn scale_step<const M: usize>(state: &ScaleState, byte: u8) -> Option<ScaleState> {
use ScaleState::*;
match *state {
Start => match byte & 0b11 {
0b00 => Some(Done),
0b01 => Some(TwoByte),
0b10 if M >= 4 => Some(FourByte(3, false)),
0b11 => {
let n = (byte >> 2) + 4;
(n as usize <= M).then_some(NByte { left: n, n })
}
_ => None,
},
Done => None,
TwoByte => (byte != 0).then_some(Done),
FourByte(3, _) => Some(FourByte(2, false)),
FourByte(2, _) => Some(FourByte(1, byte != 0)),
FourByte(_, third_nonzero) => (third_nonzero || byte != 0).then_some(Done),
NByte { left: 1, n } => (byte != 0 && (n != 4 || byte >= 64)).then_some(Done),
NByte { left, n } => Some(NByte { left: left - 1, n }),
}
}
macro_rules! scale_shape {
($ty:ty, $m:literal) => {
impl EncodingShape for $ty {
type State = ScaleState;
fn start() -> ScaleState {
ScaleState::Start
}
fn step(state: &ScaleState, byte: u8) -> Option<ScaleState> {
scale_step::<$m>(state, byte)
}
fn accepts(state: &ScaleState) -> bool {
*state == ScaleState::Done
}
}
};
}
scale_shape!(u32, 4);
scale_shape!(u64, 8);
scale_shape!(u128, 16);
encoding_shape_as!((), [u8; 0]);
encoding_shape_as!(u8, [u8; 1]);
encoding_shape_as!(u16, [u8; 2]);
encoding_shape_as!(i8, [u8; 1]);
encoding_shape_as!(i16, [u8; 2]);
encoding_shape_as!(i32, [u8; 4]);
encoding_shape_as!(i64, [u8; 8]);
encoding_shape_as!(i128, [u8; 16]);
#[cfg(feature = "proptest")]
pub fn check_encoding_shape<T>()
where
T: EncodingShape + crate::Serializable + crate::Deserializable + proptest::arbitrary::Arbitrary,
{
use proptest::prelude::*;
use proptest::test_runner::{Config, TestRunner};
let decodes = |bytes: &[u8]| {
let mut reader = bytes;
T::deserialize(&mut reader, 0).is_ok() && reader.is_empty()
};
let encoding = || {
any::<T>().prop_map(|v| {
let mut bytes = Vec::new();
v.serialize(&mut bytes)
.expect("in-memory serialization succeeds");
bytes
})
};
let corrupted = (
encoding(),
any::<prop::sample::Index>(),
any::<u8>(),
0u8..4,
)
.prop_map(|(mut bytes, i, b, how): (Vec<u8>, _, u8, u8)| {
match how {
0 if !bytes.is_empty() => bytes.truncate(i.index(bytes.len())),
1 => bytes.push(b),
2 if !bytes.is_empty() => {
let i = i.index(bytes.len());
bytes[i] = b;
}
_ => bytes.insert(0, b),
}
bytes
});
let short = prop::collection::vec(any::<u8>(), 0..12);
let strategy = prop_oneof![encoding(), corrupted, short];
TestRunner::new(Config::with_cases(4096))
.run(&strategy, |bytes: Vec<u8>| {
prop_assert_eq!(
shape_accepts::<T>(&bytes),
decodes(&bytes),
"{:02x?}",
bytes
);
Ok(())
})
.unwrap_or_else(|e| {
panic!(
"{} shape disagrees with decoding: {e}",
std::any::type_name::<T>()
)
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Deserializable, Serializable};
#[test]
fn primitive_shapes_match_decoding() {
check_encoding_shape::<()>();
check_encoding_shape::<bool>();
check_encoding_shape::<u8>();
check_encoding_shape::<u16>();
check_encoding_shape::<i32>();
check_encoding_shape::<i128>();
check_encoding_shape::<u32>();
check_encoding_shape::<u64>();
check_encoding_shape::<u128>();
check_encoding_shape::<[u8; 3]>();
check_encoding_shape::<(u64, bool)>();
check_encoding_shape::<(u8, u32)>();
}
#[test]
fn scale_shapes_match_decoding_at_mode_boundaries() {
fn check<T: EncodingShape + Serializable + Deserializable + TryFrom<u128>>(m: u32) {
for bits in 0..(8 * m) {
for v in [
(1u128 << bits).wrapping_sub(1),
1u128 << bits,
(1u128 << bits) + 1,
] {
let Ok(v) = T::try_from(v) else { continue };
let mut bytes = Vec::new();
v.serialize(&mut bytes).unwrap();
assert!(shape_accepts::<T>(&bytes), "{bytes:02x?}");
for i in 0..bytes.len() {
assert!(!shape_accepts::<T>(&bytes[..i]), "{bytes:02x?}");
}
}
}
}
check::<u32>(4);
check::<u64>(8);
check::<u128>(16);
}
}