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
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#[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#[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#[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
397pub 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#[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#[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#[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#[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
499pub 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 #[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 #[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 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 #[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}