moq_flate/lib.rs
1//! Group-scoped DEFLATE: a stream of self-delimited frames sharing one compression window.
2//!
3//! A sequence of frame payloads is compressed into a single raw DEFLATE ([RFC 1951]) stream,
4//! sync-flushed at each frame boundary. Every frame is therefore self-delimited (byte-aligned, the
5//! window retained) while later frames reuse the earlier ones as context, so a stream of similar
6//! payloads (a snapshot followed by deltas, repeated records, log lines) compresses far better than
7//! each payload alone. The [`Encoder`]/[`Decoder`] hold that shared window; create a fresh pair per
8//! independent stream (in moq-net terms, per group).
9//!
10//! This is plain raw DEFLATE with a `Z_SYNC_FLUSH` after each frame, so any peer using the same
11//! primitive (zlib's sync flush, the browser's `deflate-raw`) interoperates on the wire. There is no
12//! length prefix: the caller is expected to frame each slice (moq-net already does). A small slice
13//! can still inflate to far more than its own size, so [`Decoder::frame`] bounds each frame's output.
14//!
15//! A sync flush always ends in the 4-byte empty-block marker `00 00 ff ff`. That marker is fixed, so
16//! [`Encoder::frame`] drops it from each slice and [`Decoder::frame`] re-appends it before inflating,
17//! saving 4 bytes per frame. This is the same trick [RFC 7692] (permessage-deflate) uses for
18//! WebSocket messages.
19//!
20//! ```ignore
21//! let mut encoder = moq_flate::Encoder::new();
22//! let a = encoder.frame(b"the quick brown fox");
23//! let b = encoder.frame(b"the quick brown dog"); // smaller: reuses the window
24//!
25//! let mut decoder = moq_flate::Decoder::new();
26//! assert_eq!(decoder.frame(&a)?, &b"the quick brown fox"[..]);
27//! assert_eq!(decoder.frame(&b)?, &b"the quick brown dog"[..]);
28//! ```
29//!
30//! [RFC 1951]: https://www.rfc-editor.org/rfc/rfc1951.html
31//! [RFC 7692]: https://www.rfc-editor.org/rfc/rfc7692.html#section-7.2.1
32
33use bytes::Bytes;
34use flate2::{Compress, Decompress, FlushCompress, FlushDecompress, Status};
35
36/// The default DEFLATE level ([`Encoder::new`]): zlib's own default, a good size/speed balance for
37/// the small, repetitive payloads this targets.
38pub const DEFAULT_LEVEL: u32 = 6;
39
40/// The default per-frame decompressed-size cap ([`Decoder::new`]): 64 MiB.
41pub const DEFAULT_MAX_FRAME_SIZE: u64 = 64 * 1024 * 1024;
42
43/// The trailing bytes of a DEFLATE sync flush, stripped on the wire and re-appended to decode.
44const SYNC_FLUSH_TAIL: [u8; 4] = [0x00, 0x00, 0xff, 0xff];
45
46/// Scratch buffer size for the streaming (de)compress loops.
47const CHUNK: usize = 8 * 1024;
48
49/// Errors produced while decoding a frame.
50#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum Error {
53 /// A frame could not be decoded (malformed or truncated stream, or fed out of order).
54 #[error("decompression failed")]
55 Decompress,
56
57 /// A frame's decompressed size exceeded the configured limit (zip-bomb guard).
58 #[error("decompressed frame exceeded {0} bytes")]
59 TooLarge(u64),
60}
61
62/// A [`Result`](std::result::Result) using this crate's [`Error`].
63pub type Result<T> = std::result::Result<T, Error>;
64
65/// Encodes a stream's frame payloads into one shared DEFLATE window, one self-delimited slice per
66/// frame. Hold one per stream; create a fresh one for each independent stream.
67pub struct Encoder(Compress);
68
69impl Encoder {
70 /// Start a fresh encoder with a cold window at [`DEFAULT_LEVEL`].
71 pub fn new() -> Self {
72 Self::with_level(DEFAULT_LEVEL)
73 }
74
75 /// Start a fresh encoder with a cold window at the given DEFLATE level (`0..=9`; higher is
76 /// smaller and slower). Values above `9` are clamped.
77 pub fn with_level(level: u32) -> Self {
78 // `false`: raw DEFLATE, no zlib header/trailer, matching `deflate-raw` on the browser side.
79 Self(Compress::new(flate2::Compression::new(level.min(9)), false))
80 }
81
82 /// Compress the next frame's `payload`, returning its slice of the stream: the DEFLATE bytes minus
83 /// the fixed sync-flush marker. Empty in yields empty out. Later frames reuse earlier ones as
84 /// context, so slices must be produced (and later decoded) in frame order.
85 pub fn frame(&mut self, payload: &[u8]) -> Bytes {
86 if payload.is_empty() {
87 return Bytes::new();
88 }
89
90 let mut out = Vec::with_capacity(payload.len() / 2 + 16);
91 let mut tmp = [0u8; CHUNK];
92 let mut input = payload;
93
94 // Drive the stream with a sync flush so this frame's slice is self-delimited (byte-aligned,
95 // window retained). The classic zlib loop: keep going while the output buffer fills up.
96 loop {
97 let before_in = self.0.total_in();
98 let before_out = self.0.total_out();
99 self.0.compress(input, &mut tmp, FlushCompress::Sync).expect("deflate");
100 let consumed = (self.0.total_in() - before_in) as usize;
101 let produced = (self.0.total_out() - before_out) as usize;
102 out.extend_from_slice(&tmp[..produced]);
103 input = &input[consumed..];
104 if produced < tmp.len() {
105 break;
106 }
107 }
108
109 // Drop the fixed sync-flush marker; the decoder re-appends it (see the module docs).
110 debug_assert!(
111 out.ends_with(&SYNC_FLUSH_TAIL),
112 "a sync flush must end in the deflate marker"
113 );
114 out.truncate(out.len() - SYNC_FLUSH_TAIL.len());
115 Bytes::from(out)
116 }
117}
118
119impl Default for Encoder {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125/// Decodes a stream's frame slices back into the original payloads. Hold one per stream; feed slices
126/// in frame order (each frame builds on the earlier ones).
127pub struct Decoder {
128 inner: Decompress,
129 max_frame_size: u64,
130}
131
132impl Decoder {
133 /// Start a fresh decoder with a cold window and the [`DEFAULT_MAX_FRAME_SIZE`] cap.
134 pub fn new() -> Self {
135 Self::with_max_frame_size(DEFAULT_MAX_FRAME_SIZE)
136 }
137
138 /// Start a fresh decoder with a cold window and a custom per-frame decompressed-size cap.
139 ///
140 /// A malicious or buggy peer could send a tiny slice that inflates hugely, so [`frame`](Self::frame)
141 /// stops and returns [`Error::TooLarge`] once a single frame's output would exceed `max_frame_size`.
142 pub fn with_max_frame_size(max_frame_size: u64) -> Self {
143 // `false`: raw DEFLATE, matching the encoder.
144 Self {
145 inner: Decompress::new(false),
146 max_frame_size,
147 }
148 }
149
150 /// Decompress the next frame's `slice` back into its payload.
151 ///
152 /// An empty slice yields an empty payload. Returns [`Error::TooLarge`] if the frame inflates past
153 /// the configured cap (checked as output is produced, not from any declared size), and
154 /// [`Error::Decompress`] on malformed input.
155 pub fn frame(&mut self, slice: &[u8]) -> Result<Bytes> {
156 if slice.is_empty() {
157 return Ok(Bytes::new());
158 }
159
160 let mut out = Vec::new();
161 let mut tmp = [0u8; CHUNK];
162
163 // Feed the wire slice followed by the re-appended sync-flush marker, which delimits the frame
164 // and flushes its last bytes out of the inflate buffer.
165 for segment in [slice, &SYNC_FLUSH_TAIL] {
166 let mut input = segment;
167 loop {
168 let before_in = self.inner.total_in();
169 let before_out = self.inner.total_out();
170 let status = self
171 .inner
172 .decompress(input, &mut tmp, FlushDecompress::Sync)
173 .map_err(|_| Error::Decompress)?;
174 let consumed = (self.inner.total_in() - before_in) as usize;
175 let produced = (self.inner.total_out() - before_out) as usize;
176 // Bound the inflated output as it is produced; a tiny slice can expand enormously.
177 if out.len() as u64 + produced as u64 > self.max_frame_size {
178 return Err(Error::TooLarge(self.max_frame_size));
179 }
180 out.extend_from_slice(&tmp[..produced]);
181 input = &input[consumed..];
182
183 // Move to the next segment once this one is drained and the buffer wasn't saturated. The
184 // no-progress guard avoids spinning when the marker needs no further output.
185 if matches!(status, Status::StreamEnd) || (input.is_empty() && produced < tmp.len()) {
186 break;
187 }
188 if consumed == 0 && produced == 0 {
189 break;
190 }
191 }
192 }
193
194 Ok(Bytes::from(out))
195 }
196}
197
198impl Default for Decoder {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[cfg(test)]
205mod test {
206 use super::*;
207
208 /// Round-trip a sequence of frames through an encoder/decoder pair.
209 fn roundtrip(frames: &[&[u8]]) -> Vec<Vec<u8>> {
210 let mut enc = Encoder::new();
211 let slices: Vec<Bytes> = frames.iter().map(|f| enc.frame(f)).collect();
212
213 let mut dec = Decoder::new();
214 slices.iter().map(|s| dec.frame(s).unwrap().to_vec()).collect()
215 }
216
217 #[test]
218 fn stream_roundtrip() {
219 let frames: &[&[u8]] = &[b"the quick brown fox", b"the quick brown dog", b"the lazy fox"];
220 let got = roundtrip(frames);
221 for (a, b) in frames.iter().zip(&got) {
222 assert_eq!(*a, b.as_slice());
223 }
224 }
225
226 #[test]
227 fn empty_frames_roundtrip() {
228 assert!(Encoder::new().frame(b"").is_empty());
229 assert!(Decoder::new().frame(b"").unwrap().is_empty());
230 }
231
232 #[test]
233 fn cross_frame_context_shrinks() {
234 // A later frame identical to an earlier one compresses to far fewer bytes once the window
235 // holds the earlier copy: this is the whole point of a shared stream.
236 let payload = b"Media over QUIC delivers real-time latency at massive scale.".repeat(6);
237 let mut enc = Encoder::new();
238 let first = enc.frame(&payload);
239 let second = enc.frame(&payload);
240 assert!(
241 second.len() < first.len(),
242 "repeat frame {} should be smaller than first {}",
243 second.len(),
244 first.len()
245 );
246 }
247
248 #[test]
249 fn frame_larger_than_chunk_roundtrips() {
250 // High-entropy data barely compresses, so its slice exceeds the streaming `CHUNK` scratch
251 // buffer and the (de)compress loops must iterate. Verify it still round-trips byte for byte.
252 let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
253 let payload: Vec<u8> = (0..64 * 1024)
254 .map(|_| {
255 state ^= state << 13;
256 state ^= state >> 7;
257 state ^= state << 17;
258 (state >> 56) as u8
259 })
260 .collect();
261
262 let mut enc = Encoder::new();
263 let slice = enc.frame(&payload);
264 assert!(slice.len() > CHUNK, "slice {} should exceed CHUNK {CHUNK}", slice.len());
265
266 let mut dec = Decoder::new();
267 assert_eq!(dec.frame(&slice).unwrap(), Bytes::from(payload));
268 }
269
270 #[test]
271 fn decompress_rejects_garbage() {
272 let mut dec = Decoder::new();
273 assert_eq!(dec.frame(b"not a deflate stream at all"), Err(Error::Decompress));
274 }
275
276 #[test]
277 fn enforces_max_frame_size() {
278 // A tiny slice of a highly compressible payload inflates past a small cap.
279 let payload = vec![0u8; 1024];
280 let slice = Encoder::new().frame(&payload);
281
282 let mut dec = Decoder::with_max_frame_size(512);
283 assert_eq!(dec.frame(&slice), Err(Error::TooLarge(512)));
284 }
285
286 #[test]
287 fn custom_level_roundtrips() {
288 let payload = b"compress me at maximum effort".repeat(8);
289 let mut enc = Encoder::with_level(9);
290 let slice = enc.frame(&payload);
291 let mut dec = Decoder::new();
292 assert_eq!(dec.frame(&slice).unwrap(), Bytes::from(payload));
293 }
294}