Skip to main content

spacedb_store/
compress.rs

1//! Transparent per-row compression **inside** the AEAD boundary.
2//!
3//! The law is *compress, then encrypt*: ciphertext is incompressible, so the
4//! only place compression can work is on the plaintext, before [`crate::crypto::seal_row`].
5//! A prefixed collection therefore seals `format_byte ‖ payload`:
6//!
7//! - [`FORMAT_RAW`] (`0x00`) — payload is the encoded value, verbatim;
8//! - [`FORMAT_ZSTD`] (`0x01`) — payload is an RFC 8878 zstd frame of it;
9//! - `0x02` is **reserved** for dictionary frames (a dictionary is part of the
10//!   on-disk format and must be stored, versioned and re-wrapped like a key —
11//!   deliberately not shipped in the first cut).
12//!
13//! The format byte rides inside the sealed plaintext, so it is authenticated by
14//! the AEAD and invisible to whoever holds the ciphertext. Compression is
15//! attempted only above a floor ([`Compression::min_len`]) and kept only when
16//! it actually shrank the payload — incompressible rows store raw at a cost of
17//! one byte.
18//!
19//! **The length side-channel rule (normative, from the deploy plan W1.2):**
20//! compression leaks plaintext redundancy through ciphertext *length*. Never
21//! mix user-secret and attacker-influenced data in one compressed row; a
22//! collection whose rows do so must opt out with [`Compression::Off`].
23
24use crate::error::{StoreError, StoreResult};
25
26/// Format byte: the payload is the encoded value, verbatim.
27pub const FORMAT_RAW: u8 = 0x00;
28/// Format byte: the payload is a zstd frame of the encoded value.
29pub const FORMAT_ZSTD: u8 = 0x01;
30
31/// The default zstd level. Level 3 is the ratio/CPU knee for small structured
32/// rows; raise it only with a measurement.
33pub const DEFAULT_COMPRESSION_LEVEL: i32 = 3;
34
35/// The default floor below which compression is not attempted. Set from the
36/// floor sweep in `examples/seal_ab.rs` (2026-08-28, structured-row corpus,
37/// floor disabled): at ≤64 B compression never won (every row fell back raw);
38/// at 96 B marginal wins appeared (−0.0%); at 128 B real wins began (−5.6%),
39/// growing monotonically (−23.5% at 256 B, −59.4% at 512 B). The floor's only
40/// cost is a wasted compress attempt — the strictly-smaller check already
41/// protects size — so it sits where wins *start existing*, not where they get
42/// big. Re-derive with the sweep, don't hand-tune.
43pub const DEFAULT_MIN_LEN: usize = 96;
44
45/// Per-collection write-side compression policy. Reads always honor the
46/// per-row format byte regardless of policy, so flipping the policy never
47/// strands existing rows.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum Compression {
50    /// Compress rows of at least `min_len` encoded bytes at `level`, keeping
51    /// the result only when it is strictly smaller than storing raw.
52    On {
53        /// zstd level (see [`DEFAULT_COMPRESSION_LEVEL`]).
54        level: i32,
55        /// Encoded-value floor below which compression is not attempted.
56        min_len: usize,
57    },
58    /// Never compress. Required for collections whose rows mix user-secret and
59    /// attacker-influenced bytes (the length side-channel rule above).
60    Off,
61}
62
63impl Default for Compression {
64    fn default() -> Self {
65        Compression::On {
66            level: DEFAULT_COMPRESSION_LEVEL,
67            min_len: DEFAULT_MIN_LEN,
68        }
69    }
70}
71
72/// Wrap an encoded value in the prefixed format, compressing when the policy
73/// says to and it pays. Infallible by design: a compression failure falls back
74/// to raw — a put must never fail because a compressor declined.
75pub(crate) fn pack_value(plain: &[u8], policy: Compression) -> Vec<u8> {
76    if let Compression::On { level, min_len } = policy {
77        if plain.len() >= min_len {
78            if let Ok(z) = rusty_zstd::compress(plain, level) {
79                if z.len() < plain.len() {
80                    let mut out = Vec::with_capacity(1 + z.len());
81                    out.push(FORMAT_ZSTD);
82                    out.extend_from_slice(&z);
83                    return out;
84                }
85            }
86        }
87    }
88    let mut out = Vec::with_capacity(1 + plain.len());
89    out.push(FORMAT_RAW);
90    out.extend_from_slice(plain);
91    out
92}
93
94/// Unwrap a prefixed payload back to the encoded value. The input is
95/// AEAD-authenticated (it came out of `open_row`), so a bad format byte or a
96/// broken frame is corruption or a version mix-up, not attacker data — it
97/// fails loudly rather than decoding garbage.
98pub(crate) fn unpack_value(packed: &[u8]) -> StoreResult<Vec<u8>> {
99    match packed.split_first() {
100        Some((&FORMAT_RAW, rest)) => Ok(rest.to_vec()),
101        Some((&FORMAT_ZSTD, rest)) => rusty_zstd::decompress(rest)
102            .map_err(|e| StoreError::Compression(format!("zstd frame: {e:?}"))),
103        Some((&byte, _)) => Err(StoreError::Compression(format!(
104            "unknown row format byte {byte:#04x}"
105        ))),
106        None => Err(StoreError::Compression("empty sealed payload".into())),
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn raw_round_trips() {
116        let plain = b"hello world";
117        let packed = pack_value(plain, Compression::Off);
118        assert_eq!(packed[0], FORMAT_RAW);
119        assert_eq!(unpack_value(&packed).unwrap(), plain);
120    }
121
122    #[test]
123    fn compressible_payload_shrinks_and_round_trips() {
124        let plain = vec![b'a'; 4096];
125        let packed = pack_value(&plain, Compression::default());
126        assert_eq!(packed[0], FORMAT_ZSTD);
127        assert!(packed.len() < plain.len());
128        assert_eq!(unpack_value(&packed).unwrap(), plain);
129    }
130
131    #[test]
132    fn incompressible_payload_stays_raw() {
133        // Deterministic pseudo-random bytes do not compress; the policy must
134        // fall back to raw rather than storing an expanded frame.
135        let mut state = 0x9E3779B97F4A7C15u64;
136        let plain: Vec<u8> = (0..4096)
137            .map(|_| {
138                state ^= state << 13;
139                state ^= state >> 7;
140                state ^= state << 17;
141                (state >> 56) as u8
142            })
143            .collect();
144        let packed = pack_value(&plain, Compression::default());
145        assert_eq!(packed[0], FORMAT_RAW);
146        assert_eq!(packed.len(), plain.len() + 1);
147        assert_eq!(unpack_value(&packed).unwrap(), plain);
148    }
149
150    #[test]
151    fn below_the_floor_is_not_attempted() {
152        let plain = vec![b'a'; 8];
153        let packed = pack_value(
154            &plain,
155            Compression::On {
156                level: DEFAULT_COMPRESSION_LEVEL,
157                min_len: 64,
158            },
159        );
160        assert_eq!(packed[0], FORMAT_RAW);
161    }
162
163    #[test]
164    fn unknown_format_byte_fails_loudly() {
165        assert!(unpack_value(&[0x7F, 1, 2, 3]).is_err());
166        assert!(unpack_value(&[]).is_err());
167    }
168}