base_d/encoders/algorithms/schema/
compression.rs

1use super::types::SchemaError;
2use crate::features::compression::{CompressionAlgorithm, compress, decompress};
3
4/// Algorithm byte prefix values
5const ALGO_NONE: u8 = 0x00;
6const ALGO_BROTLI: u8 = 0x01;
7const ALGO_LZ4: u8 = 0x02;
8const ALGO_ZSTD: u8 = 0x03;
9
10/// Default compression level for schema encoding
11const DEFAULT_LEVEL: u32 = 6;
12
13/// Compression algorithms for schema encoding
14///
15/// These algorithms are applied to the binary payload before display96 encoding.
16/// The algorithm is stored as a 1-byte prefix in the compressed payload.
17///
18/// # Algorithms
19///
20/// * `Brotli` - Best compression ratio (prefix: 0x01)
21/// * `Lz4` - Fastest compression/decompression (prefix: 0x02)
22/// * `Zstd` - Balanced compression and speed (prefix: 0x03)
23///
24/// All algorithms use default compression level 6.
25///
26/// # Examples
27///
28/// ```ignore
29/// use base_d::{encode_schema, SchemaCompressionAlgo};
30///
31/// let json = r#"{"data":[1,2,3,4,5]}"#;
32///
33/// // Best compression
34/// let encoded = encode_schema(json, Some(SchemaCompressionAlgo::Brotli))?;
35///
36/// // Fastest
37/// let encoded = encode_schema(json, Some(SchemaCompressionAlgo::Lz4))?;
38///
39/// // Balanced
40/// let encoded = encode_schema(json, Some(SchemaCompressionAlgo::Zstd))?;
41/// ```
42#[derive(Clone, Copy, Debug)]
43pub enum SchemaCompressionAlgo {
44    Brotli,
45    Lz4,
46    Zstd,
47}
48
49/// Apply compression to binary data with algorithm prefix
50///
51/// Compresses the binary payload and prepends a 1-byte algorithm identifier.
52/// This allows automatic detection of the compression algorithm during decoding.
53///
54/// # Format
55///
56/// ```text
57/// [algo_byte: u8][compressed_payload: bytes]
58/// ```
59///
60/// # Algorithm Bytes
61///
62/// * `0x00` - No compression (payload is raw binary)
63/// * `0x01` - Brotli (level 6)
64/// * `0x02` - LZ4 (level 6)
65/// * `0x03` - Zstd (level 6)
66///
67/// # Arguments
68///
69/// * `binary` - Raw binary data to compress
70/// * `algo` - Optional compression algorithm (None = no compression)
71///
72/// # Returns
73///
74/// Returns a byte vector with algorithm prefix followed by (possibly compressed) payload.
75///
76/// # Errors
77///
78/// * `SchemaError::Compression` - Compression failure
79///
80/// # Examples
81///
82/// ```ignore
83/// use base_d::{compress_with_prefix, SchemaCompressionAlgo};
84///
85/// let data = b"Hello, world!";
86///
87/// // No compression (returns [0x00, ...data])
88/// let result = compress_with_prefix(data, None)?;
89///
90/// // With brotli (returns [0x01, ...compressed])
91/// let result = compress_with_prefix(data, Some(SchemaCompressionAlgo::Brotli))?;
92/// ```
93pub fn compress_with_prefix(
94    binary: &[u8],
95    algo: Option<SchemaCompressionAlgo>,
96) -> Result<Vec<u8>, SchemaError> {
97    let (algo_byte, compressed) = match algo {
98        None => (ALGO_NONE, binary.to_vec()),
99        Some(SchemaCompressionAlgo::Brotli) => {
100            let c = compress(binary, CompressionAlgorithm::Brotli, DEFAULT_LEVEL)
101                .map_err(|e| SchemaError::Compression(e.to_string()))?;
102            (ALGO_BROTLI, c)
103        }
104        Some(SchemaCompressionAlgo::Lz4) => {
105            let c = compress(binary, CompressionAlgorithm::Lz4, DEFAULT_LEVEL)
106                .map_err(|e| SchemaError::Compression(e.to_string()))?;
107            (ALGO_LZ4, c)
108        }
109        Some(SchemaCompressionAlgo::Zstd) => {
110            let c = compress(binary, CompressionAlgorithm::Zstd, DEFAULT_LEVEL)
111                .map_err(|e| SchemaError::Compression(e.to_string()))?;
112            (ALGO_ZSTD, c)
113        }
114    };
115
116    // Prepend algorithm byte
117    let mut result = Vec::with_capacity(1 + compressed.len());
118    result.push(algo_byte);
119    result.extend_from_slice(&compressed);
120    Ok(result)
121}
122
123/// Decompress binary data using algorithm prefix
124///
125/// Reads the first byte to determine the compression algorithm, then decompresses
126/// the remaining payload. Supports all algorithms from [`compress_with_prefix`].
127///
128/// # Arguments
129///
130/// * `binary` - Byte slice with algorithm prefix + payload
131///
132/// # Returns
133///
134/// Returns the decompressed binary data (or raw data if uncompressed).
135///
136/// # Errors
137///
138/// * `SchemaError::UnexpectedEndOfData` - Empty input (missing prefix byte)
139/// * `SchemaError::InvalidCompressionAlgorithm` - Invalid algorithm byte
140/// * `SchemaError::Decompression` - Decompression failure
141///
142/// # Examples
143///
144/// ```ignore
145/// use base_d::decompress_with_prefix;
146///
147/// // Decompress data (auto-detects algorithm from prefix)
148/// let compressed = vec![0x01, /* brotli compressed bytes */];
149/// let data = decompress_with_prefix(&compressed)?;
150///
151/// // Uncompressed data (prefix 0x00)
152/// let uncompressed = vec![0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F];
153/// let data = decompress_with_prefix(&uncompressed)?;
154/// // Returns: b"Hello"
155/// ```
156pub fn decompress_with_prefix(binary: &[u8]) -> Result<Vec<u8>, SchemaError> {
157    if binary.is_empty() {
158        return Err(SchemaError::UnexpectedEndOfData {
159            context: "compression prefix".to_string(),
160            position: 0,
161        });
162    }
163
164    let algo_byte = binary[0];
165    let payload = &binary[1..];
166
167    match algo_byte {
168        ALGO_NONE => Ok(payload.to_vec()),
169        ALGO_BROTLI => decompress(payload, CompressionAlgorithm::Brotli)
170            .map_err(|e| SchemaError::Decompression(e.to_string())),
171        ALGO_LZ4 => decompress(payload, CompressionAlgorithm::Lz4)
172            .map_err(|e| SchemaError::Decompression(e.to_string())),
173        ALGO_ZSTD => decompress(payload, CompressionAlgorithm::Zstd)
174            .map_err(|e| SchemaError::Decompression(e.to_string())),
175        _ => Err(SchemaError::InvalidCompressionAlgorithm(algo_byte)),
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_no_compression() {
185        let data = b"Hello, world!";
186        let compressed = compress_with_prefix(data, None).unwrap();
187
188        // Should have algo byte + raw data
189        assert_eq!(compressed[0], ALGO_NONE);
190        assert_eq!(&compressed[1..], data);
191
192        let decompressed = decompress_with_prefix(&compressed).unwrap();
193        assert_eq!(decompressed, data);
194    }
195
196    #[test]
197    fn test_brotli_roundtrip() {
198        let data = b"Hello, world! This is a test of brotli compression in schema encoding.";
199        let compressed = compress_with_prefix(data, Some(SchemaCompressionAlgo::Brotli)).unwrap();
200
201        assert_eq!(compressed[0], ALGO_BROTLI);
202
203        let decompressed = decompress_with_prefix(&compressed).unwrap();
204        assert_eq!(decompressed, data);
205    }
206
207    #[test]
208    fn test_lz4_roundtrip() {
209        let data = b"Hello, world! This is a test of lz4 compression in schema encoding.";
210        let compressed = compress_with_prefix(data, Some(SchemaCompressionAlgo::Lz4)).unwrap();
211
212        assert_eq!(compressed[0], ALGO_LZ4);
213
214        let decompressed = decompress_with_prefix(&compressed).unwrap();
215        assert_eq!(decompressed, data);
216    }
217
218    #[test]
219    fn test_zstd_roundtrip() {
220        let data = b"Hello, world! This is a test of zstd compression in schema encoding.";
221        let compressed = compress_with_prefix(data, Some(SchemaCompressionAlgo::Zstd)).unwrap();
222
223        assert_eq!(compressed[0], ALGO_ZSTD);
224
225        let decompressed = decompress_with_prefix(&compressed).unwrap();
226        assert_eq!(decompressed, data);
227    }
228
229    #[test]
230    fn test_small_payload() {
231        // Small payloads may expand with compression overhead
232        let data = b"Hi";
233        let compressed = compress_with_prefix(data, Some(SchemaCompressionAlgo::Brotli)).unwrap();
234        let decompressed = decompress_with_prefix(&compressed).unwrap();
235        assert_eq!(decompressed, data);
236    }
237
238    #[test]
239    fn test_empty_payload() {
240        let data = b"";
241        let compressed = compress_with_prefix(data, None).unwrap();
242        assert_eq!(compressed, vec![ALGO_NONE]);
243
244        let decompressed = decompress_with_prefix(&compressed).unwrap();
245        assert_eq!(decompressed, data);
246    }
247
248    #[test]
249    fn test_invalid_algorithm() {
250        let invalid = vec![0xFF, 0x01, 0x02, 0x03];
251        let result = decompress_with_prefix(&invalid);
252        assert!(matches!(
253            result,
254            Err(SchemaError::InvalidCompressionAlgorithm(0xFF))
255        ));
256    }
257
258    #[test]
259    fn test_missing_prefix() {
260        let result = decompress_with_prefix(&[]);
261        assert!(matches!(
262            result,
263            Err(SchemaError::UnexpectedEndOfData { .. })
264        ));
265    }
266}