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