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