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