main pico / pkg / rsync / checksum.go
Antonio Mika  ·  2026-10-09
  1package rsync
  2
  3import (
  4	"crypto/md5"
  5	"encoding/binary"
  6	"fmt"
  7	"hash"
  8
  9	"github.com/cespare/xxhash/v2"
 10	"github.com/mmcloughlin/md4"
 11)
 12
 13// csumType identifies a strong checksum. md4Old is the implied checksum of
 14// protocols 27-29, which seeds the whole-file sum; md4 is the same digest
 15// when negotiated by name in protocol 30+.
 16type csumType int
 17
 18const (
 19	csumNone csumType = iota
 20	csumMD4Old
 21	csumMD4
 22	csumMD5
 23	csumXXH64
 24)
 25
 26// checksumNames is our preference order for negotiation. It matches rsync's
 27// table order so both sides converge on the same choice.
 28var checksumNames = []struct {
 29	name string
 30	typ  csumType
 31}{
 32	{"xxh64", csumXXH64},
 33	{"xxhash", csumXXH64},
 34	{"md5", csumMD5},
 35	{"md4", csumMD4},
 36	{"none", csumNone},
 37}
 38
 39func parseChecksumName(name string) (csumType, error) {
 40	for _, c := range checksumNames {
 41		if c.name == name {
 42			return c.typ, nil
 43		}
 44	}
 45	return 0, fmt.Errorf("unknown checksum name: %s", name)
 46}
 47
 48func (t csumType) size() int {
 49	switch t {
 50	case csumNone:
 51		return 1
 52	case csumXXH64:
 53		return 8
 54	default:
 55		return 16
 56	}
 57}
 58
 59// checksum1 is rsync's rolling weak checksum. The bytes are treated as
 60// signed chars, as rsync's C implementation does.
 61func checksum1(buf []byte) uint32 {
 62	var s1, s2 uint32
 63	i := 0
 64	for ; i < len(buf)-4; i += 4 {
 65		b0 := uint32(int8(buf[i]))
 66		b1 := uint32(int8(buf[i+1]))
 67		b2 := uint32(int8(buf[i+2]))
 68		b3 := uint32(int8(buf[i+3]))
 69		s2 += 4*(s1+b0) + 3*b1 + 2*b2 + b3
 70		s1 += b0 + b1 + b2 + b3
 71	}
 72	for ; i < len(buf); i++ {
 73		s1 += uint32(int8(buf[i]))
 74		s2 += s1
 75	}
 76	return (s1 & 0xffff) + (s2 << 16)
 77}
 78
 79// checksums holds the negotiated digests and the session seed.
 80type checksums struct {
 81	xfer            csumType
 82	file            csumType
 83	seed            int32
 84	properSeedOrder bool
 85}
 86
 87func (c *checksums) seedBytes() []byte {
 88	var b [4]byte
 89	binary.LittleEndian.PutUint32(b[:], uint32(c.seed))
 90	return b[:]
 91}
 92
 93// blockSum is rsync's get_checksum2: the strong checksum of one block.
 94func (c *checksums) blockSum(buf []byte, out []byte) []byte {
 95	switch c.xfer {
 96	case csumXXH64:
 97		d := xxhash.NewWithSeed(uint64(int64(c.seed)))
 98		_, _ = d.Write(buf)
 99		return binary.LittleEndian.AppendUint64(out, d.Sum64())
100	case csumMD5:
101		h := md5.New()
102		if c.properSeedOrder {
103			if c.seed != 0 {
104				h.Write(c.seedBytes())
105			}
106			h.Write(buf)
107		} else {
108			h.Write(buf)
109			if c.seed != 0 {
110				h.Write(c.seedBytes())
111			}
112		}
113		return h.Sum(out)
114	case csumMD4, csumMD4Old:
115		h := md4.New()
116		h.Write(buf)
117		if c.seed != 0 {
118			h.Write(c.seedBytes())
119		}
120		return h.Sum(out)
121	default:
122		return append(out, 0)
123	}
124}
125
126// newXferSum starts the whole-file checksum that follows each transferred
127// file (rsync's sum_init with the transfer digest).
128func (c *checksums) newXferSum() hash.Hash {
129	switch c.xfer {
130	case csumMD4Old:
131		h := md4.New()
132		h.Write(c.seedBytes())
133		return h
134	default:
135		return newPlainHash(c.xfer)
136	}
137}
138
139// newFileSum starts the --checksum digest of a whole file.
140func (c *checksums) newFileSum() hash.Hash {
141	return newPlainHash(c.file)
142}
143
144func newPlainHash(t csumType) hash.Hash {
145	switch t {
146	case csumMD4, csumMD4Old:
147		return md4.New()
148	case csumMD5:
149		return md5.New()
150	case csumXXH64:
151		return &xxh64LE{d: xxhash.NewWithSeed(0)}
152	default:
153		return &noneHash{}
154	}
155}
156
157// xxh64LE emits the digest little-endian, as rsync's SIVAL64 does.
158type xxh64LE struct{ d *xxhash.Digest }
159
160func (x *xxh64LE) Write(p []byte) (int, error) { return x.d.Write(p) }
161func (x *xxh64LE) Sum(b []byte) []byte         { return binary.LittleEndian.AppendUint64(b, x.d.Sum64()) }
162func (x *xxh64LE) Reset()                      { x.d.Reset() }
163func (x *xxh64LE) Size() int                   { return 8 }
164func (x *xxh64LE) BlockSize() int              { return 32 }
165
166type noneHash struct{}
167
168func (noneHash) Write(p []byte) (int, error) { return len(p), nil }
169func (noneHash) Sum(b []byte) []byte         { return append(b, 0) }
170func (noneHash) Reset()                      {}
171func (noneHash) Size() int                   { return 1 }
172func (noneHash) BlockSize() int              { return 1 }