use bincode::config::{Configuration, standard};
use serde::{Serialize, de::DeserializeOwned};
use crate::errors::SparseErrors;
use crate::sparse::CompressedSparseData2;
use crate::traits::BixverseNumeric;
pub const SERIALISE_FORMAT_VERSION: u32 = 1;
const CSR_MAGIC: &[u8; 8] = b"BXSPCSD2";
const HEADER_LEN: usize = 8 + 4 + 2;
const CONFIG: Configuration = standard();
pub fn encode_with_header<S>(
magic: &[u8; 8],
widths: [u8; 2],
value: &S,
) -> Result<Vec<u8>, SparseErrors>
where
S: Serialize,
{
let payload = bincode::serde::encode_to_vec(value, CONFIG)
.map_err(|e| SparseErrors::SerialisationFailed(e.to_string()))?;
let mut out = Vec::with_capacity(HEADER_LEN + payload.len());
out.extend_from_slice(magic);
out.extend_from_slice(&SERIALISE_FORMAT_VERSION.to_le_bytes());
out.extend_from_slice(&widths);
out.extend_from_slice(&payload);
Ok(out)
}
pub fn decode_with_header<D>(
magic: &[u8; 8],
widths: [u8; 2],
name: &'static str,
bytes: &[u8],
) -> Result<D, SparseErrors>
where
D: DeserializeOwned,
{
if bytes.len() < 8 || &bytes[..8] != magic {
return Err(SparseErrors::NotSerialised(name));
}
if bytes.len() < HEADER_LEN {
return Err(SparseErrors::SerialisationFailed(format!(
"{name}: the header is truncated"
)));
}
let version = u32::from_le_bytes(bytes[8..12].try_into().expect("slice of four bytes"));
if version != SERIALISE_FORMAT_VERSION {
return Err(SparseErrors::SerialisedVersionMismatch {
found: version,
supported: SERIALISE_FORMAT_VERSION,
});
}
let found = [bytes[12], bytes[13]];
if found != widths {
return Err(SparseErrors::SerialisedWidthMismatch {
expected: widths,
found,
});
}
let (value, read) = bincode::serde::decode_from_slice(&bytes[HEADER_LEN..], CONFIG)
.map_err(|e| SparseErrors::SerialisationFailed(format!("{name}: {e}")))?;
if HEADER_LEN + read != bytes.len() {
return Err(SparseErrors::SerialisationFailed(format!(
"{name}: {} trailing bytes after the payload",
bytes.len() - HEADER_LEN - read
)));
}
Ok(value)
}
#[inline]
pub fn elem_width<T>() -> u8 {
std::mem::size_of::<T>() as u8
}
impl<T, U> CompressedSparseData2<T, U>
where
T: BixverseNumeric + Serialize + DeserializeOwned,
U: BixverseNumeric + Serialize + DeserializeOwned,
{
pub fn to_bytes(&self) -> Result<Vec<u8>, SparseErrors> {
encode_with_header(CSR_MAGIC, [elem_width::<T>(), elem_width::<U>()], self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SparseErrors> {
let csr: Self = decode_with_header(
CSR_MAGIC,
[elem_width::<T>(), elem_width::<U>()],
"CompressedSparseData2",
bytes,
)?;
csr.validate()?;
Ok(csr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::CompressedSparseFormat;
fn small_csr() -> CompressedSparseData2<f32, f64> {
CompressedSparseData2 {
data: vec![1.0, 2.0, 3.0, 4.0],
indices: vec![0, 3, 1, 2],
indptr: vec![0, 2, 2, 4],
cs_type: CompressedSparseFormat::Csr,
data_2: Some(vec![10.0, 20.0, 30.0, 40.0]),
shape: (3, 4),
}
}
#[test]
fn test_csr_bytes_round_trip() {
let csr = small_csr();
let back = CompressedSparseData2::<f32, f64>::from_bytes(&csr.to_bytes().unwrap()).unwrap();
assert_eq!(back.data, csr.data);
assert_eq!(back.indices, csr.indices);
assert_eq!(back.indptr, csr.indptr);
assert_eq!(back.data_2, csr.data_2);
assert_eq!(back.shape, csr.shape);
assert!(back.cs_type.is_csr());
}
#[test]
fn test_csr_bytes_bad_magic() {
let mut bytes = small_csr().to_bytes().unwrap();
bytes[0] = b'X';
let res = CompressedSparseData2::<f32, f64>::from_bytes(&bytes);
assert!(matches!(res, Err(SparseErrors::NotSerialised(_))));
let res = CompressedSparseData2::<f32, f64>::from_bytes(&[1, 2, 3]);
assert!(matches!(res, Err(SparseErrors::NotSerialised(_))));
}
#[test]
fn test_csr_bytes_version_mismatch() {
let mut bytes = small_csr().to_bytes().unwrap();
bytes[8..12].copy_from_slice(&(SERIALISE_FORMAT_VERSION + 1).to_le_bytes());
let res = CompressedSparseData2::<f32, f64>::from_bytes(&bytes);
assert!(matches!(
res,
Err(SparseErrors::SerialisedVersionMismatch { found, supported })
if found == SERIALISE_FORMAT_VERSION + 1 && supported == SERIALISE_FORMAT_VERSION
));
}
#[test]
fn test_csr_bytes_width_mismatch() {
let bytes = small_csr().to_bytes().unwrap();
let res = CompressedSparseData2::<f64, f64>::from_bytes(&bytes);
assert!(matches!(
res,
Err(SparseErrors::SerialisedWidthMismatch { .. })
));
}
#[test]
fn test_csr_bytes_truncated_and_trailing() {
let bytes = small_csr().to_bytes().unwrap();
let res = CompressedSparseData2::<f32, f64>::from_bytes(&bytes[..bytes.len() - 3]);
assert!(matches!(res, Err(SparseErrors::SerialisationFailed(_))));
let mut long = bytes.clone();
long.push(0);
let res = CompressedSparseData2::<f32, f64>::from_bytes(&long);
assert!(matches!(res, Err(SparseErrors::SerialisationFailed(_))));
}
#[test]
fn test_csr_bytes_rejects_invalid_structure() {
let mut csr = small_csr();
csr.indices[1] = 9;
let res = CompressedSparseData2::<f32, f64>::from_bytes(&csr.to_bytes().unwrap());
assert!(matches!(
res,
Err(SparseErrors::SliceIndexOutOfBounds { .. })
));
}
}