1use 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
24const INCREASE_STEPSIZE_BITSHIFT: usize = 3;
27
28const 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#[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#[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#[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 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
422pub 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#[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#[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#[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#[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
524pub 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 #[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 #[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 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 #[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}