Skip to main content

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            &params,
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;