structured_zstd/encoding/streaming_encoder.rs
1use alloc::format;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::borrow::BorrowMut;
5use core::marker::PhantomData;
6
7use crate::common::MAX_BLOCK_SIZE;
8#[cfg(feature = "hash")]
9use core::hash::Hasher;
10#[cfg(feature = "hash")]
11use twox_hash::XxHash64;
12
13use crate::encoding::levels::compress_block_encoded;
14use crate::encoding::{
15 CompressionLevel, EncoderDictionary, MatchGeneratorDriver, Matcher, block_header::BlockHeader,
16 frame_compressor::CompressState, frame_compressor::FrameTuning, frame_compressor::FseTables,
17 frame_compressor::PreviousFseTable, frame_header::FrameHeader,
18};
19use crate::io::{Error, ErrorKind, Write};
20
21/// Incremental frame encoder that implements [`Write`].
22///
23/// Data can be provided with multiple `write()` calls. Full blocks are compressed
24/// automatically, `flush()` emits the currently buffered partial block as non-last,
25/// and `finish()` closes the frame and returns the wrapped writer.
26///
27/// One encoder writes one frame into the drain it owns, through a
28/// [`CompressionContext`] it owns ([`new`](StreamingEncoder::new)) or borrows
29/// ([`with_context`](Self::with_context)). Borrowing is how frame after frame
30/// is compressed with the same settings, dictionary and match-finder
31/// allocations: the context outlives each encoder and is ready for the next
32/// frame once [`finish`](Self::finish) returns.
33pub struct StreamingEncoder<
34 W: Write,
35 M: Matcher = MatchGeneratorDriver,
36 C: BorrowMut<CompressionContext<M>> = CompressionContext<M>,
37> {
38 drain: Option<W>,
39 context: C,
40 matcher: PhantomData<M>,
41}
42
43/// A reusable streaming compression context: the settings, the attached
44/// dictionary, the match finder and its buffers, kept from one frame to the
45/// next, with the output handed in on each call rather than owned. The
46/// counterpart of upstream zstd's `ZSTD_CCtx` driven by `ZSTD_compressStream2`.
47///
48/// Settings apply from the next frame on and must be made before its first
49/// [`write`](Self::write); [`finish_frame`](Self::finish_frame) closes the
50/// frame and readies the context for another. A pledged size belongs to one
51/// frame; every other setting, the dictionary included, stays until replaced.
52/// A frame that fails leaves the context failed: every later call reports
53/// that failure, and a new context is needed.
54///
55/// Reusing a context produces the same frames as a fresh
56/// [`StreamingEncoder`] per frame, without rebuilding the match finder's
57/// tables or re-attaching the dictionary for each of them.
58///
59/// # Examples
60/// ```
61/// use structured_zstd::encoding::{CompressionContext, CompressionLevel};
62///
63/// let mut context = CompressionContext::new(CompressionLevel::Default);
64/// let mut frames = Vec::new();
65/// for payload in [&b"first frame"[..], b"second frame"] {
66/// let mut frame = Vec::new();
67/// context.set_pledged_content_size(payload.len() as u64).unwrap();
68/// context.write(&mut frame, payload).unwrap();
69/// context.finish_frame(&mut frame).unwrap();
70/// frames.push(frame);
71/// }
72/// use std::io::Read;
73/// let mut decoder = structured_zstd::decoding::StreamingDecoder::new(&frames[1][..]);
74/// let mut decoded = Vec::new();
75/// decoder.read_to_end(&mut decoded).unwrap();
76/// assert_eq!(decoded, b"second frame");
77/// ```
78pub struct CompressionContext<M: Matcher = MatchGeneratorDriver> {
79 compression_level: CompressionLevel,
80 state: CompressState<M>,
81 /// The block being written out. An uncompressed frame also buffers its
82 /// input here, behind the header, so a raw block is assembled where it
83 /// is written from; a compressed frame buffers input in the match
84 /// finder's history, uncommitted until its block is cut.
85 encoded_scratch: Vec<u8>,
86 errored: bool,
87 last_error_kind: Option<ErrorKind>,
88 last_error_message: Option<String>,
89 frame_started: bool,
90 /// Upper bound on emitted block sizes (upstream `ZSTD_c_targetCBlockSize`
91 /// semantics; see `FrameCompressor::set_target_block_size`). `None` =
92 /// the format's 128 KiB ceiling.
93 target_block_size: Option<u32>,
94 /// Largest block a frame may carry (upstream `ZSTD_c_maxBlockSize`); see
95 /// [`Self::set_max_block_size`].
96 max_block_size: Option<u32>,
97 /// The pledged size of the frame in progress; cleared when it ends.
98 pledged_content_size: Option<u64>,
99 /// Advisory source-size hint from [`set_source_size_hint`](Self::set_source_size_hint).
100 /// Unlike `pledged_content_size` it carries no end-of-frame enforcement, but
101 /// it still feeds the small-input gates (matcher sizing AND the Fast HUF
102 /// fast-path gate) so `set_source_size_hint(small)` reduces work the same way
103 /// a pledge does. The HUF gate reads `pledged_content_size.or(source_size_hint)`.
104 /// A parameter like upstream `ZSTD_c_srcSizeHint`, so it outlives a frame.
105 source_size_hint: Option<u64>,
106 /// Whether a pledged size is written into the header's
107 /// `Frame_Content_Size` field (upstream `ZSTD_c_contentSizeFlag`).
108 /// Pledge *enforcement* is independent of this flag — upstream
109 /// validates consumed bytes against the pledge at frame end even
110 /// when the header omits the field. Default `true`.
111 content_size_flag: bool,
112 bytes_consumed: u64,
113 /// Upstream `ZSTD_compress_frameChunk` `savings`: bytes consumed minus
114 /// bytes produced so far in this frame; the block pre-splitter only cuts
115 /// full blocks once the frame has saved enough.
116 savings: i64,
117 tuning: FrameTuning,
118 /// `ZSTD_f_zstd1_magicless` — omit the 4-byte magic number prefix.
119 /// Default false. See [`Self::set_magicless`].
120 magicless: bool,
121 /// Whether to emit a trailing XXH64 content checksum and set the frame
122 /// header's `Content_Checksum_flag` (upstream `ZSTD_c_checksumFlag`).
123 /// Default `false`, matching the upstream library default; combined with
124 /// the `hash` feature, so without `hash` no checksum is emitted
125 /// regardless. See [`Self::set_content_checksum`].
126 content_checksum: bool,
127 /// Dictionary applied to each frame (upstream zstd `ZSTD_CCtx_loadDictionary`
128 /// on a streaming context), with the entropy tables it seeds. `None` = no
129 /// dictionary. Set before a frame's first write.
130 dictionary: Option<EncoderDictionary>,
131 /// Whether the frame header records the attached dictionary's ID
132 /// (upstream `ZSTD_c_dictIDFlag`). Default `true`. Raw-content
133 /// dictionaries (upstream `ZSTD_CCtx_refPrefix`) carry a synthetic
134 /// non-zero ID that must not reach the wire, so their attach path
135 /// turns this off. See [`Self::set_dictionary_id_flag`].
136 dictionary_id_flag: bool,
137 #[cfg(feature = "hash")]
138 hasher: XxHash64,
139}
140
141impl<W: Write> StreamingEncoder<W, MatchGeneratorDriver> {
142 /// Creates a streaming encoder backed by the default match generator.
143 ///
144 /// The encoder writes compressed bytes into `drain` and applies `compression_level`
145 /// to all subsequently written blocks.
146 pub fn new(drain: W, compression_level: CompressionLevel) -> Self {
147 Self::with_context(drain, CompressionContext::new(compression_level))
148 }
149}
150
151impl<W: Write, C: BorrowMut<CompressionContext>> StreamingEncoder<W, MatchGeneratorDriver, C> {
152 /// Configure fine-grained compression parameters; see
153 /// [`CompressionContext::set_parameters`]. Must be called before the first
154 /// [`write`](Write::write).
155 pub fn set_parameters(
156 &mut self,
157 params: &crate::encoding::CompressionParameters,
158 ) -> Result<(), Error> {
159 self.context.borrow_mut().set_parameters(params)
160 }
161}
162
163impl<W: Write, M: Matcher> StreamingEncoder<W, M> {
164 /// Creates a streaming encoder with an explicitly provided matcher implementation.
165 ///
166 /// This constructor is primarily intended for tests and advanced callers that need
167 /// custom match-window behavior.
168 pub fn new_with_matcher(matcher: M, drain: W, compression_level: CompressionLevel) -> Self {
169 Self::with_context(
170 drain,
171 CompressionContext::new_with_matcher(matcher, compression_level),
172 )
173 }
174}
175
176impl<W: Write, M: Matcher, C: BorrowMut<CompressionContext<M>>> StreamingEncoder<W, M, C> {
177 /// Write one frame into `drain` through `context`: owned, or borrowed from
178 /// a caller that keeps it for the next frame with every setting, the
179 /// dictionary and the allocations it has.
180 ///
181 /// # Examples
182 /// ```
183 /// use std::io::Write;
184 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel, StreamingEncoder};
185 ///
186 /// let mut context = CompressionContext::new(CompressionLevel::Default);
187 /// for payload in [&b"first frame"[..], b"second frame"] {
188 /// let mut encoder = StreamingEncoder::with_context(Vec::new(), &mut context);
189 /// encoder.write_all(payload).unwrap();
190 /// let frame = encoder.finish().unwrap();
191 /// assert!(!frame.is_empty());
192 /// }
193 /// ```
194 pub fn with_context(drain: W, context: C) -> Self {
195 Self {
196 drain: Some(drain),
197 context,
198 matcher: PhantomData,
199 }
200 }
201
202 /// Bound each block's payload; see
203 /// [`CompressionContext::set_target_block_size`]. Must be set before the
204 /// first write.
205 pub fn set_target_block_size(&mut self, target: Option<u32>) -> Result<(), Error> {
206 self.context.borrow_mut().set_target_block_size(target)
207 }
208
209 /// Cap each block's size; see [`CompressionContext::set_max_block_size`].
210 /// Must be set before the first write.
211 pub fn set_max_block_size(&mut self, size: Option<u32>) -> Result<(), Error> {
212 self.context.borrow_mut().set_max_block_size(size)
213 }
214
215 /// Enable or disable the trailing XXH64 content checksum; see
216 /// [`CompressionContext::set_content_checksum`]. Must be called before the
217 /// first write.
218 pub fn set_content_checksum(&mut self, emit: bool) -> Result<(), Error> {
219 self.context.borrow_mut().set_content_checksum(emit)
220 }
221
222 /// Enable or disable the magicless frame format; see
223 /// [`CompressionContext::set_magicless`]. Must be called before the first
224 /// write.
225 pub fn set_magicless(&mut self, magicless: bool) -> Result<(), Error> {
226 self.context.borrow_mut().set_magicless(magicless)
227 }
228
229 /// Pledge the total uncompressed content size of the frame; see
230 /// [`CompressionContext::set_pledged_content_size`]. Must be called before
231 /// the first write.
232 pub fn set_pledged_content_size(&mut self, size: u64) -> Result<(), Error> {
233 self.context.borrow_mut().set_pledged_content_size(size)
234 }
235
236 /// Control whether a pledged size reaches the header; see
237 /// [`CompressionContext::set_content_size_flag`]. Must be called before
238 /// the first write.
239 pub fn set_content_size_flag(&mut self, emit: bool) -> Result<(), Error> {
240 self.context.borrow_mut().set_content_size_flag(emit)
241 }
242
243 /// Provide an advisory size for the frame; see
244 /// [`CompressionContext::set_source_size_hint`]. Must be called before the
245 /// first write.
246 pub fn set_source_size_hint(&mut self, size: u64) -> Result<(), Error> {
247 self.context.borrow_mut().set_source_size_hint(size)
248 }
249
250 /// Attach a dictionary blob to the frame; see
251 /// [`CompressionContext::set_dictionary_from_bytes`]. Must be called before
252 /// the first write.
253 pub fn set_dictionary_from_bytes(&mut self, raw_dictionary: &[u8]) -> Result<(), Error> {
254 self.context
255 .borrow_mut()
256 .set_dictionary_from_bytes(raw_dictionary)
257 }
258
259 /// Whether the header records the dictionary ID; see
260 /// [`CompressionContext::set_dictionary_id_flag`]. Must be set before the
261 /// first write.
262 pub fn set_dictionary_id_flag(&mut self, emit: bool) -> Result<(), Error> {
263 self.context.borrow_mut().set_dictionary_id_flag(emit)
264 }
265
266 /// Attach an already-parsed [`EncoderDictionary`] to the frame; see
267 /// [`CompressionContext::set_encoder_dictionary`]. Must be called before
268 /// the first write.
269 pub fn set_encoder_dictionary(&mut self, dict: EncoderDictionary) -> Result<(), Error> {
270 self.context.borrow_mut().set_encoder_dictionary(dict)
271 }
272
273 /// Returns an immutable reference to the wrapped output drain.
274 ///
275 /// The drain remains available for the encoder lifetime; [`finish`](Self::finish)
276 /// consumes the encoder and returns ownership of the drain.
277 pub fn get_ref(&self) -> &W {
278 self.drain
279 .as_ref()
280 .expect("streaming encoder drain is present until finish consumes self")
281 }
282
283 /// Total heap bytes this encoder's allocations hold, excluding the inline
284 /// struct and the drain `W` (whose footprint the owner can measure through
285 /// [`get_ref`](Self::get_ref)); see [`CompressionContext::heap_size`].
286 pub fn heap_size(&self) -> usize {
287 self.context.borrow().heap_size()
288 }
289
290 /// Returns a mutable reference to the wrapped output drain.
291 ///
292 /// It is inadvisable to directly write to the underlying writer, as doing
293 /// so would corrupt the zstd frame being assembled by the encoder.
294 ///
295 /// The drain remains available for the encoder lifetime; [`finish`](Self::finish)
296 /// consumes the encoder and returns ownership of the drain.
297 pub fn get_mut(&mut self) -> &mut W {
298 self.drain
299 .as_mut()
300 .expect("streaming encoder drain is present until finish consumes self")
301 }
302
303 /// Finalizes the current zstd frame and returns the wrapped output drain.
304 ///
305 /// If no payload was written yet, this still emits a valid empty frame.
306 /// Calling this method consumes the encoder; a borrowed context is then
307 /// ready for the next frame, also when this fails: the frame goes with the
308 /// drain (see [`CompressionContext::abandon_frame`]).
309 pub fn finish(mut self) -> Result<W, Error> {
310 let mut drain = self
311 .drain
312 .take()
313 .expect("streaming encoder drain must be present when finishing");
314 self.context.borrow_mut().finish_frame(&mut drain)?;
315 Ok(drain)
316 }
317
318 fn drain_mut(&mut self) -> (&mut W, &mut CompressionContext<M>) {
319 let drain = self
320 .drain
321 .as_mut()
322 .expect("streaming encoder drain is present until finish consumes self");
323 (drain, self.context.borrow_mut())
324 }
325}
326
327/// The frame belongs to the drain this encoder writes into: an encoder that
328/// goes without finishing it (dropped mid-frame, or a `finish` that failed)
329/// takes it along, so a borrowed context starts the next encoder's frame
330/// afresh instead of continuing this one into another drain.
331impl<W: Write, M: Matcher, C: BorrowMut<CompressionContext<M>>> Drop for StreamingEncoder<W, M, C> {
332 fn drop(&mut self) {
333 self.context.borrow_mut().abandon_frame();
334 }
335}
336
337impl<W: Write, M: Matcher, C: BorrowMut<CompressionContext<M>>> Write
338 for StreamingEncoder<W, M, C>
339{
340 fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
341 let (drain, context) = self.drain_mut();
342 context.write(drain, buf)
343 }
344
345 fn flush(&mut self) -> Result<(), Error> {
346 let (drain, context) = self.drain_mut();
347 context.flush(drain)
348 }
349}
350
351impl CompressionContext<MatchGeneratorDriver> {
352 /// Creates a context backed by the default match generator, compressing
353 /// at `compression_level`.
354 pub fn new(compression_level: CompressionLevel) -> Self {
355 Self::new_with_matcher(
356 MatchGeneratorDriver::new(MAX_BLOCK_SIZE as usize, 1),
357 compression_level,
358 )
359 }
360
361 /// Configure fine-grained compression parameters (#27): resets the level to
362 /// the parameters' level and installs the per-knob overrides (window / hash
363 /// / chain / search logs, strategy, long-distance matching) applied at the
364 /// next frame. Mirrors [`FrameCompressor::set_parameters`](crate::encoding::FrameCompressor::set_parameters).
365 /// Must be called before the frame's first [`write`](Self::write). Only the
366 /// built-in `MatchGeneratorDriver` exposes the override knobs, so this
367 /// lives on the default-matcher impl.
368 pub fn set_parameters(
369 &mut self,
370 params: &crate::encoding::CompressionParameters,
371 ) -> Result<(), Error> {
372 self.ensure_settable("compression parameters must be set before the first write")?;
373 self.compression_level = params.level();
374 let overrides = params.overrides();
375 // Persist the strategy override so `ensure_frame_started`'s level-based
376 // resync does not discard it (matching `FrameCompressor::set_parameters`).
377 self.tuning = FrameTuning::from_overrides(&overrides);
378 self.state.strategy_tag = self.tuning.strategy.map_or_else(
379 || {
380 crate::encoding::strategy::StrategyTag::for_compression_level(
381 self.compression_level,
382 )
383 },
384 |(tag, _)| tag,
385 );
386 self.state.huf_optimal_search = crate::encoding::frame_compressor::huf_search_enabled(
387 self.state.strategy_tag,
388 self.pledged_content_size.or(self.source_size_hint),
389 );
390 self.state.matcher.set_param_overrides(Some(overrides));
391 Ok(())
392 }
393
394 /// Ask `producer` for each block's parse in place of the built-in
395 /// matchers; `None` removes it. With `fallback` a block the producer fails
396 /// on is matched as usual; without, it goes out uncompressed and
397 /// [`Self::take_sequence_producer_failure`] reports it. With long-distance
398 /// matching on, the long matches are taken outright and the producer
399 /// parses the literals between them.
400 pub fn set_sequence_producer(
401 &mut self,
402 producer: Option<alloc::boxed::Box<dyn crate::encoding::sequences::SequenceProducer>>,
403 fallback: bool,
404 ) {
405 self.state.matcher.set_sequence_producer(producer, fallback);
406 }
407
408 /// Whether a block went out uncompressed because the producer failed on it
409 /// with no fallback, since this was last asked.
410 pub fn take_sequence_producer_failure(&mut self) -> bool {
411 self.state.matcher.take_producer_failure()
412 }
413}
414
415impl<M: Matcher> CompressionContext<M> {
416 /// Creates a context with an explicitly provided matcher implementation.
417 ///
418 /// This constructor is primarily intended for tests and advanced callers that need
419 /// custom match-window behavior.
420 pub fn new_with_matcher(matcher: M, compression_level: CompressionLevel) -> Self {
421 Self {
422 compression_level,
423 state: CompressState {
424 matcher,
425 sequence_capture: None,
426 copy_kernel: crate::encoding::fastpath::select_kernel(),
427 last_huff_table: None,
428 huff_table_spare: None,
429 huff_weights: Default::default(),
430 seen_content: Default::default(),
431 fse_tables: FseTables::new(),
432 block_scratch: crate::encoding::blocks::CompressedBlockScratch::new(),
433 workspace: crate::encoding::workspace::Workspace::new(),
434 offset_hist: [1, 4, 8],
435 strategy_tag: crate::encoding::strategy::StrategyTag::for_compression_level(
436 compression_level,
437 ),
438 pre_split: crate::encoding::levels::config::level_pre_split(compression_level)
439 .map(|tier| tier as u8),
440 huf_optimal_search: true,
441 literal_compression_disabled: matches!(
442 compression_level,
443 CompressionLevel::Level(n) if n < 0
444 ),
445 post_split_allowed: true,
446 },
447 encoded_scratch: Vec::new(),
448 errored: false,
449 last_error_kind: None,
450 last_error_message: None,
451 frame_started: false,
452 target_block_size: None,
453 max_block_size: None,
454 pledged_content_size: None,
455 source_size_hint: None,
456 content_size_flag: true,
457 bytes_consumed: 0,
458 savings: 0,
459 tuning: FrameTuning::default(),
460 magicless: false,
461 content_checksum: false,
462 dictionary: None,
463 dictionary_id_flag: true,
464 #[cfg(feature = "hash")]
465 hasher: XxHash64::with_seed(0),
466 }
467 }
468
469 /// Compress the next frames at `level`, with the level's own tuning: any
470 /// parameter override installed by
471 /// [`set_parameters`](CompressionContext::set_parameters) is dropped, as
472 /// [`FrameCompressor::set_compression_level`](crate::encoding::FrameCompressor::set_compression_level)
473 /// drops it. Must be called before the frame's first [`write`](Self::write).
474 ///
475 /// # Examples
476 /// ```
477 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel};
478 ///
479 /// let mut context = CompressionContext::new(CompressionLevel::Fastest);
480 /// context.set_compression_level(CompressionLevel::Better).unwrap();
481 /// let mut frame = Vec::new();
482 /// context.write(&mut frame, b"compressed at the new level").unwrap();
483 /// context.finish_frame(&mut frame).unwrap();
484 /// ```
485 pub fn set_compression_level(&mut self, level: CompressionLevel) -> Result<(), Error> {
486 self.ensure_settable("the compression level must be set before the first write")?;
487 self.compression_level = level;
488 self.tuning = FrameTuning::default();
489 self.state.matcher.clear_param_overrides();
490 Ok(())
491 }
492
493 /// Set an upper bound on each physical block's payload (semantics of
494 /// upstream `ZSTD_c_targetCBlockSize`): every block carries at most
495 /// `target` payload bytes, +3-byte block header on the wire — the
496 /// upstream knob is likewise a convergence target for block sizing,
497 /// not a cap on header-inclusive wire bytes. Clamped to
498 /// `[MIN_TARGET_BLOCK_SIZE, MAX_BLOCK_SIZE]`; mirrors
499 /// `FrameCompressor::set_target_block_size`. Must be set before the
500 /// frame's first write.
501 pub fn set_target_block_size(&mut self, target: Option<u32>) -> Result<(), Error> {
502 self.ensure_settable("the block-size target must be set before the first write")?;
503 self.target_block_size = target.map(|t| {
504 t.clamp(
505 crate::common::MIN_TARGET_BLOCK_SIZE,
506 crate::common::MAX_BLOCK_SIZE,
507 )
508 });
509 Ok(())
510 }
511
512 /// Cap every block at `size` bytes (upstream `ZSTD_c_maxBlockSize`),
513 /// clamped to `[MIN_MAX_BLOCK_SIZE, MAX_BLOCK_SIZE]`; mirrors
514 /// `FrameCompressor::set_max_block_size`. Must be set before the frame's
515 /// first write.
516 pub fn set_max_block_size(&mut self, size: Option<u32>) -> Result<(), Error> {
517 self.ensure_settable("the maximum block size must be set before the first write")?;
518 self.max_block_size = size.map(|s| {
519 s.clamp(
520 crate::common::MIN_MAX_BLOCK_SIZE,
521 crate::common::MAX_BLOCK_SIZE,
522 )
523 });
524 Ok(())
525 }
526
527 /// Enable or disable the trailing XXH64 content checksum
528 /// (upstream `ZSTD_c_checksumFlag`). Default `false`, matching the
529 /// upstream library default (`ZSTD_c_checksumFlag = 0`). Must be called
530 /// before the frame's first [`write`](Self::write); once the frame header
531 /// is emitted the flag is fixed, so a late change returns an error rather
532 /// than producing a header/trailer mismatch. Without the `hash` feature
533 /// no checksum is emitted regardless.
534 pub fn set_content_checksum(&mut self, emit: bool) -> Result<(), Error> {
535 self.ensure_settable("content checksum must be set before the first write")?;
536 self.content_checksum = emit;
537 Ok(())
538 }
539
540 /// Enable or disable magicless frame format (`ZSTD_f_zstd1_magicless`).
541 ///
542 /// When set to `true`, the frame header omits the 4-byte magic number
543 /// prefix. Must be called BEFORE the frame's first [`write`](Self::write)
544 /// call; calling it after the frame header has already been emitted
545 /// returns an error so the caller can't be misled into thinking they
546 /// produced a magicless stream.
547 pub fn set_magicless(&mut self, magicless: bool) -> Result<(), Error> {
548 self.ensure_settable("magicless format must be set before the first write")?;
549 self.magicless = magicless;
550 Ok(())
551 }
552
553 /// Pledge the total uncompressed content size of the next frame.
554 ///
555 /// When set, the frame header will include a `Frame_Content_Size` field.
556 /// This enables decoders to pre-allocate output buffers.
557 /// The pledged size is also forwarded as a source-size hint to the
558 /// matcher so small inputs can use smaller matching tables.
559 ///
560 /// Must be called **before** the frame's first [`write`](Self::write);
561 /// calling it after the frame header has already been emitted returns an
562 /// error. The pledge ends with the frame.
563 pub fn set_pledged_content_size(&mut self, size: u64) -> Result<(), Error> {
564 self.ensure_settable("pledged content size must be set before the first write")?;
565 // The matcher is handed it as its size hint when the frame starts
566 // (`ensure_frame_started`), not here: a pledge that goes unused must
567 // not stay behind in the matcher for a later frame.
568 self.pledged_content_size = Some(size);
569 Ok(())
570 }
571
572 /// Control whether the pledged size is written into the header's
573 /// `Frame_Content_Size` field (upstream `ZSTD_c_contentSizeFlag`,
574 /// default on). With the flag off the header omits the field, but a
575 /// pledge set via [`set_pledged_content_size`](Self::set_pledged_content_size)
576 /// is still enforced against the bytes actually written. Must be
577 /// called before the frame's first [`write`](Self::write).
578 pub fn set_content_size_flag(&mut self, emit: bool) -> Result<(), Error> {
579 self.ensure_settable("content size flag must be set before the first write")?;
580 self.content_size_flag = emit;
581 Ok(())
582 }
583
584 /// Provide a hint about the total uncompressed size of each frame.
585 ///
586 /// Unlike [`set_pledged_content_size`](Self::set_pledged_content_size),
587 /// this does **not** enforce that exactly `size` bytes are written; it
588 /// may reduce matcher tables, advertised frame window, and block sizing
589 /// for small inputs. A parameter, like upstream `ZSTD_c_srcSizeHint`: it
590 /// applies to every frame until replaced. Must be called before the
591 /// frame's first [`write`](Self::write).
592 pub fn set_source_size_hint(&mut self, size: u64) -> Result<(), Error> {
593 self.ensure_settable("source size hint must be set before the first write")?;
594 // Read at each frame start as `pledged_content_size.or(source_size_hint)`,
595 // by the matcher's sizing and the Fast HUF fast-path gate alike
596 // (`ensure_frame_started`), so a small advisory size also lifts Fast
597 // streams off the expensive optimal-HUF search.
598 self.source_size_hint = Some(size);
599 Ok(())
600 }
601
602 /// Drop the hint [`set_source_size_hint`](Self::set_source_size_hint)
603 /// set, so later frames are sized by their pledge alone. Must be called
604 /// before the frame's first [`write`](Self::write).
605 pub fn clear_source_size_hint(&mut self) -> Result<(), Error> {
606 self.ensure_settable("source size hint must be cleared before the first write")?;
607 self.source_size_hint = None;
608 Ok(())
609 }
610
611 /// Attach a dictionary blob to each frame (upstream zstd
612 /// `ZSTD_CCtx_loadDictionary` on a streaming context, which loads in
613 /// `ZSTD_dct_auto` mode): a
614 /// [serialized](crate::decoding::Dictionary::is_serialized) dictionary,
615 /// or raw content. The dictionary primes the
616 /// match-finder and seeds the first block's entropy tables + repeat
617 /// offsets; a serialized one's ID is written into the frame header, while
618 /// raw content has none to write, so the decoder must be given the same
619 /// bytes explicitly. Must be called before the frame's first
620 /// [`write`](Self::write); repeat offsets must be non-zero.
621 pub fn set_dictionary_from_bytes(&mut self, raw_dictionary: &[u8]) -> Result<(), Error> {
622 if raw_dictionary.is_empty() {
623 // An empty buffer is how the same upstream entry point is told
624 // there is no dictionary: it clears and succeeds. Still refused
625 // once the frame is open, like any other attach.
626 self.ensure_settable("dictionary must be attached before the first write")?;
627 // What the match finder kept of it across frames (its primed
628 // snapshot, the copy left resident) goes with it, entropy included.
629 self.state.matcher.invalidate_primed_dictionary();
630 self.dictionary = None;
631 return Ok(());
632 }
633 let dict = EncoderDictionary::from_serialized_or_raw_content(raw_dictionary)
634 .map_err(|err| invalid_input_error(&alloc::format!("invalid dictionary: {err:?}")))?;
635 self.set_encoder_dictionary(dict)
636 }
637
638 /// Whether the frame header records the attached dictionary's ID
639 /// (upstream `ZSTD_c_dictIDFlag` semantics; default `true`).
640 /// Mirrors [`FrameCompressor::set_dictionary_id_flag`](crate::encoding::FrameCompressor::set_dictionary_id_flag).
641 /// Decoders can still decode such frames by supplying the dictionary
642 /// explicitly.
643 pub fn set_dictionary_id_flag(&mut self, emit: bool) -> Result<(), Error> {
644 self.ensure_settable("dictionary ID flag must be set before the first write")?;
645 self.dictionary_id_flag = emit;
646 Ok(())
647 }
648
649 /// Attach an already-parsed [`EncoderDictionary`] to each frame. See
650 /// [`set_dictionary_from_bytes`](Self::set_dictionary_from_bytes); must be
651 /// called before the frame's first write. The entropy tables it seeds were
652 /// built when it was prepared, so attaching builds nothing.
653 pub fn set_encoder_dictionary(&mut self, dict: EncoderDictionary) -> Result<(), Error> {
654 self.ensure_settable("dictionary must be attached before the first write")?;
655 // A zero id marks a raw-content dictionary, which carries no header to
656 // hold one; the frame then records no dictionary ID and the decoder
657 // must be given the same bytes explicitly.
658 if dict.inner.offset_hist.contains(&0) {
659 return Err(invalid_input_error(
660 "dictionary carries a zero repeat offset",
661 ));
662 }
663 // The match finder's primed snapshot and resident copy belong to the
664 // dictionary being replaced; the next frame primes the new one.
665 self.state.matcher.invalidate_primed_dictionary();
666 // One under 8 bytes is attached as none; see
667 // `EncoderDictionary::is_used_for_compression`.
668 self.dictionary = dict.is_used_for_compression().then_some(dict);
669 Ok(())
670 }
671
672 /// The dictionary each frame is compressed with, if one is attached.
673 ///
674 /// # Examples
675 /// ```
676 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel, EncoderDictionary};
677 ///
678 /// let dictionary = EncoderDictionary::from_serialized_or_raw_content(b"some shared history").unwrap();
679 /// let mut context = CompressionContext::new(CompressionLevel::Default);
680 /// assert!(context.dictionary().is_none());
681 /// context.set_encoder_dictionary(dictionary).unwrap();
682 /// assert!(context.dictionary().is_some());
683 /// ```
684 pub fn dictionary(&self) -> Option<&EncoderDictionary> {
685 self.dictionary.as_ref()
686 }
687
688 /// Total heap bytes this context's allocations hold, excluding the inline
689 /// struct: match-finder tables / history, retained Huffman tables, the
690 /// `encoded_scratch` block buffer, the retained dictionary content, and
691 /// its entropy tables. Mirrors
692 /// `FrameCompressor::heap_size` so a context can report its true
693 /// footprint through `ZSTD_sizeof_CCtx`.
694 pub fn heap_size(&self) -> usize {
695 let mut total = self.state.matcher.heap_size();
696 total += self
697 .state
698 .last_huff_table
699 .as_ref()
700 .map_or(0, |table| table.heap_size());
701 total += self
702 .state
703 .huff_table_spare
704 .as_ref()
705 .map_or(0, |table| table.heap_size());
706 // Kept between blocks and frames; see `FrameCompressor::heap_size`.
707 total += self.state.huff_weights.heap_size();
708 total += self.state.retained_scratch_heap_size();
709 total += self.state.seen_content.heap_size();
710 total += self.state.captured_parse_heap_size();
711 total += self.encoded_scratch.capacity();
712 total += self
713 .dictionary
714 .as_ref()
715 .map_or(0, EncoderDictionary::heap_size);
716 total
717 }
718
719 /// Compress `buf` into the frame in progress, starting one (and writing
720 /// its header to `drain`) if none is. Full blocks are compressed as they
721 /// fill and written to `drain`; the rest stays buffered for the next call,
722 /// [`flush`](Self::flush) or [`finish_frame`](Self::finish_frame).
723 ///
724 /// Returns how much of `buf` was taken, which is all of it unless a pledge
725 /// set with [`set_pledged_content_size`](Self::set_pledged_content_size)
726 /// allows less.
727 pub fn write<D: Write + ?Sized>(&mut self, drain: &mut D, buf: &[u8]) -> Result<usize, Error> {
728 self.ensure_open()?;
729 if buf.is_empty() {
730 return Ok(0);
731 }
732
733 // Check pledge before emitting the frame header so that a misuse
734 // like set_pledged_content_size(0) + write(non_empty) doesn't leave
735 // a partially-written header in the drain.
736 if let Some(pledged) = self.pledged_content_size
737 && self.bytes_consumed >= pledged
738 {
739 return Err(invalid_input_error(
740 "write would exceed pledged content size",
741 ));
742 }
743
744 self.ensure_frame_started(drain)?;
745
746 // Enforce pledged upper bound: truncate the accepted slice to the
747 // remaining allowance so that partial-write semantics are honored
748 // (return Ok(n) with n < buf.len()) instead of failing the full call.
749 // The check above leaves at least one byte of it.
750 let buf = if let Some(pledged) = self.pledged_content_size {
751 debug_assert!(self.bytes_consumed < pledged);
752 let remaining_allowed = pledged - self.bytes_consumed;
753 let accepted = core::cmp::min(
754 buf.len(),
755 usize::try_from(remaining_allowed).unwrap_or(usize::MAX),
756 );
757 &buf[..accepted]
758 } else {
759 buf
760 };
761
762 let block_capacity = self.block_capacity();
763 let mut remaining = buf;
764 let mut consumed = 0usize;
765
766 while !remaining.is_empty() {
767 // A block is emitted the moment it fills, below, and the
768 // capacity is fixed for the frame, so there is always room here.
769 let pending = self.pending_len();
770 debug_assert!(pending < block_capacity);
771 let to_take = core::cmp::min(remaining.len(), block_capacity - pending);
772 self.buffer_input(&remaining[..to_take]);
773 remaining = &remaining[to_take..];
774 consumed += to_take;
775
776 if self.emit_full_pending_block(drain, block_capacity).is_err() {
777 // The bytes this call took are reported; the failure, sticky
778 // on the context now, is what the next call returns.
779 break;
780 }
781 }
782 self.bytes_consumed += consumed as u64;
783 Ok(consumed)
784 }
785
786 /// Emit the buffered partial block as a non-last block and flush `drain`.
787 pub fn flush<D: Write + ?Sized>(&mut self, drain: &mut D) -> Result<(), Error> {
788 self.ensure_open()?;
789 if self.pending_len() == 0 {
790 return drain.flush().map_err(|err| self.fail(err));
791 }
792 self.ensure_frame_started(drain)?;
793 self.emit_pending_block(drain, false)?;
794 drain.flush().map_err(|err| self.fail(err))
795 }
796
797 /// Close the frame in progress into `drain`: its last block, then its
798 /// checksum when enabled. A frame nothing was written to is still a valid
799 /// empty frame. The context is then ready for the next frame, with every
800 /// setting and the dictionary as they were and the pledge cleared.
801 pub fn finish_frame<D: Write + ?Sized>(&mut self, drain: &mut D) -> Result<(), Error> {
802 self.ensure_open()?;
803
804 // Validate the pledge before finalizing the frame. If this is called
805 // before any writes, this also avoids emitting a header with an
806 // incorrect FCS into the drain on mismatch.
807 if let Some(pledged) = self.pledged_content_size
808 && self.bytes_consumed != pledged
809 {
810 return Err(invalid_input_error(
811 "pledged content size does not match bytes consumed",
812 ));
813 }
814
815 self.ensure_frame_started(drain)?;
816 // An empty frame, or one whose input ended on a block boundary, closes
817 // with an empty last block.
818 self.emit_pending_block(drain, true)?;
819
820 #[cfg(feature = "hash")]
821 if self.content_checksum {
822 let checksum = self.hasher.finish() as u32;
823 drain
824 .write_all(&checksum.to_le_bytes())
825 .map_err(|err| self.fail(err))?;
826 }
827
828 drain.flush().map_err(|err| self.fail(err))?;
829 self.abandon_frame();
830 Ok(())
831 }
832
833 /// Drop the frame in progress without closing it (upstream
834 /// `ZSTD_CCtx_reset(ZSTD_reset_session_only)`): its buffered input and its
835 /// pledge go, and the next [`write`](Self::write) starts a new frame. The
836 /// settings, the dictionary and every allocation stay. What the frame
837 /// already wrote to its drain stays there, an unfinished frame.
838 ///
839 /// This is the way on from a [`finish_frame`](Self::finish_frame) refused
840 /// for a pledge the frame did not meet, when the rest of the input is not
841 /// coming; writing it and finishing again completes the frame instead.
842 ///
843 /// # Examples
844 /// ```
845 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel};
846 ///
847 /// let mut context = CompressionContext::new(CompressionLevel::Default);
848 /// let mut unfinished = Vec::new();
849 /// context.set_pledged_content_size(100).unwrap();
850 /// context.write(&mut unfinished, b"only part of it").unwrap();
851 /// assert!(context.finish_frame(&mut unfinished).is_err());
852 /// context.abandon_frame();
853 ///
854 /// let mut frame = Vec::new();
855 /// context.write(&mut frame, b"a frame of its own").unwrap();
856 /// context.finish_frame(&mut frame).unwrap();
857 /// ```
858 pub fn abandon_frame(&mut self) {
859 // What belongs to the frame goes with it; the settings, the dictionary
860 // and every allocation stay for the next one.
861 self.frame_started = false;
862 self.bytes_consumed = 0;
863 self.pledged_content_size = None;
864 // A compressed frame's buffered input is uncommitted in the match
865 // finder, which the next frame's reset drops.
866 self.encoded_scratch.clear();
867 }
868
869 /// Start the next frame now, writing its header to `drain`, rather than at
870 /// its first [`write`](Self::write). Settings are fixed from here on, as
871 /// after a write; a frame already open is left as it is.
872 ///
873 /// ```
874 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel};
875 ///
876 /// let mut context = CompressionContext::new(CompressionLevel::Default);
877 /// let mut frame = Vec::new();
878 /// context.start_frame(&mut frame).unwrap();
879 /// assert!(!frame.is_empty());
880 /// assert!(context.frame_block_size().is_some());
881 /// ```
882 pub fn start_frame<D: Write + ?Sized>(&mut self, drain: &mut D) -> Result<(), Error> {
883 self.ensure_open()?;
884 self.ensure_frame_started(drain)
885 }
886
887 /// The largest block the open frame cuts: the block size limits held to
888 /// the window (upstream `ZSTD_getBlockSize`). `None` between frames.
889 pub fn frame_block_size(&self) -> Option<usize> {
890 self.frame_started.then(|| self.block_capacity())
891 }
892
893 /// Compress `block` as one block of the open frame and append its content,
894 /// without the block header, to `payload` (upstream `ZSTD_compressBlock`).
895 /// Returns whether it compressed: `false` appends nothing, and the caller
896 /// stores the block raw, as a decoder then takes it into its history.
897 /// Either way the block joins the history later blocks match against.
898 ///
899 /// The frame must be open ([`start_frame`](Self::start_frame)), hold no
900 /// input still waiting for its block, and `block` must fit
901 /// [`frame_block_size`](Self::frame_block_size).
902 ///
903 /// ```
904 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel};
905 ///
906 /// let mut context = CompressionContext::new(CompressionLevel::Default);
907 /// context.start_frame(&mut Vec::new()).unwrap();
908 /// let mut payload = Vec::new();
909 /// let compressed = context.compress_block(&b"blocks".repeat(100), &mut payload).unwrap();
910 /// assert!(compressed && payload.len() < 600);
911 /// ```
912 pub fn compress_block(&mut self, block: &[u8], payload: &mut Vec<u8>) -> Result<bool, Error> {
913 self.ensure_open()?;
914 if !self.frame_started {
915 return Err(invalid_input_error(
916 "the frame must be started before a block of it is compressed",
917 ));
918 }
919 if self.pending_len() != 0 {
920 return Err(invalid_input_error(
921 "a block cannot follow input still waiting for its block",
922 ));
923 }
924 if block.len() > self.block_capacity() {
925 return Err(invalid_input_error(
926 "the block is larger than the frame's block size",
927 ));
928 }
929 if block.is_empty() {
930 return Ok(false);
931 }
932 self.buffer_input(block);
933 let start = payload.len();
934 // One block in, one block out: the caller writes its single header,
935 // so the post-split, which would emit several physical blocks, stays
936 // off (upstream `ZSTD_compressBlock` never splits either).
937 let post_split = core::mem::replace(&mut self.state.post_split_allowed, false);
938 let encoded = self.encode_block(payload, block.len(), false);
939 self.state.post_split_allowed = post_split;
940 encoded.map_err(|err| self.fail(err))?;
941 self.bytes_consumed += block.len() as u64;
942 // RFC 8878 3.1.1.2.2: Block_Type 2 is Compressed. A Raw or RLE block
943 // changes no entropy table and no repeat offset, exactly as a block
944 // the caller stores raw does on the decoding side.
945 let compressed = (payload[start] >> 1) & 3 == 2;
946 if compressed {
947 payload.drain(start..start + BLOCK_HEADER_LEN);
948 } else {
949 payload.truncate(start);
950 }
951 Ok(compressed)
952 }
953
954 /// Input written to the open frame that no block has compressed yet: it
955 /// waits for its block to fill, or for a flush. Zero between frames.
956 /// Backs the progress the C `ZSTD_getFrameProgression` reports.
957 ///
958 /// ```
959 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel};
960 ///
961 /// let mut context = CompressionContext::new(CompressionLevel::Default);
962 /// let mut frame = Vec::new();
963 /// context.write(&mut frame, b"less than a block").unwrap();
964 /// assert_eq!(context.buffered_input_len(), 17);
965 /// context.flush(&mut frame).unwrap();
966 /// assert_eq!(context.buffered_input_len(), 0);
967 /// ```
968 pub fn buffered_input_len(&self) -> usize {
969 self.pending_len()
970 }
971
972 /// How much more input completes the block being filled, or a whole block
973 /// when none is partly filled: the read size that lets the next write
974 /// compress a block straight away (upstream zstd `ZSTD_nextInputSizeHint`).
975 ///
976 /// ```
977 /// use structured_zstd::encoding::{CompressionContext, CompressionLevel};
978 ///
979 /// let mut context = CompressionContext::new(CompressionLevel::Default);
980 /// let mut frame = Vec::new();
981 /// context.write(&mut frame, &[7u8; 100]).unwrap();
982 /// assert_eq!(context.next_input_size_hint(), 128 * 1024 - 100);
983 /// ```
984 pub fn next_input_size_hint(&self) -> usize {
985 let block = self.block_capacity();
986 block - self.pending_len() % block
987 }
988
989 fn ensure_open(&self) -> Result<(), Error> {
990 if self.errored {
991 return Err(self.sticky_error());
992 }
993 Ok(())
994 }
995
996 /// Refuse a setting once the frame it would change is under way.
997 fn ensure_settable(&self, too_late: &str) -> Result<(), Error> {
998 self.ensure_open()?;
999 if self.frame_started {
1000 return Err(invalid_input_error(too_late));
1001 }
1002 Ok(())
1003 }
1004
1005 // Cold path (only reached after poisoning). The format!() calls still allocate
1006 // in no_std even though error_with_kind_message/other_error_owned drop the
1007 // message; this is acceptable on an error recovery path to keep match arms simple.
1008 fn sticky_error(&self) -> Error {
1009 match (self.last_error_kind, self.last_error_message.as_deref()) {
1010 (Some(kind), Some(message)) => error_with_kind_message(
1011 kind,
1012 format!(
1013 "streaming encoder is in an errored state due to previous {kind:?} failure: {message}"
1014 ),
1015 ),
1016 (Some(kind), None) => error_from_kind(kind),
1017 (None, Some(message)) => other_error_owned(format!(
1018 "streaming encoder is in an errored state: {message}"
1019 )),
1020 (None, None) => other_error("streaming encoder is in an errored state"),
1021 }
1022 }
1023
1024 fn ensure_frame_started<D: Write + ?Sized>(&mut self, drain: &mut D) -> Result<(), Error> {
1025 if self.frame_started {
1026 return Ok(());
1027 }
1028
1029 // Frames are independent, so the raw-skip's memory of emitted content
1030 // starts empty; the allocation is kept across frames.
1031 self.state.seen_content.reset_for_frame();
1032 // Same reason as the frame compressor's start: what the last frame
1033 // ended on is about to be replaced, and it is exactly the buffer this
1034 // frame wants to build into.
1035 self.state.fse_tables.park_previous_before_frame();
1036 self.ensure_level_supported()?;
1037 // A dictionary is only active when it can actually be primed: the level
1038 // compresses (not `Uncompressed`) AND the matcher supports priming AND a
1039 // dictionary is attached. Mirrors `FrameCompressor`'s `use_dictionary_state`
1040 // so a streaming frame never advertises a `Dictionary_ID` or seeds dict
1041 // entropy/offsets unless the dictionary is genuinely in play (otherwise
1042 // it would emit frames that needlessly require a dictionary at decode
1043 // time).
1044 let use_dictionary_state =
1045 !matches!(self.compression_level, CompressionLevel::Uncompressed)
1046 && self.state.matcher.supports_dictionary_priming()
1047 && self.dictionary.is_some();
1048 // The dictionary sizes select the CDict cParams tier (consumed inside
1049 // `reset`), so hand them over BEFORE reset.
1050 if use_dictionary_state && let Some(dict) = self.dictionary.as_ref() {
1051 self.state.matcher.set_dictionary_size_hint(dict.sizes());
1052 }
1053 // The matcher resolves the frame from the LAST hint it was handed, so
1054 // re-forward the authoritative size (`pledge.or(advisory)`, the same
1055 // value the gates below read) right before the reset: without this a
1056 // pledge followed by a different advisory hint (or vice versa) left
1057 // the matcher and the frame gates on different size tiers.
1058 if let Some(size) = self.pledged_content_size.or(self.source_size_hint) {
1059 self.state.matcher.set_source_size_hint(size);
1060 }
1061 let block_target = crate::encoding::frame_compressor::block_ceiling(
1062 self.target_block_size,
1063 self.max_block_size,
1064 );
1065 // Raw frames keep no history; everything else is read block by block,
1066 // and a pledge makes its length exact. One past what the address space
1067 // holds is laid out as a stream, which sizes to the window anyway.
1068 let ingest = if matches!(self.compression_level, CompressionLevel::Uncompressed) {
1069 crate::encoding::workspace::IngestPlan::Raw
1070 } else if let Some(pledged) = self
1071 .pledged_content_size
1072 .and_then(|pledged| usize::try_from(pledged).ok())
1073 {
1074 crate::encoding::workspace::IngestPlan::PledgedStream(pledged)
1075 } else {
1076 crate::encoding::workspace::IngestPlan::Stream
1077 };
1078 self.state
1079 .reset_for_frame(self.compression_level, block_target, ingest);
1080 let block_capacity = self.block_capacity();
1081 self.state.finish_layout(block_capacity);
1082 // Sync `state.strategy_tag` / `state.pre_split` to the strategy the
1083 // matcher's reset resolved (size- and dictionary-adaptive; a public
1084 // strategy override wins) so the literal-compression gates, the block
1085 // pre-splitter and the dictionary load below agree with the parse.
1086 // Mirrors `FrameCompressor::compress` and keeps both entry points
1087 // byte-equivalent.
1088 let hint = self.pledged_content_size.or(self.source_size_hint);
1089 let params = crate::encoding::frame_compressor::resolve_frame_params(
1090 self.compression_level,
1091 hint,
1092 self.dictionary.as_ref().filter(|_| use_dictionary_state),
1093 );
1094 crate::encoding::frame_compressor::sync_effective_strategy(
1095 &mut self.state,
1096 self.compression_level,
1097 ¶ms,
1098 self.tuning.strategy,
1099 );
1100 self.state.huf_optimal_search =
1101 crate::encoding::frame_compressor::huf_search_enabled(self.state.strategy_tag, hint);
1102 self.state.literal_compression_disabled =
1103 crate::encoding::frame_compressor::literal_compression_disabled(
1104 self.state.strategy_tag,
1105 self.compression_level,
1106 crate::encoding::frame_compressor::gate_target_length(
1107 self.compression_level,
1108 &self.tuning,
1109 self.dictionary.as_ref().filter(|_| use_dictionary_state),
1110 ),
1111 self.tuning.literal_compression,
1112 );
1113 // Seed the repeat-offset history from the dictionary (upstream zstd
1114 // `ZSTD_compress_insertDictionary`), or the default rep codes
1115 // otherwise, and load the dictionary into the match finder: primed,
1116 // restored from a snapshot, or, on a reused context whose reset kept
1117 // it resident, left in place with its offsets reapplied.
1118 // `dict` borrows `self.dictionary`; `self.state` is a disjoint field.
1119 self.state.offset_hist = [1, 4, 8];
1120 if use_dictionary_state && let Some(dict) = self.dictionary.as_ref() {
1121 self.state.offset_hist = dict.inner.offset_hist;
1122 crate::encoding::frame_compressor::load_frame_dictionary(
1123 &mut self.state,
1124 self.compression_level,
1125 dict,
1126 hint,
1127 );
1128 }
1129 // Seed the first block's entropy from the dictionary's encoder tables
1130 // (upstream zstd `cdict->cBlockState`), or clear to defaults.
1131 if use_dictionary_state && let Some(dict) = self.dictionary.as_ref() {
1132 let cache = &dict.inner.entropy;
1133 self.state.last_huff_table.clone_from(&cache.huff);
1134 self.state
1135 .fse_tables
1136 .ll_previous
1137 .clone_from(&cache.ll_previous);
1138 self.state
1139 .fse_tables
1140 .ml_previous
1141 .clone_from(&cache.ml_previous);
1142 self.state
1143 .fse_tables
1144 .of_previous
1145 .clone_from(&cache.of_previous);
1146 let ll_entropy = match cache.ll_previous.as_ref() {
1147 Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
1148 _ => None,
1149 };
1150 let ml_entropy = match cache.ml_previous.as_ref() {
1151 Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
1152 _ => None,
1153 };
1154 let of_entropy = match cache.of_previous.as_ref() {
1155 Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
1156 _ => None,
1157 };
1158 self.state.matcher.seed_dictionary_entropy(
1159 self.state.last_huff_table.as_ref(),
1160 ll_entropy,
1161 ml_entropy,
1162 of_entropy,
1163 );
1164 } else {
1165 self.state.last_huff_table = None;
1166 self.state.fse_tables.ll_previous = None;
1167 self.state.fse_tables.ml_previous = None;
1168 self.state.fse_tables.of_previous = None;
1169 }
1170 self.savings = 0;
1171 #[cfg(feature = "hash")]
1172 {
1173 self.hasher = XxHash64::with_seed(0);
1174 }
1175
1176 let window_size = self.state.matcher.window_size();
1177 if window_size == 0 {
1178 return Err(invalid_input_error(
1179 "matcher reported window_size == 0, which is invalid",
1180 ));
1181 }
1182
1183 let frame_content_size = self.pledged_content_size.filter(|_| self.content_size_flag);
1184 let single_segment =
1185 crate::encoding::frame_header::single_segment(frame_content_size, window_size);
1186
1187 let header = FrameHeader {
1188 frame_content_size,
1189 single_segment,
1190 content_checksum: cfg!(feature = "hash") && self.content_checksum,
1191 dictionary_id: if use_dictionary_state && self.dictionary_id_flag {
1192 // Id 0 is a raw-content dictionary: RFC 8878 spells "no
1193 // dictionary ID" as an absent field, not as a stored zero.
1194 self.dictionary
1195 .as_ref()
1196 .map(|dict| dict.inner.id)
1197 .filter(|id| *id != 0)
1198 .map(u64::from)
1199 } else {
1200 None
1201 },
1202 window_size: if single_segment {
1203 None
1204 } else {
1205 Some(window_size)
1206 },
1207 magicless: self.magicless,
1208 };
1209 let mut encoded_header = Vec::new();
1210 header.serialize(&mut encoded_header);
1211 drain
1212 .write_all(&encoded_header)
1213 .map_err(|err| self.fail(err))?;
1214
1215 if matches!(self.compression_level, CompressionLevel::Uncompressed) {
1216 // The raw block's header goes in front of its input once the
1217 // block's length is known.
1218 self.encoded_scratch.clear();
1219 self.encoded_scratch
1220 .reserve(BLOCK_HEADER_LEN + self.block_capacity());
1221 self.encoded_scratch
1222 .extend_from_slice(&[0; BLOCK_HEADER_LEN]);
1223 }
1224 self.frame_started = true;
1225 Ok(())
1226 }
1227
1228 fn block_capacity(&self) -> usize {
1229 let matcher_window = self.state.matcher.window_size() as usize;
1230 let ceiling = crate::encoding::frame_compressor::block_ceiling(
1231 self.target_block_size,
1232 self.max_block_size,
1233 );
1234 core::cmp::max(1, core::cmp::min(matcher_window, ceiling))
1235 }
1236
1237 /// Input taken for the frame that no block has carried out yet.
1238 fn pending_len(&self) -> usize {
1239 if !self.frame_started {
1240 return 0;
1241 }
1242 match self.compression_level {
1243 CompressionLevel::Uncompressed => self.encoded_scratch.len() - BLOCK_HEADER_LEN,
1244 _ => self.state.matcher.uncommitted_input().len(),
1245 }
1246 }
1247
1248 /// Buffers `bytes` for the block being filled: behind the raw block's
1249 /// header, or uncommitted in the match finder's history, where the block
1250 /// is later matched without another copy.
1251 fn buffer_input(&mut self, bytes: &[u8]) {
1252 match self.compression_level {
1253 CompressionLevel::Uncompressed => self.encoded_scratch.extend_from_slice(bytes),
1254 _ => {
1255 self.state
1256 .matcher
1257 .fill_in_place(bytes.len(), &mut |history| {
1258 history.extend_from_slice(bytes);
1259 (bytes.len(), false)
1260 });
1261 }
1262 }
1263 }
1264
1265 /// Where the full pending block is cut (upstream `ZSTD_compress_frameChunk`
1266 /// sizing every block with `ZSTD_optimalBlockSize`): the pre-splitter's
1267 /// boundary once the frame has saved enough, else the whole block.
1268 /// `remaining` is the input still to come as far as the splitter knows:
1269 /// a full block again while writes continue, the buffered bytes at the
1270 /// end of the frame.
1271 fn pre_split_len(&mut self, block_capacity: usize, remaining: usize) -> usize {
1272 if matches!(self.compression_level, CompressionLevel::Uncompressed) {
1273 return self.pending_len();
1274 }
1275 let pending = self.state.matcher.uncommitted_input();
1276 let window = pending.len();
1277 let cut = crate::encoding::frame_compressor::optimal_block_size_with(
1278 self.state.pre_split.map(usize::from),
1279 pending,
1280 remaining,
1281 block_capacity,
1282 self.savings,
1283 )
1284 .min(window);
1285 // The splitter sees byte statistics only; a cut it proposes inside a
1286 // match the matcher already knows of is moved out.
1287 if cut < window {
1288 self.state.matcher.settle_block_cut(window, cut)
1289 } else {
1290 cut
1291 }
1292 }
1293
1294 /// Emit the pending block once it is full, cut where the pre-splitter
1295 /// says; the suffix stays pending and heads the next block. A failure
1296 /// leaves the context failed.
1297 fn emit_full_pending_block<D: Write + ?Sized>(
1298 &mut self,
1299 drain: &mut D,
1300 block_capacity: usize,
1301 ) -> Result<(), Error> {
1302 if self.pending_len() != block_capacity {
1303 return Ok(());
1304 }
1305 let block_len = self.pre_split_len(block_capacity, block_capacity);
1306 self.encode_block(drain, block_len, false)
1307 .map_err(|err| self.fail(err))
1308 }
1309
1310 fn emit_pending_block<D: Write + ?Sized>(
1311 &mut self,
1312 drain: &mut D,
1313 last_block: bool,
1314 ) -> Result<(), Error> {
1315 let block_capacity = self.block_capacity();
1316 if last_block {
1317 // A full final buffer is cut like any other block (the reader
1318 // path splits it with `remaining = len`); each cut prefix goes
1319 // out as a non-last block and the suffix is re-examined.
1320 while self.pending_len() == block_capacity {
1321 let block_len = self.pre_split_len(block_capacity, block_capacity);
1322 if block_len == block_capacity {
1323 break;
1324 }
1325 self.encode_block(drain, block_len, false)
1326 .map_err(|err| self.fail(err))?;
1327 }
1328 }
1329 let block_len = self.pending_len();
1330 self.encode_block(drain, block_len, last_block)
1331 .map_err(|err| self.fail(err))
1332 }
1333
1334 // Exhaustive match kept intentionally: adding a new CompressionLevel
1335 // variant will produce a compile error here, forcing the developer to
1336 // decide whether the streaming encoder supports it before shipping.
1337 fn ensure_level_supported(&self) -> Result<(), Error> {
1338 match self.compression_level {
1339 CompressionLevel::Uncompressed
1340 | CompressionLevel::Fastest
1341 | CompressionLevel::Default
1342 | CompressionLevel::Better
1343 | CompressionLevel::Best
1344 | CompressionLevel::Level(_) => Ok(()),
1345 }
1346 }
1347
1348 /// Writes the first `block_len` pending bytes to `drain` as one block,
1349 /// `last_block` if it closes the frame.
1350 fn encode_block<D: Write + ?Sized>(
1351 &mut self,
1352 drain: &mut D,
1353 block_len: usize,
1354 last_block: bool,
1355 ) -> Result<(), Error> {
1356 if matches!(self.compression_level, CompressionLevel::Uncompressed) {
1357 // The block was assembled behind its header as it was written.
1358 debug_assert_eq!(block_len, self.pending_len());
1359 let header = BlockHeader {
1360 last_block,
1361 block_type: crate::blocks::block::BlockType::Raw,
1362 // At most one block, which a block header's 21-bit size holds.
1363 block_size: block_len as u32,
1364 };
1365 self.encoded_scratch[..BLOCK_HEADER_LEN].copy_from_slice(&header.to_le_bytes());
1366 drain.write_all(&self.encoded_scratch)?;
1367 #[cfg(feature = "hash")]
1368 if self.content_checksum {
1369 self.hasher.write(&self.encoded_scratch[BLOCK_HEADER_LEN..]);
1370 }
1371 self.encoded_scratch.truncate(BLOCK_HEADER_LEN);
1372 return Ok(());
1373 }
1374 self.encoded_scratch.clear();
1375 if block_len == 0 {
1376 // Only the last block of a frame is ever empty.
1377 let header = BlockHeader {
1378 last_block,
1379 block_type: crate::blocks::block::BlockType::Raw,
1380 block_size: 0,
1381 };
1382 header.serialize(&mut self.encoded_scratch);
1383 return drain.write_all(&self.encoded_scratch);
1384 }
1385 let dict_active =
1386 self.dictionary.is_some() && self.state.matcher.supports_dictionary_priming();
1387 compress_block_encoded(
1388 &mut self.state,
1389 self.compression_level,
1390 last_block,
1391 block_len,
1392 &mut self.encoded_scratch,
1393 dict_active,
1394 // No FrameEmitInfo on the streaming encoder path: it does not
1395 // surface per-block layout, so no sidecar.
1396 #[cfg(feature = "lsm")]
1397 None,
1398 #[cfg(all(feature = "lsm", feature = "hash"))]
1399 None,
1400 );
1401 // The block is committed to the match finder whether or not the drain
1402 // takes it; a drain that fails leaves the context failed.
1403 drain.write_all(&self.encoded_scratch)?;
1404 // `savings` counts the block header too, as upstream's
1405 // `ZSTD_compress_frameChunk` does (`cSize` includes it).
1406 self.savings += block_len as i64 - self.encoded_scratch.len() as i64;
1407 #[cfg(feature = "hash")]
1408 if self.content_checksum {
1409 self.hasher.write(self.state.matcher.get_last_space());
1410 }
1411 Ok(())
1412 }
1413
1414 fn fail(&mut self, err: Error) -> Error {
1415 self.errored = true;
1416 if self.last_error_kind.is_none() {
1417 self.last_error_kind = Some(err.kind());
1418 }
1419 if self.last_error_message.is_none() {
1420 self.last_error_message = Some(err.to_string());
1421 }
1422 err
1423 }
1424}
1425
1426/// Bytes of a block header (RFC 8878 3.1.1.2).
1427const BLOCK_HEADER_LEN: usize = 3;
1428
1429fn error_from_kind(kind: ErrorKind) -> Error {
1430 Error::from(kind)
1431}
1432
1433fn error_with_kind_message(kind: ErrorKind, message: String) -> Error {
1434 #[cfg(feature = "std")]
1435 {
1436 Error::new(kind, message)
1437 }
1438 #[cfg(not(feature = "std"))]
1439 {
1440 Error::new(kind, alloc::boxed::Box::new(message))
1441 }
1442}
1443
1444fn invalid_input_error(message: &str) -> Error {
1445 #[cfg(feature = "std")]
1446 {
1447 Error::new(ErrorKind::InvalidInput, message)
1448 }
1449 #[cfg(not(feature = "std"))]
1450 {
1451 Error::new(
1452 ErrorKind::Other,
1453 alloc::boxed::Box::new(alloc::string::String::from(message)),
1454 )
1455 }
1456}
1457
1458fn other_error_owned(message: String) -> Error {
1459 #[cfg(feature = "std")]
1460 {
1461 Error::other(message)
1462 }
1463 #[cfg(not(feature = "std"))]
1464 {
1465 Error::new(ErrorKind::Other, alloc::boxed::Box::new(message))
1466 }
1467}
1468
1469fn other_error(message: &str) -> Error {
1470 #[cfg(feature = "std")]
1471 {
1472 Error::other(message)
1473 }
1474 #[cfg(not(feature = "std"))]
1475 {
1476 Error::new(
1477 ErrorKind::Other,
1478 alloc::boxed::Box::new(alloc::string::String::from(message)),
1479 )
1480 }
1481}
1482
1483#[cfg(test)]
1484mod tests;