1use core::ops::{Deref, DerefMut};
12
13pub const MIN_MATCH_LENGTH: usize = 3;
15
16pub const MAX_MATCH_LENGTH: usize = 131074; const HASH_LOG: usize = 16;
22const HASH_SIZE: usize = 1 << HASH_LOG;
23#[allow(dead_code)]
24const HASH_MASK: u32 = (HASH_SIZE - 1) as u32;
25
26#[allow(dead_code)]
30const MAX_CHAIN_DEPTH: usize = 256;
31
32const HASH_PRIME: u32 = 0x9E3779B9;
34
35const HASH_PRIME2: u32 = 0x85EBCA6B;
37
38#[repr(C, align(64))]
47pub struct CacheAligned<T>(T);
48
49impl<T> CacheAligned<T> {
50 #[inline]
52 pub const fn new(value: T) -> Self {
53 Self(value)
54 }
55
56 #[inline]
58 pub fn inner_mut(&mut self) -> &mut T {
59 &mut self.0
60 }
61}
62
63impl<T> Deref for CacheAligned<T> {
64 type Target = T;
65
66 #[inline]
67 fn deref(&self) -> &Self::Target {
68 &self.0
69 }
70}
71
72impl<T> DerefMut for CacheAligned<T> {
73 #[inline]
74 fn deref_mut(&mut self) -> &mut Self::Target {
75 &mut self.0
76 }
77}
78
79#[repr(C, align(64))]
84struct AlignedHashTable {
85 data: [u32; HASH_SIZE],
86}
87
88impl AlignedHashTable {
89 fn new_boxed() -> Box<Self> {
91 unsafe {
94 let layout = core::alloc::Layout::new::<Self>();
95 let ptr = std::alloc::alloc_zeroed(layout) as *mut Self;
96 if ptr.is_null() {
97 std::alloc::handle_alloc_error(layout);
98 }
99 Box::from_raw(ptr)
100 }
101 }
102
103 #[inline]
105 #[allow(dead_code)]
106 fn reset(&mut self) {
107 self.data.fill(0);
108 }
109
110 #[inline(always)]
112 fn get(&self, index: usize) -> u32 {
113 self.data[index]
114 }
115
116 #[inline(always)]
118 fn set(&mut self, index: usize, value: u32) {
119 self.data[index] = value;
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct Match {
126 pub position: usize,
128 pub offset: usize,
130 pub length: usize,
132}
133
134impl Match {
135 #[inline]
137 pub fn new(position: usize, offset: usize, length: usize) -> Self {
138 Self {
139 position,
140 offset,
141 length,
142 }
143 }
144}
145
146pub struct MatchFinder {
165 search_depth: usize,
167 hash_table: Box<AlignedHashTable>,
171 generation: u32,
173 chain_table: Vec<u32>,
175 input_len: usize,
177 predicted_offset: u32,
180 predicted_offset2: u32,
182 prediction_hits: u32,
184}
185
186impl core::fmt::Debug for MatchFinder {
188 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
189 f.debug_struct("MatchFinder")
190 .field("search_depth", &self.search_depth)
191 .field(
192 "hash_table",
193 &format_args!("[AlignedHashTable; {}]", HASH_SIZE),
194 )
195 .field("chain_table_len", &self.chain_table.len())
196 .field("input_len", &self.input_len)
197 .field("predicted_offset", &self.predicted_offset)
198 .field("predicted_offset2", &self.predicted_offset2)
199 .field("prediction_hits", &self.prediction_hits)
200 .finish()
201 }
202}
203
204const GEN_MASK: u32 = 0xF0000000;
206const GEN_SHIFT: u32 = 28;
208const POS_MASK: u32 = 0x0FFFFFFF;
210
211impl MatchFinder {
212 pub fn new(search_depth: usize) -> Self {
214 Self {
215 search_depth: search_depth.clamp(1, 128),
216 hash_table: AlignedHashTable::new_boxed(),
217 generation: 0,
218 chain_table: Vec::new(),
219 input_len: 0,
220 predicted_offset: 0,
221 predicted_offset2: 0,
222 prediction_hits: 0,
223 }
224 }
225
226 #[inline]
241 pub fn early_exit_threshold(&self, position: usize) -> usize {
242 match position {
243 0..=1024 => 32, 1025..=8192 => 24, 8193..=32768 => 16, _ => 12, }
248 }
249
250 #[inline]
260 pub fn effective_depth(&self, input_len: usize) -> usize {
261 let base = self.search_depth;
262
263 let scaled = match input_len {
264 0..=4096 => base,
266 4097..=16384 => (base * 9 / 10).max(4),
268 16385..=65536 => (base * 3 / 4).max(4),
270 65537..=262144 => (base / 2).max(3),
272 _ => (base / 3).max(2),
274 };
275
276 scaled.min(base) }
278
279 #[inline]
285 pub(super) fn reset(&mut self, input_len: usize) {
286 self.generation = (self.generation + 1) & 0xF;
289
290 if self.chain_table.len() < input_len {
293 self.chain_table.resize(input_len, 0);
294 }
295 self.input_len = input_len;
298
299 self.predicted_offset = 0;
301 self.predicted_offset2 = 0;
302 self.prediction_hits = 0;
303 }
304
305 #[inline(always)]
310 pub(super) fn hash4(&self, data: &[u8], pos: usize) -> u32 {
311 debug_assert!(pos + 4 <= data.len());
312
313 let bytes = unsafe { std::ptr::read_unaligned(data.as_ptr().add(pos) as *const u32) };
315
316 bytes.wrapping_mul(HASH_PRIME) >> (32 - HASH_LOG as u32)
319 }
320
321 #[inline(always)]
323 pub(super) fn hash3(&self, data: &[u8], pos: usize) -> u32 {
324 debug_assert!(pos + 3 <= data.len());
325
326 let b0 = data[pos] as u32;
327 let b1 = data[pos + 1] as u32;
328 let b2 = data[pos + 2] as u32;
329
330 let value = b0 | (b1 << 8) | (b2 << 16);
332 value.wrapping_mul(HASH_PRIME) >> (32 - HASH_LOG as u32)
333 }
334
335 pub fn find_matches(&mut self, input: &[u8]) -> Vec<Match> {
339 if input.len() < MIN_MATCH_LENGTH {
340 return Vec::new();
341 }
342
343 self.reset(input.len());
344 let mut matches = Vec::with_capacity(input.len() / 16);
345
346 let mut pos = 0;
347 let end = input.len().saturating_sub(MIN_MATCH_LENGTH);
348
349 while pos <= end {
350 #[cfg(target_arch = "x86_64")]
352 if pos + 64 < input.len() {
353 unsafe {
354 use std::arch::x86_64::_mm_prefetch;
355 _mm_prefetch(
356 input.as_ptr().add(pos + 64) as *const i8,
357 std::arch::x86_64::_MM_HINT_T0,
358 );
359 }
360 }
361
362 let hash = if pos + 4 <= input.len() {
364 self.hash4(input, pos)
365 } else {
366 self.hash3(input, pos)
367 };
368
369 let cur_prefix =
371 unsafe { std::ptr::read_unaligned(input.as_ptr().add(pos) as *const u32) };
372
373 let mut best_match = None;
374
375 if self.predicted_offset > 0 && pos >= self.predicted_offset as usize {
377 let match_pos = pos - self.predicted_offset as usize;
378 let match_prefix = unsafe {
379 std::ptr::read_unaligned(input.as_ptr().add(match_pos) as *const u32)
380 };
381 if cur_prefix == match_prefix {
382 let length = 4 + self.match_length_from(input, match_pos + 4, pos + 4);
383 if length >= MIN_MATCH_LENGTH {
384 self.prediction_hits += 1;
385 best_match = Some(Match::new(pos, self.predicted_offset as usize, length));
386 }
387 }
388 }
389
390 if best_match.is_none()
392 && self.predicted_offset2 > 0
393 && self.predicted_offset2 != self.predicted_offset
394 && pos >= self.predicted_offset2 as usize
395 {
396 let match_pos = pos - self.predicted_offset2 as usize;
397 let match_prefix = unsafe {
398 std::ptr::read_unaligned(input.as_ptr().add(match_pos) as *const u32)
399 };
400 if cur_prefix == match_prefix {
401 let length = 4 + self.match_length_from(input, match_pos + 4, pos + 4);
402 if length >= MIN_MATCH_LENGTH {
403 self.prediction_hits += 1;
404 best_match = Some(Match::new(pos, self.predicted_offset2 as usize, length));
405 }
406 }
407 }
408
409 if best_match.is_none() {
411 best_match = self.find_best_match(input, pos, hash as usize);
412 }
413
414 if let Some(m) = best_match {
415 matches.push(m);
416
417 let new_offset = m.offset as u32;
419 if new_offset != self.predicted_offset {
420 self.predicted_offset2 = self.predicted_offset;
421 self.predicted_offset = new_offset;
422 }
423
424 self.update_hash(input, pos, hash as usize);
426
427 if m.offset <= 4 && m.length >= 32 {
430 let skip_end = (pos + m.length).min(end);
432 let mut update_pos = pos + 16;
433 while update_pos < skip_end {
434 if update_pos + 4 <= input.len() {
435 let h = self.hash4(input, update_pos);
436 self.update_hash(input, update_pos, h as usize);
437 }
438 update_pos += 16;
439 }
440 } else if m.length >= 64 {
441 let skip_end = (pos + m.length).min(end);
443 let mut update_pos = pos + 8;
444 while update_pos < skip_end {
445 if update_pos + 4 <= input.len() {
446 let h = self.hash4(input, update_pos);
447 self.update_hash(input, update_pos, h as usize);
448 }
449 update_pos += 8;
450 }
451 } else if m.length >= 8 {
452 let skip_end = (pos + m.length).min(end);
454 let mut update_pos = pos + 4;
455 while update_pos < skip_end {
456 if update_pos + 4 <= input.len() {
457 let h = self.hash4(input, update_pos);
458 self.update_hash(input, update_pos, h as usize);
459 }
460 update_pos += 4;
461 }
462 }
463 pos += m.length;
466 } else {
467 self.update_hash(input, pos, hash as usize);
468 pos += 1;
469 }
470 }
471
472 matches
473 }
474
475 #[inline(always)]
480 pub(super) fn update_hash(&mut self, _input: &[u8], pos: usize, hash: usize) {
481 let prev = self.hash_table.get(hash);
482 let prev_gen = (prev & GEN_MASK) >> GEN_SHIFT;
484 let chain_entry = if prev_gen == self.generation {
487 prev
489 } else {
490 0
492 };
493 if pos < self.chain_table.len() {
494 self.chain_table[pos] = chain_entry;
495 }
496 let encoded = (self.generation << GEN_SHIFT) | ((pos + 1) as u32);
498 self.hash_table.set(hash, encoded);
499 }
500
501 #[inline]
508 pub(super) fn find_best_match(&self, input: &[u8], pos: usize, hash: usize) -> Option<Match> {
509 let hash_entry = self.hash_table.get(hash);
510
511 let entry_gen = (hash_entry & GEN_MASK) >> GEN_SHIFT;
513 if entry_gen != self.generation {
514 return None;
515 }
516
517 let entry_pos = hash_entry & POS_MASK;
519 if entry_pos == 0 {
520 return None;
521 }
522 let mut match_pos = (entry_pos - 1) as usize;
523
524 if match_pos >= pos || pos + 4 > input.len() {
526 return None;
527 }
528
529 let cur_prefix = unsafe { std::ptr::read_unaligned(input.as_ptr().add(pos) as *const u32) };
531
532 let mut best_match: Option<Match> = None;
533 let mut best_length = MIN_MATCH_LENGTH - 1;
534 let mut depth = 0;
535 let max_offset = 1 << 28; let effective_depth = self.effective_depth(self.input_len);
539
540 while depth < effective_depth && match_pos < pos {
541 let offset = pos - match_pos;
542
543 if offset > max_offset {
544 break;
545 }
546
547 let next_chain = if match_pos < self.chain_table.len() {
550 let chain_entry = self.chain_table[match_pos];
551 let chain_gen = (chain_entry & GEN_MASK) >> GEN_SHIFT;
553 if chain_gen != self.generation {
554 0 } else {
556 let next_pos_enc = chain_entry & POS_MASK;
557 if next_pos_enc > 0 {
558 let next_pos = (next_pos_enc - 1) as usize;
559 #[cfg(target_arch = "x86_64")]
560 if next_pos < input.len() {
561 unsafe {
562 use std::arch::x86_64::_mm_prefetch;
563 _mm_prefetch(
564 input.as_ptr().add(next_pos) as *const i8,
565 std::arch::x86_64::_MM_HINT_T0,
566 );
567 }
568 }
569 }
570 next_pos_enc
571 }
572 } else {
573 0
574 };
575
576 let match_prefix = if match_pos + 4 <= input.len() {
578 unsafe { std::ptr::read_unaligned(input.as_ptr().add(match_pos) as *const u32) }
579 } else {
580 if next_chain == 0 {
582 break;
583 }
584 match_pos = (next_chain - 1) as usize;
585 depth += 1;
586 continue;
587 };
588
589 if match_prefix != cur_prefix {
591 if next_chain == 0 {
592 break;
593 }
594 match_pos = (next_chain - 1) as usize;
595 depth += 1;
596 continue;
597 }
598
599 let length = 4 + self.match_length_from(input, match_pos + 4, pos + 4);
601
602 let is_predicted = offset as u32 == self.predicted_offset;
604 let effective_length = if is_predicted && length >= MIN_MATCH_LENGTH {
605 length + 2
606 } else {
607 length
608 };
609
610 if effective_length > best_length {
611 best_length = length;
612 best_match = Some(Match::new(pos, offset, length));
613
614 let exit_threshold = self.early_exit_threshold(pos);
618 if length >= exit_threshold {
619 break;
620 }
621 }
622
623 if next_chain == 0 {
626 break; }
628 match_pos = (next_chain - 1) as usize;
629
630 depth += 1;
631 }
632
633 best_match
634 }
635
636 #[inline(always)]
641 pub fn match_length_from(&self, input: &[u8], pos1: usize, pos2: usize) -> usize {
642 if pos1 >= input.len() || pos2 >= input.len() {
644 return 0;
645 }
646
647 let max_len = (input.len() - pos2)
648 .min(input.len() - pos1)
649 .min(MAX_MATCH_LENGTH);
650
651 if max_len == 0 {
652 return 0;
653 }
654
655 #[cfg(feature = "simd")]
657 {
658 let src = &input[pos1..pos1 + max_len];
659 let cur = &input[pos2..pos2 + max_len];
660 haagenti_simd::find_match_length(src, cur, max_len)
661 }
662
663 #[cfg(not(feature = "simd"))]
665 {
666 let mut len = 0;
667
668 while len + 8 <= max_len {
670 let word1 = unsafe {
671 std::ptr::read_unaligned(input.as_ptr().add(pos1 + len) as *const u64)
672 };
673 let word2 = unsafe {
674 std::ptr::read_unaligned(input.as_ptr().add(pos2 + len) as *const u64)
675 };
676
677 let diff = word1 ^ word2;
678 if diff != 0 {
679 len += (diff.trailing_zeros() / 8) as usize;
681 return len;
682 }
683 len += 8;
684 }
685
686 while len < max_len && input[pos1 + len] == input[pos2 + len] {
688 len += 1;
689 }
690
691 len
692 }
693 }
694
695 #[inline]
715 pub fn find_matches_speculative(&mut self, input: &[u8]) -> Vec<Match> {
716 const LOOKAHEAD: usize = 4;
717
718 if input.len() < MIN_MATCH_LENGTH {
719 return Vec::new();
720 }
721
722 self.reset(input.len());
723 let mut matches = Vec::with_capacity(input.len() / 16);
724 let mut pos = 0;
725 let end = input.len().saturating_sub(MIN_MATCH_LENGTH + LOOKAHEAD);
726
727 while pos <= end && pos + 4 <= input.len() {
728 let hashes: [u32; LOOKAHEAD] = [
731 self.hash4(input, pos),
732 if pos + 5 <= input.len() {
733 self.hash4(input, pos + 1)
734 } else {
735 0
736 },
737 if pos + 6 <= input.len() {
738 self.hash4(input, pos + 2)
739 } else {
740 0
741 },
742 if pos + 7 <= input.len() {
743 self.hash4(input, pos + 3)
744 } else {
745 0
746 },
747 ];
748
749 let mut best_match: Option<Match> = None;
751 let mut best_score: usize = 0;
752
753 for (i, &hash) in hashes.iter().enumerate() {
754 if hash == 0 {
755 continue;
756 }
757
758 let check_pos = pos + i;
759 if let Some(m) = self.find_best_match(input, check_pos, hash as usize) {
760 let score = m.length * 8 - i;
763 if score > best_score {
764 best_score = score;
765 best_match = Some(m);
766 }
767 }
768 }
769
770 if let Some(m) = best_match {
771 for (i, &hash) in hashes
773 .iter()
774 .enumerate()
775 .take(LOOKAHEAD.min(m.position - pos))
776 {
777 if pos + i + 4 <= input.len() {
778 self.update_hash(input, pos + i, hash as usize);
779 }
780 }
781
782 self.update_hash(input, m.position, hashes[m.position - pos] as usize);
784
785 matches.push(m);
786
787 if m.length >= 8 {
789 let skip_end = (m.position + m.length).min(end);
790 let mut update_pos = m.position + 4;
791 while update_pos < skip_end {
792 if update_pos + 4 <= input.len() {
793 let h = self.hash4(input, update_pos);
794 self.update_hash(input, update_pos, h as usize);
795 }
796 update_pos += 4;
797 }
798 }
799
800 pos = m.position + m.length;
801 } else {
802 self.update_hash(input, pos, hashes[0] as usize);
804 pos += 1;
805 }
806 }
807
808 let final_end = input.len().saturating_sub(MIN_MATCH_LENGTH);
810 while pos <= final_end && pos + 4 <= input.len() {
811 let hash = self.hash4(input, pos);
812 if let Some(m) = self.find_best_match(input, pos, hash as usize) {
813 self.update_hash(input, pos, hash as usize);
814 matches.push(m);
815 pos += m.length;
816 } else {
817 self.update_hash(input, pos, hash as usize);
818 pos += 1;
819 }
820 }
821
822 matches
823 }
824}
825
826#[derive(Debug)]
836pub struct LazyMatchFinder {
837 pub(super) inner: MatchFinder,
839 lazy_threshold: usize,
841}
842
843impl LazyMatchFinder {
844 pub fn new(search_depth: usize) -> Self {
846 Self {
847 inner: MatchFinder::new(search_depth),
848 lazy_threshold: 24, }
850 }
851
852 #[inline]
867 pub fn configure_for_size(&mut self, input_len: usize) {
868 self.lazy_threshold = match input_len {
869 0..=4096 => 24, 4097..=16384 => 20, 16385..=65536 => 16, 65537..=262144 => 12, _ => 8, };
875
876 self.lazy_threshold = self.lazy_threshold.max(MIN_MATCH_LENGTH + 1);
878 }
879
880 #[inline]
889 pub fn find_matches_auto(&mut self, input: &[u8]) -> Vec<Match> {
890 self.configure_for_size(input.len());
891
892 if input.len() >= 131072 {
895 self.find_matches_chunked(input, 16384)
896 } else {
897 self.find_matches(input)
898 }
899 }
900
901 #[inline]
909 pub fn find_matches(&mut self, input: &[u8]) -> Vec<Match> {
910 if input.len() < MIN_MATCH_LENGTH {
911 return Vec::new();
912 }
913
914 self.inner.reset(input.len());
915 let mut matches = Vec::with_capacity(input.len() / 16);
916
917 let mut pos = 0;
918 let end = input.len().saturating_sub(MIN_MATCH_LENGTH);
919 let mut pending_match: Option<Match> = None;
920 let mut predicted_offset: usize = 0;
921
922 while pos <= end {
923 let hash = if pos + 4 <= input.len() {
924 self.inner.hash4(input, pos)
925 } else {
926 self.inner.hash3(input, pos)
927 };
928
929 let current_match =
931 if predicted_offset > 0 && pos >= predicted_offset && pos + 4 <= input.len() {
932 let match_pos = pos - predicted_offset;
933 let cur_prefix =
935 unsafe { std::ptr::read_unaligned(input.as_ptr().add(pos) as *const u32) };
936 let match_prefix = unsafe {
937 std::ptr::read_unaligned(input.as_ptr().add(match_pos) as *const u32)
938 };
939
940 if cur_prefix == match_prefix {
941 let length =
943 4 + self.inner.match_length_from(input, match_pos + 4, pos + 4);
944 if length >= MIN_MATCH_LENGTH {
945 Some(Match::new(pos, predicted_offset, length))
946 } else {
947 self.inner.find_best_match(input, pos, hash as usize)
948 }
949 } else {
950 self.inner.find_best_match(input, pos, hash as usize)
951 }
952 } else {
953 self.inner.find_best_match(input, pos, hash as usize)
954 };
955
956 if let Some(curr) = current_match {
957 predicted_offset = curr.offset;
959
960 if let Some(pending) = pending_match.take() {
961 if curr.length > pending.length + 1 {
963 matches.push(curr);
965 self.inner.update_hash(input, pos, hash as usize);
966 pos += curr.length;
967 } else {
968 matches.push(pending);
970 pos = pending.position + pending.length;
971 }
972 } else {
973 if curr.length >= self.lazy_threshold || pos + 1 > end {
975 matches.push(curr);
977 self.inner.update_hash(input, pos, hash as usize);
978 pos += curr.length;
979 } else {
980 pending_match = Some(curr);
982 self.inner.update_hash(input, pos, hash as usize);
983 pos += 1;
984 }
985 }
986 } else {
987 if let Some(pending) = pending_match.take() {
989 matches.push(pending);
990 pos = pending.position + pending.length;
991 } else {
992 self.inner.update_hash(input, pos, hash as usize);
993 pos += 1;
994 }
995 }
996 }
997
998 if let Some(pending) = pending_match {
1000 matches.push(pending);
1001 }
1002
1003 matches
1004 }
1005
1006 #[inline]
1031 pub fn find_matches_chunked(&mut self, input: &[u8], chunk_size: usize) -> Vec<Match> {
1032 if input.len() <= chunk_size {
1034 return self.find_matches(input);
1035 }
1036
1037 let chunk_size = chunk_size.max(1024); let mut all_matches = Vec::with_capacity(input.len() / 16);
1039 let mut chunk_start = 0;
1040
1041 const CHUNK_HASH_LOG: usize = 12;
1044 const CHUNK_HASH_SIZE: usize = 1 << CHUNK_HASH_LOG;
1045 const CHUNK_HASH_MASK: u32 = (CHUNK_HASH_SIZE - 1) as u32;
1046
1047 let mut chunk_hash = vec![0u32; CHUNK_HASH_SIZE];
1048 let mut chunk_chain = vec![0u32; chunk_size];
1049 let search_depth = self.inner.search_depth;
1050
1051 while chunk_start < input.len() {
1052 let chunk_end = (chunk_start + chunk_size).min(input.len());
1053 let chunk = &input[chunk_start..chunk_end];
1054
1055 if chunk.len() >= MIN_MATCH_LENGTH {
1056 chunk_hash.fill(0);
1058 if chunk_chain.len() < chunk.len() {
1059 chunk_chain.resize(chunk.len(), 0);
1060 }
1061 chunk_chain[..chunk.len()].fill(0);
1062
1063 let chunk_matches = Self::find_matches_in_chunk(
1065 chunk,
1066 &mut chunk_hash,
1067 &mut chunk_chain,
1068 CHUNK_HASH_LOG,
1069 CHUNK_HASH_MASK,
1070 search_depth,
1071 self.lazy_threshold,
1072 );
1073
1074 for m in chunk_matches {
1076 all_matches.push(Match::new(chunk_start + m.position, m.offset, m.length));
1077 }
1078 }
1079
1080 chunk_start = chunk_end;
1081 }
1082
1083 all_matches
1084 }
1085
1086 #[inline]
1089 fn find_matches_in_chunk(
1090 chunk: &[u8],
1091 hash_table: &mut [u32],
1092 chain_table: &mut [u32],
1093 hash_log: usize,
1094 hash_mask: u32,
1095 search_depth: usize,
1096 lazy_threshold: usize,
1097 ) -> Vec<Match> {
1098 if chunk.len() < MIN_MATCH_LENGTH {
1099 return Vec::new();
1100 }
1101
1102 let mut matches = Vec::with_capacity(chunk.len() / 32);
1103 let end = chunk.len().saturating_sub(MIN_MATCH_LENGTH);
1104 let mut pos = 0;
1105 let mut pending: Option<Match> = None;
1106
1107 while pos <= end {
1108 let hash = if pos + 4 <= chunk.len() {
1110 let bytes =
1111 unsafe { std::ptr::read_unaligned(chunk.as_ptr().add(pos) as *const u32) };
1112 let h = bytes.wrapping_mul(HASH_PRIME);
1113 let h = h ^ (h >> 15);
1114 let h = h.wrapping_mul(HASH_PRIME2);
1115 (h >> (32 - hash_log as u32)) & hash_mask
1116 } else {
1117 let b0 = chunk[pos] as u32;
1118 let b1 = chunk[pos + 1] as u32;
1119 let b2 = chunk[pos + 2] as u32;
1120 let value = b0 | (b1 << 8) | (b2 << 16);
1121 (value.wrapping_mul(HASH_PRIME) >> (32 - hash_log as u32)) & hash_mask
1122 };
1123
1124 let current_match = Self::find_best_match_in_chunk(
1126 chunk,
1127 pos,
1128 hash as usize,
1129 hash_table,
1130 chain_table,
1131 search_depth,
1132 );
1133
1134 if let Some(curr) = current_match {
1136 if let Some(pend) = pending.take() {
1137 if curr.length > pend.length + 1 {
1138 matches.push(curr);
1139 Self::update_chunk_hash(pos, hash as usize, hash_table, chain_table);
1140 pos += curr.length;
1141 } else {
1142 matches.push(pend);
1143 pos = pend.position + pend.length;
1144 }
1145 } else if curr.length >= lazy_threshold || pos + 1 > end {
1146 matches.push(curr);
1147 Self::update_chunk_hash(pos, hash as usize, hash_table, chain_table);
1148 pos += curr.length;
1149 } else {
1150 pending = Some(curr);
1151 Self::update_chunk_hash(pos, hash as usize, hash_table, chain_table);
1152 pos += 1;
1153 }
1154 } else if let Some(pend) = pending.take() {
1155 matches.push(pend);
1156 pos = pend.position + pend.length;
1157 } else {
1158 Self::update_chunk_hash(pos, hash as usize, hash_table, chain_table);
1159 pos += 1;
1160 }
1161 }
1162
1163 if let Some(pend) = pending {
1164 matches.push(pend);
1165 }
1166
1167 matches
1168 }
1169
1170 #[inline(always)]
1171 fn update_chunk_hash(pos: usize, hash: usize, hash_table: &mut [u32], chain_table: &mut [u32]) {
1172 let prev = hash_table[hash];
1173 if pos < chain_table.len() {
1174 chain_table[pos] = prev;
1175 }
1176 hash_table[hash] = (pos + 1) as u32;
1177 }
1178
1179 #[inline]
1180 fn find_best_match_in_chunk(
1181 chunk: &[u8],
1182 pos: usize,
1183 hash: usize,
1184 hash_table: &[u32],
1185 chain_table: &[u32],
1186 search_depth: usize,
1187 ) -> Option<Match> {
1188 let hash_entry = hash_table[hash];
1189 if hash_entry == 0 {
1190 return None;
1191 }
1192
1193 let mut match_pos = (hash_entry - 1) as usize;
1194 if match_pos >= pos || pos + 4 > chunk.len() {
1195 return None;
1196 }
1197
1198 let cur_prefix = unsafe { std::ptr::read_unaligned(chunk.as_ptr().add(pos) as *const u32) };
1199
1200 let mut best_match: Option<Match> = None;
1201 let mut best_length = MIN_MATCH_LENGTH - 1;
1202 let mut depth = 0;
1203
1204 while depth < search_depth && match_pos < pos {
1205 let offset = pos - match_pos;
1206
1207 if match_pos + 4 <= chunk.len() {
1209 let match_prefix = unsafe {
1210 std::ptr::read_unaligned(chunk.as_ptr().add(match_pos) as *const u32)
1211 };
1212
1213 if match_prefix == cur_prefix {
1214 let max_len = (chunk.len() - pos).min(chunk.len() - match_pos);
1216 let mut length = 4;
1217 while length < max_len && chunk[match_pos + length] == chunk[pos + length] {
1218 length += 1;
1219 }
1220
1221 if length > best_length {
1222 best_length = length;
1223 best_match = Some(Match::new(pos, offset, length));
1224
1225 if length >= 64 {
1226 break; }
1228 }
1229 }
1230 }
1231
1232 if match_pos < chain_table.len() {
1234 let next = chain_table[match_pos];
1235 if next == 0 {
1236 break;
1237 }
1238 match_pos = (next - 1) as usize;
1239 } else {
1240 break;
1241 }
1242
1243 depth += 1;
1244 }
1245
1246 best_match
1247 }
1248}
1249
1250#[allow(dead_code)]
1257const LONG_HASH_LOG: usize = 14;
1258#[allow(dead_code)]
1259const LONG_HASH_SIZE: usize = 1 << LONG_HASH_LOG;
1260
1261#[allow(dead_code)]
1278pub struct TwoTierMatchFinder {
1279 short_hash: Box<AlignedHashTable>,
1281 short_chain: Vec<u32>,
1282 long_hash: Vec<u32>,
1284 long_chain: Vec<u32>,
1285 search_depth: usize,
1287 input_len: usize,
1288}
1289
1290impl core::fmt::Debug for TwoTierMatchFinder {
1292 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1293 f.debug_struct("TwoTierMatchFinder")
1294 .field(
1295 "short_hash",
1296 &format_args!("[AlignedHashTable; {}]", HASH_SIZE),
1297 )
1298 .field("short_chain_len", &self.short_chain.len())
1299 .field("long_hash_len", &self.long_hash.len())
1300 .field("long_chain_len", &self.long_chain.len())
1301 .field("search_depth", &self.search_depth)
1302 .field("input_len", &self.input_len)
1303 .finish()
1304 }
1305}
1306
1307#[allow(dead_code)]
1308impl TwoTierMatchFinder {
1309 pub fn new(search_depth: usize) -> Self {
1311 Self {
1312 short_hash: AlignedHashTable::new_boxed(),
1313 short_chain: Vec::new(),
1314 long_hash: vec![0u32; LONG_HASH_SIZE],
1315 long_chain: Vec::new(),
1316 search_depth: search_depth.clamp(1, 128),
1317 input_len: 0,
1318 }
1319 }
1320
1321 fn reset(&mut self, input_len: usize) {
1323 self.short_hash.reset();
1324 if self.short_chain.len() < input_len {
1326 self.short_chain.clear();
1327 self.short_chain.resize(input_len, 0);
1328 } else {
1329 self.short_chain.truncate(input_len);
1330 self.short_chain.fill(0);
1331 }
1332
1333 self.long_hash.fill(0);
1334 if self.long_chain.len() < input_len {
1336 self.long_chain.clear();
1337 self.long_chain.resize(input_len, 0);
1338 } else {
1339 self.long_chain.truncate(input_len);
1340 self.long_chain.fill(0);
1341 }
1342
1343 self.input_len = input_len;
1344 }
1345
1346 #[inline(always)]
1348 fn hash4(&self, data: &[u8], pos: usize) -> u32 {
1349 debug_assert!(pos + 4 <= data.len());
1350 let bytes = unsafe { std::ptr::read_unaligned(data.as_ptr().add(pos) as *const u32) };
1351 let h = bytes.wrapping_mul(HASH_PRIME);
1352 let h = h ^ (h >> 15);
1353 let h = h.wrapping_mul(HASH_PRIME2);
1354 h >> (32 - HASH_LOG as u32)
1355 }
1356
1357 #[inline(always)]
1359 fn hash8(&self, data: &[u8], pos: usize) -> u32 {
1360 debug_assert!(pos + 8 <= data.len());
1361 let bytes = unsafe { std::ptr::read_unaligned(data.as_ptr().add(pos) as *const u64) };
1362 let h = (bytes as u32) ^ ((bytes >> 32) as u32);
1364 let h = h.wrapping_mul(HASH_PRIME);
1365 let h = h ^ (h >> 17);
1366 let h = h.wrapping_mul(HASH_PRIME2);
1367 h >> (32 - LONG_HASH_LOG as u32)
1368 }
1369
1370 #[inline(always)]
1372 fn update_hashes(&mut self, data: &[u8], pos: usize) {
1373 if pos + 4 <= data.len() {
1375 let h4 = self.hash4(data, pos) as usize;
1376 let prev = self.short_hash.get(h4);
1377 if pos < self.short_chain.len() {
1378 self.short_chain[pos] = prev;
1379 }
1380 self.short_hash.set(h4, (pos + 1) as u32);
1381 }
1382
1383 if pos + 8 <= data.len() {
1385 let h8 = self.hash8(data, pos) as usize;
1386 let prev = self.long_hash[h8];
1387 if pos < self.long_chain.len() {
1388 self.long_chain[pos] = prev;
1389 }
1390 self.long_hash[h8] = (pos + 1) as u32;
1391 }
1392 }
1393
1394 pub fn find_matches(&mut self, input: &[u8]) -> Vec<Match> {
1396 if input.len() < MIN_MATCH_LENGTH {
1397 return Vec::new();
1398 }
1399
1400 self.reset(input.len());
1401 let mut matches = Vec::with_capacity(input.len() / 16);
1402 let mut pos = 0;
1403 let end = input.len().saturating_sub(MIN_MATCH_LENGTH);
1404
1405 while pos <= end {
1406 let mut best_match: Option<Match> = None;
1407
1408 if pos + 8 <= input.len() {
1410 best_match = self.find_long_match(input, pos);
1411 }
1412
1413 if best_match.is_none() && pos + 4 <= input.len() {
1415 best_match = self.find_short_match(input, pos);
1416 }
1417
1418 self.update_hashes(input, pos);
1420
1421 if let Some(m) = best_match {
1422 matches.push(m);
1423 if m.length >= 8 {
1425 let skip_end = (pos + m.length).min(end);
1426 let mut update_pos = pos + 4;
1427 while update_pos < skip_end {
1428 self.update_hashes(input, update_pos);
1429 update_pos += 4;
1430 }
1431 }
1432 pos += m.length;
1433 } else {
1434 pos += 1;
1435 }
1436 }
1437
1438 matches
1439 }
1440
1441 #[inline]
1443 fn find_long_match(&self, input: &[u8], pos: usize) -> Option<Match> {
1444 let h8 = self.hash8(input, pos) as usize;
1445 let hash_entry = self.long_hash[h8];
1446
1447 if hash_entry == 0 {
1448 return None;
1449 }
1450
1451 let mut match_pos = (hash_entry - 1) as usize;
1452 if match_pos >= pos {
1453 return None;
1454 }
1455
1456 let cur_prefix = unsafe { std::ptr::read_unaligned(input.as_ptr().add(pos) as *const u64) };
1458
1459 let mut best_match: Option<Match> = None;
1460 let mut best_length = 7; let mut depth = 0;
1462
1463 while depth < self.search_depth / 2 && match_pos < pos {
1464 let offset = pos - match_pos;
1465
1466 if match_pos + 8 <= input.len() {
1467 let match_prefix = unsafe {
1468 std::ptr::read_unaligned(input.as_ptr().add(match_pos) as *const u64)
1469 };
1470
1471 if match_prefix == cur_prefix {
1472 let mut length = 8;
1474 let max_len = (input.len() - pos).min(input.len() - match_pos);
1475 while length < max_len && input[match_pos + length] == input[pos + length] {
1476 length += 1;
1477 }
1478
1479 if length > best_length {
1480 best_length = length;
1481 best_match = Some(Match::new(pos, offset, length));
1482
1483 if length >= 32 {
1485 return best_match;
1486 }
1487 }
1488 }
1489 }
1490
1491 if match_pos < self.long_chain.len() {
1493 let next = self.long_chain[match_pos];
1494 if next == 0 {
1495 break;
1496 }
1497 match_pos = (next - 1) as usize;
1498 } else {
1499 break;
1500 }
1501
1502 depth += 1;
1503 }
1504
1505 best_match
1506 }
1507
1508 #[inline]
1510 fn find_short_match(&self, input: &[u8], pos: usize) -> Option<Match> {
1511 let h4 = self.hash4(input, pos) as usize;
1512 let hash_entry = self.short_hash.get(h4);
1513
1514 if hash_entry == 0 {
1515 return None;
1516 }
1517
1518 let mut match_pos = (hash_entry - 1) as usize;
1519 if match_pos >= pos {
1520 return None;
1521 }
1522
1523 let cur_prefix = unsafe { std::ptr::read_unaligned(input.as_ptr().add(pos) as *const u32) };
1524
1525 let mut best_match: Option<Match> = None;
1526 let mut best_length = MIN_MATCH_LENGTH - 1;
1527 let mut depth = 0;
1528
1529 while depth < self.search_depth && match_pos < pos {
1530 let offset = pos - match_pos;
1531
1532 if match_pos + 4 <= input.len() {
1533 let match_prefix = unsafe {
1534 std::ptr::read_unaligned(input.as_ptr().add(match_pos) as *const u32)
1535 };
1536
1537 if match_prefix == cur_prefix {
1538 let mut length = 4;
1539 let max_len = (input.len() - pos).min(input.len() - match_pos);
1540 while length < max_len && input[match_pos + length] == input[pos + length] {
1541 length += 1;
1542 }
1543
1544 if length > best_length {
1545 best_length = length;
1546 best_match = Some(Match::new(pos, offset, length));
1547
1548 if length >= 24 {
1549 return best_match;
1550 }
1551 }
1552 }
1553 }
1554
1555 if match_pos < self.short_chain.len() {
1557 let next = self.short_chain[match_pos];
1558 if next == 0 {
1559 break;
1560 }
1561 match_pos = (next - 1) as usize;
1562 } else {
1563 break;
1564 }
1565
1566 depth += 1;
1567 }
1568
1569 best_match
1570 }
1571}
1572
1573#[cfg(test)]
1578mod tests {
1579 use super::*;
1580
1581 #[test]
1582 fn test_match_creation() {
1583 let m = Match::new(10, 5, 4);
1584 assert_eq!(m.position, 10);
1585 assert_eq!(m.offset, 5);
1586 assert_eq!(m.length, 4);
1587 }
1588
1589 #[test]
1590 fn test_match_finder_creation() {
1591 let mf = MatchFinder::new(16);
1592 assert_eq!(mf.search_depth, 16);
1593 }
1594
1595 #[test]
1596 fn test_no_matches_short_input() {
1597 let mut mf = MatchFinder::new(16);
1598 let matches = mf.find_matches(b"ab");
1599 assert!(matches.is_empty());
1600 }
1601
1602 #[test]
1603 fn test_no_matches_unique() {
1604 let mut mf = MatchFinder::new(16);
1605 let matches = mf.find_matches(b"abcdefghij");
1606 assert!(matches.is_empty());
1607 }
1608
1609 #[test]
1610 fn test_simple_repeat() {
1611 let mut mf = MatchFinder::new(16);
1612 let matches = mf.find_matches(b"abcdabcd");
1614
1615 assert!(!matches.is_empty());
1616 let m = &matches[0];
1617 assert_eq!(m.position, 4);
1618 assert_eq!(m.offset, 4);
1619 assert_eq!(m.length, 4);
1620 }
1621
1622 #[test]
1623 fn test_overlapping_repeat() {
1624 let mut mf = MatchFinder::new(16);
1625 let matches = mf.find_matches(b"aaaaaaaaa");
1627
1628 assert!(!matches.is_empty());
1629 let has_rle = matches.iter().any(|m| m.offset <= 4 && m.length >= 3);
1631 assert!(has_rle);
1632 }
1633
1634 #[test]
1635 fn test_multiple_matches() {
1636 let mut mf = MatchFinder::new(16);
1637 let input = b"abcdXabcdYabcdZ";
1639 let matches = mf.find_matches(input);
1640
1641 assert!(
1643 matches.len() >= 1,
1644 "Expected at least one match, got {:?}",
1645 matches
1646 );
1647 }
1648
1649 #[test]
1650 fn test_long_match() {
1651 let mut mf = MatchFinder::new(16);
1652 let input = b"0123456789ABCDEF0123456789ABCDEF";
1654 let matches = mf.find_matches(input);
1655
1656 assert!(!matches.is_empty());
1657 let m = &matches[0];
1658 assert_eq!(m.length, 16); assert_eq!(m.offset, 16);
1660 }
1661
1662 #[test]
1663 fn test_hash_distribution() {
1664 let mf = MatchFinder::new(16);
1665 let input = b"testdatatestdata";
1666
1667 let h1 = mf.hash4(input, 0);
1669 let h2 = mf.hash4(input, 4);
1670 let h3 = mf.hash4(input, 8);
1671
1672 assert_eq!(h1, h3); assert_ne!(h1, h2); }
1676
1677 #[test]
1678 fn test_search_depth_limit() {
1679 let mut mf = MatchFinder::new(1);
1680 let input = b"abcXabcYabcZ";
1681 let matches = mf.find_matches(input);
1682
1683 assert!(matches.len() <= 3);
1685 }
1686
1687 #[test]
1688 fn test_match_length_calculation() {
1689 let mut mf = MatchFinder::new(16);
1690 let input = b"hellohello";
1691 mf.reset(input.len());
1692
1693 let len = mf.match_length_from(input, 0, 5);
1695 assert_eq!(len, 5); }
1697
1698 #[test]
1699 fn test_lazy_match_finder() {
1700 let mut mf = LazyMatchFinder::new(16);
1701 let input = b"abcdefabcdefXabcdefabcdef";
1702 let matches = mf.find_matches(input);
1703
1704 assert!(!matches.is_empty());
1706 }
1707
1708 #[test]
1709 fn test_large_input() {
1710 let mut mf = MatchFinder::new(32);
1711
1712 let mut input = Vec::with_capacity(100_000);
1714 let pattern = b"The quick brown fox jumps over the lazy dog. ";
1715 while input.len() < 100_000 {
1716 input.extend_from_slice(pattern);
1717 }
1718
1719 let matches = mf.find_matches(&input);
1720
1721 assert!(
1724 !matches.is_empty(),
1725 "Expected to find matches in repetitive data"
1726 );
1727
1728 let total_match_len: usize = matches.iter().map(|m| m.length).sum();
1730 assert!(
1731 total_match_len > input.len() / 4,
1732 "Expected matches to cover at least 25% of input, got {} / {}",
1733 total_match_len,
1734 input.len()
1735 );
1736 }
1737
1738 #[test]
1739 fn test_aligned_hash_table_alignment() {
1740 let table = AlignedHashTable::new_boxed();
1742 let addr = table.data.as_ptr() as usize;
1743
1744 assert_eq!(
1746 addr % 64,
1747 0,
1748 "Hash table data should be 64-byte aligned, got address {:x}",
1749 addr
1750 );
1751
1752 let struct_addr = &*table as *const _ as usize;
1754 assert_eq!(
1755 struct_addr % 64,
1756 0,
1757 "AlignedHashTable struct should be 64-byte aligned, got address {:x}",
1758 struct_addr
1759 );
1760 }
1761
1762 #[test]
1763 fn test_aligned_hash_table_operations() {
1764 let mut table = AlignedHashTable::new_boxed();
1765
1766 for i in 0..HASH_SIZE {
1768 assert_eq!(table.get(i), 0);
1769 }
1770
1771 table.set(0, 42);
1773 table.set(HASH_SIZE - 1, 123);
1774 assert_eq!(table.get(0), 42);
1775 assert_eq!(table.get(HASH_SIZE - 1), 123);
1776
1777 table.reset();
1779 assert_eq!(table.get(0), 0);
1780 assert_eq!(table.get(HASH_SIZE - 1), 0);
1781 }
1782
1783 #[test]
1788 fn test_adaptive_depth_scales_with_size() {
1789 let finder = MatchFinder::new(16);
1790
1791 assert_eq!(finder.effective_depth(1024), 16);
1793 assert_eq!(finder.effective_depth(4096), 16);
1794
1795 assert_eq!(finder.effective_depth(8192), 14);
1797 assert_eq!(finder.effective_depth(16384), 14);
1798
1799 assert_eq!(finder.effective_depth(65536), 12);
1801
1802 assert_eq!(finder.effective_depth(262144), 8);
1804 }
1805
1806 #[test]
1807 fn test_adaptive_depth_respects_minimum() {
1808 let finder = MatchFinder::new(4);
1809
1810 assert!(finder.effective_depth(262144) >= 2);
1812 assert!(finder.effective_depth(1_000_000) >= 2);
1813 }
1814
1815 #[test]
1816 fn test_adaptive_depth_respects_configured_max() {
1817 let finder_shallow = MatchFinder::new(4);
1818 let finder_deep = MatchFinder::new(64);
1819
1820 assert!(finder_shallow.effective_depth(1024) <= 4);
1822 assert!(finder_deep.effective_depth(1024) <= 64);
1823
1824 let shallow_large = finder_shallow.effective_depth(65536);
1826 let deep_large = finder_deep.effective_depth(65536);
1827 assert!(deep_large > shallow_large);
1828 }
1829
1830 #[test]
1831 fn test_adaptive_finder_large_input_throughput() {
1832 use std::time::Instant;
1833
1834 let mut data = Vec::with_capacity(65536);
1836 let pattern = b"The quick brown fox jumps over the lazy dog. ";
1837 while data.len() < 65536 {
1838 data.extend_from_slice(pattern);
1839 }
1840
1841 let mut finder = MatchFinder::new(16);
1842
1843 for _ in 0..3 {
1845 let _ = finder.find_matches(&data);
1846 }
1847
1848 let iterations = 20;
1850 let start = Instant::now();
1851 for _ in 0..iterations {
1852 let _ = finder.find_matches(&data);
1853 }
1854 let elapsed = start.elapsed();
1855
1856 let total_bytes = data.len() * iterations;
1857 let throughput_mib = total_bytes as f64 / elapsed.as_secs_f64() / (1024.0 * 1024.0);
1858
1859 assert!(
1862 throughput_mib >= 30.0,
1863 "Large input throughput {:.1} MiB/s below target 30 MiB/s",
1864 throughput_mib
1865 );
1866 }
1867
1868 #[test]
1869 fn test_adaptive_finder_maintains_compression_quality() {
1870 let mut data = Vec::with_capacity(65536);
1872 let pattern = b"compression test data with repeating patterns ";
1873 while data.len() < 65536 {
1874 data.extend_from_slice(pattern);
1875 }
1876
1877 let mut finder = MatchFinder::new(16);
1878 let matches = finder.find_matches(&data);
1879
1880 let total_match_bytes: usize = matches.iter().map(|m| m.length).sum();
1882 let coverage = total_match_bytes as f64 / data.len() as f64;
1883
1884 assert!(
1886 coverage >= 0.70,
1887 "Match coverage {:.1}% below target 70%",
1888 coverage * 100.0
1889 );
1890 }
1891
1892 #[test]
1897 fn test_chunked_matching_correctness() {
1898 let mut data = Vec::with_capacity(65536);
1899 let pattern = b"The quick brown fox jumps over the lazy dog. ";
1900 while data.len() < 65536 {
1901 data.extend_from_slice(pattern);
1902 }
1903
1904 let mut finder = LazyMatchFinder::new(16);
1905
1906 let standard_matches = finder.find_matches(&data);
1908
1909 let chunked_matches = finder.find_matches_chunked(&data, 16384);
1911
1912 for m in &standard_matches {
1914 assert!(m.position + m.length <= data.len());
1915 assert!(m.position >= m.offset);
1916 }
1917 for m in &chunked_matches {
1918 assert!(m.position + m.length <= data.len());
1919 assert!(m.position >= m.offset);
1920 }
1921
1922 let chunked_coverage: usize = chunked_matches.iter().map(|m| m.length).sum();
1924 let min_coverage = data.len() / 2; assert!(
1926 chunked_coverage >= min_coverage,
1927 "Chunked coverage {} below minimum {}",
1928 chunked_coverage,
1929 min_coverage
1930 );
1931 }
1932
1933 #[test]
1934 fn test_chunked_matching_performance_reasonable() {
1935 use std::time::Instant;
1936
1937 let mut data = Vec::with_capacity(262144);
1940 let pattern = b"The quick brown fox jumps over the lazy dog. ";
1941 for i in 0..262144 {
1942 if i % 1024 < 512 {
1944 data.push(pattern[i % pattern.len()]);
1945 } else {
1946 data.push((i as u8).wrapping_mul(17).wrapping_add(i as u8 >> 4));
1947 }
1948 }
1949
1950 let mut finder = LazyMatchFinder::new(16);
1951
1952 for _ in 0..2 {
1954 let _ = finder.find_matches(&data);
1955 let _ = finder.find_matches_chunked(&data, 16384);
1956 }
1957
1958 let iterations = 10;
1960 let start = Instant::now();
1961 for _ in 0..iterations {
1962 let _ = finder.find_matches(&data);
1963 }
1964 let standard_time = start.elapsed();
1965
1966 let start = Instant::now();
1968 for _ in 0..iterations {
1969 let _ = finder.find_matches_chunked(&data, 16384);
1970 }
1971 let chunked_time = start.elapsed();
1972
1973 let ratio = chunked_time.as_secs_f64() / standard_time.as_secs_f64();
1983 assert!(
1984 ratio < 20.0,
1985 "Chunked ({:?}) is too slow compared to standard ({:?}), ratio: {:.2}x",
1986 chunked_time,
1987 standard_time,
1988 ratio
1989 );
1990 }
1991
1992 #[test]
1993 fn test_chunked_small_input_fallback() {
1994 let mut finder = LazyMatchFinder::new(16);
1995 let small_data = b"small input that fits in one chunk";
1996
1997 let matches = finder.find_matches_chunked(small_data, 16384);
1999
2000 assert!(matches.len() <= small_data.len());
2002 }
2003
2004 #[test]
2005 fn test_chunked_boundary_handling() {
2006 let chunk_size = 1024;
2008 let mut data = vec![b'A'; chunk_size - 8];
2009 data.extend_from_slice(b"PATTERN!");
2011 data.extend_from_slice(b"PATTERN!"); data.extend_from_slice(&vec![b'B'; chunk_size - 16]);
2013
2014 let mut finder = LazyMatchFinder::new(16);
2015 let matches = finder.find_matches_chunked(&data, chunk_size);
2016
2017 for m in &matches {
2019 assert!(m.position + m.length <= data.len());
2020 let src_start = m.position - m.offset;
2022 for i in 0..m.length {
2023 assert_eq!(
2024 data[src_start + i],
2025 data[m.position + i],
2026 "Match at {} offset {} length {} invalid at byte {}",
2027 m.position,
2028 m.offset,
2029 m.length,
2030 i
2031 );
2032 }
2033 }
2034 }
2035
2036 #[test]
2037 fn test_chunked_maintains_compression_ratio() {
2038 let mut data = Vec::with_capacity(65536);
2039 let pattern = b"repeating content for compression ratio test ";
2040 while data.len() < 65536 {
2041 data.extend_from_slice(pattern);
2042 }
2043
2044 let mut finder = LazyMatchFinder::new(16);
2045
2046 let standard = finder.find_matches(&data);
2047 let chunked = finder.find_matches_chunked(&data, 16384);
2048
2049 let standard_coverage: usize = standard.iter().map(|m| m.length).sum();
2050 let chunked_coverage: usize = chunked.iter().map(|m| m.length).sum();
2051
2052 let min_acceptable = (standard_coverage as f64 * 0.85) as usize;
2055 assert!(
2056 chunked_coverage >= min_acceptable,
2057 "Chunked coverage {} below 85% of standard {}",
2058 chunked_coverage,
2059 standard_coverage
2060 );
2061 }
2062
2063 #[test]
2068 fn test_early_exit_threshold_by_position() {
2069 let finder = MatchFinder::new(16);
2070
2071 let threshold_early = finder.early_exit_threshold(100);
2073 assert!(
2074 threshold_early >= 24,
2075 "Early position should have threshold >= 24, got {}",
2076 threshold_early
2077 );
2078
2079 let threshold_mid = finder.early_exit_threshold(10000);
2081 assert!(
2082 threshold_mid >= 12 && threshold_mid < 24,
2083 "Mid position should have threshold 12-23, got {}",
2084 threshold_mid
2085 );
2086
2087 let threshold_late = finder.early_exit_threshold(50000);
2089 assert!(
2090 threshold_late >= 8 && threshold_late < 16,
2091 "Late position should have threshold 8-15, got {}",
2092 threshold_late
2093 );
2094
2095 assert!(
2097 threshold_early >= threshold_mid,
2098 "Threshold should decrease with position"
2099 );
2100 assert!(
2101 threshold_mid >= threshold_late,
2102 "Threshold should decrease with position"
2103 );
2104 }
2105
2106 #[test]
2107 fn test_early_exit_excellent_match() {
2108 let mut finder = MatchFinder::new(16);
2109
2110 let mut data = vec![b'X'; 10]; let pattern = b"abcdefghijklmnopqrstuvwxyz0123456789ABCD";
2113 data.extend_from_slice(pattern);
2114 data.extend_from_slice(pattern); let matches = finder.find_matches(&data);
2117
2118 let long_match = matches.iter().any(|m| m.length >= 36);
2120 assert!(
2121 long_match,
2122 "Should find the long match >= 36 bytes, got {:?}",
2123 matches
2124 );
2125 }
2126
2127 #[test]
2128 fn test_early_exit_improves_throughput_on_repetitive() {
2129 use std::time::Instant;
2130
2131 let mut data = Vec::with_capacity(65536);
2133 let pattern = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; while data.len() < 65536 {
2135 data.extend_from_slice(pattern);
2136 }
2137
2138 let mut finder = MatchFinder::new(16);
2139
2140 for _ in 0..3 {
2142 let _ = finder.find_matches(&data);
2143 }
2144
2145 let iterations = 20;
2147 let start = Instant::now();
2148 for _ in 0..iterations {
2149 let _ = finder.find_matches(&data);
2150 }
2151 let elapsed = start.elapsed();
2152
2153 let total_bytes = data.len() * iterations;
2154 let throughput_mib = total_bytes as f64 / elapsed.as_secs_f64() / (1024.0 * 1024.0);
2155
2156 assert!(
2158 throughput_mib >= 25.0,
2159 "Repetitive data throughput {:.1} MiB/s below target 25 MiB/s",
2160 throughput_mib
2161 );
2162 }
2163
2164 #[test]
2165 fn test_early_exit_maintains_match_quality() {
2166 let mut data = Vec::with_capacity(65536);
2167 let pattern = b"The quick brown fox jumps over the lazy dog repeatedly. ";
2168 while data.len() < 65536 {
2169 data.extend_from_slice(pattern);
2170 }
2171
2172 let mut finder = MatchFinder::new(16);
2173 let matches = finder.find_matches(&data);
2174
2175 let total_match_bytes: usize = matches.iter().map(|m| m.length).sum();
2177 let coverage = total_match_bytes as f64 / data.len() as f64;
2178
2179 assert!(
2180 coverage >= 0.65,
2181 "Match coverage {:.1}% below target 65%",
2182 coverage * 100.0
2183 );
2184 }
2185
2186 #[test]
2191 fn test_lazy_threshold_scaling_by_size() {
2192 let mut finder = LazyMatchFinder::new(16);
2194 finder.configure_for_size(1024);
2195 assert_eq!(
2196 finder.lazy_threshold, 24,
2197 "Small input should use default threshold"
2198 );
2199
2200 let mut finder = LazyMatchFinder::new(16);
2202 finder.configure_for_size(16384);
2203 assert!(
2204 finder.lazy_threshold <= 20,
2205 "Medium input should lower threshold"
2206 );
2207
2208 let mut finder = LazyMatchFinder::new(16);
2210 finder.configure_for_size(65536);
2211 assert!(
2212 finder.lazy_threshold <= 16,
2213 "Large input should commit earlier"
2214 );
2215
2216 let mut finder = LazyMatchFinder::new(16);
2218 finder.configure_for_size(262144);
2219 assert!(
2220 finder.lazy_threshold <= 12,
2221 "Very large input should be aggressive"
2222 );
2223 }
2224
2225 #[test]
2226 fn test_adaptive_lazy_threshold_minimum() {
2227 let mut finder = LazyMatchFinder::new(16);
2228 finder.configure_for_size(1_000_000);
2229
2230 assert!(finder.lazy_threshold >= MIN_MATCH_LENGTH + 1);
2232 }
2233
2234 #[test]
2235 fn test_adaptive_lazy_maintains_quality() {
2236 let mut data = Vec::with_capacity(65536);
2237 let pattern = b"The quick brown fox jumps over the lazy dog. ";
2238 while data.len() < 65536 {
2239 data.extend_from_slice(pattern);
2240 }
2241
2242 let mut fixed_finder = LazyMatchFinder::new(16);
2244 let fixed_matches = fixed_finder.find_matches(&data);
2245
2246 let mut adaptive_finder = LazyMatchFinder::new(16);
2248 adaptive_finder.configure_for_size(data.len());
2249 let adaptive_matches = adaptive_finder.find_matches(&data);
2250
2251 let fixed_coverage: usize = fixed_matches.iter().map(|m| m.length).sum();
2252 let adaptive_coverage: usize = adaptive_matches.iter().map(|m| m.length).sum();
2253
2254 let min_coverage = (fixed_coverage as f64 * 0.90) as usize;
2256 assert!(
2257 adaptive_coverage >= min_coverage,
2258 "Adaptive coverage {} below 90% of fixed {}",
2259 adaptive_coverage,
2260 fixed_coverage
2261 );
2262 }
2263
2264 #[test]
2265 fn test_find_matches_auto_uses_adaptive() {
2266 let mut data = Vec::with_capacity(65536);
2267 let pattern = b"repeating patterns for auto adaptive test. ";
2268 while data.len() < 65536 {
2269 data.extend_from_slice(pattern);
2270 }
2271
2272 let mut finder = LazyMatchFinder::new(16);
2273
2274 let matches = finder.find_matches_auto(&data);
2276
2277 for m in &matches {
2279 assert!(m.position + m.length <= data.len());
2280 assert!(m.position >= m.offset);
2281 }
2282
2283 let coverage: usize = matches.iter().map(|m| m.length).sum();
2285 assert!(
2286 coverage > data.len() / 2,
2287 "Should cover at least 50% of input"
2288 );
2289 }
2290
2291 #[test]
2296 fn test_two_tier_finder_creation() {
2297 let finder = TwoTierMatchFinder::new(16);
2298 assert_eq!(finder.search_depth, 16);
2299 }
2300
2301 #[test]
2302 fn test_two_tier_finds_matches() {
2303 let mut finder = TwoTierMatchFinder::new(16);
2304 let input = b"abcdefghijklmnopabcdefghijklmnop";
2305 let matches = finder.find_matches(input);
2306
2307 assert!(!matches.is_empty(), "Should find matches");
2309 let m = &matches[0];
2310 assert_eq!(m.length, 16);
2311 assert_eq!(m.offset, 16);
2312 }
2313
2314 #[test]
2315 fn test_two_tier_finds_long_matches_via_8byte_hash() {
2316 let mut finder = TwoTierMatchFinder::new(16);
2317
2318 let mut data = vec![b'X'; 10];
2320 let pattern = b"LONGPATTERN12345678901234567890AB";
2321 data.extend_from_slice(pattern);
2322 data.extend_from_slice(pattern);
2323
2324 let matches = finder.find_matches(&data);
2325
2326 let long_match = matches.iter().any(|m| m.length >= 30);
2328 assert!(
2329 long_match,
2330 "Should find long match via 8-byte hash, got {:?}",
2331 matches
2332 );
2333 }
2334
2335 #[test]
2336 fn test_two_tier_finds_short_matches_fallback() {
2337 let mut finder = TwoTierMatchFinder::new(16);
2338
2339 let input = b"ABCDxxABCD"; let matches = finder.find_matches(input);
2342
2343 let short_match = matches.iter().any(|m| m.length >= 4);
2345 assert!(
2346 short_match,
2347 "Should find short match via 4-byte fallback, got {:?}",
2348 matches
2349 );
2350 }
2351
2352 #[test]
2353 fn test_two_tier_coverage_comparable_to_single() {
2354 let mut data = Vec::with_capacity(16384);
2355 let pattern = b"The quick brown fox jumps over the lazy dog. ";
2356 while data.len() < 16384 {
2357 data.extend_from_slice(pattern);
2358 }
2359
2360 let mut single = MatchFinder::new(16);
2361 let mut two_tier = TwoTierMatchFinder::new(16);
2362
2363 let single_matches = single.find_matches(&data);
2364 let two_tier_matches = two_tier.find_matches(&data);
2365
2366 let single_coverage: usize = single_matches.iter().map(|m| m.length).sum();
2367 let two_tier_coverage: usize = two_tier_matches.iter().map(|m| m.length).sum();
2368
2369 let min_coverage = (single_coverage as f64 * 0.90) as usize;
2371 assert!(
2372 two_tier_coverage >= min_coverage,
2373 "Two-tier coverage {} below 90% of single {}",
2374 two_tier_coverage,
2375 single_coverage
2376 );
2377 }
2378
2379 #[test]
2380 fn test_two_tier_performance_reasonable() {
2381 use std::time::Instant;
2382
2383 let mut data = Vec::with_capacity(65536);
2384 let pattern = b"repeating pattern for performance test data here. ";
2385 while data.len() < 65536 {
2386 data.extend_from_slice(pattern);
2387 }
2388
2389 let mut single = MatchFinder::new(16);
2390 let mut two_tier = TwoTierMatchFinder::new(16);
2391
2392 for _ in 0..3 {
2394 let _ = single.find_matches(&data);
2395 let _ = two_tier.find_matches(&data);
2396 }
2397
2398 let iterations = 15;
2400 let start = Instant::now();
2401 for _ in 0..iterations {
2402 let _ = single.find_matches(&data);
2403 }
2404 let single_time = start.elapsed();
2405
2406 let start = Instant::now();
2408 for _ in 0..iterations {
2409 let _ = two_tier.find_matches(&data);
2410 }
2411 let two_tier_time = start.elapsed();
2412
2413 let ratio = two_tier_time.as_secs_f64() / single_time.as_secs_f64();
2421 assert!(
2422 ratio < 30.0,
2423 "Two-tier ({:?}) too slow compared to single ({:?}), ratio: {:.2}x",
2424 two_tier_time,
2425 single_time,
2426 ratio
2427 );
2428 }
2429
2430 #[test]
2431 fn test_two_tier_8byte_hash_distribution() {
2432 let finder = TwoTierMatchFinder::new(16);
2433 let data = b"AABBCCDDAABBCCDD"; let h1 = finder.hash8(data, 0); let h2 = finder.hash8(data, 8); assert_eq!(h1, h2, "Same 8-byte sequence should have same hash");
2441
2442 let data2 = b"XYZ12345XYZ12345";
2444 let h3 = finder.hash8(data2, 0);
2445 assert!(h3 < LONG_HASH_SIZE as u32);
2448 }
2449
2450 #[test]
2455 fn test_speculative_finds_matches() {
2456 let mut finder = MatchFinder::new(16);
2457 let input = b"abcdefghijklmnopabcdefghijklmnop";
2458 let matches = finder.find_matches_speculative(input);
2459
2460 assert!(!matches.is_empty(), "Should find matches");
2461 let m = &matches[0];
2462 assert_eq!(m.length, 16);
2463 }
2464
2465 #[test]
2466 fn test_speculative_correctness() {
2467 let mut data = Vec::with_capacity(16384);
2469 let pattern = b"The quick brown fox jumps over the lazy dog. ";
2470 while data.len() < 16384 {
2471 data.extend_from_slice(pattern);
2472 }
2473
2474 let mut standard = MatchFinder::new(16);
2475 let mut speculative = MatchFinder::new(16);
2476
2477 let std_matches = standard.find_matches(&data);
2478 let spec_matches = speculative.find_matches_speculative(&data);
2479
2480 assert!(!std_matches.is_empty());
2482 assert!(!spec_matches.is_empty());
2483
2484 let std_coverage: usize = std_matches.iter().map(|m| m.length).sum();
2486 let spec_coverage: usize = spec_matches.iter().map(|m| m.length).sum();
2487
2488 let min_coverage = (std_coverage as f64 * 0.80) as usize;
2489 assert!(
2490 spec_coverage >= min_coverage,
2491 "Speculative coverage {} below 80% of standard {}",
2492 spec_coverage,
2493 std_coverage
2494 );
2495 }
2496
2497 #[test]
2498 fn test_speculative_no_overlapping_matches() {
2499 let mut finder = MatchFinder::new(16);
2500 let input = b"ABCABCABCABCABCABCABCABCABCABCABCABC";
2502 let matches = finder.find_matches_speculative(input);
2503
2504 for i in 1..matches.len() {
2506 let prev_end = matches[i - 1].position + matches[i - 1].length;
2507 assert!(
2508 matches[i].position >= prev_end,
2509 "Match {} at pos {} overlaps with previous ending at {}",
2510 i,
2511 matches[i].position,
2512 prev_end
2513 );
2514 }
2515 }
2516
2517 #[test]
2518 fn test_speculative_handles_short_input() {
2519 let mut finder = MatchFinder::new(16);
2520
2521 let matches = finder.find_matches_speculative(b"abc");
2523 assert!(matches.is_empty());
2524
2525 let matches = finder.find_matches_speculative(b"abcdabcd");
2527 assert!(matches.len() <= 1);
2529 }
2530
2531 #[test]
2532 fn test_speculative_performance_reasonable() {
2533 use std::time::Instant;
2534
2535 let mut data = Vec::with_capacity(65536);
2536 let pattern = b"repeating pattern for speculative matching test. ";
2537 while data.len() < 65536 {
2538 data.extend_from_slice(pattern);
2539 }
2540
2541 let mut standard = MatchFinder::new(16);
2542 let mut speculative = MatchFinder::new(16);
2543
2544 for _ in 0..3 {
2546 let _ = standard.find_matches(&data);
2547 let _ = speculative.find_matches_speculative(&data);
2548 }
2549
2550 let iterations = 15;
2552 let start = Instant::now();
2553 for _ in 0..iterations {
2554 let _ = standard.find_matches(&data);
2555 }
2556 let std_time = start.elapsed();
2557
2558 let start = Instant::now();
2560 for _ in 0..iterations {
2561 let _ = speculative.find_matches_speculative(&data);
2562 }
2563 let spec_time = start.elapsed();
2564
2565 let ratio = spec_time.as_secs_f64() / std_time.as_secs_f64();
2570 assert!(
2571 ratio < 8.0,
2572 "Speculative ({:?}) too slow compared to standard ({:?}), ratio: {:.2}x",
2573 spec_time,
2574 std_time,
2575 ratio
2576 );
2577 }
2578
2579 #[test]
2580 fn test_speculative_finds_better_matches() {
2581 let mut finder = MatchFinder::new(16);
2582
2583 let mut data = vec![b'X'; 5];
2587 data.extend_from_slice(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"); data.extend_from_slice(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"); let matches = finder.find_matches_speculative(&data);
2591
2592 let has_long = matches.iter().any(|m| m.length >= 20);
2594 assert!(
2595 has_long,
2596 "Speculative should find long matches: {:?}",
2597 matches
2598 );
2599 }
2600}