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