main pico / pkg / rsync / flist.go
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}