Skip to main content

structured_zstd/encoding/
streaming_encoder.rs

1use alloc::format;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::mem;
5
6use crate::common::MAX_BLOCK_SIZE;
7#[cfg(feature = "hash")]
8use core::hash::Hasher;
9#[cfg(feature = "hash")]
10use twox_hash::XxHash64;
11
12use crate::encoding::levels::compress_block_encoded;
13use crate::encoding::{
14    CompressionLevel, EncoderDictionary, MatchGeneratorDriver, Matcher, block_header::BlockHeader,
15    frame_compressor::CachedDictionaryEntropy, frame_compressor::CompressState,
16    frame_compressor::FseTables, frame_compressor::PreviousFseTable, frame_header::FrameHeader,
17};
18use crate::io::{Error, ErrorKind, Write};
19
20/// Incremental frame encoder that implements [`Write`].
21///
22/// Data can be provided with multiple `write()` calls. Full blocks are compressed
23/// automatically, `flush()` emits the currently buffered partial block as non-last,
24/// and `finish()` closes the frame and returns the wrapped writer.
25pub struct StreamingEncoder<W: Write, M: Matcher = MatchGeneratorDriver> {
26    drain: Option<W>,
27    compression_level: CompressionLevel,
28    state: CompressState<M>,
29    pending: Vec<u8>,
30    encoded_scratch: Vec<u8>,
31    errored: bool,
32    last_error_kind: Option<ErrorKind>,
33    last_error_message: Option<String>,
34    frame_started: bool,
35    /// Upper bound on emitted block sizes (upstream `ZSTD_c_targetCBlockSize`
36    /// semantics; see `FrameCompressor::set_target_block_size`). `None` =
37    /// the format's 128 KiB ceiling.
38    target_block_size: Option<u32>,
39    pledged_content_size: Option<u64>,
40    /// Advisory source-size hint from [`set_source_size_hint`](Self::set_source_size_hint).
41    /// Unlike `pledged_content_size` it carries no end-of-frame enforcement, but
42    /// it still feeds the small-input gates (matcher sizing AND the Fast HUF
43    /// fast-path gate) so `set_source_size_hint(small)` reduces work the same way
44    /// a pledge does. The HUF gate reads `pledged_content_size.or(source_size_hint)`.
45    source_size_hint: Option<u64>,
46    /// Whether a pledged size is written into the header's
47    /// `Frame_Content_Size` field (upstream `ZSTD_c_contentSizeFlag`).
48    /// Pledge *enforcement* is independent of this flag — upstream
49    /// validates consumed bytes against the pledge at frame end even
50    /// when the header omits the field. Default `true`.
51    content_size_flag: bool,
52    bytes_consumed: u64,
53    /// Upstream `ZSTD_compress_frameChunk` `savings`: bytes consumed minus
54    /// bytes produced so far in this frame; the block pre-splitter only cuts
55    /// full blocks once the frame has saved enough.
56    savings: i64,
57    /// Effective strategy tag (and lazy depth) when a public-parameter
58    /// [`Strategy`](crate::encoding::Strategy) override (#27) is active, mirroring
59    /// [`FrameCompressor`](crate::encoding::FrameCompressor)'s field. `Some`
60    /// survives frame start so the literal-compression gates and the block
61    /// splitter run the same strategy the matcher does; `None` keeps the
62    /// level-derived tag.
63    strategy_override: Option<(crate::encoding::strategy::StrategyTag, u8)>,
64    /// Public `target_length` override (#27), kept so the raw-literals gate
65    /// resolved at frame start reads the value the matcher runs (dropped on a
66    /// dictionary frame, where the CDict's targetLength applies).
67    target_length_override: Option<u32>,
68    /// `ZSTD_f_zstd1_magicless` — omit the 4-byte magic number prefix.
69    /// Default false. See [`Self::set_magicless`].
70    magicless: bool,
71    /// Whether to emit a trailing XXH64 content checksum and set the frame
72    /// header's `Content_Checksum_flag` (upstream `ZSTD_c_checksumFlag`).
73    /// Default `false`, matching the upstream library default; combined with
74    /// the `hash` feature, so without `hash` no checksum is emitted
75    /// regardless. See [`Self::set_content_checksum`].
76    content_checksum: bool,
77    /// Dictionary applied to the frame (upstream zstd `ZSTD_CCtx_loadDictionary` on a
78    /// streaming context). `None` = no dictionary. Set before the first write.
79    dictionary: Option<EncoderDictionary>,
80    /// Whether the frame header records the attached dictionary's ID
81    /// (upstream `ZSTD_c_dictIDFlag`). Default `true`. Raw-content
82    /// dictionaries (upstream `ZSTD_CCtx_refPrefix`) carry a synthetic
83    /// non-zero ID that must not reach the wire, so their attach path
84    /// turns this off. See [`Self::set_dictionary_id_flag`].
85    dictionary_id_flag: bool,
86    /// Encoder entropy tables (literals Huffman + LL/ML/OF FSE "previous"
87    /// tables) the dictionary seeds into the first block, derived once when the
88    /// dictionary is attached so each frame start is a cheap clone.
89    dictionary_entropy_cache: Option<CachedDictionaryEntropy>,
90    #[cfg(feature = "hash")]
91    hasher: XxHash64,
92}
93
94impl<W: Write> StreamingEncoder<W, MatchGeneratorDriver> {
95    /// Creates a streaming encoder backed by the default match generator.
96    ///
97    /// The encoder writes compressed bytes into `drain` and applies `compression_level`
98    /// to all subsequently written blocks.
99    pub fn new(drain: W, compression_level: CompressionLevel) -> Self {
100        Self::new_with_matcher(
101            MatchGeneratorDriver::new(MAX_BLOCK_SIZE as usize, 1),
102            drain,
103            compression_level,
104        )
105    }
106
107    /// Configure fine-grained compression parameters (#27): resets the level to
108    /// the parameters' level and installs the per-knob overrides (window / hash
109    /// / chain / search logs, strategy, long-distance matching) applied at the
110    /// next frame. Mirrors [`FrameCompressor::set_parameters`]. Must be called
111    /// before the first [`write`](Write::write). Only the built-in
112    /// `MatchGeneratorDriver` exposes the override knobs, so this lives on the
113    /// default-matcher impl.
114    pub fn set_parameters(
115        &mut self,
116        params: &crate::encoding::CompressionParameters,
117    ) -> Result<(), Error> {
118        self.ensure_open()?;
119        if self.frame_started {
120            return Err(invalid_input_error(
121                "compression parameters must be set before the first write",
122            ));
123        }
124        self.compression_level = params.level();
125        let overrides = params.overrides();
126        // Persist the strategy override so `ensure_frame_started`'s level-based
127        // resync does not discard it (matching `FrameCompressor::set_parameters`).
128        self.strategy_override = overrides.strategy.map(|s| (s.tag(), s.lazy_depth()));
129        self.target_length_override = overrides.target_length;
130        self.state.strategy_tag = self.strategy_override.map_or_else(
131            || {
132                crate::encoding::strategy::StrategyTag::for_compression_level(
133                    self.compression_level,
134                )
135            },
136            |(tag, _)| tag,
137        );
138        self.state.huf_optimal_search = crate::encoding::frame_compressor::huf_search_enabled(
139            self.state.strategy_tag,
140            self.pledged_content_size.or(self.source_size_hint),
141        );
142        self.state.matcher.set_param_overrides(Some(overrides));
143        Ok(())
144    }
145}
146
147impl<W: Write, M: Matcher> StreamingEncoder<W, M> {
148    /// Creates a streaming encoder with an explicitly provided matcher implementation.
149    ///
150    /// This constructor is primarily intended for tests and advanced callers that need
151    /// custom match-window behavior.
152    pub fn new_with_matcher(matcher: M, drain: W, compression_level: CompressionLevel) -> Self {
153        Self {
154            drain: Some(drain),
155            compression_level,
156            state: CompressState {
157                matcher,
158                copy_tier: crate::decoding::simd_copy::ExactCopyTier::resolve(),
159                last_huff_table: None,
160                huff_table_spare: None,
161                huff_rollback: None,
162                huff_weights: Default::default(),
163                seen_content: Default::default(),
164                fse_tables: FseTables::new(),
165                block_scratch: crate::encoding::blocks::CompressedBlockScratch::new(),
166                offset_hist: [1, 4, 8],
167                strategy_tag: crate::encoding::strategy::StrategyTag::for_compression_level(
168                    compression_level,
169                ),
170                pre_split: crate::encoding::levels::config::level_pre_split(compression_level)
171                    .map(|tier| tier as u8),
172                huf_optimal_search: true,
173                literal_compression_disabled: matches!(
174                    compression_level,
175                    CompressionLevel::Level(n) if n < 0
176                ),
177            },
178            pending: Vec::new(),
179            encoded_scratch: Vec::new(),
180            errored: false,
181            last_error_kind: None,
182            last_error_message: None,
183            frame_started: false,
184            target_block_size: None,
185            pledged_content_size: None,
186            source_size_hint: None,
187            content_size_flag: true,
188            bytes_consumed: 0,
189            savings: 0,
190            strategy_override: None,
191            target_length_override: None,
192            magicless: false,
193            content_checksum: false,
194            dictionary: None,
195            dictionary_id_flag: true,
196            dictionary_entropy_cache: None,
197            #[cfg(feature = "hash")]
198            hasher: XxHash64::with_seed(0),
199        }
200    }
201
202    /// Set an upper bound on each physical block's payload (semantics of
203    /// upstream `ZSTD_c_targetCBlockSize`): every block carries at most
204    /// `target` payload bytes, +3-byte block header on the wire — the
205    /// upstream knob is likewise a convergence target for block sizing,
206    /// not a cap on header-inclusive wire bytes. Clamped to
207    /// `[MIN_TARGET_BLOCK_SIZE, MAX_BLOCK_SIZE]`; mirrors
208    /// `FrameCompressor::set_target_block_size`. Must be set before the
209    /// first write.
210    pub fn set_target_block_size(&mut self, target: Option<u32>) -> Result<(), Error> {
211        self.ensure_open()?;
212        if self.frame_started {
213            return Err(invalid_input_error(
214                "the block-size target must be set before the first write",
215            ));
216        }
217        self.target_block_size = target.map(|t| {
218            t.clamp(
219                crate::common::MIN_TARGET_BLOCK_SIZE,
220                crate::common::MAX_BLOCK_SIZE,
221            )
222        });
223        Ok(())
224    }
225
226    /// Enable or disable the trailing XXH64 content checksum
227    /// (upstream `ZSTD_c_checksumFlag`). Default `false`, matching the
228    /// upstream library default (`ZSTD_c_checksumFlag = 0`). Must be called
229    /// before the first [`write`](Write::write); once the frame header is
230    /// emitted the flag is fixed, so a late change returns an error rather
231    /// than producing a header/trailer mismatch. Without the `hash` feature
232    /// no checksum is emitted regardless.
233    pub fn set_content_checksum(&mut self, emit: bool) -> Result<(), Error> {
234        self.ensure_open()?;
235        if self.frame_started {
236            return Err(invalid_input_error(
237                "content checksum must be set before the first write",
238            ));
239        }
240        self.content_checksum = emit;
241        Ok(())
242    }
243
244    /// Enable or disable magicless frame format (`ZSTD_f_zstd1_magicless`).
245    ///
246    /// When set to `true`, the frame header serialized by this encoder
247    /// omits the 4-byte magic number prefix. Must be called BEFORE the
248    /// first [`write`](Write::write) call; calling it after the frame
249    /// header has already been emitted returns an error so the caller
250    /// can't be misled into thinking they produced a magicless stream.
251    pub fn set_magicless(&mut self, magicless: bool) -> Result<(), Error> {
252        self.ensure_open()?;
253        if self.frame_started {
254            return Err(invalid_input_error(
255                "magicless format must be set before the first write",
256            ));
257        }
258        self.magicless = magicless;
259        Ok(())
260    }
261
262    /// Pledge the total uncompressed content size for this frame.
263    ///
264    /// When set, the frame header will include a `Frame_Content_Size` field.
265    /// This enables decoders to pre-allocate output buffers.
266    /// The pledged size is also forwarded as a source-size hint to the
267    /// matcher so small inputs can use smaller matching tables.
268    ///
269    /// Must be called **before** the first [`write`](Write::write) call;
270    /// calling it after the frame header has already been emitted returns an
271    /// error.
272    pub fn set_pledged_content_size(&mut self, size: u64) -> Result<(), Error> {
273        self.ensure_open()?;
274        if self.frame_started {
275            return Err(invalid_input_error(
276                "pledged content size must be set before the first write",
277            ));
278        }
279        self.pledged_content_size = Some(size);
280        // Also use pledged size as source-size hint so the matcher
281        // can select smaller tables for small inputs.
282        self.state.matcher.set_source_size_hint(size);
283        Ok(())
284    }
285
286    /// Control whether the pledged size is written into the header's
287    /// `Frame_Content_Size` field (upstream `ZSTD_c_contentSizeFlag`,
288    /// default on). With the flag off the header omits the field, but a
289    /// pledge set via [`set_pledged_content_size`](Self::set_pledged_content_size)
290    /// is still enforced against the bytes actually written. Must be
291    /// called before the first [`write`](Write::write).
292    pub fn set_content_size_flag(&mut self, emit: bool) -> Result<(), Error> {
293        self.ensure_open()?;
294        if self.frame_started {
295            return Err(invalid_input_error(
296                "content size flag must be set before the first write",
297            ));
298        }
299        self.content_size_flag = emit;
300        Ok(())
301    }
302
303    /// Provide a hint about the total uncompressed size for the next frame.
304    ///
305    /// Unlike [`set_pledged_content_size`](Self::set_pledged_content_size),
306    /// this does **not** enforce that exactly `size` bytes are written; it
307    /// may reduce matcher tables, advertised frame window, and block sizing
308    /// for small inputs. Must be called before the first
309    /// [`write`](Write::write).
310    pub fn set_source_size_hint(&mut self, size: u64) -> Result<(), Error> {
311        self.ensure_open()?;
312        if self.frame_started {
313            return Err(invalid_input_error(
314                "source size hint must be set before the first write",
315            ));
316        }
317        self.state.matcher.set_source_size_hint(size);
318        // Feed the same hint to the Fast HUF fast-path gate (resolved in
319        // `set_parameters` / `ensure_frame_started` via
320        // `pledged_content_size.or(source_size_hint)`), so a small advisory size
321        // also lifts Fast streams off the expensive optimal-HUF search.
322        self.source_size_hint = Some(size);
323        Ok(())
324    }
325
326    /// Attach a dictionary blob to the frame (upstream zstd
327    /// `ZSTD_CCtx_loadDictionary` on a streaming context, which loads in
328    /// `ZSTD_dct_auto` mode): a blob prefixed with
329    /// [`DICTIONARY_MAGIC`](crate::decoding::DICTIONARY_MAGIC) is a serialized
330    /// dictionary, anything else is raw content. The dictionary primes the
331    /// match-finder and seeds the first block's entropy tables + repeat
332    /// offsets; a serialized one's ID is written into the frame header, while
333    /// raw content has none to write, so the decoder must be given the same
334    /// bytes explicitly. Must be called before the first
335    /// [`write`](Write::write); repeat offsets must be non-zero.
336    pub fn set_dictionary_from_bytes(&mut self, raw_dictionary: &[u8]) -> Result<(), Error> {
337        if raw_dictionary.is_empty() {
338            // An empty buffer is how the same upstream entry point is told
339            // there is no dictionary: it clears and succeeds. Still refused
340            // once the frame is open, like any other attach.
341            self.ensure_open()?;
342            if self.frame_started {
343                return Err(invalid_input_error(
344                    "dictionary must be attached before the first write",
345                ));
346            }
347            // The entropy tables were built at attach time and go with it:
348            // holding them past the clear keeps Huffman and FSE allocations
349            // the encoder can no longer reach, for as long as it lives, and
350            // reports them in `heap_size`. The primed match-finder snapshot
351            // needs no such call — priming happens at the first write, which
352            // is also the point after which this setter refuses to run, so
353            // there is never one to drop here.
354            self.dictionary_entropy_cache = None;
355            self.dictionary = None;
356            return Ok(());
357        }
358        let dict = EncoderDictionary::from_serialized_or_raw_content(raw_dictionary)
359            .map_err(|err| invalid_input_error(&alloc::format!("invalid dictionary: {err:?}")))?;
360        self.set_encoder_dictionary(dict)
361    }
362
363    /// Whether the frame header records the dictionary ID when a dictionary
364    /// is attached (upstream `ZSTD_c_dictIDFlag` semantics; default `true`).
365    /// Mirrors [`FrameCompressor::set_dictionary_id_flag`]. Decoders can still
366    /// decode such frames by supplying the dictionary explicitly.
367    pub fn set_dictionary_id_flag(&mut self, emit: bool) -> Result<(), Error> {
368        self.ensure_open()?;
369        if self.frame_started {
370            return Err(invalid_input_error(
371                "dictionary ID flag must be set before the first write",
372            ));
373        }
374        self.dictionary_id_flag = emit;
375        Ok(())
376    }
377
378    /// Attach an already-parsed [`EncoderDictionary`] to the frame. See
379    /// [`set_dictionary_from_bytes`](Self::set_dictionary_from_bytes); must be
380    /// called before the first write.
381    pub fn set_encoder_dictionary(&mut self, dict: EncoderDictionary) -> Result<(), Error> {
382        self.ensure_open()?;
383        if self.frame_started {
384            return Err(invalid_input_error(
385                "dictionary must be attached before the first write",
386            ));
387        }
388        // A zero id marks a raw-content dictionary, which carries no header to
389        // hold one; the frame then records no dictionary ID and the decoder
390        // must be given the same bytes explicitly.
391        let inner = &dict.inner;
392        if inner.offset_hist.contains(&0) {
393            return Err(invalid_input_error(
394                "dictionary carries a zero repeat offset",
395            ));
396        }
397        self.dictionary_entropy_cache = Some(CachedDictionaryEntropy::from_dictionary(inner));
398        self.dictionary = Some(dict);
399        Ok(())
400    }
401
402    /// Returns an immutable reference to the wrapped output drain.
403    ///
404    /// The drain remains available for the encoder lifetime; [`finish`](Self::finish)
405    /// consumes the encoder and returns ownership of the drain.
406    pub fn get_ref(&self) -> &W {
407        self.drain
408            .as_ref()
409            .expect("streaming encoder drain is present until finish consumes self")
410    }
411
412    /// Total heap bytes this encoder's allocations hold, excluding the
413    /// inline struct and the drain `W` (whose footprint the owner can
414    /// measure through [`get_ref`](Self::get_ref)): match-finder tables /
415    /// history / recycled buffers, retained Huffman tables, the staging
416    /// `pending` / `encoded_scratch` buffers, the retained dictionary
417    /// content, and the cached dictionary entropy tables. Mirrors
418    /// `FrameCompressor::heap_size` so a context can report its true
419    /// footprint through `ZSTD_sizeof_CCtx`.
420    pub fn heap_size(&self) -> usize {
421        let mut total = self.state.matcher.heap_size();
422        total += self
423            .state
424            .last_huff_table
425            .as_ref()
426            .map_or(0, |table| table.heap_size());
427        total += self
428            .state
429            .huff_table_spare
430            .as_ref()
431            .map_or(0, |table| table.heap_size());
432        // Kept between blocks and frames; see `FrameCompressor::heap_size`.
433        total += self.state.huff_weights.heap_size();
434        total += self.state.retained_scratch_heap_size();
435        total += self.state.seen_content.heap_size();
436        total += self.pending.capacity();
437        total += self.encoded_scratch.capacity();
438        total += self
439            .dictionary
440            .as_ref()
441            .map_or(0, |d| d.inner.dict_content.capacity());
442        total += self
443            .dictionary_entropy_cache
444            .as_ref()
445            .map_or(0, CachedDictionaryEntropy::heap_size);
446        total
447    }
448
449    /// Returns a mutable reference to the wrapped output drain.
450    ///
451    /// It is inadvisable to directly write to the underlying writer, as doing
452    /// so would corrupt the zstd frame being assembled by the encoder.
453    ///
454    /// The drain remains available for the encoder lifetime; [`finish`](Self::finish)
455    /// consumes the encoder and returns ownership of the drain.
456    pub fn get_mut(&mut self) -> &mut W {
457        self.drain
458            .as_mut()
459            .expect("streaming encoder drain is present until finish consumes self")
460    }
461
462    /// Finalizes the current zstd frame and returns the wrapped output drain.
463    ///
464    /// If no payload was written yet, this still emits a valid empty frame.
465    /// Calling this method consumes the encoder.
466    pub fn finish(mut self) -> Result<W, Error> {
467        self.ensure_open()?;
468
469        // Validate the pledge before finalizing the frame. If finish() is
470        // called before any writes, this also avoids emitting a header with
471        // an incorrect FCS into the drain on mismatch.
472        if let Some(pledged) = self.pledged_content_size
473            && self.bytes_consumed != pledged
474        {
475            return Err(invalid_input_error(
476                "pledged content size does not match bytes consumed",
477            ));
478        }
479
480        self.ensure_frame_started()?;
481
482        if self.pending.is_empty() {
483            self.write_empty_last_block()
484                .map_err(|err| self.fail(err))?;
485        } else {
486            self.emit_pending_block(true)?;
487        }
488
489        let mut drain = self
490            .drain
491            .take()
492            .expect("streaming encoder drain must be present when finishing");
493
494        #[cfg(feature = "hash")]
495        if self.content_checksum {
496            let checksum = self.hasher.finish() as u32;
497            drain
498                .write_all(&checksum.to_le_bytes())
499                .map_err(|err| self.fail(err))?;
500        }
501
502        drain.flush().map_err(|err| self.fail(err))?;
503        Ok(drain)
504    }
505
506    fn ensure_open(&self) -> Result<(), Error> {
507        if self.errored {
508            return Err(self.sticky_error());
509        }
510        Ok(())
511    }
512
513    // Cold path (only reached after poisoning). The format!() calls still allocate
514    // in no_std even though error_with_kind_message/other_error_owned drop the
515    // message; this is acceptable on an error recovery path to keep match arms simple.
516    fn sticky_error(&self) -> Error {
517        match (self.last_error_kind, self.last_error_message.as_deref()) {
518            (Some(kind), Some(message)) => error_with_kind_message(
519                kind,
520                format!(
521                    "streaming encoder is in an errored state due to previous {kind:?} failure: {message}"
522                ),
523            ),
524            (Some(kind), None) => error_from_kind(kind),
525            (None, Some(message)) => other_error_owned(format!(
526                "streaming encoder is in an errored state: {message}"
527            )),
528            (None, None) => other_error("streaming encoder is in an errored state"),
529        }
530    }
531
532    fn drain_mut(&mut self) -> Result<&mut W, Error> {
533        self.drain
534            .as_mut()
535            .ok_or_else(|| other_error("streaming encoder has no active drain"))
536    }
537
538    fn ensure_frame_started(&mut self) -> Result<(), Error> {
539        if self.frame_started {
540            return Ok(());
541        }
542
543        // Frames are independent, so the raw-skip's memory of emitted content
544        // starts empty; the allocation is kept across frames.
545        self.state.seen_content.reset_for_frame();
546        // Same reason as the frame compressor's start: what the last frame
547        // ended on is about to be replaced, and it is exactly the buffer this
548        // frame wants to build into.
549        self.state.fse_tables.park_previous_before_frame();
550        self.ensure_level_supported()?;
551        // A dictionary is only active when it can actually be primed: the level
552        // compresses (not `Uncompressed`) AND the matcher supports priming AND a
553        // dictionary is attached. Mirrors `FrameCompressor`'s `use_dictionary_state`
554        // so a streaming frame never advertises a `Dictionary_ID`, disables
555        // single-segment, or seeds dict entropy/offsets unless the dictionary is
556        // genuinely in play (otherwise it would emit frames that needlessly
557        // require a dictionary at decode time).
558        let use_dictionary_state =
559            !matches!(self.compression_level, CompressionLevel::Uncompressed)
560                && self.state.matcher.supports_dictionary_priming()
561                && self.dictionary.is_some();
562        // The dictionary sizes select the CDict cParams tier (consumed inside
563        // `reset`), so hand them over BEFORE reset.
564        if use_dictionary_state && let Some(dict) = self.dictionary.as_ref() {
565            self.state.matcher.set_dictionary_size_hint(dict.sizes());
566        }
567        // The matcher resolves the frame from the LAST hint it was handed, so
568        // re-forward the authoritative size (`pledge.or(advisory)`, the same
569        // value the gates below read) right before the reset: without this a
570        // pledge followed by a different advisory hint (or vice versa) left
571        // the matcher and the frame gates on different size tiers.
572        if let Some(size) = self.pledged_content_size.or(self.source_size_hint) {
573            self.state.matcher.set_source_size_hint(size);
574        }
575        self.state.matcher.reset(self.compression_level);
576        // Seed the repeat-offset history from the dictionary (upstream zstd
577        // `ZSTD_compress_insertDictionary`), or the default rep codes otherwise.
578        self.state.offset_hist = if use_dictionary_state {
579            self.dictionary
580                .as_ref()
581                .map(|dict| dict.inner.offset_hist)
582                .unwrap_or([1, 4, 8])
583        } else {
584            [1, 4, 8]
585        };
586        // Prime the match-finder with the dictionary content + offsets.
587        // `dict` borrows `self.dictionary`; `self.state.matcher` is a disjoint
588        // field, so the immutable dict borrow and the mutable matcher borrow
589        // coexist (field-level borrow splitting) with no conflict.
590        if use_dictionary_state && let Some(dict) = self.dictionary.as_ref() {
591            let offset_hist = dict.inner.offset_hist;
592            self.state
593                .matcher
594                .prime_with_dictionary(dict.inner.dict_content.as_slice(), offset_hist);
595        }
596        // Seed the first block's entropy from the dictionary's cached encoder
597        // tables (upstream zstd `cdict->cBlockState`), or clear to defaults.
598        if use_dictionary_state && let Some(cache) = self.dictionary_entropy_cache.as_ref() {
599            self.state.last_huff_table.clone_from(&cache.huff);
600            self.state
601                .fse_tables
602                .ll_previous
603                .clone_from(&cache.ll_previous);
604            self.state
605                .fse_tables
606                .ml_previous
607                .clone_from(&cache.ml_previous);
608            self.state
609                .fse_tables
610                .of_previous
611                .clone_from(&cache.of_previous);
612            let ll_entropy = match cache.ll_previous.as_ref() {
613                Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
614                _ => None,
615            };
616            let ml_entropy = match cache.ml_previous.as_ref() {
617                Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
618                _ => None,
619            };
620            let of_entropy = match cache.of_previous.as_ref() {
621                Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
622                _ => None,
623            };
624            self.state.matcher.seed_dictionary_entropy(
625                self.state.last_huff_table.as_ref(),
626                ll_entropy,
627                ml_entropy,
628                of_entropy,
629            );
630        } else {
631            self.state.last_huff_table = None;
632            self.state.fse_tables.ll_previous = None;
633            self.state.fse_tables.ml_previous = None;
634            self.state.fse_tables.of_previous = None;
635        }
636        // Sync `state.strategy_tag` / `state.pre_split` to the strategy the
637        // matcher's reset resolved (size- and dictionary-adaptive; a public
638        // strategy override wins on a plain frame, a dictionary frame runs the
639        // CDict's strategy) so the literal-compression gates and the block
640        // pre-splitter agree with the parse. Mirrors `FrameCompressor::compress`
641        // and keeps both entry points byte-equivalent.
642        let hint = self.pledged_content_size.or(self.source_size_hint);
643        let (params, dict_frame) = crate::encoding::frame_compressor::resolve_frame_params(
644            self.compression_level,
645            hint,
646            self.dictionary.as_ref().filter(|_| use_dictionary_state),
647        );
648        crate::encoding::frame_compressor::sync_effective_strategy(
649            &mut self.state,
650            self.compression_level,
651            &params,
652            self.strategy_override.filter(|_| !dict_frame),
653        );
654        self.state.huf_optimal_search =
655            crate::encoding::frame_compressor::huf_search_enabled(self.state.strategy_tag, hint);
656        self.state.literal_compression_disabled =
657            crate::encoding::frame_compressor::literal_compression_disabled(
658                self.state.strategy_tag,
659                self.compression_level,
660                self.target_length_override.filter(|_| !dict_frame),
661            );
662        self.savings = 0;
663        #[cfg(feature = "hash")]
664        {
665            self.hasher = XxHash64::with_seed(0);
666        }
667
668        let window_size = self.state.matcher.window_size();
669        if window_size == 0 {
670            return Err(invalid_input_error(
671                "matcher reported window_size == 0, which is invalid",
672            ));
673        }
674
675        // Single-segment is incompatible with a dictionary (the dictionary
676        // pushes referenceable history before the content, so the frame needs
677        // an explicit window descriptor); gate it off when a dict is attached,
678        // mirroring `FrameCompressor`'s `!use_dictionary_state` guard.
679        // Single-segment also requires the FCS field to be present
680        // (`content_size_flag`): the layout drops the window descriptor,
681        // so the header must carry the content size for decoders to size
682        // their window.
683        let single_segment = self.content_size_flag
684            && !use_dictionary_state
685            && self
686                .pledged_content_size
687                .map(|size| (512..=(1 << 14)).contains(&size) && size <= window_size)
688                .unwrap_or(false);
689
690        let header = FrameHeader {
691            frame_content_size: if self.content_size_flag {
692                self.pledged_content_size
693            } else {
694                None
695            },
696            single_segment,
697            content_checksum: cfg!(feature = "hash") && self.content_checksum,
698            dictionary_id: if use_dictionary_state && self.dictionary_id_flag {
699                // Id 0 is a raw-content dictionary: RFC 8878 spells "no
700                // dictionary ID" as an absent field, not as a stored zero.
701                self.dictionary
702                    .as_ref()
703                    .map(|dict| dict.inner.id)
704                    .filter(|id| *id != 0)
705                    .map(u64::from)
706            } else {
707                None
708            },
709            window_size: if single_segment {
710                None
711            } else {
712                Some(window_size)
713            },
714            magicless: self.magicless,
715        };
716        let mut encoded_header = Vec::new();
717        header.serialize(&mut encoded_header);
718        self.drain_mut()
719            .and_then(|drain| drain.write_all(&encoded_header))
720            .map_err(|err| self.fail(err))?;
721
722        self.frame_started = true;
723        Ok(())
724    }
725
726    fn block_capacity(&self) -> usize {
727        let matcher_window = self.state.matcher.window_size() as usize;
728        let ceiling = self
729            .target_block_size
730            .map_or(MAX_BLOCK_SIZE as usize, |t| t as usize);
731        core::cmp::max(1, core::cmp::min(matcher_window, ceiling))
732    }
733
734    fn allocate_pending_space(&mut self, block_capacity: usize) -> Vec<u8> {
735        let mut space = match self.compression_level {
736            CompressionLevel::Fastest
737            | CompressionLevel::Default
738            | CompressionLevel::Better
739            | CompressionLevel::Best
740            | CompressionLevel::Level(_) => self.state.matcher.get_next_space(),
741            CompressionLevel::Uncompressed => Vec::new(),
742        };
743        space.clear();
744        if space.capacity() > block_capacity {
745            space.shrink_to(block_capacity);
746        }
747        if space.capacity() < block_capacity {
748            space.reserve(block_capacity - space.capacity());
749        }
750        space
751    }
752
753    /// Where the full pending block is cut (upstream `ZSTD_compress_frameChunk`
754    /// sizing every block with `ZSTD_optimalBlockSize`): the pre-splitter's
755    /// boundary once the frame has saved enough, else the whole block.
756    /// `remaining` is the input still to come as far as the splitter knows:
757    /// a full block again while writes continue, the buffered bytes at the
758    /// end of the frame.
759    fn pre_split_len(&self, block_capacity: usize, remaining: usize) -> usize {
760        if matches!(self.compression_level, CompressionLevel::Uncompressed) {
761            return self.pending.len();
762        }
763        crate::encoding::frame_compressor::optimal_block_size_with(
764            self.state.pre_split.map(usize::from),
765            &self.pending,
766            remaining,
767            block_capacity,
768            self.savings,
769        )
770        .min(self.pending.len())
771    }
772
773    /// Emit the first `block_len` pending bytes as a non-last block; the
774    /// suffix stays pending (the next block starts with it, as the frame
775    /// compressor's reader path carries a pre-split suffix). On a drain
776    /// error the whole pending buffer is restored so no input is lost.
777    fn emit_pending_prefix(
778        &mut self,
779        block_len: usize,
780        block_capacity: usize,
781    ) -> Result<(), Error> {
782        let mut suffix = self.allocate_pending_space(block_capacity);
783        suffix.extend_from_slice(&self.pending[block_len..]);
784        let mut block = mem::replace(&mut self.pending, suffix);
785        block.truncate(block_len);
786        if let Err((err, mut restored_block)) = self.encode_block(block, false) {
787            restored_block.extend_from_slice(&self.pending);
788            self.pending = restored_block;
789            return Err(err);
790        }
791        Ok(())
792    }
793
794    fn emit_full_pending_block(
795        &mut self,
796        block_capacity: usize,
797        consumed: usize,
798    ) -> Option<Result<usize, Error>> {
799        if self.pending.len() != block_capacity {
800            return None;
801        }
802        let block_len = self.pre_split_len(block_capacity, block_capacity);
803        if let Err(err) = self.emit_pending_prefix(block_len, block_capacity) {
804            let err = self.fail(err);
805            if consumed > 0 {
806                return Some(Ok(consumed));
807            }
808            return Some(Err(err));
809        }
810        None
811    }
812
813    fn emit_pending_block(&mut self, last_block: bool) -> Result<(), Error> {
814        let block_capacity = self.block_capacity();
815        if last_block {
816            // A full final buffer is cut like any other block (the reader
817            // path splits it with `remaining = len`); each cut prefix goes
818            // out as a non-last block and the suffix is re-examined.
819            while self.pending.len() == block_capacity {
820                let block_len = self.pre_split_len(block_capacity, self.pending.len());
821                if block_len == self.pending.len() {
822                    break;
823                }
824                self.emit_pending_prefix(block_len, block_capacity)
825                    .map_err(|err| self.fail(err))?;
826            }
827        }
828        let block = mem::take(&mut self.pending);
829        if let Err((err, restored_block)) = self.encode_block(block, last_block) {
830            self.pending = restored_block;
831            return Err(self.fail(err));
832        }
833        if !last_block {
834            self.pending = self.allocate_pending_space(block_capacity);
835        }
836        Ok(())
837    }
838
839    // Exhaustive match kept intentionally: adding a new CompressionLevel
840    // variant will produce a compile error here, forcing the developer to
841    // decide whether the streaming encoder supports it before shipping.
842    fn ensure_level_supported(&self) -> Result<(), Error> {
843        match self.compression_level {
844            CompressionLevel::Uncompressed
845            | CompressionLevel::Fastest
846            | CompressionLevel::Default
847            | CompressionLevel::Better
848            | CompressionLevel::Best
849            | CompressionLevel::Level(_) => Ok(()),
850        }
851    }
852
853    fn encode_block(
854        &mut self,
855        uncompressed_data: Vec<u8>,
856        last_block: bool,
857    ) -> Result<(), (Error, Vec<u8>)> {
858        let mut raw_block = Some(uncompressed_data);
859        let mut encoded = Vec::new();
860        mem::swap(&mut encoded, &mut self.encoded_scratch);
861        encoded.clear();
862        let needed_capacity = self.block_capacity() + 3;
863        if encoded.capacity() < needed_capacity {
864            encoded.reserve(needed_capacity.saturating_sub(encoded.len()));
865        }
866        let mut moved_into_matcher = false;
867        let raw_len = raw_block.as_ref().map_or(0, Vec::len);
868        if raw_block.as_ref().is_some_and(|block| block.is_empty()) {
869            let header = BlockHeader {
870                last_block,
871                block_type: crate::blocks::block::BlockType::Raw,
872                block_size: 0,
873            };
874            header.serialize(&mut encoded);
875        } else {
876            match self.compression_level {
877                CompressionLevel::Uncompressed => {
878                    let block = raw_block.as_ref().expect("raw block missing");
879                    let header = BlockHeader {
880                        last_block,
881                        block_type: crate::blocks::block::BlockType::Raw,
882                        block_size: block.len() as u32,
883                    };
884                    header.serialize(&mut encoded);
885                    encoded.extend_from_slice(block);
886                }
887                CompressionLevel::Fastest
888                | CompressionLevel::Default
889                | CompressionLevel::Better
890                | CompressionLevel::Best
891                | CompressionLevel::Level(_) => {
892                    let block = raw_block.take().expect("raw block missing");
893                    debug_assert!(!block.is_empty(), "empty blocks handled above");
894                    let dict_active = self.dictionary.is_some()
895                        && self.state.matcher.supports_dictionary_priming();
896                    compress_block_encoded(
897                        &mut self.state,
898                        self.compression_level,
899                        last_block,
900                        crate::encoding::levels::BlockInput::Staged(block),
901                        &mut encoded,
902                        dict_active,
903                        // No FrameEmitInfo on the streaming encoder path — it
904                        // does not surface per-block layout, so no sidecar.
905                        #[cfg(feature = "lsm")]
906                        None,
907                        #[cfg(all(feature = "lsm", feature = "hash"))]
908                        None,
909                    );
910                    moved_into_matcher = true;
911                }
912            }
913        }
914
915        if let Err(err) = self.drain_mut().and_then(|drain| drain.write_all(&encoded)) {
916            encoded.clear();
917            mem::swap(&mut encoded, &mut self.encoded_scratch);
918            let restored = if moved_into_matcher {
919                self.state.matcher.get_last_space().to_vec()
920            } else {
921                raw_block.unwrap_or_default()
922            };
923            return Err((err, restored));
924        }
925        // `savings` counts the block header too, as upstream's
926        // `ZSTD_compress_frameChunk` does (`cSize` includes it).
927        self.savings += raw_len as i64 - encoded.len() as i64;
928
929        if moved_into_matcher {
930            #[cfg(feature = "hash")]
931            if self.content_checksum {
932                self.hasher.write(self.state.matcher.get_last_space());
933            }
934        } else {
935            self.hash_block(raw_block.as_deref().unwrap_or(&[]));
936        }
937        encoded.clear();
938        mem::swap(&mut encoded, &mut self.encoded_scratch);
939        Ok(())
940    }
941
942    fn write_empty_last_block(&mut self) -> Result<(), Error> {
943        self.encode_block(Vec::new(), true).map_err(|(err, _)| err)
944    }
945
946    fn fail(&mut self, err: Error) -> Error {
947        self.errored = true;
948        if self.last_error_kind.is_none() {
949            self.last_error_kind = Some(err.kind());
950        }
951        if self.last_error_message.is_none() {
952            self.last_error_message = Some(err.to_string());
953        }
954        err
955    }
956
957    #[cfg(feature = "hash")]
958    fn hash_block(&mut self, uncompressed_data: &[u8]) {
959        if self.content_checksum {
960            self.hasher.write(uncompressed_data);
961        }
962    }
963
964    #[cfg(not(feature = "hash"))]
965    fn hash_block(&mut self, _uncompressed_data: &[u8]) {}
966}
967
968impl<W: Write, M: Matcher> Write for StreamingEncoder<W, M> {
969    fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
970        self.ensure_open()?;
971        if buf.is_empty() {
972            return Ok(0);
973        }
974
975        // Check pledge before emitting the frame header so that a misuse
976        // like set_pledged_content_size(0) + write(non_empty) doesn't leave
977        // a partially-written header in the drain.
978        if let Some(pledged) = self.pledged_content_size
979            && self.bytes_consumed >= pledged
980        {
981            return Err(invalid_input_error(
982                "write would exceed pledged content size",
983            ));
984        }
985
986        self.ensure_frame_started()?;
987
988        // Enforce pledged upper bound: truncate the accepted slice to the
989        // remaining allowance so that partial-write semantics are honored
990        // (return Ok(n) with n < buf.len()) instead of failing the full call.
991        let buf = if let Some(pledged) = self.pledged_content_size {
992            let remaining_allowed = pledged
993                .checked_sub(self.bytes_consumed)
994                .ok_or_else(|| invalid_input_error("bytes consumed exceed pledged content size"))?;
995            if remaining_allowed == 0 {
996                return Err(invalid_input_error(
997                    "write would exceed pledged content size",
998                ));
999            }
1000            let accepted = core::cmp::min(
1001                buf.len(),
1002                usize::try_from(remaining_allowed).unwrap_or(usize::MAX),
1003            );
1004            &buf[..accepted]
1005        } else {
1006            buf
1007        };
1008
1009        let block_capacity = self.block_capacity();
1010        if self.pending.capacity() == 0 {
1011            self.pending = self.allocate_pending_space(block_capacity);
1012        }
1013        let mut remaining = buf;
1014        let mut consumed = 0usize;
1015
1016        while !remaining.is_empty() {
1017            if let Some(result) = self.emit_full_pending_block(block_capacity, consumed) {
1018                return result;
1019            }
1020
1021            let available = block_capacity - self.pending.len();
1022            let to_take = core::cmp::min(remaining.len(), available);
1023            if to_take == 0 {
1024                break;
1025            }
1026            self.pending.extend_from_slice(&remaining[..to_take]);
1027            remaining = &remaining[to_take..];
1028            consumed += to_take;
1029
1030            if let Some(result) = self.emit_full_pending_block(block_capacity, consumed) {
1031                if let Ok(n) = &result {
1032                    self.bytes_consumed += *n as u64;
1033                }
1034                return result;
1035            }
1036        }
1037        self.bytes_consumed += consumed as u64;
1038        Ok(consumed)
1039    }
1040
1041    fn flush(&mut self) -> Result<(), Error> {
1042        self.ensure_open()?;
1043        if self.pending.is_empty() {
1044            return self
1045                .drain_mut()
1046                .and_then(|drain| drain.flush())
1047                .map_err(|err| self.fail(err));
1048        }
1049        self.ensure_frame_started()?;
1050        self.emit_pending_block(false)?;
1051        self.drain_mut()
1052            .and_then(|drain| drain.flush())
1053            .map_err(|err| self.fail(err))
1054    }
1055}
1056
1057fn error_from_kind(kind: ErrorKind) -> Error {
1058    Error::from(kind)
1059}
1060
1061fn error_with_kind_message(kind: ErrorKind, message: String) -> Error {
1062    #[cfg(feature = "std")]
1063    {
1064        Error::new(kind, message)
1065    }
1066    #[cfg(not(feature = "std"))]
1067    {
1068        Error::new(kind, alloc::boxed::Box::new(message))
1069    }
1070}
1071
1072fn invalid_input_error(message: &str) -> Error {
1073    #[cfg(feature = "std")]
1074    {
1075        Error::new(ErrorKind::InvalidInput, message)
1076    }
1077    #[cfg(not(feature = "std"))]
1078    {
1079        Error::new(
1080            ErrorKind::Other,
1081            alloc::boxed::Box::new(alloc::string::String::from(message)),
1082        )
1083    }
1084}
1085
1086fn other_error_owned(message: String) -> Error {
1087    #[cfg(feature = "std")]
1088    {
1089        Error::other(message)
1090    }
1091    #[cfg(not(feature = "std"))]
1092    {
1093        Error::new(ErrorKind::Other, alloc::boxed::Box::new(message))
1094    }
1095}
1096
1097fn other_error(message: &str) -> Error {
1098    #[cfg(feature = "std")]
1099    {
1100        Error::other(message)
1101    }
1102    #[cfg(not(feature = "std"))]
1103    {
1104        Error::new(
1105            ErrorKind::Other,
1106            alloc::boxed::Box::new(alloc::string::String::from(message)),
1107        )
1108    }
1109}
1110
1111#[cfg(test)]
1112mod tests;