1use std::io::Read;
23
24use crate::bounds::{
25 MAX_DECODED_BYTES, MAX_DECOMPRESSION_RATIO, checked_add, checked_mul, decoded_len,
26 encode_input_len, u32_to_usize,
27};
28use crate::error::CodecError;
29
30pub const DEFAULT_LEVEL: i32 = 3;
32
33pub const HIGH_LEVEL: i32 = 19;
35
36const HEADER_SIZE: usize = 5;
38const ZSTD_MAGIC: [u8; 4] = [0x28, 0xb5, 0x2f, 0xfd];
39const MAX_ZSTD_WINDOW_LOG: u32 = 26;
40
41pub fn encode(data: &[u8]) -> Result<Vec<u8>, CodecError> {
47 encode_with_level(data, DEFAULT_LEVEL)
48}
49
50pub fn encode_with_level(data: &[u8], level: i32) -> Result<Vec<u8>, CodecError> {
52 let encoded_len = encode_input_len(data.len(), "Zstd")?;
53 let level = level.clamp(1, 22);
54
55 let compressed = compress_native(data, level)?;
56
57 let mut out = Vec::with_capacity(HEADER_SIZE + compressed.len());
58 out.extend_from_slice(&encoded_len.to_le_bytes());
59 out.push(level as u8);
60 out.extend_from_slice(&compressed);
61 Ok(out)
62}
63
64pub fn decode(data: &[u8]) -> Result<Vec<u8>, CodecError> {
66 if data.len() < HEADER_SIZE {
67 return Err(CodecError::Truncated {
68 expected: HEADER_SIZE,
69 actual: data.len(),
70 });
71 }
72
73 let uncompressed_size = decoded_len(
74 u32_to_usize(
75 u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
76 "Zstd output",
77 )?,
78 "Zstd",
79 )?;
80 let frame = &data[HEADER_SIZE..];
81 if frame.is_empty() && uncompressed_size != 0 {
82 return Err(CodecError::Truncated {
83 expected: HEADER_SIZE + 1,
84 actual: data.len(),
85 });
86 }
87 validate_frame_window(frame, uncompressed_size)?;
88 let ratio_limit = checked_mul(
89 frame.len().max(1),
90 MAX_DECOMPRESSION_RATIO,
91 "Zstd ratio limit",
92 )?;
93 if uncompressed_size > ratio_limit {
94 return Err(CodecError::ResourceLimit {
95 resource: "Zstd decompression ratio".into(),
96 requested: uncompressed_size,
97 limit: ratio_limit,
98 });
99 }
100 decompress_native(frame, uncompressed_size)
101}
102
103pub fn uncompressed_size(data: &[u8]) -> Result<usize, CodecError> {
105 if data.len() < HEADER_SIZE {
106 return Err(CodecError::Truncated {
107 expected: HEADER_SIZE,
108 actual: data.len(),
109 });
110 }
111 decoded_len(
112 u32_to_usize(
113 u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
114 "Zstd output",
115 )?,
116 "Zstd",
117 )
118}
119
120pub fn compression_level(data: &[u8]) -> Result<i32, CodecError> {
122 if data.len() < HEADER_SIZE {
123 return Err(CodecError::Truncated {
124 expected: HEADER_SIZE,
125 actual: data.len(),
126 });
127 }
128 Ok(data[4] as i32)
129}
130
131fn validate_frame_window(frame: &[u8], expected_size: usize) -> Result<(), CodecError> {
132 let magic = frame.get(..4).ok_or(CodecError::Truncated {
133 expected: 4,
134 actual: frame.len(),
135 })?;
136 if magic != ZSTD_MAGIC {
137 return Err(CodecError::Corrupt {
138 detail: "invalid Zstd frame magic".into(),
139 });
140 }
141 let descriptor = *frame.get(4).ok_or(CodecError::Truncated {
142 expected: 5,
143 actual: frame.len(),
144 })?;
145 if descriptor & 0x08 != 0 {
146 return Err(CodecError::Corrupt {
147 detail: "reserved Zstd frame-header bit is set".into(),
148 });
149 }
150
151 let single_segment = descriptor & 0x20 != 0;
152 let mut cursor = 5usize;
153 if !single_segment {
154 let window_descriptor = *frame.get(cursor).ok_or(CodecError::Truncated {
155 expected: cursor + 1,
156 actual: frame.len(),
157 })?;
158 cursor = checked_add(cursor, 1, "Zstd window descriptor")?;
159 let window_log = 10u32 + u32::from(window_descriptor >> 3);
160 if window_log > MAX_ZSTD_WINDOW_LOG {
161 return Err(CodecError::ResourceLimit {
162 resource: "Zstd window bytes".into(),
163 requested: usize::MAX,
164 limit: MAX_DECODED_BYTES,
165 });
166 }
167 let window_base = 1usize << window_log;
168 let window_add = checked_mul(
169 window_base / 8,
170 usize::from(window_descriptor & 0x07),
171 "Zstd window mantissa",
172 )?;
173 let window_size = checked_add(window_base, window_add, "Zstd window size")?;
174 decoded_len(window_size, "Zstd window")?;
175 }
176
177 let dictionary_id_size = match descriptor & 0x03 {
178 0 => 0,
179 1 => 1,
180 2 => 2,
181 _ => 4,
182 };
183 cursor = checked_add(cursor, dictionary_id_size, "Zstd dictionary id")?;
184 frame.get(..cursor).ok_or(CodecError::Truncated {
185 expected: cursor,
186 actual: frame.len(),
187 })?;
188
189 let content_flag = descriptor >> 6;
190 let content_size_len = match (content_flag, single_segment) {
191 (0, false) => 0,
192 (0, true) => 1,
193 (1, _) => 2,
194 (2, _) => 4,
195 _ => 8,
196 };
197 if content_size_len != 0 {
198 let end = checked_add(cursor, content_size_len, "Zstd content size")?;
199 let bytes = frame.get(cursor..end).ok_or(CodecError::Truncated {
200 expected: end,
201 actual: frame.len(),
202 })?;
203 let mut declared = 0u64;
204 for (shift, byte) in bytes.iter().enumerate() {
205 declared |= u64::from(*byte) << (shift * 8);
206 }
207 if content_size_len == 2 {
208 declared = declared
209 .checked_add(256)
210 .ok_or_else(|| CodecError::Corrupt {
211 detail: "Zstd content size overflow".into(),
212 })?;
213 }
214 let declared = usize::try_from(declared).map_err(|_| CodecError::ResourceLimit {
215 resource: "Zstd declared content bytes".into(),
216 requested: usize::MAX,
217 limit: MAX_DECODED_BYTES,
218 })?;
219 decoded_len(declared, "Zstd declared content")?;
220 if declared != expected_size {
221 return Err(CodecError::Corrupt {
222 detail: format!(
223 "Zstd frame content size {declared} differs from envelope {expected_size}"
224 ),
225 });
226 }
227 }
228 Ok(())
229}
230
231#[cfg(not(target_arch = "wasm32"))]
236fn compress_native(data: &[u8], level: i32) -> Result<Vec<u8>, CodecError> {
237 use std::io::Write;
238
239 let mut encoder =
240 zstd::Encoder::new(Vec::new(), level).map_err(|error| CodecError::CompressFailed {
241 detail: format!("zstd encoder init: {error}"),
242 })?;
243 encoder
244 .window_log(MAX_ZSTD_WINDOW_LOG)
245 .map_err(|error| CodecError::CompressFailed {
246 detail: format!("zstd encoder window limit: {error}"),
247 })?;
248 encoder
249 .write_all(data)
250 .map_err(|error| CodecError::CompressFailed {
251 detail: format!("zstd compress: {error}"),
252 })?;
253 encoder
254 .finish()
255 .map_err(|error| CodecError::CompressFailed {
256 detail: format!("zstd encoder finish: {error}"),
257 })
258}
259
260#[cfg(not(target_arch = "wasm32"))]
261fn decompress_native(frame: &[u8], expected_size: usize) -> Result<Vec<u8>, CodecError> {
262 let mut output = Vec::with_capacity(expected_size);
263 let mut decoder = zstd::Decoder::new(std::io::Cursor::new(frame)).map_err(|e| {
264 CodecError::DecompressFailed {
265 detail: format!("zstd decoder init: {e}"),
266 }
267 })?;
268 decoder
269 .window_log_max(MAX_ZSTD_WINDOW_LOG)
270 .map_err(|error| CodecError::DecompressFailed {
271 detail: format!("zstd window limit: {error}"),
272 })?;
273 let mut buffer = [0u8; 8192];
274 loop {
275 let count = decoder
276 .read(&mut buffer)
277 .map_err(|e| CodecError::DecompressFailed {
278 detail: format!("zstd decompress: {e}"),
279 })?;
280 if count == 0 {
281 break;
282 }
283 let next_len = checked_add(output.len(), count, "Zstd streaming output")?;
284 if next_len > expected_size || next_len > MAX_DECODED_BYTES {
285 return Err(CodecError::ResourceLimit {
286 resource: "Zstd streamed output".into(),
287 requested: next_len,
288 limit: expected_size.min(MAX_DECODED_BYTES),
289 });
290 }
291 output.extend_from_slice(&buffer[..count]);
292 }
293
294 if output.len() != expected_size {
295 return Err(CodecError::Corrupt {
296 detail: format!(
297 "zstd size mismatch: expected {expected_size}, got {}",
298 output.len()
299 ),
300 });
301 }
302
303 Ok(output)
304}
305
306#[cfg(target_arch = "wasm32")]
313fn compress_native(_data: &[u8], _level: i32) -> Result<Vec<u8>, CodecError> {
314 Err(CodecError::CompressFailed {
318 detail: "Zstd encoding not available on WASM — use LZ4 codec instead".into(),
319 })
320}
321
322#[cfg(target_arch = "wasm32")]
323fn decompress_native(frame: &[u8], expected_size: usize) -> Result<Vec<u8>, CodecError> {
324 use ruzstd::decoding::StreamingDecoder;
325
326 let mut decoder = StreamingDecoder::new(std::io::Cursor::new(frame)).map_err(|e| {
327 CodecError::DecompressFailed {
328 detail: format!("ruzstd decoder init: {e}"),
329 }
330 })?;
331
332 let mut output = Vec::with_capacity(expected_size);
333 let mut buffer = [0u8; 8192];
334 loop {
335 let count = decoder
336 .read(&mut buffer)
337 .map_err(|e| CodecError::DecompressFailed {
338 detail: format!("ruzstd decompress: {e}"),
339 })?;
340 if count == 0 {
341 break;
342 }
343 let next_len = checked_add(output.len(), count, "ruzstd streaming output")?;
344 if next_len > expected_size || next_len > MAX_DECODED_BYTES {
345 return Err(CodecError::ResourceLimit {
346 resource: "ruzstd streamed output".into(),
347 requested: next_len,
348 limit: expected_size.min(MAX_DECODED_BYTES),
349 });
350 }
351 output.extend_from_slice(&buffer[..count]);
352 }
353
354 if output.len() != expected_size {
355 return Err(CodecError::Corrupt {
356 detail: format!(
357 "zstd size mismatch: expected {expected_size}, got {}",
358 output.len()
359 ),
360 });
361 }
362
363 Ok(output)
364}
365
366pub struct ZstdEncoder {
372 buf: Vec<u8>,
373 level: i32,
374}
375
376impl ZstdEncoder {
377 pub fn new() -> Self {
378 Self {
379 buf: Vec::with_capacity(4096),
380 level: DEFAULT_LEVEL,
381 }
382 }
383
384 pub fn with_level(level: i32) -> Self {
385 Self {
386 buf: Vec::with_capacity(4096),
387 level: level.clamp(1, 22),
388 }
389 }
390
391 pub fn push(&mut self, data: &[u8]) -> Result<(), CodecError> {
392 let next_len = checked_add(self.buf.len(), data.len(), "Zstd streaming input")?;
393 decoded_len(next_len, "Zstd streaming input")?;
394 self.buf.extend_from_slice(data);
395 Ok(())
396 }
397
398 pub fn len(&self) -> usize {
399 self.buf.len()
400 }
401
402 pub fn is_empty(&self) -> bool {
403 self.buf.is_empty()
404 }
405
406 pub fn finish(self) -> Result<Vec<u8>, CodecError> {
407 encode_with_level(&self.buf, self.level)
408 }
409}
410
411impl Default for ZstdEncoder {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417pub struct ZstdDecoder;
419
420impl ZstdDecoder {
421 pub fn decode_all(data: &[u8]) -> Result<Vec<u8>, CodecError> {
422 decode(data)
423 }
424
425 pub fn uncompressed_size(data: &[u8]) -> Result<usize, CodecError> {
426 uncompressed_size(data)
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn empty_data() {
436 let encoded = encode(&[]).unwrap();
437 let decoded = decode(&encoded).unwrap();
438 assert!(decoded.is_empty());
439 }
440
441 #[test]
442 fn small_data_roundtrip() {
443 let data = b"hello world, zstd compression test";
444 let encoded = encode(data).unwrap();
445 let decoded = decode(&encoded).unwrap();
446 assert_eq!(decoded, data);
447 }
448
449 #[test]
450 fn large_data_roundtrip() {
451 let line = "2024-01-15 ERROR database connection timeout host=db-prod-01 retry=3\n";
452 let data: Vec<u8> = line.as_bytes().repeat(1000);
453 let encoded = encode(&data).unwrap();
454 let decoded = decode(&encoded).unwrap();
455 assert_eq!(decoded, data);
456
457 let ratio = data.len() as f64 / encoded.len() as f64;
458 assert!(
459 ratio > 5.0,
460 "repetitive logs should compress >5x with zstd, got {ratio:.1}x"
461 );
462 }
463
464 #[test]
465 fn high_compression_level() {
466 let data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
467 let default_encoded = encode(&data).unwrap();
468 let high_encoded = encode_with_level(&data, HIGH_LEVEL).unwrap();
469
470 assert!(high_encoded.len() <= default_encoded.len() + 10);
472
473 assert_eq!(decode(&default_encoded).unwrap(), data);
475 assert_eq!(decode(&high_encoded).unwrap(), data);
476 }
477
478 #[test]
479 fn header_metadata() {
480 let data = vec![42u8; 1000];
481 let encoded = encode_with_level(&data, 7).unwrap();
482
483 assert_eq!(uncompressed_size(&encoded).unwrap(), 1000);
484 assert_eq!(compression_level(&encoded).unwrap(), 7);
485 }
486
487 #[test]
488 fn better_ratio_than_lz4() {
489 let mut data = Vec::new();
491 for i in 0..5000 {
492 let line = format!(
493 "{{\"timestamp\":{},\"level\":\"INFO\",\"msg\":\"request handled\",\"duration\":{}}}",
494 1700000000 + i,
495 i % 100
496 );
497 data.extend_from_slice(line.as_bytes());
498 data.push(b'\n');
499 }
500
501 let zstd_encoded = encode(&data).unwrap();
502 let lz4_encoded = crate::lz4::encode(&data).expect("LZ4 encode");
503
504 assert!(
506 zstd_encoded.len() < lz4_encoded.len(),
507 "zstd ({}) should be smaller than lz4 ({})",
508 zstd_encoded.len(),
509 lz4_encoded.len()
510 );
511
512 assert_eq!(decode(&zstd_encoded).unwrap(), data);
514 assert_eq!(crate::lz4::decode(&lz4_encoded).unwrap(), data);
515 }
516
517 #[test]
518 fn hostile_declared_output_and_ratio_are_rejected_before_decoder_allocation() {
519 let mut huge = vec![0u8; HEADER_SIZE];
520 huge[..4].copy_from_slice(&u32::MAX.to_le_bytes());
521 assert!(matches!(
522 decode(&huge),
523 Err(CodecError::ResourceLimit { .. })
524 ));
525
526 let mut ratio = vec![0u8; HEADER_SIZE];
527 ratio[..4].copy_from_slice(&(MAX_DECODED_BYTES as u32).to_le_bytes());
528 ratio.extend_from_slice(&ZSTD_MAGIC);
529 ratio.push(0xa0); ratio.extend_from_slice(&(MAX_DECODED_BYTES as u32).to_le_bytes());
531 assert!(matches!(
532 decode(&ratio),
533 Err(CodecError::ResourceLimit { .. })
534 ));
535 assert!(matches!(
536 decode(&ratio[..HEADER_SIZE]),
537 Err(CodecError::Truncated { .. })
538 ));
539 }
540
541 #[test]
542 fn hostile_zstd_window_is_rejected_before_decoder_creation() {
543 let mut frame = vec![0u8; HEADER_SIZE];
544 frame[..4].copy_from_slice(&1u32.to_le_bytes());
545 frame.extend_from_slice(&ZSTD_MAGIC);
546 frame.push(0); frame.push(0xf8); assert!(matches!(
549 decode(&frame),
550 Err(CodecError::ResourceLimit { resource, .. }) if resource == "Zstd window bytes"
551 ));
552 }
553
554 #[test]
555 fn streaming_encoder() {
556 let parts: Vec<&[u8]> = vec![b"part one ", b"part two ", b"part three"];
557 let full: Vec<u8> = parts.iter().flat_map(|p| p.iter().copied()).collect();
558
559 let mut enc = ZstdEncoder::new();
560 for part in &parts {
561 enc.push(part).expect("push");
562 }
563 let encoded = enc.finish().unwrap();
564 let decoded = decode(&encoded).unwrap();
565 assert_eq!(decoded, full);
566 }
567
568 #[test]
569 fn truncated_input_errors() {
570 assert!(decode(&[]).is_err());
571 assert!(decode(&[0, 0, 0, 0]).is_err()); }
573
574 #[test]
575 fn streaming_input_limit_precedes_buffer_growth() {
576 let mut encoder = ZstdEncoder {
577 buf: vec![0; MAX_DECODED_BYTES],
578 level: DEFAULT_LEVEL,
579 };
580 assert!(matches!(
581 encoder.push(&[1]),
582 Err(CodecError::ResourceLimit { .. })
583 ));
584 assert_eq!(encoder.len(), MAX_DECODED_BYTES);
585 }
586
587 #[test]
588 fn level_clamping() {
589 let data = b"test data for clamping";
590 let encoded_low = encode_with_level(data, 0).unwrap();
592 let encoded_high = encode_with_level(data, 99).unwrap();
593 assert_eq!(decode(&encoded_low).unwrap(), data);
594 assert_eq!(decode(&encoded_high).unwrap(), data);
595 }
596}