Antonio Mika
·
2026-10-09
1package rsync
2
3import (
4 "errors"
5 "fmt"
6 "path"
7 "slices"
8 "strings"
9)
10
11// Live flags of a file-list entry.
12const (
13 flagTopDir = 1 << 0
14 flagContentDir = 1 << 2
15 flagImpliedDir = 1 << 6
16)
17
18// fileEntry is one file-list entry.
19type fileEntry struct {
20 dirname string // "" for entries at the top
21 basename string
22 mode uint32 // unix mode including type bits
23 size int64
24 mtime int64
25 nsec uint32
26 uid uint32
27 gid uint32
28 link string
29 sum []byte // whole-file checksum with --checksum
30 flags int
31 inactive bool // removed as a duplicate
32
33 // sender only: where to read the file from
34 src string
35}
36
37func (f *fileEntry) name() string {
38 if f.dirname == "" {
39 return f.basename
40 }
41 return f.dirname + "/" + f.basename
42}
43
44func (f *fileEntry) isDir() bool { return f.mode&sIFMT == sIFDIR }
45func (f *fileEntry) isReg() bool { return f.mode&sIFMT == sIFREG }
46func (f *fileEntry) isLink() bool { return f.mode&sIFMT == sIFLNK }
47func (f *fileEntry) isDevice() bool {
48 t := f.mode & sIFMT
49 return t == sIFCHR || t == sIFBLK
50}
51func (f *fileEntry) isSpecial() bool {
52 t := f.mode & sIFMT
53 return t == sIFIFO || t == sIFSOCK
54}
55
56func newFileEntry(name string) *fileEntry {
57 dir, base := path.Split(name)
58 return &fileEntry{dirname: strings.TrimSuffix(dir, "/"), basename: base}
59}
60
61// flistCodec holds the state and options shared by the encoder and decoder.
62// Both compress each entry against the previous one.
63type flistCodec struct {
64 protocol int
65 varintFlags bool
66 safeFlist bool
67 preserveUID bool
68 preserveGID bool
69 preserveLinks bool
70 preserveDevs bool
71 preserveSpecs bool
72 preserveAtime bool
73 alwaysSum bool
74 sumLen int
75
76 last fileEntry
77 lastName string
78 lastAtime int64
79 rdevMaj uint32
80}
81
82func (fc *flistCodec) sendEntry(c *conn, f *fileEntry) error {
83 name := f.name()
84 var xflags int
85 switch {
86 case f.isDir():
87 if fc.protocol >= 30 {
88 switch {
89 case f.flags&flagContentDir != 0:
90 xflags = f.flags & flagTopDir
91 case f.flags&flagImpliedDir != 0:
92 xflags = xmitTopDir | xmitNoContentDir
93 default:
94 xflags = xmitNoContentDir
95 }
96 } else {
97 xflags = f.flags & flagTopDir
98 }
99 }
100 if f.mode == fc.last.mode && fc.lastName != "" {
101 xflags |= xmitSameMode
102 }
103 if !fc.preserveUID || (f.uid == fc.last.uid && fc.lastName != "") {
104 xflags |= xmitSameUID
105 }
106 if !fc.preserveGID || (f.gid == fc.last.gid && fc.lastName != "") {
107 xflags |= xmitSameGID
108 }
109 if f.mtime == fc.last.mtime && fc.lastName != "" {
110 xflags |= xmitSameTime
111 }
112 if f.nsec != 0 && fc.protocol >= 31 {
113 xflags |= xmitModNsec
114 }
115 // We don't track access times, so the modification time stands in.
116 atime := f.mtime
117 if fc.preserveAtime && !f.isDir() {
118 if atime == fc.lastAtime {
119 xflags |= xmitSameAtime
120 } else {
121 fc.lastAtime = atime
122 }
123 }
124 if fc.preserveSpecs && f.isSpecial() && fc.protocol < 31 {
125 if fc.protocol < 28 {
126 xflags |= xmitSameRdevPre28
127 } else {
128 xflags |= xmitSameRdevMajor
129 if fc.protocol < 30 {
130 xflags |= xmitRdevMinor8Pre30
131 }
132 }
133 }
134
135 l1 := 0
136 for l1 < len(fc.lastName) && l1 < len(name) && l1 < 255 && name[l1] == fc.lastName[l1] {
137 l1++
138 }
139 l2 := len(name) - l1
140 if l1 > 0 {
141 xflags |= xmitSameName
142 }
143 if l2 > 255 {
144 xflags |= xmitLongName
145 }
146
147 switch {
148 case fc.varintFlags:
149 if xflags == 0 {
150 xflags = xmitExtendedFlags
151 }
152 if err := c.writeVarint(int32(xflags)); err != nil {
153 return err
154 }
155 case fc.protocol >= 28:
156 if xflags == 0 && !f.isDir() {
157 xflags |= xmitTopDir
158 }
159 if xflags&0xFF00 != 0 || xflags == 0 {
160 xflags |= xmitExtendedFlags
161 if err := c.writeShortint(uint16(xflags)); err != nil {
162 return err
163 }
164 } else if err := c.writeByte(byte(xflags)); err != nil {
165 return err
166 }
167 default:
168 if xflags&0xFF == 0 {
169 if f.isDir() {
170 xflags |= xmitLongName
171 } else {
172 xflags |= xmitTopDir
173 }
174 }
175 if err := c.writeByte(byte(xflags)); err != nil {
176 return err
177 }
178 }
179 if xflags&xmitSameName != 0 {
180 if err := c.writeByte(byte(l1)); err != nil {
181 return err
182 }
183 }
184 if xflags&xmitLongName != 0 {
185 if err := c.writeVarint30(int32(l2)); err != nil {
186 return err
187 }
188 } else if err := c.writeByte(byte(l2)); err != nil {
189 return err
190 }
191 if err := c.write([]byte(name[l1:])); err != nil {
192 return err
193 }
194
195 if err := c.writeVarlong30(f.size, 3); err != nil {
196 return err
197 }
198 if xflags&xmitSameTime == 0 {
199 var err error
200 if fc.protocol >= 30 {
201 err = c.writeVarlong(f.mtime, 4)
202 } else {
203 err = c.writeInt32(int32(f.mtime))
204 }
205 if err != nil {
206 return err
207 }
208 }
209 if xflags&xmitModNsec != 0 {
210 if err := c.writeVarint(int32(f.nsec)); err != nil {
211 return err
212 }
213 }
214 if xflags&xmitSameMode == 0 {
215 if err := c.writeInt32(int32(f.mode)); err != nil {
216 return err
217 }
218 }
219 if fc.preserveAtime && !f.isDir() && xflags&xmitSameAtime == 0 {
220 if err := c.writeVarlong(atime, 4); err != nil {
221 return err
222 }
223 }
224 if fc.preserveUID && xflags&xmitSameUID == 0 {
225 if err := c.writeVarint30(int32(f.uid)); err != nil {
226 return err
227 }
228 }
229 if fc.preserveGID && xflags&xmitSameGID == 0 {
230 if err := c.writeVarint30(int32(f.gid)); err != nil {
231 return err
232 }
233 }
234 if fc.preserveSpecs && f.isSpecial() && fc.protocol < 31 {
235 // The rdev of a special file is meaningless, so it is always
236 // sent as "same major" with a zero minor.
237 switch {
238 case fc.protocol >= 30:
239 if err := c.writeVarint(0); err != nil {
240 return err
241 }
242 case fc.protocol >= 28:
243 if err := c.writeByte(0); err != nil {
244 return err
245 }
246 }
247 }
248 if fc.preserveLinks && f.isLink() {
249 if err := c.writeVarint30(int32(len(f.link))); err != nil {
250 return err
251 }
252 if err := c.write([]byte(f.link)); err != nil {
253 return err
254 }
255 }
256 if fc.alwaysSum && (f.isReg() || fc.protocol < 28) {
257 sum := f.sum
258 if !f.isReg() || len(sum) != fc.sumLen {
259 sum = make([]byte, fc.sumLen)
260 }
261 if err := c.write(sum); err != nil {
262 return err
263 }
264 }
265
266 fc.last = *f
267 fc.lastName = name
268 return nil
269}
270
271// sendEnd terminates the file list.
272func (fc *flistCodec) sendEnd(c *conn, ioError int32) error {
273 switch {
274 case fc.varintFlags:
275 if err := c.writeVarint(0); err != nil {
276 return err
277 }
278 return c.writeVarint(ioError)
279 case ioError != 0 && fc.safeFlist:
280 if err := c.writeShortint(xmitExtendedFlags | xmitIOErrorEndList); err != nil {
281 return err
282 }
283 return c.writeVarint(ioError)
284 default:
285 return c.writeByte(0)
286 }
287}
288
289// recvFlags reads the flags of the next entry. It returns ok=false at the end
290// of the list, along with any I/O error the sender reported there.
291func (fc *flistCodec) recvFlags(c *conn) (xflags int, ok bool, ioError int32, err error) {
292 if fc.varintFlags {
293 v, err := c.readVarint()
294 if err != nil {
295 return 0, false, 0, err
296 }
297 if v == 0 {
298 e, err := c.readVarint()
299 return 0, false, e, err
300 }
301 return int(v), true, 0, nil
302 }
303 b, err := c.readByte()
304 if err != nil {
305 return 0, false, 0, err
306 }
307 if b == 0 {
308 return 0, false, 0, nil
309 }
310 xflags = int(b)
311 if fc.protocol >= 28 && xflags&xmitExtendedFlags != 0 {
312 hi, err := c.readByte()
313 if err != nil {
314 return 0, false, 0, err
315 }
316 xflags |= int(hi) << 8
317 }
318 if xflags == xmitExtendedFlags|xmitIOErrorEndList {
319 if !fc.safeFlist {
320 return 0, false, 0, fmt.Errorf("invalid flist flag: %x", xflags)
321 }
322 e, err := c.readVarint()
323 return 0, false, e, err
324 }
325 return xflags, true, 0, nil
326}
327
328func (fc *flistCodec) recvEntry(c *conn, xflags int) (*fileEntry, error) {
329 var l1, l2 int
330 if xflags&xmitSameName != 0 {
331 b, err := c.readByte()
332 if err != nil {
333 return nil, err
334 }
335 l1 = int(b)
336 }
337 if xflags&xmitLongName != 0 {
338 v, err := c.readVarint30()
339 if err != nil {
340 return nil, err
341 }
342 l2 = int(v)
343 } else {
344 b, err := c.readByte()
345 if err != nil {
346 return nil, err
347 }
348 l2 = int(b)
349 }
350 if l1 > len(fc.lastName) || l2 < 0 || l2 >= maxPathLen-l1 {
351 return nil, fmt.Errorf("overflow: xflags=0x%x l1=%d l2=%d lastname=%s", xflags, l1, l2, fc.lastName)
352 }
353 nameBuf := make([]byte, l1+l2)
354 copy(nameBuf, fc.lastName[:l1])
355 if err := c.readFull(nameBuf[l1:]); err != nil {
356 return nil, err
357 }
358 rawName := string(nameBuf)
359 fc.lastName = rawName
360
361 name, err := cleanName(rawName)
362 if err != nil {
363 return nil, err
364 }
365 f := newFileEntry(name)
366 if name == "." {
367 f.dirname, f.basename = "", "."
368 }
369
370 if xflags&(xmitHlinked) != 0 && fc.protocol >= 30 && xflags&xmitHlinkFirst == 0 {
371 return nil, errors.New("hard links are not supported")
372 }
373
374 if f.size, err = c.readVarlong30(3); err != nil {
375 return nil, err
376 }
377 if f.size < 0 {
378 return nil, errors.New("offset underflow: file-length is negative")
379 }
380 if xflags&xmitSameTime != 0 {
381 f.mtime = fc.last.mtime
382 } else if fc.protocol >= 30 {
383 if f.mtime, err = c.readVarlong(4); err != nil {
384 return nil, err
385 }
386 } else {
387 v, err := c.readInt32()
388 if err != nil {
389 return nil, err
390 }
391 f.mtime = int64(uint32(v))
392 }
393 if xflags&xmitModNsec != 0 {
394 v, err := c.readVarint()
395 if err != nil {
396 return nil, err
397 }
398 if v < 0 || v > maxWireNsec {
399 return nil, fmt.Errorf("invalid modtime nsec %d", v)
400 }
401 f.nsec = uint32(v)
402 }
403 if xflags&xmitSameMode != 0 {
404 f.mode = fc.last.mode
405 } else {
406 v, err := c.readInt32()
407 if err != nil {
408 return nil, err
409 }
410 f.mode = uint32(v)
411 switch f.mode & sIFMT {
412 case sIFREG, sIFDIR, sIFLNK, sIFCHR, sIFBLK, sIFIFO, sIFSOCK:
413 default:
414 return nil, fmt.Errorf("invalid file mode 0%o for %s", f.mode, name)
415 }
416 }
417 if fc.preserveAtime && !f.isDir() && xflags&xmitSameAtime == 0 {
418 if fc.lastAtime, err = c.readVarlong(4); err != nil {
419 return nil, err
420 }
421 }
422 f.uid, f.gid = fc.last.uid, fc.last.gid
423 if fc.preserveUID && xflags&xmitSameUID == 0 {
424 v, err := c.readVarint30()
425 if err != nil {
426 return nil, err
427 }
428 f.uid = uint32(v)
429 if fc.protocol >= 30 && xflags&xmitUserNameFollows != 0 {
430 if _, err := fc.readIDName(c); err != nil {
431 return nil, err
432 }
433 }
434 }
435 if fc.preserveGID && xflags&xmitSameGID == 0 {
436 v, err := c.readVarint30()
437 if err != nil {
438 return nil, err
439 }
440 f.gid = uint32(v)
441 if fc.protocol >= 30 && xflags&xmitGroupNameFollows != 0 {
442 if _, err := fc.readIDName(c); err != nil {
443 return nil, err
444 }
445 }
446 }
447 if (fc.preserveDevs && f.isDevice()) || (fc.preserveSpecs && f.isSpecial() && fc.protocol < 31) {
448 if err := fc.skipRdev(c, xflags); err != nil {
449 return nil, err
450 }
451 f.size = 0
452 } else if f.isDevice() {
453 f.size = 0
454 }
455 if fc.preserveLinks && f.isLink() {
456 n, err := c.readVarint30()
457 if err != nil {
458 return nil, err
459 }
460 if n < 0 || n >= maxPathLen {
461 return nil, fmt.Errorf("overflow: linkname_len=%d", n)
462 }
463 link := make([]byte, n)
464 if err := c.readFull(link); err != nil {
465 return nil, err
466 }
467 f.link = string(link)
468 }
469 if xflags&xmitHlinked != 0 && fc.protocol < 30 {
470 return nil, errors.New("hard links are not supported")
471 }
472 if fc.alwaysSum && (f.isReg() || fc.protocol < 28) {
473 sum := make([]byte, fc.sumLen)
474 if err := c.readFull(sum); err != nil {
475 return nil, err
476 }
477 if f.isReg() {
478 f.sum = sum
479 }
480 }
481
482 if name == "." && !f.isDir() {
483 return nil, fmt.Errorf("rejecting non-directory transfer-root entry: %s", name)
484 }
485 if f.isDir() {
486 if fc.protocol >= 30 {
487 if xflags&xmitNoContentDir == 0 {
488 if xflags&xmitTopDir != 0 {
489 f.flags |= flagTopDir
490 }
491 f.flags |= flagContentDir
492 } else if xflags&xmitTopDir != 0 {
493 f.flags |= flagImpliedDir
494 }
495 } else if xflags&xmitTopDir != 0 {
496 f.flags |= flagTopDir | flagContentDir
497 }
498 }
499
500 fc.last = *f
501 return f, nil
502}
503
504func (fc *flistCodec) readIDName(c *conn) (string, error) {
505 n, err := c.readByte()
506 if err != nil {
507 return "", err
508 }
509 buf := make([]byte, n)
510 if err := c.readFull(buf); err != nil {
511 return "", err
512 }
513 return string(buf), nil
514}
515
516func (fc *flistCodec) skipRdev(c *conn, xflags int) error {
517 if fc.protocol < 28 {
518 if xflags&xmitSameRdevPre28 == 0 {
519 _, err := c.readInt32()
520 return err
521 }
522 return nil
523 }
524 if xflags&xmitSameRdevMajor == 0 {
525 v, err := c.readVarint30()
526 if err != nil {
527 return err
528 }
529 fc.rdevMaj = uint32(v)
530 }
531 var err error
532 switch {
533 case fc.protocol >= 30:
534 _, err = c.readVarint()
535 case xflags&xmitRdevMinor8Pre30 != 0:
536 _, err = c.readByte()
537 default:
538 _, err = c.readInt32()
539 }
540 return err
541}
542
543// cleanName validates a name received from the peer and normalizes it the
544// way clean_fname does: collapse slashes, drop "." components, refuse "..".
545func cleanName(name string) (string, error) {
546 if name == "" {
547 return "", errors.New("empty file name from sender")
548 }
549 if strings.HasPrefix(name, "/") {
550 return "", fmt.Errorf("ABORTING due to unsafe pathname from sender: %s", name)
551 }
552 var parts []string
553 for _, p := range strings.Split(name, "/") {
554 switch p {
555 case "", ".":
556 continue
557 case "..":
558 return "", fmt.Errorf("ABORTING due to unsafe pathname from sender: %s", name)
559 }
560 parts = append(parts, p)
561 }
562 if len(parts) == 0 {
563 return ".", nil
564 }
565 return strings.Join(parts, "/"), nil
566}
567
568// Name-comparison states and types for fileNameCmp.
569const (
570 sDir = iota
571 sSlash
572 sBase
573 sTrailing
574)
575
576const (
577 tPath = iota
578 tItem
579)
580
581// fileNameCmp orders entries the way rsync's f_name_cmp does. Both sides
582// sort the list independently and address files by index, so this has to
583// match byte for byte. From protocol 29 on, a directory sorts after the
584// files beside it, as if its name ended in "/".
585func fileNameCmp(f1, f2 *fileEntry, protocol int) int {
586 if f1.inactive {
587 if f2.inactive {
588 return 0
589 }
590 return -1
591 }
592 if f2.inactive {
593 return 1
594 }
595 tp := tItem
596 if protocol >= 29 {
597 tp = tPath
598 }
599
600 a := newNameCursor(f1, tp)
601 b := newNameCursor(f2, tp)
602 if f1.dirname == f2.dirname {
603 a.atBase()
604 b.atBase()
605 }
606 if a.typ != b.typ {
607 if a.typ == tPath {
608 return 1
609 }
610 return -1
611 }
612
613 for {
614 if a.empty() {
615 a.advance()
616 if !b.empty() && a.typ != b.typ {
617 if a.typ == tPath {
618 return 1
619 }
620 return -1
621 }
622 }
623 if b.empty() {
624 if b.state == sTrailing {
625 if a.empty() {
626 return 0
627 }
628 b.typ = tItem
629 } else {
630 b.advance()
631 }
632 if a.typ != b.typ {
633 if a.typ == tPath {
634 return 1
635 }
636 return -1
637 }
638 }
639 ca, cb := a.next(), b.next()
640 if ca != cb {
641 return int(ca) - int(cb)
642 }
643 if ca == 0 {
644 // Both strings ended in the trailing state.
645 return 0
646 }
647 }
648}
649
650type nameCursor struct {
651 f *fileEntry
652 tp int
653 s string
654 i int
655 state int
656 typ int
657}
658
659func newNameCursor(f *fileEntry, tp int) *nameCursor {
660 c := &nameCursor{f: f, tp: tp}
661 if f.dirname == "" {
662 c.atBase()
663 } else {
664 c.s = f.dirname
665 c.typ = tp
666 c.state = sDir
667 }
668 return c
669}
670
671func (c *nameCursor) atBase() {
672 c.typ = tItem
673 if c.f.isDir() {
674 c.typ = c.tp
675 }
676 c.s, c.i = c.f.basename, 0
677 if c.typ == tPath && c.s == "." {
678 c.typ = tItem
679 c.state = sTrailing
680 c.s = ""
681 } else {
682 c.state = sBase
683 }
684}
685
686func (c *nameCursor) empty() bool { return c.i >= len(c.s) }
687
688// advance moves past the end of the current string the way the C state
689// machine does when it hits a NUL.
690func (c *nameCursor) advance() {
691 switch c.state {
692 case sDir:
693 c.state = sSlash
694 c.s, c.i = "/", 0
695 case sSlash:
696 c.atBase()
697 case sBase:
698 c.state = sTrailing
699 if c.typ == tPath {
700 c.s, c.i = "/", 0
701 return
702 }
703 c.typ = tItem
704 case sTrailing:
705 c.typ = tItem
706 }
707}
708
709func (c *nameCursor) next() byte {
710 if c.i >= len(c.s) {
711 return 0
712 }
713 b := c.s[c.i]
714 c.i++
715 return b
716}
717
718// sortFileList sorts the list and, on the receiving side, marks duplicate
719// names inactive without disturbing the indexes (flist_sort_and_clean).
720func sortFileList(files []*fileEntry, protocol int, receiver bool) {
721 slices.SortStableFunc(files, func(a, b *fileEntry) int {
722 return fileNameCmp(a, b, protocol)
723 })
724 if !receiver {
725 return
726 }
727 seen := make(map[string]int, len(files))
728 for i, f := range files {
729 if f.inactive {
730 continue
731 }
732 name := f.name()
733 j, dup := seen[name]
734 if !dup {
735 seen[name] = i
736 continue
737 }
738 // Keep a directory over a non-directory, otherwise the first.
739 prev := files[j]
740 switch {
741 case f.isDir() && !prev.isDir():
742 prev.inactive = true
743 seen[name] = i
744 case f.isDir():
745 prev.flags |= f.flags & (flagTopDir | flagContentDir)
746 prev.flags &= f.flags | ^flagImpliedDir
747 f.inactive = true
748 default:
749 f.inactive = true
750 }
751 }
752}