Skip to main content

zrip_encode/
context.rs

1#[cfg(feature = "alloc")]
2use alloc::borrow::Cow;
3#[cfg(feature = "alloc")]
4use alloc::vec;
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7
8use crate::block_encoder::{self, BlockEncodeWorkspace};
9use crate::strategy::{self, LevelParams, Strategy};
10use crate::{block_looks_incompressible, dfast, fast, write_frame_header_with_checksum};
11use zrip_core::Sequence;
12use zrip_core::dict::Dictionary;
13use zrip_core::error::CompressError;
14use zrip_core::frame::MAX_BLOCK_SIZE;
15use zrip_core::huffman::encode::HuffmanEncodeTable;
16use zrip_core::xxhash::xxh64;
17
18/// Pre-computed dictionary state for hot-loop compression.
19///
20/// Built once from a [`Dictionary`] + [`LevelParams`]. Caches the pre-filled
21/// hash table(s) and a combined buffer with the dict prefix already loaded,
22/// plus encode-side entropy tables built from the dict's decode tables.
23const ATTACH_THRESHOLD: usize = 16384;
24
25pub(crate) struct PreparedDict {
26    combined: Vec<u8>,
27    hash_snapshot: Vec<u32>,
28    hash_long_snapshot: Vec<u32>,
29    hash_log: u32,
30    prefix_len: usize,
31    rep_offsets: [u32; 3],
32    dict_id: u32,
33    huf_table: Option<HuffmanEncodeTable>,
34    ll_table: Option<block_encoder::FseEncodeTable>,
35    of_table: Option<block_encoder::FseEncodeTable>,
36    ml_table: Option<block_encoder::FseEncodeTable>,
37}
38
39impl PreparedDict {
40    pub fn new(dict: &Dictionary, params: &LevelParams) -> Self {
41        let prefix = dict.content();
42        let prefix_len = prefix.len();
43
44        let mut combined = Vec::with_capacity(prefix_len + MAX_BLOCK_SIZE);
45        combined.extend_from_slice(prefix);
46
47        let (hash_snapshot, hash_long_snapshot) = match params.strategy {
48            Strategy::Fast => {
49                let hash_size = 1usize << params.hash_log;
50                let mut hash_table = vec![0u32; hash_size];
51                fast::prefill_hash_table(&combined, prefix_len, params.hash_log, &mut hash_table);
52                (hash_table, Vec::new())
53            }
54            Strategy::DFast => {
55                let short_size = 1usize << params.chain_log;
56                let long_size = 1usize << params.hash_log;
57                let mut hash_short = vec![0u32; short_size];
58                let mut hash_long = vec![0u32; long_size];
59                dfast::prefill_hash_tables(
60                    &combined,
61                    prefix_len,
62                    params.hash_log,
63                    params.chain_log,
64                    params.min_match,
65                    &mut hash_short,
66                    &mut hash_long,
67                );
68                (hash_short, hash_long)
69            }
70        };
71
72        let huf_table = dict
73            .huf_table()
74            .and_then(|(dt, tl)| HuffmanEncodeTable::from_decode_table(dt, tl));
75
76        let ll_table = dict
77            .ll_table()
78            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 35));
79        let of_table = dict
80            .of_table()
81            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 31));
82        let ml_table = dict
83            .ml_table()
84            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 52));
85
86        Self {
87            combined,
88            hash_snapshot,
89            hash_long_snapshot,
90            hash_log: params.hash_log,
91            prefix_len,
92            rep_offsets: *dict.rep_offsets(),
93            dict_id: dict.id(),
94            huf_table,
95            ll_table,
96            of_table,
97            ml_table,
98        }
99    }
100}
101
102/// Reusable compression context that amortizes hash table and buffer allocations.
103///
104/// Holds internal state (hash tables, output buffer, block encoder workspace)
105/// across calls. Useful when compressing many small inputs in a loop.
106///
107/// ```
108/// let mut ctx = zrip::CompressContext::new(1).unwrap();
109/// for i in 0..10 {
110///     let data = format!("message {i}").repeat(100);
111///     let compressed = ctx.compress(data.as_bytes()).unwrap();
112///     assert!(compressed.len() < data.len());
113/// }
114/// ```
115pub struct CompressContext {
116    level: i32,
117    content_checksum: bool,
118    prepared: Option<PreparedDict>,
119    hash_table: Vec<u32>,
120    hash_long: Vec<u32>,
121    dict_hash: Vec<u32>,
122    small_hash: Vec<u32>,
123    sequences: Vec<Sequence>,
124    output: Vec<u8>,
125    workspace: BlockEncodeWorkspace,
126    combined: Vec<u8>,
127}
128
129impl CompressContext {
130    /// Creates a new context for the given compression level (-8..=4).
131    pub fn new(level: i32) -> Result<Self, CompressError> {
132        let params = strategy::level_params(level).ok_or(CompressError::InvalidLevel(level))?;
133        let max_log = strategy::max_hash_log(level).expect("level validated above");
134        let alloc_size = 1usize << max_log;
135        let (hash_table, hash_long) = match params.strategy {
136            Strategy::Fast => (vec![0u32; alloc_size], Vec::new()),
137            Strategy::DFast => (vec![0u32; alloc_size], vec![0u32; alloc_size]),
138        };
139        Ok(Self {
140            level,
141            content_checksum: true,
142            prepared: None,
143            hash_table,
144            hash_long,
145            dict_hash: Vec::new(),
146            small_hash: Vec::new(),
147            sequences: Vec::new(),
148            output: Vec::new(),
149            workspace: BlockEncodeWorkspace::new(),
150            combined: Vec::new(),
151        })
152    }
153
154    /// Creates a new context with a pre-loaded dictionary.
155    ///
156    /// The prepared hash table snapshot is built for the T0 (>256 KB)
157    /// parameter tier. Inputs whose tiered params match T0's hash sizes
158    /// use the fast snapshot-restore path; others fall back to per-call
159    /// prefix hashing.
160    ///
161    /// Use [`with_dict_for_size`] to build the snapshot for a specific
162    /// input size tier.
163    pub fn with_dict(level: i32, dict: Dictionary) -> Result<Self, CompressError> {
164        Self::with_dict_for_size(level, dict, usize::MAX)
165    }
166
167    /// Creates a new context with a pre-loaded dictionary, optimized for
168    /// inputs of approximately `expected_size` bytes.
169    ///
170    /// The prepared hash table snapshot is built for the parameter tier
171    /// matching `expected_size`. Inputs in the same tier use the fast
172    /// snapshot-restore path.
173    pub fn with_dict_for_size(
174        level: i32,
175        dict: Dictionary,
176        expected_size: usize,
177    ) -> Result<Self, CompressError> {
178        let total_window = dict.content().len().saturating_add(expected_size);
179        let params = strategy::level_params_for_size(level, total_window)
180            .ok_or(CompressError::InvalidLevel(level))?;
181        let prepared = PreparedDict::new(&dict, &params);
182        let hash_table = vec![0u32; prepared.hash_snapshot.len()];
183        let hash_long = vec![0u32; prepared.hash_long_snapshot.len()];
184        Ok(Self {
185            level,
186            content_checksum: true,
187            prepared: Some(prepared),
188            hash_table,
189            hash_long,
190            dict_hash: Vec::new(),
191            small_hash: Vec::new(),
192            sequences: Vec::new(),
193            output: Vec::new(),
194            workspace: BlockEncodeWorkspace::new(),
195            combined: Vec::new(),
196        })
197    }
198
199    /// Enables or disables the standard Zstandard frame content checksum.
200    ///
201    /// Checksums are enabled by default. Disabling them is useful when an
202    /// enclosing format already authenticates the decoded content.
203    pub const fn set_content_checksum(&mut self, enabled: bool) {
204        self.content_checksum = enabled;
205    }
206
207    /// Whether newly compressed frames include a content checksum.
208    pub const fn content_checksum(&self) -> bool {
209        self.content_checksum
210    }
211
212    /// Compresses `input` using the context's level and optional dictionary.
213    pub fn compress(&mut self, input: &[u8]) -> Result<Cow<'_, [u8]>, CompressError> {
214        if self.prepared.is_some() {
215            return self.compress_with_prepared(input);
216        }
217        let params = strategy::level_params_for_size(self.level, input.len())
218            .expect("level validated at construction");
219        compress_core(
220            input,
221            params,
222            None,
223            &[],
224            [1u32, 4, 8],
225            &mut self.hash_table,
226            &mut self.hash_long,
227            &mut self.dict_hash,
228            &mut self.sequences,
229            &mut self.output,
230            &mut self.workspace,
231            &mut self.combined,
232            self.content_checksum,
233        )?;
234        Ok(self.take_or_borrow_output())
235    }
236
237    /// Compresses `input` using an ad-hoc dictionary (overrides the stored one).
238    pub fn compress_with_dict(
239        &mut self,
240        input: &[u8],
241        dict: &Dictionary,
242    ) -> Result<Cow<'_, [u8]>, CompressError> {
243        let total_window = dict.content().len().saturating_add(input.len());
244        let params = strategy::level_params_for_size(self.level, total_window)
245            .expect("level validated at construction");
246        self.workspace.prev_ll = dict
247            .ll_table()
248            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 35));
249        self.workspace.prev_of = dict
250            .of_table()
251            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 31));
252        self.workspace.prev_ml = dict
253            .ml_table()
254            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 52));
255        self.workspace.prev_huffman = dict
256            .huf_table()
257            .and_then(|(dt, tl)| HuffmanEncodeTable::from_decode_table(dt, tl));
258        compress_core(
259            input,
260            params,
261            Some(dict.id()),
262            dict.content(),
263            *dict.rep_offsets(),
264            &mut self.hash_table,
265            &mut self.hash_long,
266            &mut self.dict_hash,
267            &mut self.sequences,
268            &mut self.output,
269            &mut self.workspace,
270            &mut self.combined,
271            self.content_checksum,
272        )?;
273        Ok(self.take_or_borrow_output())
274    }
275
276    fn compress_with_prepared(&mut self, input: &[u8]) -> Result<Cow<'_, [u8]>, CompressError> {
277        let prep = self.prepared.as_ref().unwrap();
278        let total_window = prep.prefix_len + input.len();
279        let mut params = strategy::level_params_for_size(self.level, total_window)
280            .expect("level validated at construction");
281        strategy::apply_raw_literals_size_override(&mut params, input.len());
282
283        let use_attached = !input.is_empty()
284            && input.len() <= ATTACH_THRESHOLD
285            && params.strategy == Strategy::Fast;
286
287        let dict_id = prep.dict_id;
288        let prefix_len = prep.prefix_len;
289        let dict_hash_log = prep.hash_log;
290
291        if !use_attached {
292            let snapshot_matches = match params.strategy {
293                Strategy::Fast => (1usize << params.hash_log) == prep.hash_snapshot.len(),
294                Strategy::DFast => {
295                    (1usize << params.chain_log) == prep.hash_snapshot.len()
296                        && (1usize << params.hash_log) == prep.hash_long_snapshot.len()
297                }
298            };
299            if !snapshot_matches {
300                return self.compress_with_dict_fallback(input, dict_id, prefix_len);
301            }
302        }
303
304        {
305            let prep = self.prepared.as_mut().unwrap();
306            if !use_attached {
307                self.hash_table.copy_from_slice(&prep.hash_snapshot);
308                if !prep.hash_long_snapshot.is_empty() {
309                    self.hash_long.copy_from_slice(&prep.hash_long_snapshot);
310                }
311            }
312            prep.combined.truncate(prep.prefix_len);
313            prep.combined.extend_from_slice(input);
314        }
315
316        let prep = self.prepared.as_ref().unwrap();
317
318        if use_attached {
319            self.workspace.prev_huffman = if params.force_raw_literals {
320                None
321            } else {
322                prep.huf_table.clone()
323            };
324        } else if let Some(ref huf) = prep.huf_table {
325            self.workspace.prev_huffman = Some(huf.clone());
326        } else {
327            self.workspace.prev_huffman = None;
328        }
329        self.workspace.prev_ll = prep.ll_table.clone();
330        self.workspace.prev_of = prep.of_table.clone();
331        self.workspace.prev_ml = prep.ml_table.clone();
332
333        self.output.clear();
334        self.output.reserve(input.len() + 32);
335        write_frame_header_with_checksum(
336            &mut self.output,
337            input.len(),
338            Some(dict_id),
339            params.window_log,
340            self.content_checksum,
341        )?;
342
343        if input.is_empty() {
344            block_encoder::encode_raw_block(&[], true, &mut self.output)?;
345        } else if use_attached {
346            let input_hash_log = if input.len() >= 2 {
347                let src_log = 32 - ((input.len() as u32) - 1).leading_zeros();
348                params.hash_log.min(src_log).max(strategy::HASH_LOG_MIN)
349            } else {
350                strategy::HASH_LOG_MIN
351            };
352            let input_hash_size = 1usize << input_hash_log;
353            self.small_hash.resize(input_hash_size, 0);
354            self.small_hash.fill(0);
355
356            let mut rep_offsets = prep.rep_offsets;
357
358            fast::compress_fast_attached(
359                &prep.combined,
360                prefix_len,
361                prefix_len + input.len(),
362                &params,
363                &prep.rep_offsets,
364                &prep.hash_snapshot,
365                dict_hash_log,
366                &mut self.small_hash,
367                input_hash_log,
368                &mut self.sequences,
369            );
370
371            if params.force_raw_literals {
372                block_encoder::encode_compressed_block_raw(
373                    input,
374                    &self.sequences,
375                    &mut rep_offsets,
376                    true,
377                    &mut self.output,
378                    &mut self.workspace,
379                )?;
380            } else {
381                block_encoder::encode_compressed_block(
382                    input,
383                    &self.sequences,
384                    &mut rep_offsets,
385                    true,
386                    &mut self.output,
387                    &mut self.workspace,
388                    strategy::use_custom_sequence_tables(&params, input.len()),
389                )?;
390            }
391        } else {
392            let combined = &prep.combined;
393            let mut rep_offsets = prep.rep_offsets;
394
395            if input.len() <= MAX_BLOCK_SIZE {
396                match params.strategy {
397                    Strategy::Fast => {
398                        fast::compress_fast_block(
399                            combined,
400                            prefix_len,
401                            prefix_len + input.len(),
402                            &params,
403                            &rep_offsets,
404                            &mut self.hash_table,
405                            &mut self.sequences,
406                        );
407                    }
408                    Strategy::DFast => {
409                        dfast::compress_dfast_block(
410                            combined,
411                            prefix_len,
412                            prefix_len + input.len(),
413                            &params,
414                            &rep_offsets,
415                            &mut self.hash_table,
416                            &mut self.hash_long,
417                            &mut self.sequences,
418                        );
419                    }
420                }
421                if params.force_raw_literals {
422                    block_encoder::encode_compressed_block_raw(
423                        input,
424                        &self.sequences,
425                        &mut rep_offsets,
426                        true,
427                        &mut self.output,
428                        &mut self.workspace,
429                    )?;
430                } else {
431                    block_encoder::encode_compressed_block(
432                        input,
433                        &self.sequences,
434                        &mut rep_offsets,
435                        true,
436                        &mut self.output,
437                        &mut self.workspace,
438                        strategy::use_custom_sequence_tables(&params, input.len()),
439                    )?;
440                }
441            } else {
442                let mut offset = 0;
443                while offset < input.len() {
444                    let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
445                    let is_last = offset + chunk_size >= input.len();
446                    match params.strategy {
447                        Strategy::Fast => {
448                            fast::compress_fast_block(
449                                combined,
450                                prefix_len + offset,
451                                prefix_len + offset + chunk_size,
452                                &params,
453                                &rep_offsets,
454                                &mut self.hash_table,
455                                &mut self.sequences,
456                            );
457                        }
458                        Strategy::DFast => {
459                            dfast::compress_dfast_block(
460                                combined,
461                                prefix_len + offset,
462                                prefix_len + offset + chunk_size,
463                                &params,
464                                &rep_offsets,
465                                &mut self.hash_table,
466                                &mut self.hash_long,
467                                &mut self.sequences,
468                            );
469                        }
470                    }
471                    if params.force_raw_literals {
472                        block_encoder::encode_compressed_block_raw(
473                            &input[offset..offset + chunk_size],
474                            &self.sequences,
475                            &mut rep_offsets,
476                            is_last,
477                            &mut self.output,
478                            &mut self.workspace,
479                        )?;
480                    } else {
481                        block_encoder::encode_compressed_block(
482                            &input[offset..offset + chunk_size],
483                            &self.sequences,
484                            &mut rep_offsets,
485                            is_last,
486                            &mut self.output,
487                            &mut self.workspace,
488                            strategy::use_custom_sequence_tables(&params, input.len()),
489                        )?;
490                    }
491                    offset += chunk_size;
492                }
493            }
494        }
495
496        if self.content_checksum {
497            let hash = xxh64(input, 0);
498            let checksum = (hash & 0xFFFF_FFFF) as u32;
499            self.output.extend_from_slice(&checksum.to_le_bytes());
500        }
501
502        Ok(self.take_or_borrow_output())
503    }
504
505    fn compress_with_dict_fallback(
506        &mut self,
507        input: &[u8],
508        dict_id: u32,
509        prefix_len: usize,
510    ) -> Result<Cow<'_, [u8]>, CompressError> {
511        let prep = self.prepared.as_ref().unwrap();
512        let rep_offsets = prep.rep_offsets;
513        let prefix = &prep.combined[..prefix_len];
514
515        let total_window = prefix_len.saturating_add(input.len());
516        let params = strategy::level_params_for_size(self.level, total_window)
517            .expect("level validated at construction");
518        self.workspace.prev_huffman = prep.huf_table.clone();
519        self.workspace.prev_ll = prep.ll_table.clone();
520        self.workspace.prev_of = prep.of_table.clone();
521        self.workspace.prev_ml = prep.ml_table.clone();
522        compress_core(
523            input,
524            params,
525            Some(dict_id),
526            prefix,
527            rep_offsets,
528            &mut self.hash_table,
529            &mut self.hash_long,
530            &mut self.dict_hash,
531            &mut self.sequences,
532            &mut self.output,
533            &mut self.workspace,
534            &mut self.combined,
535            self.content_checksum,
536        )?;
537        Ok(self.take_or_borrow_output())
538    }
539
540    fn take_or_borrow_output(&mut self) -> Cow<'_, [u8]> {
541        if self.output.len() >= zrip_core::LARGE_OUTPUT_THRESHOLD {
542            Cow::Owned(core::mem::take(&mut self.output))
543        } else {
544            Cow::Borrowed(&self.output)
545        }
546    }
547}
548
549#[allow(clippy::too_many_arguments, clippy::unnecessary_wraps)]
550fn compress_core(
551    input: &[u8],
552    params: LevelParams,
553    dict_id: Option<u32>,
554    prefix: &[u8],
555    init_rep_offsets: [u32; 3],
556    hash_table: &mut Vec<u32>,
557    hash_long: &mut Vec<u32>,
558    dict_hash: &mut Vec<u32>,
559    sequences: &mut Vec<Sequence>,
560    output: &mut Vec<u8>,
561    workspace: &mut BlockEncodeWorkspace,
562    combined: &mut Vec<u8>,
563    content_checksum: bool,
564) -> Result<(), CompressError> {
565    let mut params = params;
566    strategy::apply_raw_literals_size_override(&mut params, input.len());
567
568    let hash_size = match params.strategy {
569        Strategy::Fast => 1usize << params.hash_log,
570        Strategy::DFast => 1usize << params.chain_log,
571    };
572    let long_size = 1usize << params.hash_log;
573
574    if prefix.is_empty() {
575        workspace.prev_huffman = None;
576        workspace.prev_ll = None;
577        workspace.prev_of = None;
578        workspace.prev_ml = None;
579    }
580
581    output.clear();
582    output.reserve(input.len() + 32);
583    write_frame_header_with_checksum(
584        output,
585        input.len(),
586        dict_id,
587        params.window_log,
588        content_checksum,
589    )?;
590
591    if input.is_empty() {
592        block_encoder::encode_raw_block(&[], true, output)?;
593    } else {
594        let has_prefix = !prefix.is_empty();
595        let mut rep_offsets = init_rep_offsets;
596        let mut offset = 0;
597
598        if hash_table.len() != hash_size {
599            hash_table.resize(hash_size, 0);
600        }
601
602        match params.strategy {
603            Strategy::Fast => {
604                if has_prefix && input.len() <= MAX_BLOCK_SIZE {
605                    if dict_hash.len() != hash_size {
606                        dict_hash.resize(hash_size, 0);
607                    }
608                    fast::compress_fast_with_prefix_reuse(
609                        input,
610                        &params,
611                        &rep_offsets,
612                        prefix,
613                        dict_hash,
614                        hash_table,
615                        sequences,
616                        combined,
617                    );
618                    if params.force_raw_literals {
619                        block_encoder::encode_compressed_block_raw(
620                            input,
621                            sequences,
622                            &mut rep_offsets,
623                            true,
624                            output,
625                            workspace,
626                        )?;
627                    } else {
628                        block_encoder::encode_compressed_block(
629                            input,
630                            sequences,
631                            &mut rep_offsets,
632                            true,
633                            output,
634                            workspace,
635                            strategy::use_custom_sequence_tables(&params, input.len()),
636                        )?;
637                    }
638                } else if has_prefix {
639                    combined.clear();
640                    combined.reserve(prefix.len() + input.len());
641                    combined.extend_from_slice(prefix);
642                    combined.extend_from_slice(input);
643                    let plen = prefix.len();
644                    fast::prefill_hash_table(combined, plen, params.hash_log, hash_table);
645
646                    while offset < input.len() {
647                        let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
648                        let is_last = offset + chunk_size >= input.len();
649                        fast::compress_fast_block(
650                            combined,
651                            plen + offset,
652                            plen + offset + chunk_size,
653                            &params,
654                            &rep_offsets,
655                            hash_table,
656                            sequences,
657                        );
658                        if params.force_raw_literals {
659                            block_encoder::encode_compressed_block_raw(
660                                &input[offset..offset + chunk_size],
661                                sequences,
662                                &mut rep_offsets,
663                                is_last,
664                                output,
665                                workspace,
666                            )?;
667                        } else {
668                            block_encoder::encode_compressed_block(
669                                &input[offset..offset + chunk_size],
670                                sequences,
671                                &mut rep_offsets,
672                                is_last,
673                                output,
674                                workspace,
675                                strategy::use_custom_sequence_tables(&params, input.len()),
676                            )?;
677                        }
678                        offset += chunk_size;
679                    }
680                } else {
681                    hash_table.fill(0);
682                    while offset < input.len() {
683                        let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
684                        let block_end = offset + chunk_size;
685                        let is_last = block_end >= input.len();
686                        let block = &input[offset..block_end];
687
688                        if block_looks_incompressible(block) {
689                            block_encoder::encode_raw_block(block, is_last, output)?;
690                        } else {
691                            fast::compress_fast_block(
692                                input,
693                                offset,
694                                block_end,
695                                &params,
696                                &rep_offsets,
697                                hash_table,
698                                sequences,
699                            );
700                            if params.force_raw_literals {
701                                block_encoder::encode_compressed_block_raw(
702                                    block,
703                                    sequences,
704                                    &mut rep_offsets,
705                                    is_last,
706                                    output,
707                                    workspace,
708                                )?;
709                            } else {
710                                block_encoder::encode_compressed_block(
711                                    block,
712                                    sequences,
713                                    &mut rep_offsets,
714                                    is_last,
715                                    output,
716                                    workspace,
717                                    strategy::use_custom_sequence_tables(&params, input.len()),
718                                )?;
719                            }
720                        }
721                        offset = block_end;
722                    }
723                }
724            }
725            Strategy::DFast => {
726                if hash_long.len() != long_size {
727                    hash_long.resize(long_size, 0);
728                }
729                if has_prefix && input.len() <= MAX_BLOCK_SIZE {
730                    dfast::compress_dfast_with_prefix_reuse(
731                        input,
732                        &params,
733                        &rep_offsets,
734                        prefix,
735                        hash_table,
736                        hash_long,
737                        sequences,
738                        combined,
739                    );
740                    block_encoder::encode_compressed_block(
741                        input,
742                        sequences,
743                        &mut rep_offsets,
744                        true,
745                        output,
746                        workspace,
747                        strategy::use_custom_sequence_tables(&params, input.len()),
748                    )?;
749                } else if has_prefix {
750                    combined.clear();
751                    combined.reserve(prefix.len() + input.len());
752                    combined.extend_from_slice(prefix);
753                    combined.extend_from_slice(input);
754                    let plen = prefix.len();
755                    dfast::prefill_hash_tables(
756                        combined,
757                        plen,
758                        params.hash_log,
759                        params.chain_log,
760                        params.min_match,
761                        hash_table,
762                        hash_long,
763                    );
764
765                    while offset < input.len() {
766                        let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
767                        let is_last = offset + chunk_size >= input.len();
768                        dfast::compress_dfast_block(
769                            combined,
770                            plen + offset,
771                            plen + offset + chunk_size,
772                            &params,
773                            &rep_offsets,
774                            hash_table,
775                            hash_long,
776                            sequences,
777                        );
778                        block_encoder::encode_compressed_block(
779                            &input[offset..offset + chunk_size],
780                            sequences,
781                            &mut rep_offsets,
782                            is_last,
783                            output,
784                            workspace,
785                            strategy::use_custom_sequence_tables(&params, input.len()),
786                        )?;
787                        offset += chunk_size;
788                    }
789                } else {
790                    hash_table.fill(0);
791                    hash_long.fill(0);
792                    while offset < input.len() {
793                        let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
794                        let block_end = offset + chunk_size;
795                        let is_last = block_end >= input.len();
796                        let block = &input[offset..block_end];
797
798                        if block_looks_incompressible(block) {
799                            block_encoder::encode_raw_block(block, is_last, output)?;
800                        } else {
801                            dfast::compress_dfast_block(
802                                input,
803                                offset,
804                                block_end,
805                                &params,
806                                &rep_offsets,
807                                hash_table,
808                                hash_long,
809                                sequences,
810                            );
811                            block_encoder::encode_compressed_block(
812                                block,
813                                sequences,
814                                &mut rep_offsets,
815                                is_last,
816                                output,
817                                workspace,
818                                strategy::use_custom_sequence_tables(&params, input.len()),
819                            )?;
820                        }
821                        offset = block_end;
822                    }
823                }
824            }
825        }
826    }
827
828    if content_checksum {
829        let hash = xxh64(input, 0);
830        let checksum = (hash & 0xFFFF_FFFF) as u32;
831        output.extend_from_slice(&checksum.to_le_bytes());
832    }
833
834    Ok(())
835}