1use std::io::Write;
37
38use crate::error::{CompressError, DecompressError};
39use crate::{CHUNK_SIZE, STREAM_BUF_SIZE};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum CompressionFormat {
45 #[serde(rename = "identity")]
47 None,
48 Zrip,
50}
51
52impl CompressionFormat {
53 pub fn as_str(self) -> &'static str {
56 match self {
57 CompressionFormat::None => "identity",
58 CompressionFormat::Zrip => "zrip",
59 }
60 }
61
62 pub fn parse_header(s: &str) -> Option<Self> {
64 match s.trim().to_ascii_lowercase().as_str() {
65 "" | "identity" | "none" => Some(CompressionFormat::None),
66 "zrip" | "zstd" => Some(CompressionFormat::Zrip),
67 _ => None,
68 }
69 }
70}
71
72pub const MAX_FRAME_OUTPUT: usize = CHUNK_SIZE as usize;
77
78pub const MAX_PENDING_FRAME: usize = CHUNK_SIZE as usize + STREAM_BUF_SIZE;
83
84pub const MAX_OUTPUT_PER_CALL: usize = MAX_FRAME_OUTPUT.saturating_mul(8);
94
95pub const ZRIP_DEFAULT_LEVEL: i32 = 1;
97
98pub const ZRIP_MIN_LEVEL: i32 = -8;
100
101pub const ZRIP_MAX_LEVEL: i32 = 4;
103
104pub fn is_valid_zrip_level(level: i32) -> bool {
106 (ZRIP_MIN_LEVEL..=ZRIP_MAX_LEVEL).contains(&level)
107}
108
109pub fn negotiate_level(req: Option<i32>, min: i32, max: i32, default: i32) -> i32 {
115 match req {
116 None => default,
117 Some(l) => l.clamp(min, max),
118 }
119}
120
121pub trait Compressor: Send {
124 fn format(&self) -> CompressionFormat;
126
127 fn compress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), CompressError>;
133
134 fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), CompressError>;
138}
139
140pub trait Decompressor: Send {
143 fn format(&self) -> CompressionFormat;
145
146 fn decompress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), DecompressError>;
151
152 fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), DecompressError>;
157}
158
159pub fn compressor(format: CompressionFormat) -> Result<Box<dyn Compressor>, CompressError> {
161 compressor_with_level(format, ZRIP_DEFAULT_LEVEL)
162}
163
164pub fn compressor_with_level(
172 format: CompressionFormat,
173 level: i32,
174) -> Result<Box<dyn Compressor>, CompressError> {
175 match format {
176 CompressionFormat::None => Ok(Box::new(PassthroughCompressor)),
177 CompressionFormat::Zrip => Ok(Box::new(ZripCompressor::new(level)?)),
178 }
179}
180
181pub fn decompressor(format: CompressionFormat) -> Box<dyn Decompressor> {
183 decompressor_with_limit(format, MAX_OUTPUT_PER_CALL)
184}
185
186pub fn decompressor_with_limit(
193 format: CompressionFormat,
194 max_output_per_call: usize,
195) -> Box<dyn Decompressor> {
196 match format {
197 CompressionFormat::None => Box::new(PassthroughDecompressor),
198 CompressionFormat::Zrip => Box::new(ZripDecompressor::with_max_output(max_output_per_call)),
199 }
200}
201
202struct PassthroughCompressor;
207
208impl Compressor for PassthroughCompressor {
209 fn format(&self) -> CompressionFormat {
210 CompressionFormat::None
211 }
212
213 fn compress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), CompressError> {
214 out.extend_from_slice(input);
215 Ok(())
216 }
217
218 fn finish(&mut self, _out: &mut Vec<u8>) -> Result<(), CompressError> {
219 Ok(())
220 }
221}
222
223struct PassthroughDecompressor;
224
225impl Decompressor for PassthroughDecompressor {
226 fn format(&self) -> CompressionFormat {
227 CompressionFormat::None
228 }
229
230 fn decompress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), DecompressError> {
231 out.extend_from_slice(input);
232 Ok(())
233 }
234
235 fn finish(&mut self, _out: &mut Vec<u8>) -> Result<(), DecompressError> {
236 Ok(())
237 }
238}
239
240pub struct ZripCompressor {
246 encoder: Option<zrip::FrameEncoder<Vec<u8>>>,
247 dirty: bool,
249}
250
251impl ZripCompressor {
252 pub fn new(level: i32) -> Result<Self, CompressError> {
254 Ok(ZripCompressor {
255 encoder: Some(zrip::FrameEncoder::new(Vec::new(), level)?),
256 dirty: false,
257 })
258 }
259}
260
261impl Compressor for ZripCompressor {
262 fn format(&self) -> CompressionFormat {
263 CompressionFormat::Zrip
264 }
265
266 fn compress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), CompressError> {
267 if input.is_empty() {
268 return Ok(());
269 }
270 let encoder = self
271 .encoder
272 .as_mut()
273 .ok_or_else(|| std::io::Error::other("compressor already finished"))?;
274 if self.dirty {
276 let finished = encoder.reset(Vec::new())?;
277 out.extend_from_slice(&finished);
278 }
279 encoder.write_all(input)?;
280 self.dirty = true;
281 Ok(())
282 }
283
284 fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), CompressError> {
285 let encoder = self
286 .encoder
287 .take()
288 .ok_or_else(|| std::io::Error::other("compressor already finished"))?;
289 let tail = encoder.finish()?;
290 out.extend_from_slice(&tail);
291 self.dirty = false;
292 Ok(())
293 }
294}
295
296pub struct ZripDecompressor {
298 pending: Vec<u8>,
300 finished: bool,
301 max_output_per_call: usize,
304}
305
306impl ZripDecompressor {
307 pub fn new() -> Self {
310 ZripDecompressor::with_max_output(MAX_OUTPUT_PER_CALL)
311 }
312
313 pub fn with_max_output(max_output_per_call: usize) -> Self {
315 ZripDecompressor {
316 pending: Vec::with_capacity(STREAM_BUF_SIZE),
317 finished: false,
318 max_output_per_call,
319 }
320 }
321
322 fn drain_frames(&mut self, out: &mut Vec<u8>, max_add: usize) -> Result<(), DecompressError> {
325 let mut added = 0usize;
326 let mut consumed = 0usize;
330 loop {
331 let boundary = frame_boundary(&self.pending[consumed..]);
332 match boundary {
333 FrameBoundary::Complete { len, content_size } => {
334 if content_size.is_some_and(|cs| cs > MAX_FRAME_OUTPUT as u64) {
335 return Err(DecompressError::TooLarge {
336 limit: MAX_FRAME_OUTPUT,
337 });
338 }
339 let decoded = {
340 let frame = &self.pending[consumed..consumed + len];
341 zrip::decompress_with_limit(frame, MAX_FRAME_OUTPUT).map_err(|e| {
342 DecompressError::Io(std::io::Error::new(
343 std::io::ErrorKind::InvalidData,
344 e,
345 ))
346 })?
347 };
348 if added.saturating_add(decoded.len()) > max_add {
351 return Err(DecompressError::TooLarge { limit: max_add });
352 }
353 out.extend_from_slice(&decoded);
354 added = added.saturating_add(decoded.len());
355 consumed += len;
356 }
357 FrameBoundary::Incomplete => {
358 if consumed > 0 {
361 self.pending.drain(..consumed);
362 }
363 return Ok(());
364 }
365 FrameBoundary::Invalid => {
366 return Err(DecompressError::Io(std::io::Error::new(
367 std::io::ErrorKind::InvalidData,
368 "invalid zstd frame data",
369 )))
370 }
371 }
372 }
373 }
374}
375
376impl Default for ZripDecompressor {
377 fn default() -> Self {
378 ZripDecompressor::new()
379 }
380}
381
382impl Decompressor for ZripDecompressor {
383 fn format(&self) -> CompressionFormat {
384 CompressionFormat::Zrip
385 }
386
387 fn decompress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), DecompressError> {
388 if self.finished {
389 return Err(DecompressError::Io(std::io::Error::other(
390 "decompressor already finished",
391 )));
392 }
393 if !input.is_empty() {
394 self.pending.extend_from_slice(input);
395 }
396 self.drain_frames(out, self.max_output_per_call)?;
397 if self.pending.len() > MAX_PENDING_FRAME {
398 return Err(DecompressError::TooLarge {
399 limit: MAX_PENDING_FRAME,
400 });
401 }
402 Ok(())
403 }
404
405 fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), DecompressError> {
406 if self.finished {
407 return Ok(());
408 }
409 self.finished = true;
410 self.drain_frames(out, self.max_output_per_call)?;
411 if !self.pending.is_empty() {
412 return Err(DecompressError::Truncated(std::io::Error::new(
413 std::io::ErrorKind::UnexpectedEof,
414 format!("{} trailing compressed bytes", self.pending.len()),
415 )));
416 }
417 Ok(())
418 }
419}
420
421const ZSTD_MAGIC: u32 = 0xFD2F_B528;
426const SKIPPABLE_MASK: u32 = 0xFFFF_FFF0;
427const SKIPPABLE_MAGIC: u32 = 0x184D_2A50;
428
429enum FrameBoundary {
430 Complete { len: usize, content_size: Option<u64> },
433 Incomplete,
435 Invalid,
437}
438
439fn frame_boundary(buf: &[u8]) -> FrameBoundary {
441 if buf.len() < 4 {
442 return FrameBoundary::Incomplete;
443 }
444 let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
445 if magic == ZSTD_MAGIC {
446 zstd_frame_boundary(buf)
447 } else if (magic & SKIPPABLE_MASK) == SKIPPABLE_MAGIC {
448 skippable_frame_boundary(buf)
449 } else {
450 FrameBoundary::Invalid
451 }
452}
453
454fn skippable_frame_boundary(buf: &[u8]) -> FrameBoundary {
456 if buf.len() < 8 {
457 return FrameBoundary::Incomplete;
458 }
459 let skip = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
460 let total = 8usize.saturating_add(skip);
461 if buf.len() >= total {
462 FrameBoundary::Complete {
463 len: total,
464 content_size: Some(0),
465 }
466 } else {
467 FrameBoundary::Incomplete
468 }
469}
470
471fn zstd_frame_boundary(buf: &[u8]) -> FrameBoundary {
476 if buf.len() < 5 {
477 return FrameBoundary::Incomplete;
478 }
479 let descriptor = buf[4];
480 if descriptor & 0x18 != 0 {
482 return FrameBoundary::Invalid;
483 }
484 let single_segment = descriptor & 0x20 != 0;
485 let checksum = descriptor & 0x04 != 0;
486 let dict_id_flag = descriptor & 0x03;
487 let fcs_flag = (descriptor >> 6) & 0x03;
488
489 let mut hdr_len = 5usize;
490 if !single_segment {
491 hdr_len += 1; }
493 let dict_id_size = match dict_id_flag {
494 0 => 0,
495 1 => 1,
496 2 => 2,
497 3 => 4,
498 _ => return FrameBoundary::Invalid,
499 };
500 hdr_len += dict_id_size;
501 let fcs_size: usize = match fcs_flag {
502 0 if single_segment => 1,
503 0 => 0,
504 1 => 2,
505 2 => 4,
506 3 => 8,
507 _ => return FrameBoundary::Invalid,
508 };
509 hdr_len += fcs_size;
510
511 if buf.len() < hdr_len {
512 return FrameBoundary::Incomplete;
513 }
514 let content_size = if fcs_size > 0 {
515 let mut v = 0u64;
516 for (i, &b) in buf[5..5 + fcs_size].iter().enumerate() {
517 v |= (b as u64) << (8 * i);
518 }
519 Some(v)
520 } else {
521 None
522 };
523
524 let mut off = hdr_len;
526 loop {
527 if buf.len() < off + 3 {
528 return FrameBoundary::Incomplete;
529 }
530 let block_header = u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], 0]);
533 let last = block_header & 0x01 != 0;
534 let block_type = (block_header >> 1) & 0x03;
535 let block_size = (block_header >> 3) as usize;
536 if block_type == 3 {
537 return FrameBoundary::Invalid;
539 }
540 off += 3 + block_size;
541 if off > MAX_PENDING_FRAME {
542 return FrameBoundary::Invalid;
543 }
544 if last {
545 break;
546 }
547 }
548 if checksum {
549 off += 4;
550 }
551 if buf.len() < off {
552 return FrameBoundary::Incomplete;
553 }
554 FrameBoundary::Complete {
555 len: off,
556 content_size,
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 fn roundtrip_chunks(data: &[u8], feed: &[usize]) {
565 let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
566 let mut compressed = Vec::new();
567 let mut off = 0;
568 for &n in feed {
569 let end = (off + n).min(data.len());
570 if end > off {
571 c.compress(&data[off..end], &mut compressed).unwrap();
572 }
573 off = end;
574 }
575 c.finish(&mut compressed).unwrap();
576 assert!(!compressed.is_empty());
577
578 let mut d = ZripDecompressor::new();
580 let mut plain = Vec::new();
581 let mut step = 1;
582 let mut i = 0;
583 while i < compressed.len() {
584 let end = (i + step).min(compressed.len());
585 d.decompress(&compressed[i..end], &mut plain).unwrap();
586 i = end;
587 step = step % 5 + 1; }
589 d.finish(&mut plain).unwrap();
590 assert_eq!(plain, data, "roundtrip mismatch with feed {feed:?}");
591 }
592
593 #[test]
594 fn roundtrip_single_chunk() {
595 let data: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
596 roundtrip_chunks(&data, &[data.len()]);
597 }
598
599 #[test]
600 fn roundtrip_multi_chunk_64k_windows() {
601 let data: Vec<u8> = (0..300_000u32).map(|i| (i / 7) as u8).collect();
603 let feed: Vec<usize> = std::iter::repeat(STREAM_BUF_SIZE).take(5).collect();
604 roundtrip_chunks(&data, &feed);
605 }
606
607 #[test]
608 fn roundtrip_highly_compressible() {
609 let data = b"libfw streaming compression test. ".repeat(10_000);
610 let mut feed = Vec::new();
612 let mut consumed = 0;
613 for (_, &size) in [1024usize, 2048, 4096, 8192, 16384].iter().cycle().enumerate() {
614 feed.push(size);
615 consumed += size;
616 if consumed >= data.len() {
617 break;
618 }
619 }
620 roundtrip_chunks(&data, &feed);
621 }
622
623 #[test]
624 fn roundtrip_empty_stream() {
625 let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
626 let mut compressed = Vec::new();
627 c.finish(&mut compressed).unwrap();
628
629 let mut d = ZripDecompressor::new();
630 let mut plain = Vec::new();
631 d.decompress(&compressed, &mut plain).unwrap();
632 d.finish(&mut plain).unwrap();
633 assert!(plain.is_empty());
634 }
635
636 #[test]
637 fn malformed_zstd_headers_are_rejected_without_panicking() {
638 let bad = [0xFD, 0x2F, 0xB5, 0x28, 0xFF];
642 assert!(matches!(frame_boundary(&bad), FrameBoundary::Invalid));
643
644 let bad2 = [0xFD, 0x2F, 0xB5, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00];
645 assert!(matches!(frame_boundary(&bad2), FrameBoundary::Invalid));
646 }
647
648 #[test]
649 fn empty_input_chunks_are_noops() {
650 let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
651 let mut out = Vec::new();
652 c.compress(&[], &mut out).unwrap();
653 c.compress(b"hello", &mut out).unwrap();
654 c.finish(&mut out).unwrap();
655
656 let mut d = ZripDecompressor::new();
657 let mut plain = Vec::new();
658 d.decompress(&out, &mut plain).unwrap();
659 d.finish(&mut plain).unwrap();
660 assert_eq!(plain, b"hello");
661 }
662
663 #[test]
664 fn format_header_roundtrip() {
665 assert_eq!(CompressionFormat::parse_header("zrip"), Some(CompressionFormat::Zrip));
666 assert_eq!(CompressionFormat::parse_header("ZSTD"), Some(CompressionFormat::Zrip));
667 assert_eq!(CompressionFormat::parse_header("identity"), Some(CompressionFormat::None));
668 assert_eq!(CompressionFormat::parse_header(""), Some(CompressionFormat::None));
669 assert_eq!(CompressionFormat::parse_header("br"), None);
670 assert_eq!(CompressionFormat::Zrip.as_str(), "zrip");
671 assert_eq!(CompressionFormat::None.as_str(), "identity");
672 }
673
674 #[test]
675 fn passthrough_roundtrip() {
676 let mut c = compressor(CompressionFormat::None).unwrap();
677 let mut d = decompressor(CompressionFormat::None);
678 let mut compressed = Vec::new();
679 let mut plain = Vec::new();
680 c.compress(b"abc", &mut compressed).unwrap();
681 c.finish(&mut compressed).unwrap();
682 d.decompress(&compressed, &mut plain).unwrap();
683 d.finish(&mut plain).unwrap();
684 assert_eq!(plain, b"abc");
685 }
686
687 #[test]
688 fn truncated_stream_is_detected() {
689 let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
690 let mut compressed = Vec::new();
691 c.compress(&vec![7u8; 5000], &mut compressed).unwrap();
692 c.finish(&mut compressed).unwrap();
693 compressed.truncate(compressed.len() - 1); let mut d = ZripDecompressor::new();
696 let mut plain = Vec::new();
697 d.decompress(&compressed, &mut plain).unwrap();
698 assert!(matches!(d.finish(&mut plain), Err(DecompressError::Truncated(_))));
699 }
700
701 #[test]
702 fn corrupt_stream_is_detected() {
703 let mut d = ZripDecompressor::new();
704 let mut plain = Vec::new();
705 let err = d.decompress(b"this is not a zstd frame at all", &mut plain);
706 assert!(err.is_err());
707 }
708
709 #[test]
710 fn per_call_output_budget_rejects_multi_frame_bomb() {
711 let mut compressed = Vec::new();
715 for _ in 0..64 {
716 let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
717 c.compress(&vec![7u8; STREAM_BUF_SIZE], &mut compressed)
718 .unwrap();
719 c.finish(&mut compressed).unwrap();
720 }
721 let mut d = ZripDecompressor::with_max_output(MAX_FRAME_OUTPUT);
722 let mut plain = Vec::new();
723 let err = d.decompress(&compressed, &mut plain);
724 assert!(
725 matches!(err, Err(DecompressError::TooLarge { .. })),
726 "expected TooLarge, got {err:?}"
727 );
728 assert!(plain.len() <= MAX_FRAME_OUTPUT);
730 }
731
732 #[test]
733 fn generous_default_budget_allows_coalesced_frames() {
734 let mut compressed = Vec::new();
737 for _ in 0..8 {
738 let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
739 c.compress(&vec![7u8; STREAM_BUF_SIZE], &mut compressed)
740 .unwrap();
741 c.finish(&mut compressed).unwrap();
742 }
743 let mut d = ZripDecompressor::new();
744 let mut plain = Vec::new();
745 d.decompress(&compressed, &mut plain).unwrap();
746 d.finish(&mut plain).unwrap();
747 assert_eq!(plain.len(), 8 * STREAM_BUF_SIZE);
748 }
749
750 #[test]
751 fn zstd_compat_interop() {
752 let data: Vec<u8> = (0..50_000u32).map(|i| (i % 31) as u8).collect();
755 let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
756 let mut compressed = Vec::new();
757 c.compress(&data, &mut compressed).unwrap();
758 c.finish(&mut compressed).unwrap();
759 let decoded = zstd::stream::decode_all(&compressed[..]).unwrap();
760 assert_eq!(decoded, data);
761
762 let zstd_enc = zstd::stream::encode_all(&data[..], 1).unwrap();
763 let mut d = ZripDecompressor::new();
764 let mut plain = Vec::new();
765 d.decompress(&zstd_enc, &mut plain).unwrap();
766 d.finish(&mut plain).unwrap();
767 assert_eq!(plain, data);
768 }
769}