Antonio Mika
·
2026-10-10
1package rsync
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "path"
8 "slices"
9 "strings"
10 "testing"
11 "time"
12)
13
14type loopback struct {
15 bytes.Buffer
16}
17
18func newTestConn(t *testing.T, protocol int) (*conn, *loopback) {
19 var buf loopback
20 c := newConn(&buf)
21 c.protocol = protocol
22 t.Cleanup(func() { _ = c.close() })
23 return c, &buf
24}
25
26func TestVarintRoundTrip(t *testing.T) {
27 values := []int32{0, 1, 0x7f, 0x80, 0xff, 0x3fff, 0x4000, 0x1fffff, 0x200000, 0x0fffffff, 0x10000000, 0x7fffffff, -1, -2, -0x80000000}
28 for _, v := range values {
29 c, _ := newTestConn(t, 31)
30 if err := c.writeVarint(v); err != nil {
31 t.Fatal(err)
32 }
33 if err := c.flush(); err != nil {
34 t.Fatal(err)
35 }
36 got, err := c.readVarint()
37 if err != nil || got != v {
38 t.Errorf("varint %d: got %d, %v", v, got, err)
39 }
40 }
41 if got := appendVarint(nil, 0x80); !bytes.Equal(got, []byte{0x80, 0x80}) {
42 t.Errorf("varint 0x80 encodes as % x", got)
43 }
44}
45
46func TestVarlongRoundTrip(t *testing.T) {
47 values := []int64{0, 1, 0xffffff, 0x1000000, 0x7fffffff, 0x80000000, 1 << 40, 1<<62 + 12345, -1}
48 for _, min := range []int{3, 4} {
49 for _, v := range values {
50 c, _ := newTestConn(t, 31)
51 if err := c.writeVarlong(v, min); err != nil {
52 t.Fatal(err)
53 }
54 if err := c.flush(); err != nil {
55 t.Fatal(err)
56 }
57 got, err := c.readVarlong(min)
58 if err != nil || got != v {
59 t.Errorf("varlong(%d) %d: got %d, %v", min, v, got, err)
60 }
61 }
62 }
63}
64
65func TestLongintRoundTrip(t *testing.T) {
66 for _, v := range []int64{0, 0x7fffffff, 0x80000000, 1 << 40, -5} {
67 c, _ := newTestConn(t, 29)
68 if err := c.writeLongint(v); err != nil {
69 t.Fatal(err)
70 }
71 if err := c.flush(); err != nil {
72 t.Fatal(err)
73 }
74 got, err := c.readLongint()
75 if err != nil || got != v {
76 t.Errorf("longint %d: got %d, %v", v, got, err)
77 }
78 }
79}
80
81func TestNdxRoundTrip(t *testing.T) {
82 seq := []int32{0, 1, 2, 5, 300, 299, 40000, 70000, 3, ndxDone, ndxDelStats, ndxFlistEOF, 7, ndxDone}
83 for _, proto := range []int{29, 30} {
84 c, _ := newTestConn(t, proto)
85 for _, v := range seq {
86 if err := c.writeNdx(v); err != nil {
87 t.Fatal(err)
88 }
89 }
90 if err := c.flush(); err != nil {
91 t.Fatal(err)
92 }
93 for _, v := range seq {
94 got, err := c.readNdx()
95 if err != nil || got != v {
96 t.Fatalf("protocol %d: ndx %d: got %d, %v", proto, v, got, err)
97 }
98 }
99 }
100}
101
102func TestMultiplexedMessages(t *testing.T) {
103 c, buf := newTestConn(t, 31)
104 if err := c.w.setMultiplex(true); err != nil {
105 t.Fatal(err)
106 }
107 _ = c.writeInt32(42)
108 _ = c.writeMsg(msgInfo, []byte("hello\n"))
109 _ = c.writeInt32(43)
110 if err := c.flush(); err != nil {
111 t.Fatal(err)
112 }
113
114 r, _ := newTestConn(t, 31)
115 r.r.r.Reset(bytes.NewReader(buf.Bytes()))
116 r.r.multiplex = true
117 var msgs []string
118 r.r.onMsg = func(tag byte, data []byte) error {
119 msgs = append(msgs, string(data))
120 return nil
121 }
122 a, err := r.readInt32()
123 if err != nil {
124 t.Fatal(err)
125 }
126 b, err := r.readInt32()
127 if err != nil {
128 t.Fatal(err)
129 }
130 if a != 42 || b != 43 || !slices.Equal(msgs, []string{"hello\n"}) {
131 t.Fatalf("got %d %d %q", a, b, msgs)
132 }
133 if _, err := r.readByte(); err != io.ErrUnexpectedEOF {
134 t.Fatalf("want EOF, got %v", err)
135 }
136}
137
138// A generator can still be writing when the session gives up and closes the
139// writer; it has to get an error instead of waiting on a queue nobody drains.
140func TestMuxWriterAfterClose(t *testing.T) {
141 m := newMuxWriter(io.Discard)
142 if err := m.Close(); err != nil {
143 t.Fatal(err)
144 }
145 done := make(chan error, 1)
146 go func() {
147 chunk := make([]byte, muxBatchSize)
148 for range 2 * muxMaxQueued / muxBatchSize {
149 if _, err := m.Write(chunk); err != nil {
150 done <- err
151 return
152 }
153 }
154 done <- m.Flush()
155 }()
156 select {
157 case err := <-done:
158 if err == nil {
159 t.Fatal("writes after Close succeeded")
160 }
161 case <-time.After(5 * time.Second):
162 t.Fatal("write after Close blocked")
163 }
164}
165
166func TestChecksum1(t *testing.T) {
167 // Bytes above 0x7f count as negative, as in rsync's C code.
168 if got := checksum1([]byte{0xff, 1, 2, 3, 4, 5}); got != 0x001d000e {
169 t.Errorf("checksum1 = %#x", got)
170 }
171 if got := checksum1(nil); got != 0 {
172 t.Errorf("checksum1(nil) = %#x", got)
173 }
174}
175
176func TestSumSizes(t *testing.T) {
177 tests := []struct {
178 length int64
179 proto int
180 want sumHead
181 }{
182 {0, 31, sumHead{0, 700, 2, 0}},
183 {1, 31, sumHead{1, 700, 2, 1}},
184 {1_000_000, 31, sumHead{1000, 1000, 2, 0}},
185 {100 << 20, 31, sumHead{10240, 10240, 3, 0}},
186 {100<<20 + 1, 29, sumHead{10241, 10240, 3, 1}},
187 }
188 for _, tt := range tests {
189 got, err := sumSizesSqroot(tt.length, tt.proto, 0, shortSumLength, 16)
190 if err != nil || got != tt.want {
191 t.Errorf("sumSizes(%d, %d) = %+v, %v; want %+v", tt.length, tt.proto, got, err, tt.want)
192 }
193 }
194 got, _ := sumSizesSqroot(1_000_000, 31, 0, sumLength, 8)
195 if got.s2length != 8 {
196 t.Errorf("full checksum length capped at digest size: got %d", got.s2length)
197 }
198}
199
200func TestFileNameOrder(t *testing.T) {
201 mk := func(name string, dir bool) *fileEntry {
202 f := newFileEntry(name)
203 if name == "." {
204 f.dirname, f.basename = "", "."
205 }
206 f.mode = sIFREG
207 if dir {
208 f.mode = sIFDIR
209 }
210 return f
211 }
212 entries := func() []*fileEntry {
213 return []*fileEntry{
214 mk("b.txt", false), mk("a/x", false), mk("a", true), mk(".", true),
215 mk("a.b", false), mk("a-b", false), mk("a/y/z", false), mk("a/y", true), mk("a/w", false),
216 }
217 }
218 names := func(fs []*fileEntry) []string {
219 var out []string
220 for _, f := range fs {
221 out = append(out, f.name())
222 }
223 return out
224 }
225
226 modern := entries()
227 sortFileList(modern, 31, false)
228 want := []string{".", "a-b", "a.b", "b.txt", "a", "a/w", "a/x", "a/y", "a/y/z"}
229 if got := names(modern); !slices.Equal(got, want) {
230 t.Errorf("protocol 31 order:\n got %q\nwant %q", got, want)
231 }
232
233 old := entries()
234 sortFileList(old, 28, false)
235 want = []string{".", "a", "a-b", "a.b", "a/w", "a/x", "a/y", "a/y/z", "b.txt"}
236 if got := names(old); !slices.Equal(got, want) {
237 t.Errorf("protocol 28 order:\n got %q\nwant %q", got, want)
238 }
239}
240
241func TestDuplicateNames(t *testing.T) {
242 a := newFileEntry("x")
243 a.mode = sIFREG
244 b := newFileEntry("x")
245 b.mode = sIFDIR
246 c := newFileEntry("x")
247 c.mode = sIFREG
248 files := []*fileEntry{a, b, c}
249 sortFileList(files, 31, true)
250 var active []*fileEntry
251 for _, f := range files {
252 if !f.inactive {
253 active = append(active, f)
254 }
255 }
256 if len(active) != 1 || !active[0].isDir() {
257 t.Fatalf("want only the directory kept, got %d entries", len(active))
258 }
259}
260
261func TestWildmatch(t *testing.T) {
262 tests := []struct {
263 pattern, text string
264 want bool
265 }{
266 {"foo", "foo", true},
267 {"foo", "bar", false},
268 {"???", "foo", true},
269 {"??", "foo", false},
270 {"*", "foo", true},
271 {"f*", "foo", true},
272 {"*f", "foo", false},
273 {"*ob*a*r*", "foobar", true},
274 {"*ab", "aaaaaaabababab", true},
275 {`foo\*`, "foo*", true},
276 {`foo\*bar`, "foobar", false},
277 {"*[al]?", "ball", true},
278 {"[ten]", "ten", false},
279 {"**[!te]", "ten", true},
280 {"**[!ten]", "ten", false},
281 {"t[a-g]n", "ten", true},
282 {"t[!a-g]n", "ten", false},
283 {"t[^a-g]n", "ton", true},
284 {"a[]]b", "a]b", true},
285 {"[[:digit:]]x", "7x", true},
286 {"[[:alpha:]]x", "7x", false},
287 {"[[:bogus:]]", "a", false},
288 {"*", "a/b", false},
289 {"**", "a/b", true},
290 {"a/*", "a/b/c", false},
291 {"a/**", "a/b/c", true},
292 {"**/c", "a/b/c", true},
293 {"a?b", "a/b", false},
294 {"[a/]b", "/b", false},
295 }
296 for _, tt := range tests {
297 if got := wildmatch(tt.pattern, tt.text); got != tt.want {
298 t.Errorf("wildmatch(%q, %q) = %v, want %v", tt.pattern, tt.text, got, tt.want)
299 }
300 }
301}
302
303func mapReader(files map[string]string) func(string) ([]byte, error) {
304 return func(name string) ([]byte, error) {
305 data, ok := files[name]
306 if !ok {
307 return nil, nil
308 }
309 return []byte(data), nil
310 }
311}
312
313func TestPerDirMergeRules(t *testing.T) {
314 files := map[string]string{
315 ".rsync-filter": "- /site/scanned.txt\n- /outside.txt\n",
316 "site/.rsync-filter": "# comment\n; comment\n- *.log\nP protected.txt\nH hidden.txt\nexclude long.txt\r\n",
317 "site/sub/.rsync-filter": "+ important.log\n- /anchored.txt\n",
318 "site/clear/.rsync-filter": "!\n- z\n",
319 "site/.cvsignore": "a.o b.o\n",
320 "site/sub/.cvsignore": "! c.o",
321 }
322 tests := []struct {
323 otherSide int
324 name string
325 want int
326 }{
327 {ruleSenderSide, "a.log", -1},
328 {ruleSenderSide, "sub/x.log", -1},
329 {ruleSenderSide, "sub/important.log", 1},
330 {ruleSenderSide, "long.txt", -1},
331 {ruleSenderSide, "scanned.txt", -1},
332 {ruleSenderSide, "outside.txt", 0},
333 {ruleSenderSide, "sub/anchored.txt", -1},
334 {ruleSenderSide, "anchored.txt", 0},
335 {ruleSenderSide, "sub/deeper/anchored.txt", 0},
336 {ruleSenderSide, "clear/a.log", 0},
337 {ruleSenderSide, "clear/z", -1},
338 {ruleSenderSide, ".rsync-filter", -1},
339 {ruleSenderSide, "a.o", -1},
340 {ruleSenderSide, "sub/a.o", 0},
341 {ruleSenderSide, "sub/c.o", -1},
342 {ruleSenderSide, "protected.txt", -1},
343 {ruleSenderSide, "hidden.txt", 0},
344 {ruleReceiverSide, "protected.txt", 0},
345 {ruleReceiverSide, "hidden.txt", -1},
346 }
347 for _, tt := range tests {
348 l := filterList{otherSide: tt.otherSide}
349 for _, rule := range []string{":e /.rsync-filter", ": .rsync-filter", ":C"} {
350 if err := l.parse(rule, false); err != nil {
351 t.Fatalf("parse %q: %v", rule, err)
352 }
353 }
354 if len(l.merges) != 2 {
355 t.Fatalf("duplicate merge rule kept: %d merges", len(l.merges))
356 }
357 sc, err := l.newScopeTree("site", "site", mapReader(files)).scope(path.Dir(tt.name))
358 if err != nil {
359 t.Fatal(err)
360 }
361 if got := l.check(tt.name, false, sc); got != tt.want {
362 t.Errorf("otherSide=%d check(%q) = %d, want %d", tt.otherSide, tt.name, got, tt.want)
363 }
364 }
365}
366
367func TestPerDirMergeDeleteExcluded(t *testing.T) {
368 l := filterList{otherSide: ruleSenderSide, deleteExcluded: true}
369 if err := l.parse(": .rsync-filter", false); err != nil {
370 t.Fatal(err)
371 }
372 files := map[string]string{"site/.rsync-filter": "- excluded.txt\nP protected.txt\n"}
373 sc, err := l.newScopeTree("site", "site", mapReader(files)).scope(".")
374 if err != nil {
375 t.Fatal(err)
376 }
377 if l.excluded("excluded.txt", false, sc) {
378 t.Error("--delete-excluded should let the receiver delete excluded files")
379 }
380 if !l.excluded("protected.txt", false, sc) {
381 t.Error("protect rules still apply with --delete-excluded")
382 }
383}
384
385func TestPerDirMergeNoInherit(t *testing.T) {
386 l := filterList{otherSide: ruleSenderSide}
387 if err := l.parse(":n /.rsync-filter", false); err != nil {
388 t.Fatal(err)
389 }
390 files := map[string]string{
391 ".rsync-filter": "- *.root\n",
392 "site/.rsync-filter": "- *.tmp\n",
393 }
394 scopes := l.newScopeTree("site", "site", mapReader(files))
395 for _, tt := range []struct {
396 name string
397 want int
398 }{{"x.tmp", -1}, {"sub/x.tmp", 0}, {"x.root", 0}} {
399 sc, err := scopes.scope(path.Dir(tt.name))
400 if err != nil {
401 t.Fatal(err)
402 }
403 if got := l.check(tt.name, false, sc); got != tt.want {
404 t.Errorf("check(%q) = %d, want %d", tt.name, got, tt.want)
405 }
406 }
407}
408
409func TestPerDirMergeErrors(t *testing.T) {
410 for _, rule := range []string{". rules", "merge rules", ":! .rsync-filter", ": x/"} {
411 var l filterList
412 if err := l.parse(rule, false); err == nil {
413 t.Errorf("parse %q: want an error", rule)
414 }
415 }
416 for _, contents := range []string{
417 "merge other\n",
418 ": .nested\n",
419 "dir-merge .nested\n",
420 "bogus\n",
421 "-foo\n",
422 "! trailing\n",
423 "- " + strings.Repeat("x", maxPathLen) + "\n",
424 } {
425 l := filterList{otherSide: ruleSenderSide}
426 if err := l.parse(": .rsync-filter", false); err != nil {
427 t.Fatal(err)
428 }
429 files := map[string]string{"site/.rsync-filter": contents}
430 _, err := l.newScopeTree("site", "site", mapReader(files)).scope(".")
431 var mergeErr *mergeFileError
432 if !errors.As(err, &mergeErr) {
433 t.Errorf("merge file %q: want a mergeFileError, got %v", contents, err)
434 }
435 }
436
437 l := filterList{otherSide: ruleSenderSide}
438 if err := l.parse(": .rsync-filter", false); err != nil {
439 t.Fatal(err)
440 }
441 readErr := errors.New("backend down")
442 _, err := l.newScopeTree("site", "site", func(string) ([]byte, error) { return nil, readErr }).scope(".")
443 if !errors.Is(err, readErr) {
444 t.Errorf("read error not reported: %v", err)
445 }
446
447 for _, pattern := range []string{". rules", ": rules"} {
448 var l filterList
449 if err := l.parse(pattern, true); err != nil {
450 t.Fatal(err)
451 }
452 if l.check(pattern, false, nil) != -1 {
453 t.Errorf("protocol 28 pattern %q should remain an exclusion", pattern)
454 }
455 }
456}
457
458func TestFilterRules(t *testing.T) {
459 var l filterList
460 for _, rule := range []string{"+ keep.log", "- *.log", "- /build/", "- node_modules/", "- docs/*.tmp", "-! *.html"} {
461 if err := l.parse(rule, false); err != nil {
462 t.Fatalf("parse %q: %v", rule, err)
463 }
464 }
465 tests := []struct {
466 name string
467 isDir bool
468 want int
469 }{
470 {"keep.log", false, 1},
471 {"sub/keep.log", false, 1},
472 {"x.log", false, -1},
473 {"sub/x.log", false, -1},
474 {"build", true, -1},
475 {"build", false, -1}, // "-! *.html" excludes anything not ending in .html
476 {"sub/build", true, -1},
477 {"src/node_modules", true, -1},
478 {"docs/a.tmp", false, -1},
479 {"index.html", false, 0},
480 }
481 for _, tt := range tests {
482 if got := l.check(tt.name, tt.isDir, nil); got != tt.want {
483 t.Errorf("check(%q, dir=%v) = %d, want %d", tt.name, tt.isDir, got, tt.want)
484 }
485 }
486
487 var old filterList
488 _ = old.parse("*.o", true)
489 _ = old.parse("+ keep.o", true)
490 if old.check("x.o", false, nil) != -1 {
491 t.Error("protocol 28 bare pattern is an exclude")
492 }
493 _ = old.parse("!", true)
494 if len(old.rules) != 0 {
495 t.Error("! clears the list")
496 }
497}