Skip to main content

nodedb_codec/
rans.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Interleaved rANS (Asymmetric Numeral Systems) entropy coder.
4//!
5//! Compresses byte streams to the Shannon entropy limit — optimal
6//! compression ratio at Huffman-like speed. Used as the terminal
7//! compressor for cold/S3 tier partitions where ratio matters more
8//! than decompression speed.
9//!
10//! 4-stream interleaving breaks the sequential dependency chain:
11//! the CPU processes all streams simultaneously, achieving high
12//! throughput despite the inherently sequential nature of ANS.
13//!
14//! Wire format:
15//! ```text
16//! [4 bytes] uncompressed size (LE u32)
17//! [256 × 4 bytes] frequency table (LE u32 per byte value)
18//! [4 bytes] compressed size (LE u32)
19//! [N bytes] interleaved rANS bitstream (4 streams)
20//! ```
21
22use crate::bounds::{
23    checked_add, checked_capacity, checked_range, decoded_len, encode_input_len, encode_u32_len,
24    u32_to_usize,
25};
26use crate::error::CodecError;
27
28/// Number of interleaved streams.
29const NUM_STREAMS: usize = 4;
30
31/// rANS probability scale (power of 2 for fast division).
32const PROB_BITS: u32 = 14;
33const PROB_SCALE: u32 = 1 << PROB_BITS;
34
35/// rANS state lower bound.
36const RANS_L: u32 = 1 << 23;
37const RANS_STATE_UPPER_BOUND: u32 = RANS_L << 8;
38
39/// Frequency table header size: 256 × 4 bytes = 1024 bytes.
40const FREQ_TABLE_SIZE: usize = 256 * 4;
41
42/// Full header: 4 (uncomp size) + 1024 (freq table) + 4 (comp size).
43const HEADER_SIZE: usize = 4 + FREQ_TABLE_SIZE + 4;
44
45// ---------------------------------------------------------------------------
46// Public API
47// ---------------------------------------------------------------------------
48
49/// Compress bytes using interleaved rANS.
50pub fn encode(data: &[u8]) -> Result<Vec<u8>, CodecError> {
51    let encoded_len = encode_input_len(data.len(), "rANS")?;
52    if data.is_empty() {
53        let out = vec![0u8; HEADER_SIZE];
54        // uncompressed_size = 0, freq table = all zeros, compressed_size = 0
55        return Ok(out);
56    }
57
58    // Build frequency table.
59    let mut freqs = [0u32; 256];
60    for &b in data {
61        freqs[b as usize] += 1;
62    }
63
64    // Normalize frequencies to sum to PROB_SCALE.
65    let norm_freqs = normalize_frequencies(&freqs, data.len());
66
67    // Build cumulative frequency table.
68    let (cum_freqs, sym_freqs) = build_cum_table(&norm_freqs);
69
70    // Encode using 4 interleaved streams.
71    // Each stream processes every 4th byte: stream 0 gets bytes 0,4,8,...
72    let mut streams: [Vec<u8>; NUM_STREAMS] = std::array::from_fn(|_| Vec::new());
73    let mut states = [RANS_L; NUM_STREAMS];
74
75    // Encode in REVERSE order (rANS encodes backward, decodes forward).
76    for i in (0..data.len()).rev() {
77        let stream_idx = i % NUM_STREAMS;
78        let sym = data[i] as usize;
79        let freq = sym_freqs[sym];
80        let start = cum_freqs[sym];
81
82        if freq == 0 {
83            continue; // Symbol with zero frequency — shouldn't happen after normalization.
84        }
85
86        rans_encode_symbol(
87            &mut states[stream_idx],
88            &mut streams[stream_idx],
89            start,
90            freq,
91        );
92    }
93
94    // Flush final states.
95    for i in 0..NUM_STREAMS {
96        let s = states[i];
97        streams[i].push((s & 0xFF) as u8);
98        streams[i].push(((s >> 8) & 0xFF) as u8);
99        streams[i].push(((s >> 16) & 0xFF) as u8);
100        streams[i].push(((s >> 24) & 0xFF) as u8);
101    }
102
103    // Build output.
104    let total_compressed: usize = streams.iter().map(|s| s.len()).sum();
105    let mut out = Vec::with_capacity(HEADER_SIZE + total_compressed + NUM_STREAMS * 4);
106
107    // Header: uncompressed size.
108    out.extend_from_slice(&encoded_len.to_le_bytes());
109
110    // Frequency table (raw counts for decoding).
111    for &f in &norm_freqs {
112        out.extend_from_slice(&f.to_le_bytes());
113    }
114
115    // Compressed size.
116    let comp_payload_size = total_compressed + NUM_STREAMS * 4; // streams + per-stream sizes
117    out.extend_from_slice(
118        &encode_u32_len(comp_payload_size, "rANS compressed payload")?.to_le_bytes(),
119    );
120
121    // Per-stream sizes (4 bytes each).
122    for s in &streams {
123        out.extend_from_slice(&encode_u32_len(s.len(), "rANS stream")?.to_le_bytes());
124    }
125
126    // Stream data (reversed — rANS bitstream is read backward).
127    for s in &streams {
128        out.extend_from_slice(s);
129    }
130
131    Ok(out)
132}
133
134/// Decompress interleaved rANS data.
135pub fn decode(data: &[u8]) -> Result<Vec<u8>, CodecError> {
136    let header = checked_range(data, 0, HEADER_SIZE, "rANS header")?;
137    let uncompressed_size = decoded_len(
138        u32_to_usize(
139            u32::from_le_bytes(header[0..4].try_into().map_err(|_| CodecError::Corrupt {
140                detail: "rANS output size".into(),
141            })?),
142            "rANS output",
143        )?,
144        "rANS",
145    )?;
146    let mut norm_freqs = [0u32; 256];
147    for (index, frequency) in norm_freqs.iter_mut().enumerate() {
148        let offset = 4 + index * 4;
149        *frequency = u32::from_le_bytes(header[offset..offset + 4].try_into().map_err(|_| {
150            CodecError::Corrupt {
151                detail: "rANS frequency".into(),
152            }
153        })?);
154    }
155    let compressed_size = u32_to_usize(
156        u32::from_le_bytes(header[HEADER_SIZE - 4..].try_into().map_err(|_| {
157            CodecError::Corrupt {
158                detail: "rANS compressed size".into(),
159            }
160        })?),
161        "rANS compressed size",
162    )?;
163    if uncompressed_size == 0 {
164        if compressed_size != 0 || data.len() != HEADER_SIZE {
165            return Err(CodecError::Corrupt {
166                detail: "nonempty rANS payload for empty output".into(),
167            });
168        }
169        return Ok(Vec::new());
170    }
171    validate_frequencies(&norm_freqs)?;
172    let stream_sizes_len = NUM_STREAMS * 4;
173    let frame = checked_range(
174        data,
175        HEADER_SIZE,
176        compressed_size,
177        "rANS compressed payload",
178    )?;
179    if checked_add(HEADER_SIZE, compressed_size, "rANS frame end")? != data.len() {
180        return Err(CodecError::Corrupt {
181            detail: "trailing bytes after rANS frame".into(),
182        });
183    }
184    if compressed_size < stream_sizes_len {
185        return Err(CodecError::Corrupt {
186            detail: "rANS compressed size omits stream sizes".into(),
187        });
188    }
189    let mut stream_sizes = [0usize; NUM_STREAMS];
190    let mut payload_len = 0usize;
191    for (index, size) in stream_sizes.iter_mut().enumerate() {
192        let start = index * 4;
193        *size = u32_to_usize(
194            u32::from_le_bytes(frame[start..start + 4].try_into().map_err(|_| {
195                CodecError::Corrupt {
196                    detail: "rANS stream size".into(),
197                }
198            })?),
199            "rANS stream size",
200        )?;
201        if *size < 4 {
202            return Err(CodecError::Corrupt {
203                detail: format!("rANS stream {index} too short for state"),
204            });
205        }
206        payload_len = checked_add(payload_len, *size, "rANS stream lengths")?;
207    }
208    if checked_add(stream_sizes_len, payload_len, "rANS stream framing")? != compressed_size {
209        return Err(CodecError::Corrupt {
210            detail: "rANS compressed-size metadata mismatch".into(),
211        });
212    }
213    let (cum_freqs, sym_freqs) = build_cum_table(&norm_freqs);
214    let lookup = build_decode_table(&cum_freqs, &sym_freqs);
215    let mut streams: [&[u8]; NUM_STREAMS] = [&[]; NUM_STREAMS];
216    let mut pos = stream_sizes_len;
217    for (index, stream) in streams.iter_mut().enumerate() {
218        *stream = checked_range(frame, pos, stream_sizes[index], "rANS stream range")?;
219        pos = checked_add(pos, stream_sizes[index], "rANS stream offset")?;
220    }
221    let mut states = [0u32; NUM_STREAMS];
222    let mut stream_pos = [0usize; NUM_STREAMS];
223    for index in 0..NUM_STREAMS {
224        let stream = streams[index];
225        let end = stream.len();
226        states[index] = u32::from_le_bytes(stream[end - 4..end].try_into().map_err(|_| {
227            CodecError::Corrupt {
228                detail: "rANS state".into(),
229            }
230        })?);
231        if !(RANS_L..RANS_STATE_UPPER_BOUND).contains(&states[index]) {
232            return Err(CodecError::Corrupt {
233                detail: format!("rANS stream {index} has invalid initial state"),
234            });
235        }
236        stream_pos[index] = end - 4;
237    }
238    let output_capacity = checked_capacity(uncompressed_size, 1, "rANS decoded bytes")?;
239    let mut output = vec![0u8; output_capacity];
240    for (index, out_byte) in output.iter_mut().enumerate() {
241        let stream_index = index % NUM_STREAMS;
242        let (symbol, state) =
243            rans_decode_symbol(states[stream_index], &lookup, &cum_freqs, &sym_freqs);
244        *out_byte = symbol;
245        states[stream_index] =
246            rans_decode_renorm(state, streams[stream_index], &mut stream_pos[stream_index]);
247    }
248    for index in 0..NUM_STREAMS {
249        if stream_pos[index] != 0 || states[index] != RANS_L {
250            return Err(CodecError::Corrupt {
251                detail: format!("rANS stream {index} has noncanonical terminal state"),
252            });
253        }
254    }
255    Ok(output)
256}
257
258// ---------------------------------------------------------------------------
259// rANS core operations
260// ---------------------------------------------------------------------------
261
262fn rans_encode_symbol(state: &mut u32, bitstream: &mut Vec<u8>, start: u32, freq: u32) {
263    // Renormalize: output bytes until state is in the correct range.
264    let max_state = ((RANS_L >> PROB_BITS) << 8) * freq;
265    while *state >= max_state {
266        bitstream.push((*state & 0xFF) as u8);
267        *state >>= 8;
268    }
269
270    // Encode symbol.
271    *state = ((*state / freq) << PROB_BITS) + (*state % freq) + start;
272}
273
274fn rans_decode_symbol(
275    state: u32,
276    lookup: &[u8; PROB_SCALE as usize],
277    cum_freqs: &[u32; 257],
278    sym_freqs: &[u32; 256],
279) -> (u8, u32) {
280    let slot = state & (PROB_SCALE - 1);
281    let sym = lookup[slot as usize];
282    let start = cum_freqs[sym as usize];
283    let freq = sym_freqs[sym as usize];
284
285    let new_state = freq * (state >> PROB_BITS) + slot - start;
286    (sym, new_state)
287}
288
289fn rans_decode_renorm(mut state: u32, stream: &[u8], pos: &mut usize) -> u32 {
290    while state < RANS_L && *pos > 0 {
291        *pos -= 1;
292        state = (state << 8) | stream[*pos] as u32;
293    }
294    state
295}
296
297// ---------------------------------------------------------------------------
298// Frequency table operations
299// ---------------------------------------------------------------------------
300
301fn validate_frequencies(freqs: &[u32; 256]) -> Result<(), CodecError> {
302    let mut total = 0u32;
303    for &frequency in freqs {
304        total = total
305            .checked_add(frequency)
306            .ok_or_else(|| CodecError::Corrupt {
307                detail: "rANS frequency total overflows".into(),
308            })?;
309    }
310    if total != PROB_SCALE {
311        return Err(CodecError::Corrupt {
312            detail: format!("rANS frequencies sum to {total}, expected {PROB_SCALE}"),
313        });
314    }
315    Ok(())
316}
317
318/// Normalize raw frequencies to sum to PROB_SCALE.
319fn normalize_frequencies(freqs: &[u32; 256], total: usize) -> [u32; 256] {
320    let mut norm = [0u32; 256];
321    let mut sum = 0u32;
322    let total_f64 = total as f64;
323
324    // First pass: proportional scaling.
325    for i in 0..256 {
326        if freqs[i] > 0 {
327            // Ensure every present symbol gets at least frequency 1.
328            norm[i] = ((freqs[i] as f64 / total_f64 * PROB_SCALE as f64).round() as u32).max(1);
329            sum += norm[i];
330        }
331    }
332
333    // Adjust to make sum exactly PROB_SCALE.
334    if sum > 0 {
335        while sum > PROB_SCALE {
336            // Find the symbol with the highest frequency and reduce it.
337            let max_idx = norm
338                .iter()
339                .enumerate()
340                .filter(|(_, f)| **f > 1)
341                .max_by_key(|(_, f)| **f)
342                .map(|(i, _)| i)
343                .unwrap_or(0);
344            norm[max_idx] -= 1;
345            sum -= 1;
346        }
347        while sum < PROB_SCALE {
348            let max_idx = norm
349                .iter()
350                .enumerate()
351                .max_by_key(|(_, f)| **f)
352                .map(|(i, _)| i)
353                .unwrap_or(0);
354            norm[max_idx] += 1;
355            sum += 1;
356        }
357    }
358
359    norm
360}
361
362/// Build cumulative frequency table.
363fn build_cum_table(freqs: &[u32; 256]) -> ([u32; 257], [u32; 256]) {
364    let mut cum = [0u32; 257];
365    let sym_freqs = *freqs;
366    for i in 0..256 {
367        cum[i + 1] = cum[i] + freqs[i];
368    }
369    (cum, sym_freqs)
370}
371
372/// Build decode lookup table: for each slot in [0, PROB_SCALE), which symbol?
373fn build_decode_table(
374    cum_freqs: &[u32; 257],
375    _sym_freqs: &[u32; 256],
376) -> [u8; PROB_SCALE as usize] {
377    let mut table = [0u8; PROB_SCALE as usize];
378    for sym in 0..256u16 {
379        let start = cum_freqs[sym as usize] as usize;
380        let end = cum_freqs[sym as usize + 1] as usize;
381        for entry in table[start..end].iter_mut() {
382            *entry = sym as u8;
383        }
384    }
385    table
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn empty_roundtrip() {
394        let encoded = encode(&[]).expect("encode");
395        let decoded = decode(&encoded).unwrap();
396        assert!(decoded.is_empty());
397    }
398
399    #[test]
400    fn single_byte() {
401        let encoded = encode(&[42]).expect("encode");
402        let decoded = decode(&encoded).unwrap();
403        assert_eq!(decoded, vec![42]);
404    }
405
406    #[test]
407    fn repeated_bytes() {
408        let data = vec![0u8; 10_000];
409        let encoded = encode(&data).expect("encode");
410        let decoded = decode(&encoded).unwrap();
411        assert_eq!(decoded, data);
412
413        // Highly repetitive → near-zero entropy → excellent compression.
414        let ratio = data.len() as f64 / encoded.len() as f64;
415        assert!(
416            ratio > 2.0,
417            "repeated bytes should compress >2x, got {ratio:.1}x"
418        );
419    }
420
421    #[test]
422    fn text_data() {
423        let text = b"the quick brown fox jumps over the lazy dog. ";
424        let data: Vec<u8> = text.iter().copied().cycle().take(10_000).collect();
425        let encoded = encode(&data).expect("encode");
426        let decoded = decode(&encoded).unwrap();
427        assert_eq!(decoded, data);
428
429        let ratio = data.len() as f64 / encoded.len() as f64;
430        assert!(ratio > 1.5, "text should compress >1.5x, got {ratio:.1}x");
431    }
432
433    #[test]
434    fn uniform_random_data() {
435        // Uniform random → ~8 bits/byte → no compression possible.
436        let mut data = vec![0u8; 5000];
437        let mut rng: u64 = 12345;
438        for byte in &mut data {
439            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
440            *byte = (rng >> 33) as u8;
441        }
442        let encoded = encode(&data).expect("encode");
443        let decoded = decode(&encoded).unwrap();
444        assert_eq!(decoded, data);
445    }
446
447    #[test]
448    fn all_byte_values() {
449        // All 256 byte values present.
450        let data: Vec<u8> = (0..=255u8).cycle().take(4096).collect();
451        let encoded = encode(&data).expect("encode");
452        let decoded = decode(&encoded).unwrap();
453        assert_eq!(decoded, data);
454    }
455
456    #[test]
457    fn skewed_distribution() {
458        // 90% zeros, 10% ones — should compress well.
459        let mut data = vec![0u8; 10_000];
460        for i in 0..1000 {
461            data[i * 10] = 1;
462        }
463        let encoded = encode(&data).expect("encode");
464        let decoded = decode(&encoded).unwrap();
465        assert_eq!(decoded, data);
466
467        let ratio = data.len() as f64 / encoded.len() as f64;
468        assert!(
469            ratio > 1.5,
470            "skewed data should compress >1.5x, got {ratio:.1}x"
471        );
472    }
473
474    #[test]
475    fn better_than_raw_on_structured() {
476        // Structured data after type-aware preprocessing (typical pipeline output).
477        let mut data = Vec::with_capacity(10_000);
478        for i in 0..10_000 {
479            data.push((i % 16) as u8); // Low entropy, 4 bits/byte → 2x compression.
480        }
481        let encoded = encode(&data).expect("encode");
482        let decoded = decode(&encoded).unwrap();
483        assert_eq!(decoded, data);
484
485        let ratio = data.len() as f64 / encoded.len() as f64;
486        assert!(
487            ratio > 1.5,
488            "low-entropy data should compress >1.5x, got {ratio:.1}x"
489        );
490    }
491
492    #[test]
493    fn truncated_input_errors() {
494        assert!(decode(&[]).is_err());
495        assert!(decode(&[1, 0, 0, 0]).is_err()); // too short for freq table
496    }
497
498    #[test]
499    fn hostile_lengths_and_frequencies_fail_before_output_allocation() {
500        let mut frame = vec![0u8; HEADER_SIZE];
501        frame[..4].copy_from_slice(&u32::MAX.to_le_bytes());
502        assert!(matches!(
503            decode(&frame),
504            Err(CodecError::ResourceLimit { .. })
505        ));
506
507        let encoded = encode(b"bounded rans").expect("encode");
508        let mut malformed = encoded.clone();
509        malformed[4..8].copy_from_slice(&PROB_SCALE.wrapping_add(1).to_le_bytes());
510        assert!(matches!(
511            decode(&malformed),
512            Err(CodecError::Corrupt { .. })
513        ));
514        let mut bad_size = encoded;
515        bad_size[HEADER_SIZE - 4..HEADER_SIZE].copy_from_slice(&u32::MAX.to_le_bytes());
516        assert!(decode(&bad_size).is_err());
517    }
518
519    #[test]
520    fn noncanonical_stream_padding_and_high_state_are_rejected() {
521        let encoded = encode(&[42]).expect("encode");
522        let size_start = HEADER_SIZE;
523        let stream_zero_size = u32::from_le_bytes([
524            encoded[size_start],
525            encoded[size_start + 1],
526            encoded[size_start + 2],
527            encoded[size_start + 3],
528        ]);
529        let stream_zero_start = HEADER_SIZE + NUM_STREAMS * 4;
530        let state_start = stream_zero_start + stream_zero_size as usize - 4;
531
532        let mut padded = encoded.clone();
533        padded.insert(state_start, 0xaa);
534        padded[size_start..size_start + 4].copy_from_slice(&(stream_zero_size + 1).to_le_bytes());
535        let compressed_size = u32::from_le_bytes([
536            padded[HEADER_SIZE - 4],
537            padded[HEADER_SIZE - 3],
538            padded[HEADER_SIZE - 2],
539            padded[HEADER_SIZE - 1],
540        ]);
541        padded[HEADER_SIZE - 4..HEADER_SIZE].copy_from_slice(&(compressed_size + 1).to_le_bytes());
542        assert!(matches!(decode(&padded), Err(CodecError::Corrupt { .. })));
543
544        let mut high_state = encoded;
545        high_state[state_start..state_start + 4].copy_from_slice(&u32::MAX.to_le_bytes());
546        assert!(matches!(
547            decode(&high_state),
548            Err(CodecError::Corrupt { .. })
549        ));
550    }
551}