Skip to main content

open_eeg_codec_standard/
adapter.rs

1//! Reference codec adapters.
2//!
3//! An adapter wraps a codec behind the uniform [`Codec`] byte interface
4//! so the harness can grade any codec the same way. Three reference
5//! adapters ship so the suite has something to run out of the box:
6//!
7//! - [`Store`] — identity/raw passthrough. The CR≈1.0 lossless baseline.
8//! - [`Gzip`]  — pure-Rust miniz_oxide via `flate2` (no system dep), the
9//!   always-available compressed lossless reference.
10//! - [`Zstd`]  — optional, behind `#[cfg(feature = "zstd")]`, so the
11//!   default CI build carries no system dependency.
12//!
13//! ## Codec interface
14//!
15//! A [`Codec`] takes a per-channel signal (`&[Vec<i64>]`, one `Vec<i64>`
16//! of samples per channel) plus the sample rate, and produces an opaque
17//! `Vec<u8>` blob; [`Codec::decode`] inverts that. The reference adapters
18//! are all *declared lossless* and round-trip the channel layout
19//! bit-exactly.
20//!
21//! ## Reference serialization
22//!
23//! The reference adapters share one tiny, explicit, byte-exact container
24//! (see [`serialize`] / [`deserialize`]): a fixed header followed by the
25//! samples. Layout (all integers little-endian):
26//!
27//! ```text
28//! magic   : 4 bytes  = b"ECS0"
29//! n_chan  : u32       = number of channels
30//! per chan: u32 len, then `len` × i64 samples
31//! ```
32//!
33//! The sample rate `fs` is *not* part of the byte stream: it is metadata
34//! the harness already knows, and the reference codecs reconstruct the
35//! integer samples losslessly without it. A real codec is free to embed
36//! `fs` in its own blob; the trait simply makes it available.
37
38use std::io::Write;
39
40use flate2::read::GzDecoder;
41use flate2::write::GzEncoder;
42use flate2::Compression;
43
44/// Magic prefix for the reference serialization container.
45const MAGIC: &[u8; 4] = b"ECS0";
46
47/// The codec-agnostic interface every benchmarked codec implements.
48///
49/// `signal` is one `Vec<i64>` of samples per channel; `fs` is the sample
50/// rate in Hz (metadata the codec may use or ignore). [`encode`] returns
51/// an opaque blob and [`decode`] reconstructs the per-channel samples.
52///
53/// A codec that advertises [`declared_lossless`] `== true` is *claiming*
54/// bit-exact reconstruction; the harness verifies that claim against the
55/// L-tier gate. The claim is what is graded — it is not assumed true.
56///
57/// [`encode`]: Codec::encode
58/// [`decode`]: Codec::decode
59/// [`declared_lossless`]: Codec::declared_lossless
60pub trait Codec {
61    /// Short, stable identifier used in reports (e.g. `"store"`).
62    fn name(&self) -> &str;
63
64    /// Whether the codec *claims* bit-exact reconstruction.
65    fn declared_lossless(&self) -> bool;
66
67    /// Compress a per-channel integer signal into an opaque blob.
68    fn encode(&self, signal: &[Vec<i64>], fs: f64) -> Vec<u8>;
69
70    /// Reconstruct the per-channel integer signal from a blob.
71    fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>>;
72}
73
74/// Serialize a per-channel integer signal to the reference container.
75///
76/// Deterministic and byte-exact: the same input always produces the same
77/// bytes, so two backends can be compared for byte-equality. Integers are
78/// little-endian. See the module docs for the layout.
79pub fn serialize(signal: &[Vec<i64>]) -> Vec<u8> {
80    // Pre-size: magic + n_chan, then per channel a u32 length + the samples.
81    let total_samples: usize = signal.iter().map(|c| c.len()).sum();
82    let mut out = Vec::with_capacity(4 + 4 + signal.len() * 4 + total_samples * 8);
83    out.extend_from_slice(MAGIC);
84    out.extend_from_slice(&(signal.len() as u32).to_le_bytes());
85    for chan in signal {
86        out.extend_from_slice(&(chan.len() as u32).to_le_bytes());
87        for &s in chan {
88            out.extend_from_slice(&s.to_le_bytes());
89        }
90    }
91    out
92}
93
94/// Inverse of [`serialize`].
95///
96/// Returns an empty `Vec` if the buffer is malformed (bad magic, or a
97/// truncated header / sample run). The reference adapters only ever feed
98/// this their own [`serialize`] output, so a malformed buffer means a
99/// corrupt blob rather than a recoverable state; the harness's L-tier
100/// gate then sees a length/value mismatch and fails the lossless claim,
101/// which is the correct outcome.
102pub fn deserialize(buf: &[u8]) -> Vec<Vec<i64>> {
103    // Header: 4-byte magic + 4-byte channel count.
104    if buf.len() < 8 || &buf[..4] != MAGIC {
105        return Vec::new();
106    }
107    let n_chan = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
108    let mut pos = 8usize;
109    let mut out = Vec::with_capacity(n_chan);
110    for _ in 0..n_chan {
111        // Per-channel sample count.
112        if pos + 4 > buf.len() {
113            return Vec::new();
114        }
115        let len = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
116        pos += 4;
117        // `len` × i64 samples must fit in the remaining buffer.
118        let need = len.checked_mul(8);
119        match need {
120            Some(bytes) if pos + bytes <= buf.len() => {}
121            _ => return Vec::new(),
122        }
123        let mut chan = Vec::with_capacity(len);
124        for _ in 0..len {
125            let mut b = [0u8; 8];
126            b.copy_from_slice(&buf[pos..pos + 8]);
127            chan.push(i64::from_le_bytes(b));
128            pos += 8;
129        }
130        out.push(chan);
131    }
132    out
133}
134
135/// Gzip-compress a byte buffer with the pure-Rust backend.
136///
137/// This is the [`Gzip`] adapter's compression primitive. It never fails
138/// for in-memory buffers (miniz_oxide writes to a `Vec`); the `expect`
139/// guards a genuinely unreachable I/O error on an in-memory writer.
140pub fn gzip_compress(data: &[u8]) -> Vec<u8> {
141    let mut enc = GzEncoder::new(Vec::new(), Compression::default());
142    enc.write_all(data).expect("gzip write to Vec is infallible");
143    enc.finish().expect("gzip finish on Vec is infallible")
144}
145
146/// Gunzip a byte buffer produced by [`gzip_compress`].
147///
148/// Returns an empty `Vec` if the input is not valid gzip; as with
149/// [`deserialize`] this surfaces a corrupt blob to the L-tier gate as a
150/// failed lossless claim rather than a panic.
151pub fn gzip_decompress(data: &[u8]) -> Vec<u8> {
152    use std::io::Read;
153    let mut dec = GzDecoder::new(data);
154    let mut out = Vec::new();
155    match dec.read_to_end(&mut out) {
156        Ok(_) => out,
157        Err(_) => Vec::new(),
158    }
159}
160
161#[cfg(feature = "zstd")]
162/// Zstd-compress a byte buffer (level 19). Only present under the
163/// `zstd` feature so the default CI build carries no system dependency.
164pub fn zstd_compress(data: &[u8]) -> Vec<u8> {
165    zstd::stream::encode_all(data, 19).expect("zstd encode of in-memory buffer")
166}
167
168#[cfg(feature = "zstd")]
169/// Zstd-decompress a buffer produced by [`zstd_compress`]. Returns an
170/// empty `Vec` on malformed input (see [`gzip_decompress`]).
171pub fn zstd_decompress(data: &[u8]) -> Vec<u8> {
172    zstd::stream::decode_all(data).unwrap_or_default()
173}
174
175/// Identity/raw passthrough adapter — the CR≈1.0 lossless baseline.
176///
177/// `encode` is just [`serialize`]; `decode` is just [`deserialize`]. No
178/// compression happens, so this is the reference point against which
179/// every other codec's compression ratio is measured.
180#[derive(Clone, Copy, Debug, Default)]
181pub struct Store;
182
183impl Codec for Store {
184    fn name(&self) -> &str {
185        "store"
186    }
187
188    fn declared_lossless(&self) -> bool {
189        true
190    }
191
192    fn encode(&self, signal: &[Vec<i64>], _fs: f64) -> Vec<u8> {
193        serialize(signal)
194    }
195
196    fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>> {
197        deserialize(blob)
198    }
199}
200
201/// Gzip adapter — pure-Rust miniz_oxide via `flate2`, always available.
202///
203/// `encode` = [`serialize`] then [`gzip_compress`]; `decode` =
204/// [`gzip_decompress`] then [`deserialize`]. Bit-exact lossless.
205#[derive(Clone, Copy, Debug, Default)]
206pub struct Gzip;
207
208impl Codec for Gzip {
209    fn name(&self) -> &str {
210        "gzip"
211    }
212
213    fn declared_lossless(&self) -> bool {
214        true
215    }
216
217    fn encode(&self, signal: &[Vec<i64>], _fs: f64) -> Vec<u8> {
218        gzip_compress(&serialize(signal))
219    }
220
221    fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>> {
222        deserialize(&gzip_decompress(blob))
223    }
224}
225
226/// Zstd adapter — optional, compiled in only under the `zstd` feature.
227///
228/// `encode` = [`serialize`] then [`zstd_compress`]; `decode` =
229/// [`zstd_decompress`] then [`deserialize`]. Bit-exact lossless.
230#[cfg(feature = "zstd")]
231#[derive(Clone, Copy, Debug, Default)]
232pub struct Zstd;
233
234#[cfg(feature = "zstd")]
235impl Codec for Zstd {
236    fn name(&self) -> &str {
237        "zstd"
238    }
239
240    fn declared_lossless(&self) -> bool {
241        true
242    }
243
244    fn encode(&self, signal: &[Vec<i64>], _fs: f64) -> Vec<u8> {
245        zstd_compress(&serialize(signal))
246    }
247
248    fn decode(&self, blob: &[u8]) -> Vec<Vec<i64>> {
249        deserialize(&zstd_decompress(blob))
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    /// A known multi-channel i64 signal that exercises sign, zero, the
258    /// i64 extremes, a ragged channel length, and an empty channel.
259    fn fixture() -> Vec<Vec<i64>> {
260        vec![
261            vec![0, 1, -1, 1000, -1000, i64::MAX, i64::MIN],
262            vec![-42, 42, 0, 7],
263            vec![], // empty channel must survive the round trip
264            vec![123_456_789_012, -987_654_321_098],
265        ]
266    }
267
268    /// Round-trip helper: assert the codec reconstructs the fixture
269    /// exactly and (for the declared-lossless reference adapters) keeps
270    /// the lossless claim honest.
271    fn assert_roundtrip<C: Codec>(codec: C) {
272        let signal = fixture();
273        let blob = codec.encode(&signal, 256.0);
274        let back = codec.decode(&blob);
275        assert_eq!(
276            back,
277            signal,
278            "{} failed bit-exact round trip",
279            codec.name()
280        );
281        assert!(
282            codec.declared_lossless(),
283            "{} reference adapter should declare lossless",
284            codec.name()
285        );
286    }
287
288    #[test]
289    fn serialize_deserialize_is_exact() {
290        let signal = fixture();
291        let bytes = serialize(&signal);
292        assert_eq!(deserialize(&bytes), signal);
293    }
294
295    #[test]
296    fn serialize_is_deterministic() {
297        // Byte-for-byte stable across calls — the property the
298        // cross-backend byte-equality gate relies on.
299        let signal = fixture();
300        assert_eq!(serialize(&signal), serialize(&signal));
301    }
302
303    #[test]
304    fn deserialize_rejects_bad_magic() {
305        assert!(deserialize(b"XXXX\x00\x00\x00\x00").is_empty());
306        assert!(deserialize(&[]).is_empty());
307        // Truncated header (claims 1 channel, no length follows).
308        let mut buf = MAGIC.to_vec();
309        buf.extend_from_slice(&1u32.to_le_bytes());
310        assert!(deserialize(&buf).is_empty());
311    }
312
313    #[test]
314    fn store_roundtrips() {
315        assert_roundtrip(Store);
316        // Store does not compress: blob is exactly the serialization.
317        let signal = fixture();
318        assert_eq!(Store.encode(&signal, 256.0), serialize(&signal));
319    }
320
321    #[test]
322    fn gzip_roundtrips() {
323        assert_roundtrip(Gzip);
324    }
325
326    #[test]
327    fn gzip_decompress_rejects_garbage() {
328        // Not valid gzip => empty, then empty deserialize => empty signal.
329        assert!(gzip_decompress(b"not gzip data").is_empty());
330        assert!(Gzip.decode(b"not gzip data").is_empty());
331    }
332
333    #[cfg(feature = "zstd")]
334    #[test]
335    fn zstd_roundtrips() {
336        assert_roundtrip(Zstd);
337    }
338
339    #[test]
340    fn empty_signal_roundtrips() {
341        // Zero channels is a valid (degenerate) signal.
342        let empty: Vec<Vec<i64>> = Vec::new();
343        assert_eq!(Store.decode(&Store.encode(&empty, 256.0)), empty);
344        assert_eq!(Gzip.decode(&Gzip.encode(&empty, 256.0)), empty);
345    }
346}