libzstd_bitexact_rs/stream_encode.rs
1//! Streaming compression with `ZSTD_compressStream2` semantics, aiming for
2//! **byte-identical output to C libzstd 1.5.7** for the same sequence of
3//! compress/flush/finish operations.
4//!
5//! This is a port of the buffered-mode path (`ZSTD_bm_buffered`, the
6//! `ZSTD_compressStream2` default): input is staged in an internal buffer of
7//! `windowSize + blockSize` bytes (`ZSTD_resetCCtx_internal`) and compressed
8//! one `blockSize` chunk at a time through the shared frame machinery
9//! ([`crate::compress::FrameCompressor`] = `ZSTD_compressContinue` /
10//! `ZSTD_compressEnd`). The output-side staging buffer of the C code is pure
11//! plumbing (bytes are identical with any output capacity), so it is not
12//! reproduced.
13//!
14//! Parameter derivation differs from the one-shot path exactly as in C: with
15//! no pledged content size, `ZSTD_getCParamsFromCCtxParams` resolves with
16//! `ZSTD_CONTENTSIZE_UNKNOWN`, which selects the "default" srcSize class and
17//! skips the window resize — so a stream compressed in chunks legitimately
18//! differs from `ZSTD_compress` of the same bytes (windowed frame header, no
19//! content size) unless the size is pledged up front or the whole input is
20//! handed to the first [`StreamEncoder::finish`] call (the C auto-pledge).
21//!
22//! Current scope: when the staged stream outgrows the input buffer
23//! (`windowSize + blockSize` bytes — 640 KiB at level 1 up to 64 MiB+ at the
24//! top levels), the buffer wraps and the previous segment becomes an
25//! *extDict*. All nine strategies' extDict match finders are ported, and
26//! index overflow correction recycles the 32-bit index space past 3500 MiB,
27//! so every level streams without any length limit — including the
28//! configurations where C auto-enables long-distance matching (`strategy >=
29//! btopt && windowLog >= 27`, i.e. level 22 at unknown content size), whose
30//! LDM match finder ([`crate::ldm`]) is bit-exact.
31//!
32//! [`StreamEncoder::with_workers`] switches to the **multithreaded** streaming
33//! path (`ZSTD_compressStream2` with `nbWorkers >= 1`), reproduced
34//! single-threaded by buffering the input into job-sized sections
35//! ([`crate::compress::MtStreamState`] / [`crate::compress_mt`]).
36
37use crate::compress::{FrameCompressor, MtStreamState, ZSTDMT_JOBSIZE_MIN, compress_mt};
38use crate::error::Error;
39
40/// `ZSTD_EndDirective`.
41#[derive(PartialEq, Eq, Clone, Copy)]
42enum EndOp {
43 Continue,
44 Flush,
45 End,
46}
47
48/// Streaming Zstandard encoder (`ZSTD_compressStream2` semantics).
49///
50/// Output produced by a given sequence of [`compress`](Self::compress),
51/// [`flush`](Self::flush) and [`finish`](Self::finish) calls is byte-identical
52/// to C libzstd 1.5.7 fed the same input chunks with the same
53/// `ZSTD_e_continue` / `ZSTD_e_flush` / `ZSTD_e_end` directives.
54///
55/// ```
56/// let mut out = Vec::new();
57/// let mut enc = libzstd_bitexact_rs::StreamEncoder::new(3);
58/// enc.compress(b"hello ", &mut out).unwrap();
59/// enc.compress(b"world", &mut out).unwrap();
60/// enc.finish(b"", &mut out).unwrap();
61/// assert_eq!(libzstd_bitexact_rs::decompress(&out).unwrap(), b"hello world");
62/// ```
63pub struct StreamEncoder {
64 level: i32,
65 /// `ZSTD_CCtx_setPledgedSrcSize`: applies at (deferred) init time.
66 requested_pledged: Option<u64>,
67 checksum: bool,
68 /// `ZSTD_CCtx_loadDictionary`: the dictionary primes the frame via an
69 /// internally-built CDict (Path B). `None` for plain streaming.
70 dict: Option<Vec<u8>>,
71 /// `ZSTD_c_nbWorkers` / `ZSTD_c_jobSize` / `ZSTD_c_overlapLog`: when
72 /// `nb_workers >= 1` the stream uses the multithreaded job-splitting path
73 /// (reproduced single-threaded via [`MtStreamState`]).
74 nb_workers: u32,
75 job_size: u64,
76 overlap_log: i32,
77 /// `None` until the first operation (`zcss_init`): parameters are
78 /// resolved lazily so that a first-call `finish` can auto-pledge.
79 state: Option<StreamState>,
80 /// Set instead of `state` when the multithreaded streaming path is active.
81 mt_state: Option<MtStreamState>,
82 frame_ended: bool,
83}
84
85struct StreamState {
86 fc: FrameCompressor,
87 /// The C `inBuff`, `windowSize + blockSize` bytes.
88 in_buff: Vec<u8>,
89 /// `zcs->inToCompress` / `zcs->inBuffPos` / `zcs->inBuffTarget`.
90 in_to_compress: usize,
91 in_buff_pos: usize,
92 in_buff_target: usize,
93}
94
95impl StreamEncoder {
96 /// A streaming encoder with unknown content size (the
97 /// `ZSTD_compressStream2` default).
98 pub fn new(level: i32) -> Self {
99 StreamEncoder {
100 level,
101 requested_pledged: None,
102 checksum: false,
103 dict: None,
104 nb_workers: 0,
105 job_size: 0,
106 overlap_log: 0,
107 state: None,
108 mt_state: None,
109 frame_ended: false,
110 }
111 }
112
113 /// A streaming encoder primed with a dictionary (`ZSTD_CCtx_loadDictionary`
114 /// semantics: an internal CDict is built and **attached**, Path B). Output is
115 /// byte-identical to C `ZSTD_compressStream2` after `ZSTD_CCtx_loadDictionary`
116 /// (e.g. `zstd::stream::write::Encoder::with_dictionary`) for the same
117 /// operations.
118 ///
119 /// Current scope: the unknown-content-size (attach) path — the natural
120 /// streaming case. A stream whose source grows past the window (so C would
121 /// drop the attached dictionary) returns a clean [`Error::Encode`]; so does a
122 /// pledged size above the strategy's attach cutoff (the copy path) and a
123 /// dictionary with <= 8 bytes of content. Raw and trained dictionaries are
124 /// both supported.
125 pub fn with_dictionary(level: i32, dict: &[u8]) -> Self {
126 StreamEncoder {
127 level,
128 requested_pledged: None,
129 checksum: false,
130 dict: if dict.is_empty() {
131 None
132 } else {
133 Some(dict.to_vec())
134 },
135 nb_workers: 0,
136 job_size: 0,
137 overlap_log: 0,
138 state: None,
139 mt_state: None,
140 frame_ended: false,
141 }
142 }
143
144 /// `ZSTD_CCtx_setPledgedSrcSize`: declare the total content size up
145 /// front. Compression parameters are then derived from it exactly as in
146 /// the one-shot path, the frame header carries the content size, and the
147 /// stream errors if the fed input does not match the pledge. Note that C
148 /// overrides the pledge when the *first* operation is `finish` (the input
149 /// of that call becomes the pledge); this port is faithful to that.
150 pub fn with_pledged_src_size(level: i32, size: u64) -> Self {
151 StreamEncoder {
152 level,
153 requested_pledged: Some(size),
154 checksum: false,
155 dict: None,
156 nb_workers: 0,
157 job_size: 0,
158 overlap_log: 0,
159 state: None,
160 mt_state: None,
161 frame_ended: false,
162 }
163 }
164
165 /// Enable the content checksum (`ZSTD_c_checksumFlag`): XXH64 of the
166 /// content, low 32 bits appended to the frame.
167 ///
168 /// # Panics
169 /// If streaming has already started (the flag applies at init time).
170 pub fn with_checksum(mut self, on: bool) -> Self {
171 assert!(
172 self.state.is_none() && self.mt_state.is_none(),
173 "checksum flag must be set before streaming starts"
174 );
175 self.checksum = on;
176 self
177 }
178
179 /// `ZSTD_c_nbWorkers` (+ optional `ZSTD_c_jobSize` / `ZSTD_c_overlapLog`,
180 /// `0` = C default): enable multithreaded streaming. C's MT output is
181 /// deterministic and worker-count-independent, so this reproduces it
182 /// **single-threaded** (see [`crate::compress_mt`]).
183 ///
184 /// Current scope: unknown-size streaming (the default) via
185 /// [`compress`](Self::compress) + [`finish`](Self::finish), with or without a
186 /// content checksum ([`with_checksum`](Self::with_checksum)). A first-call
187 /// `finish` below `ZSTDMT_JOBSIZE_MIN` (512 KiB) produces the single-threaded
188 /// frame, and above it the one-shot MT frame. [`flush`](Self::flush), a
189 /// pledged size, and a dictionary with workers are not supported yet (clean
190 /// [`Error::Encode`]).
191 ///
192 /// # Panics
193 /// If streaming has already started (workers apply at init time).
194 pub fn with_workers(mut self, nb_workers: u32, job_size: u64, overlap_log: i32) -> Self {
195 assert!(
196 self.state.is_none() && self.mt_state.is_none(),
197 "workers must be set before streaming starts"
198 );
199 self.nb_workers = nb_workers;
200 self.job_size = job_size;
201 self.overlap_log = overlap_log;
202 self
203 }
204
205 /// `ZSTD_compressStream2(.., ZSTD_e_continue)`: consume `input`, appending
206 /// any output produced to `out`. Input is buffered internally; output is
207 /// only produced once full blocks are available.
208 pub fn compress(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), Error> {
209 self.stream_op(input, EndOp::Continue, out)
210 }
211
212 /// `ZSTD_compressStream2(.., ZSTD_e_flush)`: compress whatever is
213 /// buffered and emit it, ending the current block. The frame stays open.
214 pub fn flush(&mut self, out: &mut Vec<u8>) -> Result<(), Error> {
215 self.stream_op(&[], EndOp::Flush, out)
216 }
217
218 /// `ZSTD_compressStream2(.., ZSTD_e_end)`: consume `input` (pass `b""`
219 /// for none), then end the frame — last block, optional checksum.
220 ///
221 /// When this is the *first* operation on the encoder, C auto-pledges the
222 /// content size from this call's input, making the result byte-identical
223 /// to the one-shot `ZSTD_compress2`; this port does the same.
224 pub fn finish(mut self, input: &[u8], out: &mut Vec<u8>) -> Result<(), Error> {
225 self.stream_op(input, EndOp::End, out)
226 }
227
228 /// `ZSTD_CCtx_init_compressStream2` + `ZSTD_compressBegin_internal`
229 /// (buffered): resolve parameters (auto-pledging if the first operation
230 /// is `ZSTD_e_end`) and size the input staging buffer.
231 fn init(&mut self, end_op: EndOp, in_size: usize) -> Result<(), Error> {
232 // Multithreaded streaming. A first-call `finish` auto-pledges to its
233 // input size (the no-dict large case was already delegated to the
234 // one-shot MT frame in `stream_op`; here it reaches only the dictionary
235 // path, which streams through `MtStreamState`).
236 if self.nb_workers > 0 {
237 let pledged = if end_op == EndOp::End {
238 Some(in_size as u64)
239 } else {
240 self.requested_pledged
241 };
242 // Engage MT for an unknown size, or a pledged size above the MT floor;
243 // a pledge at or below it falls through to the single-threaded path.
244 let engages = match pledged {
245 Some(p) => p > ZSTDMT_JOBSIZE_MIN,
246 None => true,
247 };
248 if engages {
249 self.mt_state = Some(MtStreamState::new(
250 self.level,
251 self.job_size,
252 self.overlap_log,
253 self.checksum,
254 pledged,
255 self.dict.as_deref(),
256 )?);
257 return Ok(());
258 }
259 }
260 let pledged = if end_op == EndOp::End {
261 // "auto-determine pledgedSrcSize" — overrides any prior pledge.
262 Some(in_size as u64)
263 } else {
264 self.requested_pledged
265 };
266 // `inBuffTarget = blockSizeMax + (blockSizeMax == pledgedSrcSize)`: for a
267 // pledge of exactly one block, avoid the automatic flush on reaching end
268 // of block, which would cost a 3-byte empty last block.
269 let one_block_pledge = |bs: usize| (pledged == Some(bs as u64)) as usize;
270 self.state = Some(if let Some(dict) = &self.dict {
271 // `ZSTD_CCtx_loadDictionary` → internal CDict → attach. The dict
272 // content is a permanent prefix of the staging buffer; input is staged
273 // (and the first chunk compressed) from `content_len`.
274 let init =
275 crate::compress::streaming_cdict_init(dict, self.level, pledged, self.checksum)?;
276 let block_size = init.fc.block_size_max();
277 let in_buff_target = init.content_len + block_size + one_block_pledge(block_size);
278 StreamState {
279 fc: init.fc,
280 in_buff: init.in_buff,
281 in_to_compress: init.content_len,
282 in_buff_pos: init.content_len,
283 in_buff_target,
284 }
285 } else {
286 let fc = FrameCompressor::new(self.level, pledged, self.checksum);
287 let block_size = fc.block_size_max();
288 let in_buff_size = fc.window_size() + block_size;
289 StreamState {
290 fc,
291 in_buff: vec![0u8; in_buff_size],
292 in_to_compress: 0,
293 in_buff_pos: 0,
294 in_buff_target: block_size + one_block_pledge(block_size),
295 }
296 });
297 Ok(())
298 }
299
300 /// Drive the multithreaded streaming path: buffer `input` into jobs
301 /// ([`MtStreamState`]). A `flush` emits the buffered partial section as a
302 /// (non-last) job, leaving the frame open.
303 fn mt_drive(&mut self, input: &[u8], op: EndOp, out: &mut Vec<u8>) -> Result<(), Error> {
304 match op {
305 EndOp::Continue => self.mt_state.as_mut().unwrap().push(input, out),
306 EndOp::Flush => self.mt_state.as_mut().unwrap().flush(out),
307 EndOp::End => {
308 self.mt_state.as_mut().unwrap().end(input, out)?;
309 self.frame_ended = true;
310 Ok(())
311 }
312 }
313 }
314
315 /// `ZSTD_compressStream_generic`, buffered mode. The output never blocks
316 /// (we append to a `Vec`), so the `zcss_flush` stage disappears and the
317 /// loop alternates load → compress until the directive is satisfied.
318 fn stream_op(&mut self, mut input: &[u8], op: EndOp, out: &mut Vec<u8>) -> Result<(), Error> {
319 if self.frame_ended {
320 return Err(Error::Encode("frame already finished"));
321 }
322 if self.state.is_none() && self.mt_state.is_none() {
323 // A first-call `finish` auto-pledges the content size. With workers and
324 // a size above the MT floor, C delegates to `ZSTD_compress2` — the
325 // one-shot MT frame (known size), not unknown-size streaming.
326 // A first-call `finish` auto-pledges to this call's input size
327 // (overriding any prior pledge), so above the MT floor it is the
328 // one-shot MT frame for that size regardless of `requested_pledged`.
329 if op == EndOp::End
330 && self.nb_workers > 0
331 && self.dict.is_none()
332 && input.len() as u64 > ZSTDMT_JOBSIZE_MIN
333 {
334 out.extend_from_slice(&compress_mt(
335 input,
336 self.level,
337 self.nb_workers,
338 self.job_size,
339 self.overlap_log,
340 self.checksum,
341 )?);
342 self.frame_ended = true;
343 return Ok(());
344 }
345 self.init(op, input.len())?;
346 }
347 if self.mt_state.is_some() {
348 return self.mt_drive(input, op, out);
349 }
350 let st = self.state.as_mut().expect("initialized above");
351 let block_size = st.fc.block_size_max();
352
353 loop {
354 // zcss_load: complete loading into the input buffer.
355 let to_load = st.in_buff_target - st.in_buff_pos;
356 let loaded = to_load.min(input.len());
357 st.in_buff[st.in_buff_pos..st.in_buff_pos + loaded].copy_from_slice(&input[..loaded]);
358 st.in_buff_pos += loaded;
359 input = &input[loaded..];
360 if op == EndOp::Continue && st.in_buff_pos < st.in_buff_target {
361 // Not enough input to fill a full block: stop here.
362 break;
363 }
364 if op == EndOp::Flush && st.in_buff_pos == st.in_to_compress {
365 // Nothing pending.
366 break;
367 }
368
369 // Streaming CDict attach: stop cleanly before the source grows past
370 // the window (where C's checkDictValidity/enforceMaxDist would drop
371 // the attached dict — that loadedDictEnd machinery isn't ported).
372 if st.fc.cdict_attach_overflow(st.in_buff_pos) {
373 return Err(Error::Encode(
374 "streaming source outgrew the window with an attached dictionary \
375 (large dict streams are not supported yet)",
376 ));
377 }
378
379 // Compress the staged chunk.
380 let last_block = op == EndOp::End && input.is_empty();
381 if last_block {
382 st.fc
383 .compress_end(out, &st.in_buff, st.in_to_compress, st.in_buff_pos)?;
384 self.frame_ended = true;
385 } else {
386 st.fc.compress_continue(
387 out,
388 &st.in_buff,
389 st.in_to_compress,
390 st.in_buff_pos,
391 false,
392 )?;
393 }
394
395 // Prepare the next block; past the buffer end, wrap to the start.
396 // The wrapped chunk is non-contiguous, turning the live window
397 // into the extDict.
398 st.in_buff_target = st.in_buff_pos + block_size;
399 if st.in_buff_target > st.in_buff.len() {
400 st.in_buff_pos = 0;
401 st.in_buff_target = block_size;
402 }
403 st.in_to_compress = st.in_buff_pos;
404 if self.frame_ended {
405 break;
406 }
407 }
408 Ok(())
409 }
410}