Skip to main content

zsync_rs/
matcher.rs

1use crate::checksum::calc_md4;
2use crate::control::{ControlFile, HashLengths};
3use crate::rsum::{Rsum, calc_rsum_block};
4
5#[derive(Debug, thiserror::Error)]
6pub enum MatchError {
7    #[error("IO error: {0}")]
8    Io(#[from] std::io::Error),
9}
10
11const HASH_EMPTY: u32 = u32::MAX;
12const BITHASH_BITS: u32 = 3;
13
14#[derive(Debug, Clone, Copy)]
15struct TargetBlock {
16    rsum: Rsum,
17    checksum: [u8; 16],
18}
19
20/// Read-only scan state shared across threads.
21struct ScanState<'a> {
22    targets: &'a [TargetBlock],
23    hash_table: &'a [u32],
24    hash_next: &'a [u32],
25    bithash: &'a [u8],
26    blocksize: usize,
27    blockshift: u8,
28    seq_matches: usize,
29    checksum_bytes: usize,
30    rsum_a_mask: u16,
31    hash_func_shift: u32,
32    hash_mask: u32,
33    bithash_mask: u32,
34}
35
36impl ScanState<'_> {
37    #[inline(always)]
38    fn calc_hash_rolling(&self, r0: &Rsum, r1: &Rsum) -> u32 {
39        let mut h = r0.b as u32;
40        if self.seq_matches > 1 {
41            h ^= (r1.b as u32) << self.hash_func_shift;
42        } else {
43            h ^= ((r0.a & self.rsum_a_mask) as u32) << self.hash_func_shift;
44        }
45        h
46    }
47
48    #[inline(always)]
49    fn rsum_match(&self, target: &Rsum, rolling: &Rsum) -> bool {
50        target.a == (rolling.a & self.rsum_a_mask) && target.b == rolling.b
51    }
52
53    /// Scan a chunk of data for matching blocks. Pure read-only, no mutations.
54    /// `base_offset` is the absolute byte offset of `data[0]` within the source file.
55    /// Returns vec of (target_block_id, absolute_source_offset).
56    fn scan_chunk(&self, data: &[u8], base_offset: usize) -> Vec<(usize, usize)> {
57        let blocksize = self.blocksize;
58        let blockshift = self.blockshift;
59        let seq_matches = self.seq_matches;
60        let checksum_bytes = self.checksum_bytes;
61        let context = blocksize * seq_matches;
62        let mut matched_blocks = Vec::new();
63
64        if data.len() < context {
65            return matched_blocks;
66        }
67
68        let x_limit = data.len() - context;
69        let mut x = 0usize;
70        let mut next_match_id: Option<usize> = None;
71
72        let mut r0 = calc_rsum_block(&data[0..blocksize]);
73        let mut r1 = if seq_matches > 1 {
74            calc_rsum_block(&data[blocksize..blocksize * 2])
75        } else {
76            Rsum { a: 0, b: 0 }
77        };
78
79        while x < x_limit {
80            let mut blocks_matched = 0usize;
81
82            if let Some(hint_id) = next_match_id.take()
83                && seq_matches > 1
84                && hint_id < self.targets.len()
85            {
86                let target = &self.targets[hint_id];
87                if self.rsum_match(&target.rsum, &r0) {
88                    let checksum = calc_md4(&data[x..x + blocksize]);
89                    if checksum[..checksum_bytes] == target.checksum[..checksum_bytes] {
90                        matched_blocks.push((hint_id, base_offset + x));
91                        blocks_matched = 1;
92                        if hint_id + 1 < self.targets.len() {
93                            next_match_id = Some(hint_id + 1);
94                        }
95                    }
96                }
97            }
98
99            while blocks_matched == 0 && x < x_limit {
100                let hash = self.calc_hash_rolling(&r0, &r1);
101
102                let bh = (hash & self.bithash_mask) as usize;
103                if self.bithash[bh >> 3] & (1 << (bh & 7)) != 0 {
104                    let mut block_idx = self.hash_table[(hash & self.hash_mask) as usize];
105
106                    while block_idx != HASH_EMPTY {
107                        let block_id = block_idx as usize;
108                        block_idx = self.hash_next[block_id];
109
110                        let target = &self.targets[block_id];
111                        if !self.rsum_match(&target.rsum, &r0) {
112                            continue;
113                        }
114
115                        if seq_matches > 1 && block_id + 1 < self.targets.len() {
116                            let next_target = &self.targets[block_id + 1];
117                            if !self.rsum_match(&next_target.rsum, &r1) {
118                                continue;
119                            }
120
121                            let checksum = calc_md4(&data[x..x + blocksize]);
122                            if checksum[..checksum_bytes] != target.checksum[..checksum_bytes] {
123                                continue;
124                            }
125
126                            let next_checksum = calc_md4(&data[x + blocksize..x + blocksize * 2]);
127                            if next_checksum[..checksum_bytes]
128                                == next_target.checksum[..checksum_bytes]
129                            {
130                                matched_blocks.push((block_id, base_offset + x));
131                                matched_blocks.push((block_id + 1, base_offset + x + blocksize));
132                                blocks_matched = seq_matches;
133
134                                if block_id + 2 < self.targets.len() {
135                                    next_match_id = Some(block_id + 2);
136                                }
137                                break;
138                            }
139                        } else {
140                            let checksum = calc_md4(&data[x..x + blocksize]);
141                            if checksum[..checksum_bytes] == target.checksum[..checksum_bytes] {
142                                matched_blocks.push((block_id, base_offset + x));
143                                blocks_matched = 1;
144                                break;
145                            }
146                        }
147                    }
148                }
149
150                if blocks_matched == 0 {
151                    let oc = data[x];
152                    let nc = data[x + blocksize];
153                    r0.a = r0.a.wrapping_add(u16::from(nc)).wrapping_sub(u16::from(oc));
154                    r0.b =
155                        r0.b.wrapping_add(r0.a)
156                            .wrapping_sub(u16::from(oc) << blockshift);
157
158                    if seq_matches > 1 {
159                        let nc2 = data[x + blocksize * 2];
160                        r1.a =
161                            r1.a.wrapping_add(u16::from(nc2))
162                                .wrapping_sub(u16::from(nc));
163                        r1.b =
164                            r1.b.wrapping_add(r1.a)
165                                .wrapping_sub(u16::from(nc) << blockshift);
166                    }
167
168                    x += 1;
169                }
170            }
171
172            if blocks_matched > 0 {
173                x += blocksize * blocks_matched;
174
175                if x >= x_limit {
176                    // Can't calculate rsums for remaining data
177                } else {
178                    if seq_matches > 1 && blocks_matched == 1 {
179                        r0 = r1;
180                    } else {
181                        r0 = calc_rsum_block(&data[x..x + blocksize]);
182                    }
183                    if seq_matches > 1 {
184                        r1 = calc_rsum_block(&data[x + blocksize..x + blocksize * 2]);
185                    }
186                }
187            }
188        }
189
190        matched_blocks
191    }
192}
193
194pub struct BlockMatcher {
195    blocksize: usize,
196    blockshift: u8,
197    hash_lengths: HashLengths,
198    rsum_a_mask: u16,
199    hash_func_shift: u32,
200    targets: Vec<TargetBlock>,
201    known_blocks: Vec<bool>,
202    hash_table: Vec<u32>,
203    hash_next: Vec<u32>,
204    hash_mask: u32,
205    bithash: Vec<u8>,
206    bithash_mask: u32,
207}
208
209impl BlockMatcher {
210    pub fn new(control: &ControlFile) -> Self {
211        let num_blocks = control.block_checksums.len();
212        let seq_matches = control.hash_lengths.seq_matches as u32;
213        let rsum_bytes = control.hash_lengths.rsum_bytes as u32;
214
215        let rsum_a_mask: u16 = match rsum_bytes {
216            0..=2 => 0,
217            3 => 0x00ff,
218            _ => 0xffff,
219        };
220
221        let targets: Vec<TargetBlock> = control
222            .block_checksums
223            .iter()
224            .map(|bc| TargetBlock {
225                rsum: Rsum {
226                    a: bc.rsum.a & rsum_a_mask,
227                    b: bc.rsum.b,
228                },
229                checksum: bc.checksum,
230            })
231            .collect();
232
233        let rsum_bits = rsum_bytes * 8;
234        let avail_bits = if seq_matches > 1 {
235            rsum_bits.min(16) * 2
236        } else {
237            rsum_bits
238        };
239
240        let mut hash_bits = avail_bits;
241        while hash_bits > 5 && (1u32 << (hash_bits - 1)) > num_blocks as u32 {
242            hash_bits -= 1;
243        }
244        let hash_mask = (1u32 << hash_bits) - 1;
245
246        // Safe only because the parser caps a control file at 2^26 blocks,
247        // which holds hash_bits at 27 and this sum at 30. Raising that cap
248        // past 2^28 makes this reach 32, where the shift below overflows:
249        // a panic in debug, and a mask that collapses every bucket to one
250        // in release.
251        let bithash_bits_total = (hash_bits + BITHASH_BITS).min(avail_bits);
252        let bithash_mask = (1u32 << bithash_bits_total) - 1;
253
254        let hash_func_shift = if seq_matches > 1 && avail_bits < 24 {
255            bithash_bits_total.saturating_sub(avail_bits / 2)
256        } else {
257            bithash_bits_total.saturating_sub(avail_bits - 16)
258        };
259
260        let blockshift = control.blocksize.trailing_zeros() as u8;
261
262        let mut matcher = Self {
263            blocksize: control.blocksize,
264            blockshift,
265            hash_lengths: control.hash_lengths,
266            rsum_a_mask,
267            hash_func_shift,
268            targets,
269            known_blocks: vec![false; num_blocks],
270            hash_table: vec![HASH_EMPTY; (hash_mask + 1) as usize],
271            hash_next: vec![HASH_EMPTY; num_blocks],
272            hash_mask,
273            bithash: vec![0u8; ((bithash_mask + 1) >> 3) as usize + 1],
274            bithash_mask,
275        };
276
277        for id in (0..num_blocks).rev() {
278            let h = matcher.calc_hash(id);
279            let bucket = (h & hash_mask) as usize;
280            matcher.hash_next[id] = matcher.hash_table[bucket];
281            matcher.hash_table[bucket] = id as u32;
282            let bh = (h & bithash_mask) as usize;
283            matcher.bithash[bh >> 3] |= 1 << (bh & 7);
284        }
285
286        matcher
287    }
288
289    fn calc_hash(&self, block_id: usize) -> u32 {
290        let mut h = self.targets[block_id].rsum.b as u32;
291        if self.hash_lengths.seq_matches > 1 {
292            let next_b = if block_id + 1 < self.targets.len() {
293                self.targets[block_id + 1].rsum.b as u32
294            } else {
295                0
296            };
297            h ^= next_b << self.hash_func_shift;
298        } else {
299            h ^= (self.targets[block_id].rsum.a as u32) << self.hash_func_shift;
300        }
301        h
302    }
303
304    fn remove_block_from_hash(&mut self, id: usize) {
305        let h = self.calc_hash(id);
306        let bucket = (h & self.hash_mask) as usize;
307
308        let mut prev = HASH_EMPTY;
309        let mut curr = self.hash_table[bucket];
310
311        while curr != HASH_EMPTY {
312            if curr as usize == id {
313                if prev == HASH_EMPTY {
314                    self.hash_table[bucket] = self.hash_next[id];
315                } else {
316                    self.hash_next[prev as usize] = self.hash_next[id];
317                }
318                return;
319            }
320            prev = curr;
321            curr = self.hash_next[curr as usize];
322        }
323    }
324
325    fn scan_state(&self) -> ScanState<'_> {
326        ScanState {
327            targets: &self.targets,
328            hash_table: &self.hash_table,
329            hash_next: &self.hash_next,
330            bithash: &self.bithash,
331            blocksize: self.blocksize,
332            blockshift: self.blockshift,
333            seq_matches: self.hash_lengths.seq_matches as usize,
334            checksum_bytes: self.hash_lengths.checksum_bytes as usize,
335            rsum_a_mask: self.rsum_a_mask,
336            hash_func_shift: self.hash_func_shift,
337            hash_mask: self.hash_mask,
338            bithash_mask: self.bithash_mask,
339        }
340    }
341
342    pub fn submit_blocks(&mut self, data: &[u8], block_start: usize) -> Result<bool, MatchError> {
343        let blocksize = self.blocksize;
344        let checksum_bytes = self.hash_lengths.checksum_bytes as usize;
345        let num_blocks = data.len() / blocksize;
346
347        for i in 0..num_blocks {
348            let block_data = &data[i * blocksize..(i + 1) * blocksize];
349            let block_id = block_start + i;
350
351            if block_id >= self.targets.len() {
352                break;
353            }
354
355            let checksum = calc_md4(block_data);
356            if checksum[..checksum_bytes] == self.targets[block_id].checksum[..checksum_bytes] {
357                self.known_blocks[block_id] = true;
358            } else {
359                return Ok(false);
360            }
361        }
362
363        Ok(true)
364    }
365
366    pub fn submit_source_data(&mut self, data: &[u8], offset: u64) -> Vec<(usize, usize)> {
367        let context = self.blocksize * self.hash_lengths.seq_matches as usize;
368        if data.len() < context {
369            return Vec::new();
370        }
371
372        let num_threads = std::thread::available_parallelism()
373            .map(|n| n.get())
374            .unwrap_or(1);
375
376        let min_per_thread = 16 * 1024 * 1024; // 16 MB per thread minimum
377        let scannable = data.len() - context;
378
379        let candidates = if num_threads > 1 && scannable >= min_per_thread * 2 {
380            let state = self.scan_state();
381            let actual_threads = num_threads.min(scannable / min_per_thread);
382            let chunk_size = scannable / actual_threads;
383
384            std::thread::scope(|s| {
385                let handles: Vec<_> = (0..actual_threads)
386                    .map(|i| {
387                        let start = i * chunk_size;
388                        let end = if i == actual_threads - 1 {
389                            data.len()
390                        } else {
391                            (i + 1) * chunk_size + context
392                        };
393                        let chunk = &data[start..end];
394                        let state = &state;
395                        let base = offset as usize + start;
396                        s.spawn(move || state.scan_chunk(chunk, base))
397                    })
398                    .collect();
399
400                let mut all: Vec<(usize, usize)> = Vec::new();
401                for h in handles {
402                    all.extend(h.join().unwrap());
403                }
404                all
405            })
406        } else {
407            let state = self.scan_state();
408            state.scan_chunk(data, offset as usize)
409        };
410
411        // Deduplicate: first match per block_id wins
412        let mut seen = vec![false; self.targets.len()];
413        let mut matched_blocks = Vec::new();
414        for (block_id, offset) in candidates {
415            if !seen[block_id] {
416                seen[block_id] = true;
417                self.known_blocks[block_id] = true;
418                self.remove_block_from_hash(block_id);
419                matched_blocks.push((block_id, offset));
420            }
421        }
422
423        matched_blocks
424    }
425
426    pub fn needed_block_ranges(&self) -> Vec<(usize, usize)> {
427        let mut ranges = Vec::new();
428        let mut start: Option<usize> = None;
429
430        for (i, &known) in self.known_blocks.iter().enumerate() {
431            if !known && start.is_none() {
432                start = Some(i);
433            } else if known && start.is_some() {
434                ranges.push((start.unwrap(), i));
435                start = None;
436            }
437        }
438
439        if let Some(s) = start {
440            ranges.push((s, self.known_blocks.len()));
441        }
442
443        ranges
444    }
445
446    pub fn is_block_known(&self, block_id: usize) -> bool {
447        block_id < self.known_blocks.len() && self.known_blocks[block_id]
448    }
449
450    pub fn blocks_todo(&self) -> usize {
451        self.known_blocks.iter().filter(|&&k| !k).count()
452    }
453
454    pub fn is_complete(&self) -> bool {
455        self.known_blocks.iter().all(|&k| k)
456    }
457
458    pub fn total_blocks(&self) -> usize {
459        self.targets.len()
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use crate::control::{BlockChecksum, ControlFile, HashLengths};
467
468    fn make_control(data: &[u8], blocksize: usize) -> ControlFile {
469        let num_blocks = data.len().div_ceil(blocksize);
470        let mut block_checksums = Vec::with_capacity(num_blocks);
471
472        for i in 0..num_blocks {
473            let start = i * blocksize;
474            let end = std::cmp::min(start + blocksize, data.len());
475            let mut block = data[start..end].to_vec();
476            block.resize(blocksize, 0);
477
478            let rsum = calc_rsum_block(&block);
479            let checksum = calc_md4(&block);
480
481            block_checksums.push(BlockChecksum { rsum, checksum });
482        }
483
484        ControlFile {
485            version: "0.6.2".to_string(),
486            filename: Some("test.bin".to_string()),
487            mtime: None,
488            blocksize,
489            length: data.len() as u64,
490            hash_lengths: HashLengths {
491                seq_matches: 1,
492                rsum_bytes: 4,
493                checksum_bytes: 16,
494            },
495            urls: vec!["http://example.com/test.bin".to_string()],
496            sha1: None,
497            block_checksums,
498        }
499    }
500
501    #[test]
502    fn test_matcher_new() {
503        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
504        let control = make_control(&data, 4);
505        let matcher = BlockMatcher::new(&control);
506        assert_eq!(matcher.blocks_todo(), 2);
507    }
508
509    #[test]
510    fn test_submit_source_data() {
511        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
512        let control = make_control(&data, 4);
513        let mut matcher = BlockMatcher::new(&control);
514
515        // Pad with context bytes (blocksize * seq_matches) like submit_source_file does
516        let mut padded = data.clone();
517        padded.resize(data.len() + 4, 0);
518        let got = matcher.submit_source_data(&padded, 0);
519        assert_eq!(got.len(), 3);
520        assert!(matcher.is_complete());
521    }
522
523    #[test]
524    fn test_needed_block_ranges() {
525        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
526        let control = make_control(&data, 4);
527        let matcher = BlockMatcher::new(&control);
528        let ranges = matcher.needed_block_ranges();
529        assert_eq!(ranges, vec![(0, 2)]);
530    }
531}