use std::io::Write;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
const MAGIC: &[u8; 4] = b"ECS0";
pub trait Codec {
fn name(&self) -> &str;
fn declared_lossless(&self) -> bool;
fn encode(&self, signal: &[Vec<i64>], fs: f64) -> Vec<u8>;
fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>>;
}
pub fn serialize(signal: &[Vec<i64>]) -> Vec<u8> {
let total_samples: usize = signal.iter().map(|c| c.len()).sum();
let mut out = Vec::with_capacity(4 + 4 + signal.len() * 4 + total_samples * 8);
out.extend_from_slice(MAGIC);
out.extend_from_slice(&(signal.len() as u32).to_le_bytes());
for chan in signal {
out.extend_from_slice(&(chan.len() as u32).to_le_bytes());
for &s in chan {
out.extend_from_slice(&s.to_le_bytes());
}
}
out
}
pub fn deserialize(buf: &[u8]) -> Vec<Vec<i64>> {
if buf.len() < 8 || &buf[..4] != MAGIC {
return Vec::new();
}
let n_chan = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
let mut pos = 8usize;
let mut out = Vec::with_capacity(n_chan);
for _ in 0..n_chan {
if pos + 4 > buf.len() {
return Vec::new();
}
let len = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
pos += 4;
let need = len.checked_mul(8);
match need {
Some(bytes) if pos + bytes <= buf.len() => {}
_ => return Vec::new(),
}
let mut chan = Vec::with_capacity(len);
for _ in 0..len {
let mut b = [0u8; 8];
b.copy_from_slice(&buf[pos..pos + 8]);
chan.push(i64::from_le_bytes(b));
pos += 8;
}
out.push(chan);
}
out
}
pub fn gzip_compress(data: &[u8]) -> Vec<u8> {
let mut enc = GzEncoder::new(Vec::new(), Compression::default());
enc.write_all(data).expect("gzip write to Vec is infallible");
enc.finish().expect("gzip finish on Vec is infallible")
}
pub fn gzip_decompress(data: &[u8]) -> Vec<u8> {
use std::io::Read;
let mut dec = GzDecoder::new(data);
let mut out = Vec::new();
match dec.read_to_end(&mut out) {
Ok(_) => out,
Err(_) => Vec::new(),
}
}
#[cfg(feature = "zstd")]
pub fn zstd_compress(data: &[u8]) -> Vec<u8> {
zstd::stream::encode_all(data, 19).expect("zstd encode of in-memory buffer")
}
#[cfg(feature = "zstd")]
pub fn zstd_decompress(data: &[u8]) -> Vec<u8> {
zstd::stream::decode_all(data).unwrap_or_default()
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Store;
impl Codec for Store {
fn name(&self) -> &str {
"store"
}
fn declared_lossless(&self) -> bool {
true
}
fn encode(&self, signal: &[Vec<i64>], _fs: f64) -> Vec<u8> {
serialize(signal)
}
fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>> {
deserialize(blob)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Gzip;
impl Codec for Gzip {
fn name(&self) -> &str {
"gzip"
}
fn declared_lossless(&self) -> bool {
true
}
fn encode(&self, signal: &[Vec<i64>], _fs: f64) -> Vec<u8> {
gzip_compress(&serialize(signal))
}
fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>> {
deserialize(&gzip_decompress(blob))
}
}
#[cfg(feature = "zstd")]
#[derive(Clone, Copy, Debug, Default)]
pub struct Zstd;
#[cfg(feature = "zstd")]
impl Codec for Zstd {
fn name(&self) -> &str {
"zstd"
}
fn declared_lossless(&self) -> bool {
true
}
fn encode(&self, signal: &[Vec<i64>], _fs: f64) -> Vec<u8> {
zstd_compress(&serialize(signal))
}
fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>> {
deserialize(&zstd_decompress(blob))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> Vec<Vec<i64>> {
vec![
vec![0, 1, -1, 1000, -1000, i64::MAX, i64::MIN],
vec![-42, 42, 0, 7],
vec![], vec![123_456_789_012, -987_654_321_098],
]
}
fn assert_roundtrip<C: Codec>(codec: C) {
let signal = fixture();
let blob = codec.encode(&signal, 256.0);
let back = codec.decode(&blob);
assert_eq!(
back,
signal,
"{} failed bit-exact round trip",
codec.name()
);
assert!(
codec.declared_lossless(),
"{} reference adapter should declare lossless",
codec.name()
);
}
#[test]
fn serialize_deserialize_is_exact() {
let signal = fixture();
let bytes = serialize(&signal);
assert_eq!(deserialize(&bytes), signal);
}
#[test]
fn serialize_is_deterministic() {
let signal = fixture();
assert_eq!(serialize(&signal), serialize(&signal));
}
#[test]
fn deserialize_rejects_bad_magic() {
assert!(deserialize(b"XXXX\x00\x00\x00\x00").is_empty());
assert!(deserialize(&[]).is_empty());
let mut buf = MAGIC.to_vec();
buf.extend_from_slice(&1u32.to_le_bytes());
assert!(deserialize(&buf).is_empty());
}
#[test]
fn store_roundtrips() {
assert_roundtrip(Store);
let signal = fixture();
assert_eq!(Store.encode(&signal, 256.0), serialize(&signal));
}
#[test]
fn gzip_roundtrips() {
assert_roundtrip(Gzip);
}
#[test]
fn gzip_decompress_rejects_garbage() {
assert!(gzip_decompress(b"not gzip data").is_empty());
assert!(Gzip.decode(b"not gzip data").is_empty());
}
#[cfg(feature = "zstd")]
#[test]
fn zstd_roundtrips() {
assert_roundtrip(Zstd);
}
#[test]
fn empty_signal_roundtrips() {
let empty: Vec<Vec<i64>> = Vec::new();
assert_eq!(Store.decode(&Store.encode(&empty, 256.0)), empty);
assert_eq!(Gzip.decode(&Gzip.encode(&empty, 256.0)), empty);
}
}