Skip to main content

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.
67// Boxed: zlib-rs keeps its ~140-byte stream header inline, which would bloat every type that embeds
68// an encoder or decoder.
69pub struct Encoder(Box<Compress>);
70
71impl Encoder {
72	/// Start a fresh encoder with a cold window at [`DEFAULT_LEVEL`].
73	pub fn new() -> Self {
74		Self::with_level(DEFAULT_LEVEL)
75	}
76
77	/// Start a fresh encoder with a cold window at the given DEFLATE level (`0..=9`; higher is
78	/// smaller and slower). Values above `9` are clamped.
79	pub fn with_level(level: u32) -> Self {
80		// `false`: raw DEFLATE, no zlib header/trailer, matching `deflate-raw` on the browser side.
81		Self(Box::new(Compress::new(flate2::Compression::new(level.min(9)), false)))
82	}
83
84	/// Compress the next frame's `payload`, returning its slice of the stream: the DEFLATE bytes minus
85	/// the fixed sync-flush marker. Empty in yields empty out. Later frames reuse earlier ones as
86	/// context, so slices must be produced (and later decoded) in frame order.
87	pub fn frame(&mut self, payload: &[u8]) -> Bytes {
88		if payload.is_empty() {
89			return Bytes::new();
90		}
91
92		let mut out = Vec::with_capacity(payload.len() / 2 + 16);
93		let mut tmp = [0u8; CHUNK];
94		let mut input = payload;
95
96		// Drive the stream with a sync flush so this frame's slice is self-delimited (byte-aligned,
97		// window retained). The classic zlib loop: keep going while the output buffer fills up.
98		loop {
99			let before_in = self.0.total_in();
100			let before_out = self.0.total_out();
101			self.0.compress(input, &mut tmp, FlushCompress::Sync).expect("deflate");
102			let consumed = (self.0.total_in() - before_in) as usize;
103			let produced = (self.0.total_out() - before_out) as usize;
104			out.extend_from_slice(&tmp[..produced]);
105			input = &input[consumed..];
106			if produced < tmp.len() {
107				break;
108			}
109		}
110
111		// Drop the fixed sync-flush marker; the decoder re-appends it (see the module docs). A missing
112		// marker means the backend returned before finishing the flush; panic rather than emit a
113		// truncated frame.
114		assert!(
115			out.ends_with(&SYNC_FLUSH_TAIL),
116			"a sync flush must end in the deflate marker"
117		);
118		out.truncate(out.len() - SYNC_FLUSH_TAIL.len());
119		Bytes::from(out)
120	}
121}
122
123impl Default for Encoder {
124	fn default() -> Self {
125		Self::new()
126	}
127}
128
129/// Decodes a stream's frame slices back into the original payloads. Hold one per stream; feed slices
130/// in frame order (each frame builds on the earlier ones).
131pub struct Decoder {
132	inner: Box<Decompress>,
133	max_frame_size: u64,
134}
135
136impl Decoder {
137	/// Start a fresh decoder with a cold window and the [`DEFAULT_MAX_FRAME_SIZE`] cap.
138	pub fn new() -> Self {
139		Self::with_max_frame_size(DEFAULT_MAX_FRAME_SIZE)
140	}
141
142	/// Start a fresh decoder with a cold window and a custom per-frame decompressed-size cap.
143	///
144	/// A malicious or buggy peer could send a tiny slice that inflates hugely, so [`frame`](Self::frame)
145	/// stops and returns [`Error::TooLarge`] once a single frame's output would exceed `max_frame_size`.
146	pub fn with_max_frame_size(max_frame_size: u64) -> Self {
147		// `false`: raw DEFLATE, matching the encoder.
148		Self {
149			inner: Box::new(Decompress::new(false)),
150			max_frame_size,
151		}
152	}
153
154	/// Decompress the next frame's `slice` back into its payload.
155	///
156	/// An empty slice yields an empty payload. Returns [`Error::TooLarge`] if the frame inflates past
157	/// the configured cap (checked as output is produced, not from any declared size), and
158	/// [`Error::Decompress`] on malformed input.
159	pub fn frame(&mut self, slice: &[u8]) -> Result<Bytes> {
160		let mut out = Vec::new();
161		self.frame_into(slice, &mut out)?;
162		Ok(Bytes::from(out))
163	}
164
165	/// Inflate the next frame into a reusable buffer, replacing its previous contents.
166	///
167	/// A caller that consumes frames immediately can keep one buffer for the whole stream.
168	pub fn frame_into(&mut self, slice: &[u8], out: &mut Vec<u8>) -> Result<()> {
169		out.clear();
170		if slice.is_empty() {
171			return Ok(());
172		}
173		let mut tmp = [0u8; CHUNK];
174
175		// Feed the wire slice followed by the re-appended sync-flush marker, which delimits the frame
176		// and flushes its last bytes out of the inflate buffer.
177		for segment in [slice, &SYNC_FLUSH_TAIL] {
178			let mut input = segment;
179			loop {
180				let before_in = self.inner.total_in();
181				let before_out = self.inner.total_out();
182				let status = self
183					.inner
184					.decompress(input, &mut tmp, FlushDecompress::Sync)
185					.map_err(|_| Error::Decompress)?;
186				let consumed = (self.inner.total_in() - before_in) as usize;
187				let produced = (self.inner.total_out() - before_out) as usize;
188				// Bound the inflated output as it is produced; a tiny slice can expand enormously.
189				if out.len() as u64 + produced as u64 > self.max_frame_size {
190					return Err(Error::TooLarge(self.max_frame_size));
191				}
192				out.extend_from_slice(&tmp[..produced]);
193				input = &input[consumed..];
194
195				// Move to the next segment once this one is drained and the buffer wasn't saturated. The
196				// no-progress guard avoids spinning when the marker needs no further output.
197				if matches!(status, Status::StreamEnd) || (input.is_empty() && produced < tmp.len()) {
198					break;
199				}
200				if consumed == 0 && produced == 0 {
201					break;
202				}
203			}
204		}
205
206		Ok(())
207	}
208}
209
210impl Default for Decoder {
211	fn default() -> Self {
212		Self::new()
213	}
214}
215
216#[cfg(test)]
217mod test {
218	use super::*;
219
220	/// Round-trip a sequence of frames through an encoder/decoder pair.
221	fn roundtrip(frames: &[&[u8]]) -> Vec<Vec<u8>> {
222		let mut enc = Encoder::new();
223		let slices: Vec<Bytes> = frames.iter().map(|f| enc.frame(f)).collect();
224
225		let mut dec = Decoder::new();
226		slices.iter().map(|s| dec.frame(s).unwrap().to_vec()).collect()
227	}
228
229	#[test]
230	fn stream_roundtrip() {
231		let frames: &[&[u8]] = &[b"the quick brown fox", b"the quick brown dog", b"the lazy fox"];
232		let got = roundtrip(frames);
233		for (a, b) in frames.iter().zip(&got) {
234			assert_eq!(*a, b.as_slice());
235		}
236	}
237
238	#[test]
239	fn frame_into_reuses_its_output_buffer() {
240		let mut encoder = Encoder::new();
241		let frames = [b"first payload".as_slice(), b"second payload".as_slice()];
242		let mut decoder = Decoder::new();
243		let mut out = Vec::with_capacity(64);
244		let ptr = out.as_ptr();
245		for frame in frames {
246			decoder.frame_into(&encoder.frame(frame), &mut out).unwrap();
247			assert_eq!(out, frame);
248			assert_eq!(out.as_ptr(), ptr);
249		}
250		decoder.frame_into(b"", &mut out).unwrap();
251		assert!(out.is_empty());
252	}
253
254	#[test]
255	fn empty_frames_roundtrip() {
256		assert!(Encoder::new().frame(b"").is_empty());
257		assert!(Decoder::new().frame(b"").unwrap().is_empty());
258	}
259
260	#[test]
261	fn cross_frame_context_shrinks() {
262		// A later frame identical to an earlier one compresses to far fewer bytes once the window
263		// holds the earlier copy: this is the whole point of a shared stream.
264		let payload = b"Media over QUIC delivers real-time latency at massive scale.".repeat(6);
265		let mut enc = Encoder::new();
266		let first = enc.frame(&payload);
267		let second = enc.frame(&payload);
268		assert!(
269			second.len() < first.len(),
270			"repeat frame {} should be smaller than first {}",
271			second.len(),
272			first.len()
273		);
274	}
275
276	/// Deterministic high-entropy bytes: they barely compress, so slices outgrow `CHUNK`.
277	fn noise(len: usize) -> Vec<u8> {
278		let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
279		(0..len)
280			.map(|_| {
281				state ^= state << 13;
282				state ^= state >> 7;
283				state ^= state << 17;
284				(state >> 56) as u8
285			})
286			.collect()
287	}
288
289	#[test]
290	fn frame_larger_than_chunk_roundtrips() {
291		// High-entropy data barely compresses, so its slice exceeds the streaming `CHUNK` scratch
292		// buffer and the (de)compress loops must iterate. Verify it still round-trips byte for byte.
293		let payload = noise(64 * 1024);
294
295		let mut enc = Encoder::new();
296		let slice = enc.frame(&payload);
297		assert!(slice.len() > CHUNK, "slice {} should exceed CHUNK {CHUNK}", slice.len());
298
299		let mut dec = Decoder::new();
300		assert_eq!(dec.frame(&slice).unwrap(), Bytes::from(payload));
301	}
302
303	#[test]
304	fn block_boundary_at_frame_end_roundtrips() {
305		// Sweep frame sizes so a DEFLATE block closes within some frame's final bytes, while its sync
306		// flush is still pending (miniz_oxide closes one every ~31 KiB of incompressible input and
307		// then returned early, truncating the frame). Each frame is fresh noise; a repeat would match
308		// the window instead.
309		let lens: Vec<usize> = (31 * 1024..32 * 1024 + 256).step_by(16).collect();
310		let noise = noise(lens.iter().sum());
311
312		let mut enc = Encoder::new();
313		let mut dec = Decoder::new();
314		let mut rest = noise.as_slice();
315		for len in lens {
316			let (frame, next) = rest.split_at(len);
317			rest = next;
318			let got = dec
319				.frame(&enc.frame(frame))
320				.unwrap_or_else(|err| panic!("{len} byte frame: {err}"));
321			assert!(got == frame, "{len} byte frame corrupted");
322		}
323	}
324
325	#[test]
326	fn decompress_rejects_garbage() {
327		let mut dec = Decoder::new();
328		assert_eq!(dec.frame(b"not a deflate stream at all"), Err(Error::Decompress));
329	}
330
331	#[test]
332	fn enforces_max_frame_size() {
333		// A tiny slice of a highly compressible payload inflates past a small cap.
334		let payload = vec![0u8; 1024];
335		let slice = Encoder::new().frame(&payload);
336
337		let mut dec = Decoder::with_max_frame_size(512);
338		assert_eq!(dec.frame(&slice), Err(Error::TooLarge(512)));
339	}
340
341	#[test]
342	fn custom_level_roundtrips() {
343		let payload = b"compress me at maximum effort".repeat(8);
344		let mut enc = Encoder::with_level(9);
345		let slice = enc.frame(&payload);
346		let mut dec = Decoder::new();
347		assert_eq!(dec.frame(&slice).unwrap(), Bytes::from(payload));
348	}
349}