1#![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
25const 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#[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#[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#[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
398pub 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#[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#[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#[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
478pub 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 pub fn new() -> Self {
525 Compressor {
526 tables: CompressorTables::Plain {
527 table: HashTableU32::new(),
528 stream_offset: 0,
529 },
530 }
531 }
532
533 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 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 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}