Skip to main content

lz4rip_encode/
compress.rs

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