prs_rs/impls/comp/
lz77_matcher.rs

1use super::comp_dict::CompDict;
2use crate::prelude::Allocator;
3use core::mem::size_of;
4use core::ptr::read_unaligned;
5
6/// This trait specifies constant parameters for the [`lz77_get_longest_match_fast`]
7/// and [`lz77_get_longest_match_slow`] functions.
8///
9/// This allows for the compiler to generate different optimized versions of the function,
10/// via the use of monomorphization and constant propagation.
11pub trait Lz77Parameters {
12    /// Maximum offset (from the current position) to search for a match.
13    /// Specified as positive, so 0x1000 means 0x1000 bytes back.
14    const MAX_OFFSET: usize;
15    /// Maximum length of the match.
16    const MAX_LENGTH: usize;
17}
18
19/// Searches back up to 'COPY_MAX_LENGTH' bytes and returns the length of the longest matching
20/// sequence of bytes. This is the fast version that assumes there are more than 'COPY_MAX_LENGTH'
21/// bytes left.
22///
23/// # Parameters
24///
25/// - `dict`: The dictionary used to speed up computation.
26/// - `source_ptr`: The data where the match is to be searched.
27/// - `source_len`: The length of the data.
28/// - `source_index`: The index of the current byte in the source.
29///
30/// # Safety
31///
32/// Should be safe provided `dict` is initialized with `source` and composed of valid data.
33#[inline(never)] // faster on x86_64
34pub unsafe fn lz77_get_longest_match_fast<
35    P: Lz77Parameters,
36    L: Allocator + Copy,
37    S: Allocator + Copy,
38>(
39    dict: &mut CompDict<L, S>,
40    source_ptr: *const u8,
41    source_index: usize,
42) -> Lz77Match {
43    let mut best_match = Lz77Match {
44        offset: 0,
45        length: 0,
46    };
47
48    // Calculate the minimum offset to consider for a match
49    let min_offset = source_index.saturating_sub(P::MAX_OFFSET);
50
51    // Read the 2-byte sequence from source at the current index
52    let key = read_unaligned(source_ptr.add(source_index) as *const u16);
53
54    // Retrieve possible match offsets from the dictionary
55    let offsets = dict.get_item(key, min_offset, source_index.saturating_sub(1));
56    for &match_offset in offsets.iter().rev() {
57        let match_offset = match_offset as usize;
58
59        // Determine the length of the match
60        let mut match_length = 2;
61
62        // Check the next 2 bytes.
63        let offset_src_ptr = source_ptr.add(match_offset);
64        let offset_dst_ptr = source_ptr.add(source_index);
65        let initial_match = read_unaligned(offset_src_ptr as *const u32)
66            == read_unaligned(offset_dst_ptr as *const u32);
67
68        if !initial_match {
69            // Length is 2 or 3 bytes.
70            match_length +=
71                (*offset_src_ptr.add(match_length) == *offset_dst_ptr.add(match_length)) as usize;
72        } else {
73            match_length = 4;
74
75            // We are usize aligned (for perf) and MAX_LENGTH should be divisible by usize.
76            // Therefore there is no risk of running out of bounds here in the usize matching.
77            debug_assert!(P::MAX_LENGTH % size_of::<usize>() == 0);
78
79            // First 8 bytes match.
80            while match_length < (P::MAX_LENGTH - size_of::<u32>())
81                && read_unaligned(offset_src_ptr.add(match_length) as *const usize)
82                    == read_unaligned(offset_dst_ptr.add(match_length) as *const usize)
83            {
84                match_length += size_of::<usize>();
85            }
86
87            while match_length < P::MAX_LENGTH
88                && *offset_src_ptr.add(match_length) == *offset_dst_ptr.add(match_length)
89            {
90                match_length += 1;
91            }
92        }
93
94        // Update the best match if this match is longer
95        if match_length > best_match.length {
96            best_match.length = match_length;
97            best_match.offset = match_offset as isize - source_index as isize;
98
99            if match_length == P::MAX_LENGTH {
100                break;
101            }
102        }
103    }
104
105    best_match
106}
107
108/// Searches back up to 'COPY_MAX_LENGTH' bytes and returns the length of the longest matching
109/// sequence of bytes. This is the slow version that ensures we don't overrun past the end of file.
110///
111/// # Parameters
112///
113/// - `dict`: The dictionary used to speed up computation.
114/// - `source_ptr`: The data where the match is to be searched.
115/// - `source_len`: The length of the data.
116/// - `source_index`: The index of the current byte in the source.
117///
118/// # Safety
119///
120/// Should be safe provided `dict` is initialized with `source` and composed of valid data.
121#[inline(never)] // faster on x86_64
122pub unsafe fn lz77_get_longest_match_slow<
123    P: Lz77Parameters,
124    L: Allocator + Copy,
125    S: Allocator + Copy,
126>(
127    dict: &mut CompDict<L, S>,
128    source_ptr: *const u8,
129    source_len: usize,
130    source_index: usize,
131) -> Lz77Match {
132    let mut best_match = Lz77Match {
133        offset: 0,
134        length: 0,
135    };
136
137    // Calculate the minimum offset to consider for a match
138    let min_offset = source_index.saturating_sub(P::MAX_OFFSET);
139
140    // Read the 2-byte sequence from source at the current index
141    let key = read_unaligned(source_ptr.add(source_index) as *const u16);
142
143    // Calculate the maximum possible match length
144    let max_match_length = P::MAX_LENGTH.min(source_len - source_index);
145
146    // Retrieve possible match offsets from the dictionary
147    let offsets = dict.get_item(key, min_offset, source_index.saturating_sub(1));
148    for &match_offset in offsets.iter().rev() {
149        let match_offset = match_offset as usize;
150
151        // We start having matched 2 and match byte by byte
152        let mut match_length = 2;
153        let offset_src_ptr = source_ptr.add(match_offset);
154        let offset_dst_ptr = source_ptr.add(source_index);
155        while match_length < max_match_length
156            && *offset_src_ptr.add(match_length) == *offset_dst_ptr.add(match_length)
157        {
158            match_length += 1;
159        }
160
161        // Update the best match if this match is longer
162        if match_length > best_match.length {
163            best_match.length = match_length;
164            best_match.offset = match_offset as isize - source_index as isize;
165
166            if match_length == max_match_length {
167                break;
168            }
169        }
170    }
171
172    best_match
173}
174
175/// Represents a match in the LZ77 algorithm.
176pub struct Lz77Match {
177    /// Offset of the LZ77 match, expressed as a negative number.
178    pub offset: isize,
179    /// Length of the match.
180    pub length: usize,
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::test_prelude::*;
187
188    #[test]
189    fn test_longest_match_with_repetition() {
190        let data = b"abcabcabcabcabc";
191        let mut dict = CompDict::new(data.len());
192        unsafe { dict.init(data, 0) }
193
194        // Longest match for "abc" starting from index 3 should be of length 12
195        let match_result = unsafe {
196            lz77_get_longest_match_slow::<CompressParameters, Global, Global>(
197                &mut dict,
198                data.as_ptr(),
199                data.len(),
200                3,
201            )
202        };
203        assert_eq!(match_result.length, 12);
204        assert_eq!(match_result.offset, -3);
205    }
206
207    #[test]
208    fn test_no_match() {
209        let data = b"abcdefgh";
210        let mut dict = CompDict::new(data.len());
211        unsafe { dict.init(data, 0) }
212
213        // No repetition, so no match
214        let match_result = unsafe {
215            lz77_get_longest_match_slow::<CompressParameters, Global, Global>(
216                &mut dict,
217                data.as_ptr(),
218                data.len(),
219                2,
220            )
221        };
222        assert_eq!(match_result.length, 0);
223    }
224
225    #[test]
226    fn test_multiple_matches() {
227        let data = b"ababababab";
228        let mut dict = CompDict::new(data.len());
229        unsafe { dict.init(data, 0) }
230
231        // Multiple "ab" patterns, longest match from index 2 should be length 8
232        let match_result = unsafe {
233            lz77_get_longest_match_slow::<CompressParameters, Global, Global>(
234                &mut dict,
235                data.as_ptr(),
236                data.len(),
237                2,
238            )
239        };
240        assert_eq!(match_result.length, 8);
241        assert_eq!(match_result.offset, -2);
242    }
243
244    #[test]
245    fn test_boundary_conditions() {
246        let data = b"ababababab";
247        let mut dict = CompDict::new(data.len());
248        unsafe { dict.init(data, 0) }
249
250        // Testing boundary condition: match at the very end
251        let match_result = unsafe {
252            lz77_get_longest_match_slow::<CompressParameters, Global, Global>(
253                &mut dict,
254                data.as_ptr(),
255                data.len(),
256                data.len() - 3,
257            )
258        };
259        assert_eq!(match_result.length, 3);
260        assert_eq!(match_result.offset, -2);
261    }
262
263    #[test]
264    fn test_last_match_on_boundary() {
265        let data = b"acacacabab";
266        let mut dict = CompDict::new(data.len());
267        unsafe { dict.init(data, 0) }
268
269        // Testing boundary condition: match at the very end, when very end is only pattern
270        let match_result = unsafe {
271            lz77_get_longest_match_slow::<CompressParameters, Global, Global>(
272                &mut dict,
273                data.as_ptr(),
274                data.len(),
275                data.len() - 2,
276            )
277        };
278        assert_eq!(match_result.length, 2);
279        assert_eq!(match_result.offset, -2);
280    }
281
282    struct CompressParameters;
283    impl Lz77Parameters for CompressParameters {
284        const MAX_OFFSET: usize = 0x1FFF;
285        const MAX_LENGTH: usize = 256;
286    }
287}