Skip to main content

libfw_core/
compress.rs

1//! Streaming compression abstractions backed by [`zrip`] (zstd).
2//!
3//! # Design
4//!
5//! Both [`Compressor`] and [`Decompressor`] are *stream-oriented*: they
6//! accept bounded input chunks and produce bounded output chunks, so the
7//! heap footprint of processing a file does **not** grow with the file
8//! size. The [`STREAM_BUF_SIZE`](crate::STREAM_BUF_SIZE) sliding window
9//! keeps memory constant (~64 KiB typical, capped well below 4 MiB even
10//! for adversarial frames).
11//!
12//! # Wire format
13//!
14//! The compressed body is a concatenation of independent zstd *frames*:
15//! each [`Compressor::compress`] call emits one complete frame when the
16//! next call arrives (or on [`Compressor::finish`]).
17//!
18//! The decompressor buffers compressed bytes until a complete frame is
19//! present (frame boundaries are detected by a small built-in zstd frame
20//! parser), then decodes exactly that frame with
21//! [`zrip::decompress_with_limit`]. A frame may therefore span several
22//! `decompress()` inputs, and one input may contain several frames.
23//!
24//! # Constant memory
25//!
26//! - **Compressor**: buffers at most one input chunk internally plus the
27//!   zrip encoder workspace (~150 KiB).
28//! - **Decompressor**: holds at most one *compressed* frame
29//!   ([`MAX_PENDING_FRAME`]) and one *decompressed* frame
30//!   ([`MAX_FRAME_OUTPUT`]) at a time.
31//!
32//! The server should feed [`Compressor::compress`] in
33//! [`STREAM_BUF_SIZE`](crate::STREAM_BUF_SIZE) windows (64 KiB), keeping
34//! frames ~64 KiB and the client's transient buffers small.
35
36use std::io::Write;
37
38use crate::error::{CompressError, DecompressError};
39use crate::{CHUNK_SIZE, STREAM_BUF_SIZE};
40
41/// Compression formats understood by libfw.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CompressionFormat {
44    /// No compression; the body passes through verbatim.
45    None,
46    /// zstd via the [`zrip`] codec (levels -8..=4).
47    Zrip,
48}
49
50impl CompressionFormat {
51    /// Wire representation used in
52    /// [`HEADER_COMPRESS`](crate::HEADER_COMPRESS) / `Content-Encoding`.
53    pub fn as_str(self) -> &'static str {
54        match self {
55            CompressionFormat::None => "identity",
56            CompressionFormat::Zrip => "zrip",
57        }
58    }
59
60    /// Parses a header value (`identity`, `zrip`, …).
61    pub fn parse_header(s: &str) -> Option<Self> {
62        match s.trim().to_ascii_lowercase().as_str() {
63            "" | "identity" | "none" => Some(CompressionFormat::None),
64            "zrip" | "zstd" => Some(CompressionFormat::Zrip),
65            _ => None,
66        }
67    }
68}
69
70/// Safety ceiling for the decompressed size of a single frame.
71///
72/// The protocol keeps frames ≤ [`CHUNK_SIZE`](crate::CHUNK_SIZE); this
73/// limit is a guard against buggy or hostile peers.
74pub const MAX_FRAME_OUTPUT: usize = CHUNK_SIZE as usize;
75
76/// Maximum compressed bytes buffered while waiting for a frame boundary.
77///
78/// The worst-case incompressible [`CHUNK_SIZE`](crate::CHUNK_SIZE) chunk
79/// compresses to ≈ chunk size + framing overhead, hence the slack.
80pub const MAX_PENDING_FRAME: usize = CHUNK_SIZE as usize + STREAM_BUF_SIZE;
81
82/// Default zrip compression level (Fast strategy, good for network transfer).
83pub const ZRIP_DEFAULT_LEVEL: i32 = 1;
84
85/// Streaming compressor. Feed it bounded input chunks; it appends the
86/// compressed bytes produced so far to the provided output buffer.
87pub trait Compressor: Send {
88    /// The format this compressor produces.
89    fn format(&self) -> CompressionFormat;
90
91    /// Compress `input`, appending compressed bytes to `out`.
92    ///
93    /// `input` should be kept ≤ [`CHUNK_SIZE`](crate::CHUNK_SIZE) to keep
94    /// the output frames small and memory constant. When the next chunk
95    /// arrives, the previous chunk's frame is finalized and emitted.
96    fn compress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), CompressError>;
97
98    /// Finalize the stream, appending the last frame (if any) to `out`.
99    ///
100    /// Must be called exactly once; afterwards the compressor is spent.
101    fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), CompressError>;
102}
103
104/// Streaming decompressor. Feed it arbitrary compressed bytes; decoded
105/// output is appended to the provided buffer.
106pub trait Decompressor: Send {
107    /// The format this decompressor consumes.
108    fn format(&self) -> CompressionFormat;
109
110    /// Decompress `input`, appending decoded bytes to `out`.
111    ///
112    /// Input may split frames arbitrarily. `out` should be drained by the
113    /// caller after each call to keep memory bounded.
114    fn decompress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), DecompressError>;
115
116    /// Signal the end of the stream.
117    ///
118    /// Flushes any complete trailing frames and verifies the stream is
119    /// well-formed (no truncated frames). Call exactly once.
120    fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), DecompressError>;
121}
122
123/// Construct a compressor for `format`.
124pub fn compressor(format: CompressionFormat) -> Result<Box<dyn Compressor>, CompressError> {
125    match format {
126        CompressionFormat::None => Ok(Box::new(PassthroughCompressor)),
127        CompressionFormat::Zrip => Ok(Box::new(ZripCompressor::new(ZRIP_DEFAULT_LEVEL)?)),
128    }
129}
130
131/// Construct a decompressor for `format`.
132pub fn decompressor(format: CompressionFormat) -> Box<dyn Decompressor> {
133    match format {
134        CompressionFormat::None => Box::new(PassthroughDecompressor),
135        CompressionFormat::Zrip => Box::new(ZripDecompressor::new()),
136    }
137}
138
139// ---------------------------------------------------------------------------
140// Passthrough (identity)
141// ---------------------------------------------------------------------------
142
143struct PassthroughCompressor;
144
145impl Compressor for PassthroughCompressor {
146    fn format(&self) -> CompressionFormat {
147        CompressionFormat::None
148    }
149
150    fn compress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), CompressError> {
151        out.extend_from_slice(input);
152        Ok(())
153    }
154
155    fn finish(&mut self, _out: &mut Vec<u8>) -> Result<(), CompressError> {
156        Ok(())
157    }
158}
159
160struct PassthroughDecompressor;
161
162impl Decompressor for PassthroughDecompressor {
163    fn format(&self) -> CompressionFormat {
164        CompressionFormat::None
165    }
166
167    fn decompress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), DecompressError> {
168        out.extend_from_slice(input);
169        Ok(())
170    }
171
172    fn finish(&mut self, _out: &mut Vec<u8>) -> Result<(), DecompressError> {
173        Ok(())
174    }
175}
176
177// ---------------------------------------------------------------------------
178// zrip (zstd) implementation
179// ---------------------------------------------------------------------------
180
181/// zstd compressor emitting one independent frame per input chunk.
182pub struct ZripCompressor {
183    encoder: Option<zrip::FrameEncoder<Vec<u8>>>,
184    /// Whether data has been written since the last frame boundary.
185    dirty: bool,
186}
187
188impl ZripCompressor {
189    /// Create a compressor at `level` (-8..=4; 0 = library default).
190    pub fn new(level: i32) -> Result<Self, CompressError> {
191        Ok(ZripCompressor {
192            encoder: Some(zrip::FrameEncoder::new(Vec::new(), level)?),
193            dirty: false,
194        })
195    }
196}
197
198impl Compressor for ZripCompressor {
199    fn format(&self) -> CompressionFormat {
200        CompressionFormat::Zrip
201    }
202
203    fn compress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), CompressError> {
204        if input.is_empty() {
205            return Ok(());
206        }
207        let encoder = self
208            .encoder
209            .as_mut()
210            .ok_or_else(|| std::io::Error::other("compressor already finished"))?;
211        // Finalize the previous chunk's frame and hand its compressed bytes back.
212        if self.dirty {
213            let finished = encoder.reset(Vec::new())?;
214            out.extend_from_slice(&finished);
215        }
216        encoder.write_all(input)?;
217        self.dirty = true;
218        Ok(())
219    }
220
221    fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), CompressError> {
222        let encoder = self
223            .encoder
224            .take()
225            .ok_or_else(|| std::io::Error::other("compressor already finished"))?;
226        let tail = encoder.finish()?;
227        out.extend_from_slice(&tail);
228        self.dirty = false;
229        Ok(())
230    }
231}
232
233/// zstd decompressor reassembling frames across arbitrary chunk boundaries.
234pub struct ZripDecompressor {
235    /// Compressed bytes not yet turned into complete frames.
236    pending: Vec<u8>,
237    finished: bool,
238}
239
240impl ZripDecompressor {
241    /// Create a decompressor.
242    pub fn new() -> Self {
243        ZripDecompressor {
244            pending: Vec::with_capacity(STREAM_BUF_SIZE),
245            finished: false,
246        }
247    }
248
249    /// Decode as many complete frames as `pending` holds.
250    fn drain_frames(&mut self, out: &mut Vec<u8>) -> Result<(), DecompressError> {
251        loop {
252            let boundary = frame_boundary(&self.pending);
253            match boundary {
254                FrameBoundary::Complete { len, content_size } => {
255                    if content_size.is_some_and(|cs| cs > MAX_FRAME_OUTPUT as u64) {
256                        return Err(DecompressError::TooLarge {
257                            limit: MAX_FRAME_OUTPUT,
258                        });
259                    }
260                    let decoded = {
261                        let frame = &self.pending[..len];
262                        zrip::decompress_with_limit(frame, MAX_FRAME_OUTPUT).map_err(|e| {
263                            DecompressError::Io(std::io::Error::new(
264                                std::io::ErrorKind::InvalidData,
265                                e,
266                            ))
267                        })?
268                    };
269                    out.extend_from_slice(&decoded);
270                    self.pending.drain(..len);
271                }
272                FrameBoundary::Incomplete => return Ok(()),
273                FrameBoundary::Invalid => {
274                    return Err(DecompressError::Io(std::io::Error::new(
275                        std::io::ErrorKind::InvalidData,
276                        "invalid zstd frame data",
277                    )))
278                }
279            }
280        }
281    }
282}
283
284impl Default for ZripDecompressor {
285    fn default() -> Self {
286        ZripDecompressor::new()
287    }
288}
289
290impl Decompressor for ZripDecompressor {
291    fn format(&self) -> CompressionFormat {
292        CompressionFormat::Zrip
293    }
294
295    fn decompress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), DecompressError> {
296        if self.finished {
297            return Err(DecompressError::Io(std::io::Error::other(
298                "decompressor already finished",
299            )));
300        }
301        if !input.is_empty() {
302            self.pending.extend_from_slice(input);
303        }
304        self.drain_frames(out)?;
305        if self.pending.len() > MAX_PENDING_FRAME {
306            return Err(DecompressError::TooLarge {
307                limit: MAX_PENDING_FRAME,
308            });
309        }
310        Ok(())
311    }
312
313    fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), DecompressError> {
314        if self.finished {
315            return Ok(());
316        }
317        self.finished = true;
318        self.drain_frames(out)?;
319        if !self.pending.is_empty() {
320            return Err(DecompressError::Truncated(std::io::Error::new(
321                std::io::ErrorKind::UnexpectedEof,
322                format!("{} trailing compressed bytes", self.pending.len()),
323            )));
324        }
325        Ok(())
326    }
327}
328
329// ---------------------------------------------------------------------------
330// Minimal zstd frame-boundary parser
331// ---------------------------------------------------------------------------
332
333const ZSTD_MAGIC: u32 = 0xFD2F_B528;
334const SKIPPABLE_MASK: u32 = 0xFFFF_FFF0;
335const SKIPPABLE_MAGIC: u32 = 0x184D_2A50;
336
337enum FrameBoundary {
338    /// A complete frame occupying `len` bytes, declaring `content_size`
339    /// output bytes (`None` when the frame header does not state it).
340    Complete { len: usize, content_size: Option<u64> },
341    /// Need more bytes to know the boundary.
342    Incomplete,
343    /// Corrupt / unsupported.
344    Invalid,
345}
346
347/// Determine the byte length of the next zstd frame in `buf` (if present).
348fn frame_boundary(buf: &[u8]) -> FrameBoundary {
349    if buf.len() < 4 {
350        return FrameBoundary::Incomplete;
351    }
352    let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
353    if magic == ZSTD_MAGIC {
354        zstd_frame_boundary(buf)
355    } else if (magic & SKIPPABLE_MASK) == SKIPPABLE_MAGIC {
356        skippable_frame_boundary(buf)
357    } else {
358        FrameBoundary::Invalid
359    }
360}
361
362/// Skippable frames: `magic(4) | skip_size u32(4) | payload(skip_size)`.
363fn skippable_frame_boundary(buf: &[u8]) -> FrameBoundary {
364    if buf.len() < 8 {
365        return FrameBoundary::Incomplete;
366    }
367    let skip = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
368    let total = 8usize.saturating_add(skip);
369    if buf.len() >= total {
370        FrameBoundary::Complete {
371            len: total,
372            content_size: Some(0),
373        }
374    } else {
375        FrameBoundary::Incomplete
376    }
377}
378
379/// Walk the frame header + blocks of a standard zstd frame.
380///
381/// Layout: `magic(4) descriptor(1) [window(1)] [dict_id(0/1/2/4)]
382/// [content_size(0/1/2/4/8)] block* checksum(0/4)`.
383fn zstd_frame_boundary(buf: &[u8]) -> FrameBoundary {
384    if buf.len() < 5 {
385        return FrameBoundary::Incomplete;
386    }
387    let descriptor = buf[4];
388    // Reserved bits (3..=4) must be zero.
389    if descriptor & 0x18 != 0 {
390        return FrameBoundary::Invalid;
391    }
392    let single_segment = descriptor & 0x20 != 0;
393    let checksum = descriptor & 0x04 != 0;
394    let dict_id_flag = descriptor & 0x03;
395    let fcs_flag = (descriptor >> 6) & 0x03;
396
397    let mut hdr_len = 5usize;
398    if !single_segment {
399        hdr_len += 1; // window descriptor
400    }
401    hdr_len += match dict_id_flag {
402        0 => 0,
403        1 => 1,
404        2 => 2,
405        3 => 4,
406        _ => unreachable!(),
407    };
408    let fcs_size: usize = match fcs_flag {
409        0 if single_segment => 1,
410        0 => 0,
411        1 => 2,
412        2 => 4,
413        3 => 8,
414        _ => unreachable!(),
415    };
416    hdr_len += fcs_size;
417
418    if buf.len() < hdr_len {
419        return FrameBoundary::Incomplete;
420    }
421    let content_size = if fcs_size > 0 {
422        let mut v = 0u64;
423        for (i, &b) in buf[5..5 + fcs_size].iter().enumerate() {
424            v |= (b as u64) << (8 * i);
425        }
426        Some(v)
427    } else {
428        None
429    };
430
431    // Walk blocks until the last-block flag.
432    let mut off = hdr_len;
433    loop {
434        if buf.len() < off + 3 {
435            return FrameBoundary::Incomplete;
436        }
437        // Standard zstd block header (as read by the reference decoder):
438        // bit 0 = last_block, bits 1..=2 = block_type, bits 3..=23 = size.
439        let block_header = u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], 0]);
440        let last = block_header & 0x01 != 0;
441        let block_type = (block_header >> 1) & 0x03;
442        let block_size = (block_header >> 3) as usize;
443        if block_type == 3 {
444            // Reserved block type.
445            return FrameBoundary::Invalid;
446        }
447        off += 3 + block_size;
448        if off > MAX_PENDING_FRAME {
449            return FrameBoundary::Invalid;
450        }
451        if last {
452            break;
453        }
454    }
455    if checksum {
456        off += 4;
457    }
458    if buf.len() < off {
459        return FrameBoundary::Incomplete;
460    }
461    FrameBoundary::Complete {
462        len: off,
463        content_size,
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    fn roundtrip_chunks(data: &[u8], feed: &[usize]) {
472        let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
473        let mut compressed = Vec::new();
474        let mut off = 0;
475        for &n in feed {
476            let end = (off + n).min(data.len());
477            if end > off {
478                c.compress(&data[off..end], &mut compressed).unwrap();
479            }
480            off = end;
481        }
482        c.finish(&mut compressed).unwrap();
483        assert!(!compressed.is_empty());
484
485        // Decode with arbitrarily split inputs, including mid-frame splits.
486        let mut d = ZripDecompressor::new();
487        let mut plain = Vec::new();
488        let mut step = 1;
489        let mut i = 0;
490        while i < compressed.len() {
491            let end = (i + step).min(compressed.len());
492            d.decompress(&compressed[i..end], &mut plain).unwrap();
493            i = end;
494            step = step % 5 + 1; // 1..=5
495        }
496        d.finish(&mut plain).unwrap();
497        assert_eq!(plain, data, "roundtrip mismatch with feed {feed:?}");
498    }
499
500    #[test]
501    fn roundtrip_single_chunk() {
502        let data: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
503        roundtrip_chunks(&data, &[data.len()]);
504    }
505
506    #[test]
507    fn roundtrip_multi_chunk_64k_windows() {
508        // Server-style: feed in 64 KiB sliding windows.
509        let data: Vec<u8> = (0..300_000u32).map(|i| (i / 7) as u8).collect();
510        let feed: Vec<usize> = std::iter::repeat(STREAM_BUF_SIZE).take(5).collect();
511        roundtrip_chunks(&data, &feed);
512    }
513
514    #[test]
515    fn roundtrip_highly_compressible() {
516        let data = b"libfw streaming compression test. ".repeat(10_000);
517        // Feed sizes cycling 1KiB..16KiB until the whole input is consumed.
518        let mut feed = Vec::new();
519        let mut consumed = 0;
520        for (_, &size) in [1024usize, 2048, 4096, 8192, 16384].iter().cycle().enumerate() {
521            feed.push(size);
522            consumed += size;
523            if consumed >= data.len() {
524                break;
525            }
526        }
527        roundtrip_chunks(&data, &feed);
528    }
529
530    #[test]
531    fn roundtrip_empty_stream() {
532        let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
533        let mut compressed = Vec::new();
534        c.finish(&mut compressed).unwrap();
535
536        let mut d = ZripDecompressor::new();
537        let mut plain = Vec::new();
538        d.decompress(&compressed, &mut plain).unwrap();
539        d.finish(&mut plain).unwrap();
540        assert!(plain.is_empty());
541    }
542
543    #[test]
544    fn empty_input_chunks_are_noops() {
545        let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
546        let mut out = Vec::new();
547        c.compress(&[], &mut out).unwrap();
548        c.compress(b"hello", &mut out).unwrap();
549        c.finish(&mut out).unwrap();
550
551        let mut d = ZripDecompressor::new();
552        let mut plain = Vec::new();
553        d.decompress(&out, &mut plain).unwrap();
554        d.finish(&mut plain).unwrap();
555        assert_eq!(plain, b"hello");
556    }
557
558    #[test]
559    fn format_header_roundtrip() {
560        assert_eq!(CompressionFormat::parse_header("zrip"), Some(CompressionFormat::Zrip));
561        assert_eq!(CompressionFormat::parse_header("ZSTD"), Some(CompressionFormat::Zrip));
562        assert_eq!(CompressionFormat::parse_header("identity"), Some(CompressionFormat::None));
563        assert_eq!(CompressionFormat::parse_header(""), Some(CompressionFormat::None));
564        assert_eq!(CompressionFormat::parse_header("br"), None);
565        assert_eq!(CompressionFormat::Zrip.as_str(), "zrip");
566        assert_eq!(CompressionFormat::None.as_str(), "identity");
567    }
568
569    #[test]
570    fn passthrough_roundtrip() {
571        let mut c = compressor(CompressionFormat::None).unwrap();
572        let mut d = decompressor(CompressionFormat::None);
573        let mut compressed = Vec::new();
574        let mut plain = Vec::new();
575        c.compress(b"abc", &mut compressed).unwrap();
576        c.finish(&mut compressed).unwrap();
577        d.decompress(&compressed, &mut plain).unwrap();
578        d.finish(&mut plain).unwrap();
579        assert_eq!(plain, b"abc");
580    }
581
582    #[test]
583    fn truncated_stream_is_detected() {
584        let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
585        let mut compressed = Vec::new();
586        c.compress(&vec![7u8; 5000], &mut compressed).unwrap();
587        c.finish(&mut compressed).unwrap();
588        compressed.truncate(compressed.len() - 1); // chop one byte
589
590        let mut d = ZripDecompressor::new();
591        let mut plain = Vec::new();
592        d.decompress(&compressed, &mut plain).unwrap();
593        assert!(matches!(d.finish(&mut plain), Err(DecompressError::Truncated(_))));
594    }
595
596    #[test]
597    fn corrupt_stream_is_detected() {
598        let mut d = ZripDecompressor::new();
599        let mut plain = Vec::new();
600        let err = d.decompress(b"this is not a zstd frame at all", &mut plain);
601        assert!(err.is_err());
602    }
603
604    #[test]
605    fn zstd_compat_interop() {
606        // Our frames are standard zstd: the reference `zstd` codec can
607        // decode them, and we can decode its output.
608        let data: Vec<u8> = (0..50_000u32).map(|i| (i % 31) as u8).collect();
609        let mut c = ZripCompressor::new(ZRIP_DEFAULT_LEVEL).unwrap();
610        let mut compressed = Vec::new();
611        c.compress(&data, &mut compressed).unwrap();
612        c.finish(&mut compressed).unwrap();
613        let decoded = zstd::stream::decode_all(&compressed[..]).unwrap();
614        assert_eq!(decoded, data);
615
616        let zstd_enc = zstd::stream::encode_all(&data[..], 1).unwrap();
617        let mut d = ZripDecompressor::new();
618        let mut plain = Vec::new();
619        d.decompress(&zstd_enc, &mut plain).unwrap();
620        d.finish(&mut plain).unwrap();
621        assert_eq!(plain, data);
622    }
623}