Skip to main content

lz4rip_encode/
compress.rs

1//! LZ4 block compression.
2
3use core::fmt;
4
5use crate::hashtable::HashTable;
6use crate::verified_sink::VerifiedSliceSink;
7#[cfg(feature = "alloc")]
8use alloc::vec;
9use lz4rip_core::CompressError;
10use lz4rip_core::Sink;
11use lz4rip_core::END_OFFSET;
12use lz4rip_core::LZ4_MIN_LENGTH;
13use lz4rip_core::MAX_DISTANCE;
14use lz4rip_core::MFLIMIT;
15use lz4rip_core::MINMATCH;
16use lz4rip_core::WINDOW_SIZE;
17
18#[cfg(feature = "alloc")]
19use alloc::vec::Vec;
20
21pub(crate) use crate::hashtable::HashTableU32;
22pub(crate) use crate::hashtable::HashTableU32U16;
23
24/// Skip acceleration: step grows by 1 every `1 << N` consecutive non-matches.
25/// C lz4 uses 6; see DESIGN.md for tradeoff analysis.
26const INCREASE_STEPSIZE_BITSHIFT: usize = 3;
27
28/// Inputs up to this size use the dict table read-only (no per-call clearing or
29/// table writes). Self-references within small inputs are rare; the dict provides
30/// virtually all matches. Skips the 8 KB table.clear() and all put_at writes.
31const DICT_READONLY_LIMIT: usize = 256;
32
33#[inline]
34fn token_from_literal(lit_len: usize) -> u8 {
35    if lit_len < 0xF {
36        (lit_len as u8) << 4
37    } else {
38        0xF0
39    }
40}
41
42#[inline]
43fn token_from_literal_and_match_length(lit_len: usize, duplicate_length: usize) -> u8 {
44    let mut token = if lit_len < 0xF {
45        (lit_len as u8) << 4
46    } else {
47        0xF0
48    };
49
50    token |= if duplicate_length < 0xF {
51        duplicate_length as u8
52    } else {
53        0xF
54    };
55
56    token
57}
58
59/// Write a variable-length integer in the LZ4 encoding.
60#[inline]
61pub fn write_integer(output: &mut impl Sink, mut n: usize) {
62    while n >= 0xFF {
63        n -= 0xFF;
64        push_byte(output, 0xFF);
65    }
66    push_byte(output, n as u8);
67}
68
69#[cold]
70fn handle_last_literals(output: &mut impl Sink, input: &[u8], start: usize) {
71    let lit_len = input.len() - start;
72
73    let token = token_from_literal(lit_len);
74    push_byte(output, token);
75    if lit_len >= 0xF {
76        write_integer(output, lit_len - 0xF);
77    }
78    output.extend_from_slice(&input[start..]);
79}
80
81#[inline]
82fn backtrack_match(
83    input: &[u8],
84    cur: &mut usize,
85    literal_start: usize,
86    source: &[u8],
87    candidate: &mut usize,
88) {
89    while *candidate > 0 && *cur > literal_start && input[*cur - 1] == source[*candidate - 1] {
90        *cur -= 1;
91        *candidate -= 1;
92    }
93}
94
95/// Core block compression loop, monomorphized over hash table type and dict mode.
96#[inline(never)]
97pub fn compress_internal<
98    T: HashTable,
99    const USE_DICT: bool,
100    const HAS_OFFSET: bool,
101    const READONLY: bool,
102    S: Sink,
103>(
104    input: &[u8],
105    input_pos: usize,
106    output: &mut S,
107    table: &mut T,
108    ext_dict: &[u8],
109    input_stream_offset: usize,
110) -> Result<usize, CompressError> {
111    assert!(input_pos <= input.len());
112    if USE_DICT {
113        assert!(ext_dict.len() <= WINDOW_SIZE);
114        assert!(ext_dict.len() <= input_stream_offset);
115        assert!(input_stream_offset
116            .checked_add(input.len())
117            .and_then(|i| i.checked_add(ext_dict.len()))
118            .is_some_and(|i| i <= isize::MAX as usize));
119    } else {
120        assert!(ext_dict.is_empty());
121    }
122    if !HAS_OFFSET {
123        debug_assert_eq!(input_stream_offset, 0);
124    }
125    let input_stream_offset = if HAS_OFFSET { input_stream_offset } else { 0 };
126    if output.capacity() - output.pos() < get_maximum_output_size(input.len() - input_pos) {
127        return Err(CompressError::OutputTooSmall);
128    }
129
130    let output_start_pos = output.pos();
131    if input.len() - input_pos < LZ4_MIN_LENGTH {
132        handle_last_literals(output, input, input_pos);
133        return Ok(output.pos() - output_start_pos);
134    }
135
136    let ext_dict_stream_offset = input_stream_offset - ext_dict.len();
137    let end_pos_check = input.len() - MFLIMIT;
138    let mut literal_start = input_pos;
139    let mut cur = input_pos;
140
141    if cur == 0 && input_stream_offset == 0 {
142        let hash = T::get_hash_at_unchecked(input, 0);
143        if !READONLY {
144            table.put_at(hash, 0);
145        }
146        cur = 1;
147    }
148
149    let mut forward_hash = T::get_hash_at_unchecked(input, cur);
150
151    loop {
152        let mut candidate;
153        let mut candidate_source;
154        let mut offset;
155        let mut non_match_count = 1 << INCREASE_STEPSIZE_BITSHIFT;
156
157        loop {
158            let step = non_match_count >> INCREASE_STEPSIZE_BITSHIFT;
159            non_match_count += 1;
160            let next_cur = cur + step;
161
162            if next_cur > end_pos_check + 1 {
163                handle_last_literals(output, input, literal_start);
164                return Ok(output.pos() - output_start_pos);
165            }
166
167            let hash = forward_hash;
168            candidate = table.get_at(hash);
169            forward_hash = T::get_hash_at_unchecked(input, next_cur);
170            if !READONLY {
171                table.put_at(hash, cur + input_stream_offset);
172            }
173
174            debug_assert!(READONLY || candidate <= input_stream_offset + cur);
175
176            if candidate >= input_stream_offset
177                && input_stream_offset + cur - candidate <= MAX_DISTANCE
178            {
179                offset = (input_stream_offset + cur - candidate) as u16;
180                candidate -= input_stream_offset;
181                candidate_source = input;
182            } else if USE_DICT
183                && candidate >= ext_dict_stream_offset
184                && input_stream_offset + cur - candidate <= MAX_DISTANCE
185            {
186                offset = (input_stream_offset + cur - candidate) as u16;
187                candidate -= ext_dict_stream_offset;
188                candidate_source = ext_dict;
189            } else {
190                cur = next_cur;
191                continue;
192            }
193            let cand_bytes: u32 =
194                crate::hashtable::get_batch_unchecked(candidate_source, candidate);
195            let curr_bytes: u32 = crate::hashtable::get_batch_unchecked(input, cur);
196
197            if cand_bytes == curr_bytes {
198                break;
199            }
200            cur = next_cur;
201        }
202
203        loop {
204            backtrack_match(
205                input,
206                &mut cur,
207                literal_start,
208                candidate_source,
209                &mut candidate,
210            );
211
212            let lit_len = cur - literal_start;
213
214            cur += MINMATCH;
215            candidate += MINMATCH;
216            let duplicate_length = crate::hashtable::count_same_bytes_unchecked(
217                input,
218                &mut cur,
219                candidate_source,
220                candidate,
221                END_OFFSET,
222            );
223
224            let hash = T::get_hash_at_unchecked(input, cur - 2);
225            if !READONLY {
226                table.put_at(hash, cur - 2 + input_stream_offset);
227            }
228
229            let token = token_from_literal_and_match_length(lit_len, duplicate_length);
230            push_byte(output, token);
231            if lit_len >= 0xF {
232                write_integer(output, lit_len - 0xF);
233            }
234            if lit_len > 0 {
235                copy_literals_wild(output, input, literal_start, lit_len);
236            }
237            push_u16(output, offset);
238            if duplicate_length >= 0xF {
239                write_integer(output, duplicate_length - 0xF);
240            }
241            literal_start = cur;
242
243            if !USE_DICT && cur <= end_pos_check {
244                let hash = T::get_hash_at_unchecked(input, cur);
245                let rematch = table.get_at(hash);
246
247                if input_stream_offset + cur - rematch <= MAX_DISTANCE
248                    && rematch >= input_stream_offset
249                {
250                    let rc = rematch - input_stream_offset;
251                    if crate::hashtable::get_batch_unchecked(input, cur)
252                        == crate::hashtable::get_batch_unchecked(input, rc)
253                    {
254                        table.put_at(hash, cur + input_stream_offset);
255                        candidate = rc;
256                        candidate_source = input;
257                        offset = (input_stream_offset + cur - rematch) as u16;
258                        continue;
259                    }
260                }
261                forward_hash = hash;
262            } else if cur <= end_pos_check {
263                forward_hash = T::get_hash_at_unchecked(input, cur);
264            }
265            break;
266        }
267    }
268}
269
270/// Dual-table compression for `CompressorRef::with_dict`.
271#[inline(never)]
272fn compress_with_dict_table<T: HashTable, S: Sink>(
273    input: &[u8],
274    output: &mut S,
275    table: &mut T,
276    dict_table: &T,
277    ext_dict: &[u8],
278    input_stream_offset: usize,
279) -> Result<usize, CompressError> {
280    debug_assert_eq!(input_stream_offset, ext_dict.len());
281    assert!(ext_dict.len() <= WINDOW_SIZE);
282    assert!(ext_dict.len() <= input_stream_offset);
283    assert!(input_stream_offset
284        .checked_add(input.len())
285        .and_then(|i| i.checked_add(ext_dict.len()))
286        .is_some_and(|i| i <= isize::MAX as usize));
287    if output.capacity() - output.pos() < get_maximum_output_size(input.len()) {
288        return Err(CompressError::OutputTooSmall);
289    }
290
291    let output_start_pos = output.pos();
292    if input.len() < LZ4_MIN_LENGTH {
293        handle_last_literals(output, input, 0);
294        return Ok(output.pos() - output_start_pos);
295    }
296
297    let end_pos_check = input.len() - MFLIMIT;
298    let mut literal_start = 0;
299
300    let hash = T::get_hash_at_unchecked(input, 0);
301    table.put_at(hash, input_stream_offset);
302    let mut cur = 1;
303
304    let mut forward_hash = T::get_hash_at_unchecked(input, cur);
305
306    loop {
307        let mut candidate;
308        let candidate_source;
309        let offset;
310        let mut non_match_count = 1 << INCREASE_STEPSIZE_BITSHIFT;
311
312        loop {
313            let step = non_match_count >> INCREASE_STEPSIZE_BITSHIFT;
314            non_match_count += 1;
315            let next_cur = cur + step;
316
317            if next_cur > end_pos_check + 1 {
318                handle_last_literals(output, input, literal_start);
319                return Ok(output.pos() - output_start_pos);
320            }
321
322            let hash = forward_hash;
323            forward_hash = T::get_hash_at_unchecked(input, next_cur);
324            let curr_bytes: u32 = crate::hashtable::get_batch_unchecked(input, cur);
325
326            let main_candidate = table.get_at(hash);
327            table.put_at(hash, cur + input_stream_offset);
328
329            // Probe dict table first: for small inputs most matches come from
330            // the dict, and even for large inputs the dict covers the first
331            // window of repeated structure.
332            let dict_candidate = dict_table.get_at(hash);
333            if dict_candidate < input_stream_offset
334                && input_stream_offset + cur - dict_candidate <= MAX_DISTANCE
335            {
336                let cand_bytes: u32 =
337                    crate::hashtable::get_batch_unchecked(ext_dict, dict_candidate);
338                if cand_bytes == curr_bytes {
339                    offset = (input_stream_offset + cur - dict_candidate) as u16;
340                    candidate = dict_candidate;
341                    candidate_source = ext_dict;
342                    break;
343                }
344            }
345
346            if main_candidate >= input_stream_offset
347                && input_stream_offset + cur - main_candidate <= MAX_DISTANCE
348            {
349                let cand_bytes: u32 = crate::hashtable::get_batch_unchecked(
350                    input,
351                    main_candidate - input_stream_offset,
352                );
353                if cand_bytes == curr_bytes {
354                    offset = (input_stream_offset + cur - main_candidate) as u16;
355                    candidate = main_candidate - input_stream_offset;
356                    candidate_source = input;
357                    break;
358                }
359            }
360
361            cur = next_cur;
362        }
363
364        backtrack_match(
365            input,
366            &mut cur,
367            literal_start,
368            candidate_source,
369            &mut candidate,
370        );
371
372        let lit_len = cur - literal_start;
373
374        cur += MINMATCH;
375        candidate += MINMATCH;
376        let duplicate_length = crate::hashtable::count_same_bytes_unchecked(
377            input,
378            &mut cur,
379            candidate_source,
380            candidate,
381            END_OFFSET,
382        );
383
384        let hash = T::get_hash_at_unchecked(input, cur - 2);
385        table.put_at(hash, cur - 2 + input_stream_offset);
386
387        let token = token_from_literal_and_match_length(lit_len, duplicate_length);
388        push_byte(output, token);
389        if lit_len >= 0xF {
390            write_integer(output, lit_len - 0xF);
391        }
392        if lit_len > 0 {
393            copy_literals_wild(output, input, literal_start, lit_len);
394        }
395        push_u16(output, offset);
396        if duplicate_length >= 0xF {
397            write_integer(output, duplicate_length - 0xF);
398        }
399        literal_start = cur;
400
401        if cur <= end_pos_check {
402            forward_hash = T::get_hash_at_unchecked(input, cur);
403        }
404    }
405}
406
407#[inline]
408fn push_byte(output: &mut impl Sink, el: u8) {
409    output.push(el);
410}
411
412#[inline]
413fn push_u16(output: &mut impl Sink, el: u16) {
414    output.extend_from_slice(&el.to_le_bytes());
415}
416
417#[inline(always)]
418fn copy_literals_wild(output: &mut impl Sink, input: &[u8], input_start: usize, len: usize) {
419    output.extend_from_slice_wild(&input[input_start..input_start + len], len)
420}
421
422/// Compress `input` into `output` with optional dictionary data.
423pub fn compress_into_sink_with_dict<const USE_DICT: bool>(
424    input: &[u8],
425    output: &mut impl Sink,
426    mut dict_data: &[u8],
427) -> Result<usize, CompressError> {
428    if USE_DICT && dict_data.len() < MINMATCH {
429        return compress_into_sink_with_dict::<false>(input, output, b"");
430    }
431    if dict_data.len() + input.len() < u16::MAX as usize {
432        let mut dict = HashTableU32U16::new();
433        init_dict(&mut dict, &mut dict_data);
434        compress_internal::<_, USE_DICT, USE_DICT, false, _>(
435            input,
436            0,
437            output,
438            &mut dict,
439            dict_data,
440            dict_data.len(),
441        )
442    } else {
443        let mut dict = HashTableU32::new();
444        init_dict(&mut dict, &mut dict_data);
445        compress_internal::<_, USE_DICT, USE_DICT, false, _>(
446            input,
447            0,
448            output,
449            &mut dict,
450            dict_data,
451            dict_data.len(),
452        )
453    }
454}
455
456#[inline]
457fn init_dict<T: HashTable>(dict: &mut T, dict_data: &mut &[u8]) {
458    if dict_data.len() > WINDOW_SIZE {
459        *dict_data = &dict_data[dict_data.len() - WINDOW_SIZE..];
460    }
461    let mut i = 0usize;
462    while i + core::mem::size_of::<usize>() <= dict_data.len() {
463        let hash = T::get_hash_at(dict_data, i);
464        dict.put_at(hash, i);
465        i += 3;
466    }
467}
468
469/// Returns the maximum output size of the compressed data.
470/// Can be used to preallocate capacity on the output vector.
471///
472/// Returns `usize::MAX` if the result would overflow (e.g. on 32-bit with >3.9 GB input).
473#[must_use]
474#[inline]
475pub const fn get_maximum_output_size(input_len: usize) -> usize {
476    let raw = 16u64 + 4 + (input_len as u64 * 110 / 100);
477    if raw > usize::MAX as u64 {
478        usize::MAX
479    } else {
480        raw as usize
481    }
482}
483
484/// Compress all bytes of `input` into `output`.
485/// output should be preallocated with a size of
486/// `get_maximum_output_size`.
487///
488/// Returns the number of bytes written (compressed) into `output`.
489#[inline]
490pub fn compress_into(input: &[u8], output: &mut [u8]) -> Result<usize, CompressError> {
491    compress_into_sink_with_dict::<false>(input, &mut VerifiedSliceSink::new(output, 0), b"")
492}
493
494/// Compress all bytes of `input` into `output` using an external dictionary.
495///
496/// Returns the number of bytes written (compressed) into `output`.
497#[inline]
498pub fn compress_into_with_dict(
499    input: &[u8],
500    output: &mut [u8],
501    dict: &[u8],
502) -> Result<usize, CompressError> {
503    compress_into_sink_with_dict::<true>(input, &mut VerifiedSliceSink::new(output, 0), dict)
504}
505
506/// Compress all bytes of `input`.
507#[must_use]
508#[cfg(feature = "alloc")]
509#[inline]
510pub fn compress(input: &[u8]) -> Vec<u8> {
511    let max_compressed_size = get_maximum_output_size(input.len());
512    let mut compressed: Vec<u8> = vec![0u8; max_compressed_size];
513    let compressed_len = compress_into_sink_with_dict::<false>(
514        input,
515        &mut VerifiedSliceSink::new(&mut compressed, 0),
516        b"",
517    )
518    .unwrap();
519    compressed.truncate(compressed_len);
520
521    compressed
522}
523
524/// A reusable block compressor that borrows its dictionary.
525///
526/// This is the no-alloc API. With `alloc`, use
527/// [`Compressor`](crate::Compressor) instead.
528///
529/// For one-shot compression, use [`compress_into`] instead.
530///
531/// # Example
532/// ```
533/// use lz4rip_encode::{CompressorRef, get_maximum_output_size};
534///
535/// let mut comp = CompressorRef::new();
536/// let input = b"hello world, hello world, hello!";
537/// let mut output = vec![0u8; get_maximum_output_size(input.len())];
538/// let compressed_len = comp.compress_into(input, &mut output).unwrap();
539/// ```
540pub struct CompressorRef<'a> {
541    tables: CompressorTables<'a>,
542}
543
544enum CompressorTables<'a> {
545    Plain {
546        table: HashTableU32,
547        stream_offset: usize,
548    },
549    Dict {
550        table: HashTableU32U16,
551        pristine: HashTableU32U16,
552        dict: &'a [u8],
553    },
554}
555
556impl fmt::Debug for CompressorRef<'_> {
557    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558        match &self.tables {
559            CompressorTables::Plain { .. } => f
560                .debug_struct("CompressorRef")
561                .field("dict_len", &0)
562                .finish(),
563            CompressorTables::Dict { dict, .. } => f
564                .debug_struct("CompressorRef")
565                .field("dict_len", &dict.len())
566                .finish(),
567        }
568    }
569}
570
571impl CompressorRef<'static> {
572    /// Create a new compressor without a dictionary.
573    #[must_use]
574    pub fn new() -> Self {
575        CompressorRef {
576            tables: CompressorTables::Plain {
577                table: HashTableU32::new(),
578                stream_offset: 0,
579            },
580        }
581    }
582}
583
584impl<'a> CompressorRef<'a> {
585    /// Create a new compressor seeded with an external dictionary.
586    ///
587    /// If `dict` is shorter than 4 bytes, it is ignored.
588    #[must_use]
589    pub fn with_dict(dict: &'a [u8]) -> Self {
590        if dict.len() < MINMATCH {
591            return CompressorRef::new();
592        }
593        let trimmed = if dict.len() > WINDOW_SIZE {
594            &dict[dict.len() - WINDOW_SIZE..]
595        } else {
596            dict
597        };
598        let mut pristine = HashTableU32U16::new();
599        let mut dict_ref = trimmed;
600        init_dict(&mut pristine, &mut dict_ref);
601        CompressorRef {
602            tables: CompressorTables::Dict {
603                table: HashTableU32U16::new(),
604                pristine,
605                dict: trimmed,
606            },
607        }
608    }
609
610    const EPOCH_THRESHOLD: usize = 8 * 1024;
611
612    /// Compress `input` into `output`, returning the number of compressed bytes.
613    ///
614    /// `output` must be at least [`get_maximum_output_size`]`(input.len())` bytes.
615    pub fn compress_into(
616        &mut self,
617        input: &[u8],
618        output: &mut [u8],
619    ) -> Result<usize, CompressError> {
620        match &mut self.tables {
621            CompressorTables::Dict {
622                table,
623                pristine,
624                dict,
625            } => {
626                if input.len() <= DICT_READONLY_LIMIT
627                    && dict.len() + input.len() < u16::MAX as usize
628                {
629                    compress_internal::<_, true, true, true, _>(
630                        input,
631                        0,
632                        &mut VerifiedSliceSink::new(output, 0),
633                        pristine,
634                        dict,
635                        dict.len(),
636                    )
637                } else if dict.len() + input.len() < u16::MAX as usize {
638                    table.clear();
639                    compress_with_dict_table(
640                        input,
641                        &mut VerifiedSliceSink::new(output, 0),
642                        table,
643                        pristine,
644                        dict,
645                        dict.len(),
646                    )
647                } else {
648                    compress_into_sink_with_dict::<true>(
649                        input,
650                        &mut VerifiedSliceSink::new(output, 0),
651                        dict,
652                    )
653                }
654            }
655            CompressorTables::Plain {
656                table,
657                stream_offset,
658            } => {
659                let offset = prepare_plain_table(table, stream_offset, input.len());
660                if offset > 0 {
661                    compress_internal::<_, false, true, false, _>(
662                        input,
663                        0,
664                        &mut VerifiedSliceSink::new(output, 0),
665                        table,
666                        b"",
667                        offset,
668                    )
669                } else {
670                    compress_internal::<_, false, false, false, _>(
671                        input,
672                        0,
673                        &mut VerifiedSliceSink::new(output, 0),
674                        table,
675                        b"",
676                        0,
677                    )
678                }
679            }
680        }
681    }
682
683    /// Compress `input` into a new `Vec<u8>`.
684    #[cfg(feature = "alloc")]
685    pub fn compress(&mut self, input: &[u8]) -> Vec<u8> {
686        let max_compressed = get_maximum_output_size(input.len());
687        let mut compressed = vec![0u8; max_compressed];
688        let compressed_len = self.compress_into(input, &mut compressed).unwrap();
689        compressed.truncate(compressed_len);
690
691        compressed
692    }
693}
694
695#[inline]
696fn prepare_plain_table(
697    table: &mut HashTableU32,
698    stream_offset: &mut usize,
699    input_len: usize,
700) -> usize {
701    if input_len > CompressorRef::EPOCH_THRESHOLD {
702        table.clear();
703        *stream_offset = input_len + MAX_DISTANCE + 1;
704        return 0;
705    }
706    let offset = *stream_offset;
707    let next = offset
708        .checked_add(input_len)
709        .and_then(|v| v.checked_add(MAX_DISTANCE + 1));
710    if let Some(next) = next.filter(|&n| n <= u32::MAX as usize) {
711        *stream_offset = next;
712    } else {
713        table.clear();
714        *stream_offset = input_len + MAX_DISTANCE + 1;
715    }
716    offset
717}
718
719impl Default for CompressorRef<'static> {
720    fn default() -> Self {
721        Self::new()
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728
729    fn count_same_bytes(input: &[u8], cur: &mut usize, source: &[u8], candidate: usize) -> usize {
730        const USIZE_SIZE: usize = core::mem::size_of::<usize>();
731        let cur_slice = &input[*cur..input.len() - END_OFFSET];
732        let cand_slice = &source[candidate..];
733
734        let mut num = 0;
735        for (block1, block2) in cur_slice
736            .chunks_exact(USIZE_SIZE)
737            .zip(cand_slice.chunks_exact(USIZE_SIZE))
738        {
739            let input_block = usize::from_ne_bytes(block1.try_into().unwrap());
740            let match_block = usize::from_ne_bytes(block2.try_into().unwrap());
741
742            if input_block == match_block {
743                num += USIZE_SIZE;
744            } else {
745                let diff = input_block ^ match_block;
746                num += (diff.to_le().trailing_zeros() / 8) as usize;
747                *cur += num;
748                return num;
749            }
750        }
751
752        #[cold]
753        fn count_same_bytes_tail(a: &[u8], b: &[u8], offset: usize) -> usize {
754            a.iter()
755                .zip(b)
756                .skip(offset)
757                .take_while(|(a, b)| a == b)
758                .count()
759        }
760        num += count_same_bytes_tail(cur_slice, cand_slice, num);
761
762        *cur += num;
763        num
764    }
765
766    #[test]
767    fn test_count_same_bytes() {
768        let first: &[u8] = &[
769            1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
770        ];
771        let second: &[u8] = &[
772            1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
773        ];
774        assert_eq!(count_same_bytes(first, &mut 0, second, 0), 16);
775
776        for diff_idx in 8..100 {
777            let first: Vec<u8> = (0u8..255).cycle().take(100 + 12).collect();
778            let mut second = first.clone();
779            second[diff_idx] = 255;
780            for start in 0..=diff_idx {
781                let same_bytes = count_same_bytes(&first, &mut start.clone(), &second, start);
782                assert_eq!(same_bytes, diff_idx - start);
783            }
784        }
785    }
786
787    #[test]
788    fn test_bug() {
789        let input: &[u8] = &[
790            10, 12, 14, 16, 18, 10, 12, 14, 16, 18, 10, 12, 14, 16, 18, 10, 12, 14, 16, 18,
791        ];
792        let mut output = [0u8; get_maximum_output_size(20)];
793        let _ = compress_into(input, &mut output).unwrap();
794    }
795
796    #[test]
797    fn test_conformant_last_block() {
798        let aaas: &[u8] = b"aaaaaaaaaaaaaaa";
799
800        let mut out = [0u8; get_maximum_output_size(15)];
801        let n = compress_into(&aaas[..12], &mut out).unwrap();
802        assert!(n > 12);
803        let n = compress_into(&aaas[..13], &mut out).unwrap();
804        assert!(n <= 13);
805        let n = compress_into(&aaas[..14], &mut out).unwrap();
806        assert!(n <= 14);
807        let n = compress_into(&aaas[..15], &mut out).unwrap();
808        assert!(n <= 15);
809    }
810}