main pico / pkg / rsync / match.go
Antonio Mika  ·  2026-10-09
  1package rsync
  2
  3import (
  4	"bytes"
  5	"hash"
  6)
  7
  8// matcher finds the receiver's blocks in the file being sent and emits the
  9// delta (rsync's match.c).
 10type matcher struct {
 11	head      sumHead
 12	sums      []sumBuf
 13	cs        *checksums
 14	tok       tokenSender
 15	buf       *mapFile
 16	sum       hash.Hash
 17	lastMatch int64
 18	inplace   bool // the receiver updates its basis file in place
 19
 20	tablesize uint32
 21	table     []int32
 22
 23	literal int64
 24	matched int64
 25}
 26
 27// matched sends the literal data before offset and, for i >= 0, the token of
 28// block i. i == -1 ends the file and i == -2 only flushes literal data.
 29func (m *matcher) emit(offset int64, i int32) error {
 30	n := offset - m.lastMatch
 31	var block []byte
 32	if i >= 0 {
 33		block = m.buf.ptr(offset, int(m.sums[i].len))
 34		// Copy, the literal ptr below may move the window.
 35		block = append([]byte(nil), block...)
 36	}
 37	literal := m.buf.ptr(m.lastMatch, int(n))
 38	if err := m.tok.send(i, literal, block); err != nil {
 39		return err
 40	}
 41	m.literal += n
 42	total := n
 43	if i >= 0 {
 44		m.matched += int64(m.sums[i].len)
 45		total += int64(m.sums[i].len)
 46	}
 47	for j := int64(0); j < total; j += chunkSize {
 48		n1 := min(chunkSize, total-j)
 49		m.sum.Write(m.buf.ptr(m.lastMatch+j, int(n1)))
 50	}
 51	if i >= 0 {
 52		m.lastMatch = offset + int64(m.sums[i].len)
 53	} else {
 54		m.lastMatch = offset
 55	}
 56	return nil
 57}
 58
 59func (m *matcher) buildTable() {
 60	m.tablesize = uint32(m.head.count/8)*10 + 11
 61	if m.tablesize < traditionalTableSize {
 62		m.tablesize = traditionalTableSize
 63	}
 64	m.table = make([]int32, m.tablesize)
 65	for i := range m.table {
 66		m.table[i] = -1
 67	}
 68	for i := range m.sums {
 69		var t uint32
 70		if m.tablesize == traditionalTableSize {
 71			s := m.sums[i].sum1
 72			t = (s&0xFFFF + s>>16) & 0xFFFF
 73		} else {
 74			t = m.sums[i].sum1 % m.tablesize
 75		}
 76		m.sums[i].chain = m.table[t]
 77		m.table[t] = int32(i)
 78	}
 79}
 80
 81// run sends a file of length size.
 82func (m *matcher) run(size int64) error {
 83	if size > 0 && m.head.count > 0 {
 84		m.buildTable()
 85		if err := m.hashSearch(size); err != nil {
 86			return err
 87		}
 88	} else {
 89		for j := m.lastMatch + chunkSize; j < size; j += chunkSize {
 90			if err := m.emit(j, -2); err != nil {
 91				return err
 92			}
 93		}
 94		if err := m.emit(size, -1); err != nil {
 95			return err
 96		}
 97	}
 98	return nil
 99}
100
101func (m *matcher) hashSearch(size int64) error {
102	var sum2 []byte
103	wantI := int32(0)
104	blength := int64(m.head.blength)
105	k := min(size, blength)
106	sum := checksum1(m.buf.ptr(0, int(k)))
107	s1, s2 := sum&0xFFFF, sum>>16
108	var offset, alignedOffset int64
109	alignedI := int32(0)
110	end := size + 1 - int64(m.sums[len(m.sums)-1].len)
111	s2len := int(m.head.s2length)
112
113	for {
114		doneCsum2 := false
115		var hashEntry uint32
116		if m.tablesize == traditionalTableSize {
117			hashEntry = (s1 + s2) & 0xFFFF
118			sum = s1&0xffff | s2<<16
119		} else {
120			sum = s1&0xffff | s2<<16
121			hashEntry = sum % m.tablesize
122		}
123		i := m.table[hashEntry]
124		prev := &m.table[hashEntry]
125		chainLen := 0
126		for ; i >= 0; i = m.sums[i].chain {
127			// An in-place receiver can only use blocks at or after the
128			// current offset, or ones it has left untouched.
129			if m.inplace && m.sums[i].offset < offset && !m.sums[i].same {
130				*prev = m.sums[i].chain
131				continue
132			}
133			prev = &m.sums[i].chain
134			if sum != m.sums[i].sum1 {
135				continue
136			}
137			chainLen++
138			if chainLen > maxChainLen {
139				break
140			}
141			l := int32(min(blength, size-offset))
142			if l != m.sums[i].len {
143				continue
144			}
145			if !doneCsum2 {
146				sum2 = m.cs.blockSum(m.buf.ptr(offset, int(l)), sum2[:0])
147				doneCsum2 = true
148			}
149			if !bytes.Equal(sum2[:s2len], m.sums[i].sum2) {
150				continue
151			}
152
153			if m.inplace {
154				for alignedOffset < offset {
155					alignedOffset += blength
156					alignedI++
157				}
158				if (offset == alignedOffset || (sum == 0 && int64(l) == blength && alignedOffset+int64(l) <= size)) && alignedI < m.head.count {
159					ok := true
160					if i != alignedI {
161						a := &m.sums[alignedI]
162						if sum != a.sum1 || l != a.len || !bytes.Equal(sum2[:s2len], a.sum2) {
163							ok = false
164						} else {
165							i = alignedI
166						}
167					}
168					if ok && offset != alignedOffset {
169						backup := max(alignedOffset-m.lastMatch, 0)
170						block := m.buf.ptr(alignedOffset-backup, int(int64(l)+backup))[backup:]
171						if checksum1(block) != m.sums[i].sum1 ||
172							!bytes.Equal(m.cs.blockSum(block, nil)[:s2len], m.sums[i].sum2) {
173							ok = false
174						} else {
175							offset = alignedOffset
176						}
177					}
178					if ok {
179						m.sums[i].same = true
180						wantI = i
181					}
182				}
183			}
184
185			if i != wantI && wantI < m.head.count &&
186				(!m.inplace || m.sums[wantI].offset >= offset || m.sums[wantI].same) &&
187				sum == m.sums[wantI].sum1 && l == m.sums[wantI].len &&
188				bytes.Equal(sum2[:s2len], m.sums[wantI].sum2) {
189				// Prefer the adjacent block so the token runs compress.
190				i = wantI
191			}
192			wantI = i + 1
193
194			if err := m.emit(offset, i); err != nil {
195				return err
196			}
197			offset += int64(m.sums[i].len) - 1
198			k = min(blength, size-offset)
199			sum = checksum1(m.buf.ptr(offset, int(k)))
200			s1, s2 = sum&0xFFFF, sum>>16
201			break
202		}
203
204		backup := max(offset-m.lastMatch, 0)
205		more := offset+k < size
206		n := k + backup
207		if more {
208			n++
209		}
210		window := m.buf.ptr(offset-backup, int(n))[backup:]
211		first := uint32(int8(window[0]))
212		s1 -= first
213		s2 -= uint32(k) * first
214		if more {
215			s1 += uint32(int8(window[k]))
216			s2 += s1
217		} else {
218			k--
219		}
220
221		if backup >= blength+chunkSize && end-offset > chunkSize {
222			if err := m.emit(offset-blength, -2); err != nil {
223				return err
224			}
225		}
226
227		offset++
228		if offset >= end {
229			break
230		}
231	}
232	return m.emit(size, -1)
233}