use crate::declaration::{Dtype, ValueDecl};
const MAGIC: &[u8; 3] = b"BSP";
const HEADER_LEN: usize = 3 + 1 + 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Input,
Output,
}
impl Kind {
fn byte(self) -> u8 {
match self {
Self::Input => b'I',
Self::Output => b'O',
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Values {
F32(Vec<f32>),
U8(Vec<u8>),
}
impl Values {
pub fn len(&self) -> usize {
match self {
Self::F32(values) => values.len(),
Self::U8(values) => values.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn dtype(&self) -> Dtype {
match self {
Self::F32(_) => Dtype::F32,
Self::U8(_) => Dtype::U8,
}
}
pub fn get(&self, index: usize) -> Option<f32> {
match self {
Self::F32(values) => values.get(index).copied(),
Self::U8(values) => values.get(index).map(|value| f32::from(*value)),
}
}
pub fn as_f32(&self) -> Option<&[f32]> {
match self {
Self::F32(values) => Some(values),
Self::U8(_) => None,
}
}
pub fn as_u8(&self) -> Option<&[u8]> {
match self {
Self::U8(values) => Some(values),
Self::F32(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PayloadError {
TooShort,
BadHeader,
WrongKind,
WrongSchema {
expected: u32,
found: u32,
},
WrongLength {
value: String,
expected: usize,
found: usize,
},
NotFinite {
value: String,
index: usize,
},
OutOfBounds {
value: String,
index: usize,
found: f32,
min: f32,
max: f32,
},
TrailingBytes,
Mismatch(String),
}
impl std::fmt::Display for PayloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooShort => write!(f, "payload ends early"),
Self::BadHeader => write!(f, "payload header isn't BotStadia's"),
Self::WrongKind => write!(f, "payload is the wrong kind (input vs output)"),
Self::WrongSchema { expected, found } => {
write!(f, "payload schema {found}, expected {expected}")
}
Self::WrongLength {
value,
expected,
found,
} => write!(f, "`{value}` is {found} bytes, expected {expected}"),
Self::NotFinite { value, index } => {
write!(f, "`{value}`[{index}] isn't a finite number")
}
Self::OutOfBounds {
value,
index,
found,
min,
max,
} => {
write!(f, "`{value}`[{index}] = {found} is outside {min}..={max}")
}
Self::TrailingBytes => write!(f, "payload has bytes after its last value"),
Self::Mismatch(text) => write!(f, "{text}"),
}
}
}
impl std::error::Error for PayloadError {}
pub fn encoded_len(decls: &[ValueDecl]) -> usize {
HEADER_LEN + decls.iter().map(|decl| 4 + decl.bytes()).sum::<usize>()
}
pub fn encode(
kind: Kind,
schema: u32,
decls: &[ValueDecl],
values: &[Values],
) -> Result<Vec<u8>, PayloadError> {
if decls.len() != values.len() {
return Err(PayloadError::Mismatch(format!(
"{} values for {} declared",
values.len(),
decls.len()
)));
}
let mut bytes = Vec::with_capacity(encoded_len(decls));
bytes.extend_from_slice(MAGIC);
bytes.push(kind.byte());
bytes.extend_from_slice(&schema.to_le_bytes());
for (decl, value) in decls.iter().zip(values) {
if decl.dtype != value.dtype() || decl.elements() != value.len() {
return Err(PayloadError::Mismatch(format!(
"`{}` wants {} Ă— {}, got {} Ă— {}",
decl.name,
decl.elements(),
decl.dtype.name(),
value.len(),
value.dtype().name()
)));
}
let len = u32::try_from(decl.bytes())
.map_err(|_| PayloadError::Mismatch(format!("`{}` is too large", decl.name)))?;
bytes.extend_from_slice(&len.to_le_bytes());
match value {
Values::F32(elements) => elements
.iter()
.for_each(|element| bytes.extend_from_slice(&element.to_le_bytes())),
Values::U8(elements) => bytes.extend_from_slice(elements),
}
}
Ok(bytes)
}
pub fn decode(
kind: Kind,
schema: u32,
decls: &[ValueDecl],
bytes: &[u8],
) -> Result<Vec<Values>, PayloadError> {
if bytes.len() < HEADER_LEN {
return Err(PayloadError::TooShort);
}
if &bytes[..3] != MAGIC {
return Err(PayloadError::BadHeader);
}
if bytes[3] != kind.byte() {
return Err(PayloadError::WrongKind);
}
let found = u32::from_le_bytes(bytes[4..8].try_into().expect("four bytes"));
if found != schema {
return Err(PayloadError::WrongSchema {
expected: schema,
found,
});
}
let mut rest = &bytes[HEADER_LEN..];
let mut values = Vec::with_capacity(decls.len());
for decl in decls {
if rest.len() < 4 {
return Err(PayloadError::TooShort);
}
let len = u32::from_le_bytes(rest[..4].try_into().expect("four bytes")) as usize;
rest = &rest[4..];
if len != decl.bytes() {
return Err(PayloadError::WrongLength {
value: decl.name.clone(),
expected: decl.bytes(),
found: len,
});
}
if rest.len() < len {
return Err(PayloadError::TooShort);
}
let (body, tail) = rest.split_at(len);
rest = tail;
values.push(match decl.dtype {
Dtype::F32 => Values::F32(
body.as_chunks::<4>()
.0
.iter()
.map(|chunk| f32::from_le_bytes(*chunk))
.collect(),
),
Dtype::U8 => Values::U8(body.to_vec()),
});
}
if !rest.is_empty() {
return Err(PayloadError::TrailingBytes);
}
Ok(values)
}
pub fn check(
kind: Kind,
schema: u32,
decls: &[ValueDecl],
bytes: &[u8],
) -> Result<Vec<Values>, PayloadError> {
let values = decode(kind, schema, decls, bytes)?;
for (decl, value) in decls.iter().zip(&values) {
for index in 0..value.len() {
let element = value.get(index).expect("index in range");
if !element.is_finite() {
return Err(PayloadError::NotFinite {
value: decl.name.clone(),
index,
});
}
let (min, max) = decl
.bounds
.range(index)
.expect("bounds cover every element");
if element < min || element > max {
return Err(PayloadError::OutOfBounds {
value: decl.name.clone(),
index,
found: element,
min,
max,
});
}
}
}
Ok(values)
}
pub fn check_output(
schema: u32,
decls: &[ValueDecl],
bytes: &[u8],
) -> Result<Vec<Values>, PayloadError> {
check(Kind::Output, schema, decls, bytes)
}
pub fn sample_uniform(decls: &[ValueDecl], mut next: impl FnMut() -> u64) -> Vec<Values> {
let mut unit = move || (next() >> 11) as f64 / (1u64 << 53) as f64;
decls
.iter()
.map(|decl| {
let range = |index: usize| {
decl.bounds
.range(index)
.expect("bounds cover every element")
};
match decl.dtype {
Dtype::F32 => Values::F32(
(0..decl.elements())
.map(|index| {
let (min, max) = range(index);
(f64::from(min) + unit() * (f64::from(max) - f64::from(min)))
.clamp(f64::from(min), f64::from(max))
as f32
})
.collect(),
),
Dtype::U8 => Values::U8(
(0..decl.elements())
.map(|index| {
let (min, max) = range(index);
let span = f64::from(max) - f64::from(min) + 1.0;
(f64::from(min) + (unit() * span).floor()).min(f64::from(max)) as u8
})
.collect(),
),
}
})
.collect()
}
pub fn extremes(decls: &[ValueDecl], mut pick: impl FnMut() -> bool) -> Vec<Values> {
decls
.iter()
.map(|decl| {
let mut bound = |index: usize| {
let (min, max) = decl
.bounds
.range(index)
.expect("bounds cover every element");
if pick() { max } else { min }
};
match decl.dtype {
Dtype::F32 => Values::F32((0..decl.elements()).map(bound).collect()),
Dtype::U8 => Values::U8(
(0..decl.elements())
.map(|index| bound(index) as u8)
.collect(),
),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::declaration::tests::sample;
#[test]
fn round_trips_and_sizes_match() {
let declaration = sample();
let values = vec![Values::F32(vec![0.5, -0.5])];
let bytes = encode(Kind::Output, 1, &declaration.outputs, &values).unwrap();
assert_eq!(bytes.len(), declaration.output_payload_bytes());
assert_eq!(
check_output(1, &declaration.outputs, &bytes).unwrap(),
values
);
}
#[test]
fn rejects_without_clamping() {
let declaration = sample();
let outputs = &declaration.outputs;
let out_of_bounds =
encode(Kind::Output, 1, outputs, &[Values::F32(vec![1.5, 0.0])]).unwrap();
assert!(matches!(
check_output(1, outputs, &out_of_bounds),
Err(PayloadError::OutOfBounds { index: 0, .. })
));
let nan = encode(
Kind::Output,
1,
outputs,
&[Values::F32(vec![0.0, f32::NAN])],
)
.unwrap();
assert!(matches!(
check_output(1, outputs, &nan),
Err(PayloadError::NotFinite { index: 1, .. })
));
let good = encode(Kind::Output, 1, outputs, &[Values::F32(vec![0.0, 0.0])]).unwrap();
assert!(matches!(
check_output(2, outputs, &good),
Err(PayloadError::WrongSchema { .. })
));
assert!(matches!(
check(Kind::Input, 1, outputs, &good),
Err(PayloadError::WrongKind)
));
let mut long = good.clone();
long.push(0);
assert_eq!(
check_output(1, outputs, &long),
Err(PayloadError::TrailingBytes)
);
assert_eq!(
check_output(1, outputs, &good[..good.len() - 1]),
Err(PayloadError::TooShort)
);
}
}