heddle_format/compression/
mod.rs1#![deny(clippy::cast_possible_truncation)]
3
4mod dictionaries;
11mod frame;
12mod zstd_codec;
13
14pub use dictionaries::CompressionDictionary;
15use frame::{
16 DICTIONARY_HEADER_LEN as DICTIONARY_COMPRESSED_HEADER_LEN, HEADER_LEN as COMPRESSED_HEADER_LEN,
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[repr(u8)]
22enum CompressionType {
23 Zstd = 1,
25}
26
27impl CompressionType {
28 fn from_u8(value: u8) -> Option<Self> {
30 match value {
31 1 => Some(CompressionType::Zstd),
32 _ => None,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy)]
39pub struct CompressionConfig {
40 pub enabled: bool,
42 pub level: i32,
45 pub min_size: usize,
47 pub max_delta_size: usize,
49}
50
51impl Default for CompressionConfig {
52 fn default() -> Self {
53 Self {
54 enabled: cfg!(feature = "zstd"),
55 level: 3, min_size: 256, max_delta_size: 10_000_000, }
59 }
60}
61
62impl CompressionConfig {
63 pub fn from_env() -> Self {
65 let mut config = Self::default();
66
67 if let Ok(val) = std::env::var("HEDDLE_COMPRESSION") {
68 let requested = val != "0" && val.to_lowercase() != "false";
69 config.enabled = requested && cfg!(feature = "zstd");
70 }
71
72 if let Ok(val) = std::env::var("HEDDLE_COMPRESSION_LEVEL")
73 && let Ok(level) = val.parse::<i32>()
74 {
75 config.level = level.clamp(1, 22);
76 }
77
78 if let Ok(val) = std::env::var("HEDDLE_COMPRESSION_MIN_SIZE")
79 && let Ok(size) = val.parse::<usize>()
80 {
81 config.min_size = size;
82 }
83
84 config
85 }
86
87 pub fn disabled() -> Self {
89 Self {
90 enabled: false,
91 level: 0,
92 min_size: usize::MAX,
93 max_delta_size: 0,
94 }
95 }
96}
97
98#[derive(Debug, thiserror::Error)]
100pub enum CompressionError {
101 #[error("decompression failed: {0}")]
102 DecompressionFailed(String),
103 #[error("compression failed: {0}")]
104 CompressionFailed(String),
105 #[error("invalid compression type: {0}")]
106 InvalidType(u8),
107 #[error("corrupted data: {0}")]
108 CorruptedData(String),
109 #[error("invalid operation: {0}")]
110 InvalidOperation(String),
111 #[error("unknown compression dictionary id: {0}")]
112 UnknownDictionary(u32),
113 #[error("object size {size} exceeds maximum {max}")]
114 SizeLimitExceeded { size: u64, max: u64 },
115}
116
117#[cfg(feature = "bench")]
118pub fn compress_zstd(data: &[u8], level: i32) -> Result<Vec<u8>, CompressionError> {
120 zstd_codec::compress(data, level)
121}
122
123#[cfg(feature = "bench")]
124pub fn decompress_zstd(data: &[u8], expected_size: u64) -> Result<Vec<u8>, CompressionError> {
126 zstd_codec::decompress(data, expected_size)
127}
128
129pub fn compress(
134 data: &[u8],
135 config: &CompressionConfig,
136) -> Result<Option<Vec<u8>>, CompressionError> {
137 if !config.enabled || data.len() < config.min_size {
138 return Ok(None);
139 }
140
141 zstd_codec::validate_size(data.len() as u64)?;
142 let compressed = zstd_codec::compress(data, config.level)?;
143
144 if compressed.len() >= data.len() {
145 return Ok(None);
146 }
147
148 let mut result = Vec::with_capacity(COMPRESSED_HEADER_LEN + compressed.len());
149 result.push(CompressionType::Zstd as u8);
150 result.extend_from_slice(&(data.len() as u64).to_be_bytes());
151 result.extend_from_slice(&compressed);
152 Ok(Some(result))
153}
154
155pub fn compress_with_dictionary(
160 data: &[u8],
161 config: &CompressionConfig,
162 dictionary: CompressionDictionary,
163) -> Result<Option<Vec<u8>>, CompressionError> {
164 if !config.enabled || data.len() < config.min_size {
165 return Ok(None);
166 }
167
168 zstd_codec::validate_size(data.len() as u64)?;
169 let compressed = zstd_codec::compress_with_dictionary(data, config.level, dictionary.bytes())?;
170
171 if compressed.len() >= data.len() {
173 return Ok(None);
174 }
175
176 let mut result = Vec::with_capacity(DICTIONARY_COMPRESSED_HEADER_LEN + compressed.len());
177 result.push(CompressionType::Zstd as u8);
178 result.extend_from_slice(&(data.len() as u64).to_be_bytes());
179 result.extend_from_slice(&dictionary.id().to_be_bytes());
180 result.extend_from_slice(&compressed);
181
182 Ok(Some(result))
183}
184
185pub fn decompress(data: &[u8]) -> Result<Vec<u8>, CompressionError> {
189 if data.len() < COMPRESSED_HEADER_LEN {
190 return Ok(data.to_vec());
192 }
193
194 let compression_type =
195 CompressionType::from_u8(data[0]).ok_or_else(|| CompressionError::InvalidType(data[0]))?;
196
197 match compression_type {
198 CompressionType::Zstd if frame::parse_zstd(data).is_some() => {
199 decompress_zstd_with_header(data)
200 }
201 CompressionType::Zstd => Ok(data.to_vec()),
202 }
203}
204
205pub fn is_compressed(data: &[u8]) -> bool {
207 if data.len() < COMPRESSED_HEADER_LEN {
208 return false;
209 }
210
211 matches!(
212 CompressionType::from_u8(data[0]),
213 Some(CompressionType::Zstd)
214 ) && frame::parse_zstd(data).is_some()
215}
216
217pub fn header_uncompressed_size(data: &[u8]) -> Option<u64> {
225 if data.len() < COMPRESSED_HEADER_LEN {
226 return None;
227 }
228 let CompressionType::Zstd = CompressionType::from_u8(data[0])?;
229 Some(frame::parse_zstd(data)?.uncompressed_size)
230}
231
232#[cfg(test)]
233fn compression_info(data: &[u8]) -> Option<(CompressionType, u64)> {
235 if data.len() < COMPRESSED_HEADER_LEN {
236 return None;
237 }
238
239 let compression_type = CompressionType::from_u8(data[0])?;
240 let uncompressed_size = u64::from_be_bytes(data[1..COMPRESSED_HEADER_LEN].try_into().ok()?);
241
242 Some((compression_type, uncompressed_size))
243}
244
245fn decompress_zstd_with_header(data: &[u8]) -> Result<Vec<u8>, CompressionError> {
246 let header = frame::parse_zstd(data).ok_or_else(|| {
247 CompressionError::CorruptedData("zstd compression header is invalid".to_string())
248 })?;
249 zstd_codec::decompress(&data[header.len..], header.uncompressed_size)
250}
251
252pub fn decompress_with_dictionary(data: &[u8]) -> Result<Vec<u8>, CompressionError> {
257 if data.len() < DICTIONARY_COMPRESSED_HEADER_LEN {
258 return Ok(data.to_vec());
259 }
260
261 let Some(header) = frame::parse_dictionary_zstd(data) else {
262 return Ok(data.to_vec());
263 };
264 let dictionary = dictionaries::lookup(header.dictionary_id)
265 .ok_or(CompressionError::UnknownDictionary(header.dictionary_id))?;
266 zstd_codec::decompress_with_dictionary(
267 &data[DICTIONARY_COMPRESSED_HEADER_LEN..],
268 header.uncompressed_size,
269 dictionary,
270 )
271}
272
273#[cfg(test)]
274mod compression_tests;