use std::collections::BTreeMap;
use std::mem::size_of;
use oxijolt_sys::*;
use super::compound::{check_expanded, fits_jolt_ids, sub_shape_ids};
use super::{initialize, Shape};
use crate::error::{BinaryStateError, JoltMessage};
use crate::ShapeError;
mod hash;
use hash::{xxh64, Xxh64};
const MAGIC: [u8; 8] = *b"OXJSHAPE";
const HEADER_LEN: usize = 48;
const CHECKSUM_AT: usize = 40;
const FLAG_DOUBLE_PRECISION: u32 = 1;
const FLAG_CROSS_PLATFORM_DETERMINISTIC: u32 = 1 << 1;
const FLAG_LITTLE_ENDIAN: u32 = 1 << 2;
const SUB_SHAPE_ID_BITS: u32 = 32;
impl Shape {
pub fn save_binary_state(&self) -> Result<Vec<u8>, ShapeError> {
let payload = self.save_payload()?;
let mut bytes = Vec::with_capacity(HEADER_LEN + payload.len());
bytes.extend_from_slice(&header(&payload));
bytes.extend_from_slice(&payload);
Ok(bytes)
}
pub unsafe fn restore_binary_state(bytes: &[u8]) -> Result<Shape, ShapeError> {
let payload = checked_payload(bytes).map_err(ShapeError::BinaryState)?;
initialize()?;
let mut message = [0u8; JoltMessage::CAPACITY + 2];
let shape = unsafe {
JPH_Shape_RestoreBinaryState(
payload.as_ptr().cast(),
payload.len(),
message.as_mut_ptr().cast(),
message.len() as u32,
)
};
if shape.is_null() {
return Err(rejected(&message));
}
let shape = unsafe { Shape::from_raw(shape) }?;
check_restored(shape)
}
fn save_payload(&self) -> Result<Vec<u8>, ShapeError> {
let mut message = [0u8; JoltMessage::CAPACITY + 2];
let state = unsafe {
JPH_Shape_SaveBinaryState(
self.as_ptr(),
message.as_mut_ptr().cast(),
message.len() as u32,
)
};
if state.is_null() {
return Err(rejected(&message));
}
let size = unsafe { JPH_ShapeBinaryState_GetSize(state) };
let mut payload = vec![0u8; size];
unsafe {
JPH_ShapeBinaryState_CopyData(state, payload.as_mut_ptr().cast(), size);
JPH_ShapeBinaryState_Destroy(state);
}
Ok(payload)
}
}
fn rejected(message: &[u8]) -> ShapeError {
ShapeError::BinaryState(BinaryStateError::Rejected(JoltMessage::from_c_buffer(
message,
)))
}
fn build_flags() -> u32 {
let mut flags = 0;
if size_of::<Real>() == size_of::<f64>() {
flags |= FLAG_DOUBLE_PRECISION;
}
if CROSS_PLATFORM_DETERMINISTIC_ENABLED {
flags |= FLAG_CROSS_PLATFORM_DETERMINISTIC;
}
if cfg!(target_endian = "little") {
flags |= FLAG_LITTLE_ENDIAN;
}
flags
}
fn build_id() -> [u8; 16] {
let text = format!("jolt={JOLT_COMMIT};joltc={JOLTC_COMMIT};joltc_ext={JOLTC_EXT_REVISION}");
let mut id = [0; 16];
id[..8].copy_from_slice(&xxh64(text.as_bytes(), 0).to_le_bytes());
id[8..].copy_from_slice(&xxh64(text.as_bytes(), 1).to_le_bytes());
id
}
fn header(payload: &[u8]) -> [u8; HEADER_LEN] {
let mut header = [0; HEADER_LEN];
header[..8].copy_from_slice(&MAGIC);
header[8..12].copy_from_slice(&BinaryStateError::FORMAT_VERSION.to_le_bytes());
header[12..16].copy_from_slice(&build_flags().to_le_bytes());
header[16..32].copy_from_slice(&build_id());
header[32..40].copy_from_slice(&(payload.len() as u64).to_le_bytes());
let checksum = checksum(&header, payload);
header[CHECKSUM_AT..].copy_from_slice(&checksum.to_le_bytes());
header
}
fn checksum(header: &[u8; HEADER_LEN], payload: &[u8]) -> u64 {
let mut zeroed = *header;
zeroed[CHECKSUM_AT..].fill(0);
let mut hasher = Xxh64::new(0);
hasher.update(&zeroed);
hasher.update(payload);
hasher.finish()
}
fn checked_payload(bytes: &[u8]) -> Result<&[u8], BinaryStateError> {
let (header, payload) = match bytes.split_first_chunk::<HEADER_LEN>() {
Some(split) => split,
None if bytes.len() >= MAGIC.len() && bytes[..MAGIC.len()] != MAGIC => {
return Err(BinaryStateError::NotBinaryState)
}
None => return Err(BinaryStateError::Truncated),
};
if header[..8] != MAGIC {
return Err(BinaryStateError::NotBinaryState);
}
let field = |range: std::ops::Range<usize>| &header[range];
if field(8..12) != BinaryStateError::FORMAT_VERSION.to_le_bytes()
|| field(12..16) != build_flags().to_le_bytes()
|| field(16..32) != build_id()
{
return Err(BinaryStateError::OtherBuild);
}
let mut length = [0; 8];
length.copy_from_slice(field(32..40));
let length = u64::from_le_bytes(length);
if length > payload.len() as u64 {
return Err(BinaryStateError::Truncated);
}
if length < payload.len() as u64 {
return Err(BinaryStateError::Malformed("bytes follow the payload"));
}
if field(CHECKSUM_AT..HEADER_LEN) != checksum(header, payload).to_le_bytes() {
return Err(BinaryStateError::Corrupt);
}
Ok(payload)
}
fn check_restored(shape: Shape) -> Result<Shape, ShapeError> {
let malformed = |rule| ShapeError::BinaryState(BinaryStateError::Malformed(rule));
let shape = shape
.within_extent_bounds()
.map_err(|_| malformed("the shape's bounds exceed limits::MAX_SHAPE_EXTENT"))?;
let ids = unsafe { sub_shape_ids(shape.as_ptr(), &mut BTreeMap::new()) };
if ids.width > SUB_SHAPE_ID_BITS || !fits_jolt_ids(ids) {
return Err(malformed("the shape's sub-shape ids do not fit 32 bits"));
}
check_expanded(ids.expanded)
.map_err(|_| malformed("the shape expands beyond limits::MAX_EXPANDED_SUB_SHAPES"))?;
Ok(shape)
}
#[cfg(test)]
mod tests;