main pico / pkg / rsync / sender.go
Antonio Mika  ·  2026-10-10
  1package rsync
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"io/fs"
  8	"path"
  9	"slices"
 10	"strings"
 11	"time"
 12)
 13
 14const (
 15	nameNormal = iota
 16	nameDotDir
 17)
 18
 19// buildFileList turns the sender's source arguments into file-list entries
 20// (send_file_list). Errors about individual sources are reported to the
 21// client and recorded in s.ioError.
 22func (s *session) buildFileList(args []string) ([]*fileEntry, error) {
 23	var files []*fileEntry
 24	flags := 0
 25	if s.opts.recurse {
 26		flags = flagContentDir
 27	}
 28	for _, arg := range args {
 29		fbuf := strings.TrimLeft(arg, "/")
 30		nameType := nameNormal
 31		switch {
 32		case fbuf == "" || strings.HasSuffix(fbuf, "/"):
 33			if fbuf == "./" {
 34				fbuf = "."
 35			} else {
 36				fbuf += "."
 37			}
 38			nameType = nameDotDir
 39		case fbuf == "." || strings.HasSuffix(fbuf, "/."):
 40			nameType = nameDotDir
 41		}
 42		dir, fn := "", fbuf
 43		if i := strings.LastIndexByte(fbuf, '/'); i >= 0 {
 44			dir, fn = fbuf[:i], fbuf[i+1:]
 45		}
 46		if fn == "" {
 47			fn = "."
 48			nameType = nameDotDir
 49		}
 50		dir, err := cleanSourcePath(dir)
 51		if err != nil {
 52			return nil, err
 53		}
 54		if fn == ".." || strings.Contains(fn, "\x00") {
 55			return nil, fmt.Errorf("invalid source path: %s", arg)
 56		}
 57		src := dir
 58		if fn != "." {
 59			src = path.Join(dir, fn)
 60		}
 61
 62		info, err := s.fs.Stat(src)
 63		if err != nil {
 64			if errors.Is(err, fs.ErrNotExist) {
 65				s.xferError("link_stat %q failed: No such file or directory (2)", "/"+src)
 66			} else {
 67				s.ioError |= ioErrGeneral
 68				s.xferError("link_stat %q failed: %v", "/"+src, err)
 69			}
 70			continue
 71		}
 72		if nameType != nameDotDir && s.filters.excluded(fn, info.IsDir, nil) {
 73			continue
 74		}
 75		if info.IsDir && !s.opts.xferDirs {
 76			s.info("skipping directory %s", fn)
 77			continue
 78		}
 79
 80		if s.opts.recurse || (s.opts.xferDirs && nameType != nameNormal) {
 81			top := s.newEntry(fn, src, info, flagTopDir|flagContentDir|flags)
 82			files = append(files, top)
 83			if info.IsDir {
 84				children, err := s.listDir(fn, src, dir, flags)
 85				var mergeErr *mergeFileError
 86				if errors.As(err, &mergeErr) {
 87					return nil, err
 88				}
 89				if err != nil {
 90					s.ioError |= ioErrGeneral
 91					s.xferError("opendir %q failed: %v", "/"+src, err)
 92					continue
 93				}
 94				files = append(files, children...)
 95			}
 96		} else {
 97			files = append(files, s.newEntry(fn, src, info, flags))
 98		}
 99	}
100	return files, nil
101}
102
103// listDir returns the entries below a source directory, with names under
104// prefix. Directories missing from the listing are implied by their
105// contents, as object stores don't keep them. root is the storage path of
106// the transfer root.
107func (s *session) listDir(prefix, src, root string, flags int) ([]*fileEntry, error) {
108	entries, err := s.fs.ReadDir(src, s.opts.recurse)
109	if err != nil {
110		return nil, err
111	}
112	join := func(name string) string {
113		if prefix == "." {
114			return name
115		}
116		return prefix + "/" + name
117	}
118
119	byName := make(map[string]FileInfo, len(entries))
120	for _, e := range entries {
121		name, err := cleanSourcePath(e.Name)
122		if err != nil || name == "" {
123			continue
124		}
125		if !s.opts.recurse && strings.Contains(name, "/") {
126			continue
127		}
128		e.Name = name
129		byName[name] = e
130		if s.opts.recurse {
131			for d := path.Dir(name); d != "."; d = path.Dir(d) {
132				if _, ok := byName[d]; ok {
133					break
134				}
135				byName[d] = FileInfo{Name: d, IsDir: true, ModTime: e.ModTime}
136			}
137		}
138	}
139	names := make([]string, 0, len(byName))
140	regular := make(map[string]bool, len(byName))
141	for name, e := range byName {
142		names = append(names, name)
143		regular[name] = !e.IsDir
144	}
145	slices.Sort(names)
146	scopes := s.filters.newScopeTree(root, src, s.mergeReader(src, regular))
147
148	var files []*fileEntry
149	var excludedDirs []string
150	for _, name := range names {
151		e := byName[name]
152		if slices.ContainsFunc(excludedDirs, func(d string) bool { return strings.HasPrefix(name, d+"/") }) {
153			continue
154		}
155		full := join(name)
156		sc, err := scopes.scope(path.Dir(name))
157		if err != nil {
158			return nil, err
159		}
160		if s.filters.excluded(full, e.IsDir, sc) {
161			if e.IsDir {
162				excludedDirs = append(excludedDirs, name)
163			}
164			continue
165		}
166		files = append(files, s.newEntry(full, path.Join(src, name), e, flags))
167	}
168	return files, nil
169}
170
171func (s *session) newEntry(name, src string, info FileInfo, flags int) *fileEntry {
172	f := newFileEntry(name)
173	if name == "." {
174		f.dirname, f.basename = "", "."
175	}
176	f.src = src
177	f.mtime = info.ModTime.Unix()
178	f.nsec = uint32(info.ModTime.Nanosecond())
179	if info.ModTime.IsZero() {
180		f.mtime, f.nsec = time.Now().Unix(), 0
181	}
182	if info.IsDir {
183		f.mode = sIFDIR | 0o755
184		f.size = 4096
185		f.flags = flags
186	} else {
187		f.mode = sIFREG | 0o644
188		f.size = info.Size
189	}
190	return f
191}
192
193// cleanSourcePath normalizes a path inside the storage, refusing anything
194// that would leave it.
195func cleanSourcePath(p string) (string, error) {
196	var parts []string
197	for _, c := range strings.Split(p, "/") {
198		switch c {
199		case "", ".":
200		case "..":
201			return "", fmt.Errorf("path escapes the transfer root: %s", p)
202		default:
203			parts = append(parts, c)
204		}
205	}
206	return strings.Join(parts, "/"), nil
207}
208
209// runSender is the server side of a download (do_server_sender).
210func (s *session) runSender(args []string) error {
211	start := time.Now()
212	if err := s.recvFilterList(); err != nil {
213		return err
214	}
215
216	srcs := args
217	if len(srcs) > 0 {
218		// The first argument is the "." the client uses as a placeholder
219		// for the working directory.
220		srcs = srcs[1:]
221	}
222	if len(srcs) == 0 && (s.opts.recurse || s.opts.xferDirs) {
223		srcs = []string{"."}
224	}
225
226	files, err := s.buildFileList(srcs)
227	if err != nil {
228		return s.fatal(exitFileSelect, err)
229	}
230	if s.opts.alwaysChecksum {
231		for _, f := range files {
232			if f.isReg() {
233				if f.sum, err = s.fileChecksum(f.src); err != nil {
234					s.ioError |= ioErrGeneral
235					s.xferError("failed to checksum %q: %v", "/"+f.src, err)
236				}
237			}
238		}
239	}
240	buildTime := time.Since(start)
241
242	start = time.Now()
243	codec := s.newFlistCodec()
244	var totalSize int64
245	for _, f := range files {
246		if err := codec.sendEntry(s.c, f); err != nil {
247			return err
248		}
249		if f.isReg() || f.isLink() {
250			totalSize += f.size
251		}
252	}
253	ioError := s.ioError
254	if s.opts.ignoreErrors {
255		ioError = 0
256	}
257	if ioError != 0 && !s.safeFlist {
258		if err := codec.sendEnd(s.c, 0); err != nil {
259			return err
260		}
261	} else if err := codec.sendEnd(s.c, ioError); err != nil {
262		return err
263	}
264	sortFileList(files, s.c.protocol, false)
265	if err := s.sendIDLists(); err != nil {
266		return err
267	}
268	if s.c.protocol < 30 {
269		if err := s.c.writeInt32(ioError); err != nil {
270			return err
271		}
272	} else if !s.safeFlist && ioError != 0 {
273		if err := s.c.writeMsgInt(msgIOError, ioError); err != nil {
274			return err
275		}
276	}
277	if err := s.c.flush(); err != nil {
278		return err
279	}
280	xferTime := time.Since(start)
281
282	if len(files) == 0 {
283		return s.finish()
284	}
285
286	if err := s.sendFiles(files); err != nil {
287		return err
288	}
289
290	if err := s.c.writeVarlong30(s.c.bytesRead(), 3); err != nil {
291		return err
292	}
293	if err := s.c.writeVarlong30(s.c.bytesWritten(), 3); err != nil {
294		return err
295	}
296	if err := s.c.writeVarlong30(totalSize, 3); err != nil {
297		return err
298	}
299	if s.c.protocol >= 29 {
300		if err := s.c.writeVarlong30(max(buildTime.Milliseconds(), 1), 3); err != nil {
301			return err
302		}
303		if err := s.c.writeVarlong30(xferTime.Milliseconds(), 3); err != nil {
304			return err
305		}
306	}
307	if err := s.readFinalGoodbye(true); err != nil {
308		return err
309	}
310	return s.finish()
311}
312
313func (s *session) sendIDLists() error {
314	if s.opts.numericIDs {
315		return nil
316	}
317	for _, want := range []bool{s.opts.preserveUID, s.opts.preserveGID} {
318		if !want {
319			continue
320		}
321		// Every entry is owned by id 0, which is never listed by number.
322		if err := s.c.writeVarint30(0); err != nil {
323			return err
324		}
325		if s.xmitID0Names {
326			if err := s.c.writeByte(4); err != nil {
327				return err
328			}
329			if err := s.c.write([]byte("root")); err != nil {
330				return err
331			}
332		}
333	}
334	return nil
335}
336
337// sendFiles answers the receiver's requests until it is done
338// (send_files).
339func (s *session) sendFiles(files []*fileEntry) error {
340	phase := 0
341	maxPhase := 1
342	if s.c.protocol >= 29 {
343		maxPhase = 2
344	}
345	savedIOError := s.ioError
346	tok := newTokenSender(s.c, s.compression, s.compressLevel)
347	for {
348		req, err := s.readNdxAndAttrs(len(files))
349		if err != nil {
350			return err
351		}
352		if req.ndx == ndxDone {
353			phase++
354			if phase > maxPhase {
355				break
356			}
357			if err := s.c.writeNdx(ndxDone); err != nil {
358				return err
359			}
360			continue
361		}
362		f := files[req.ndx]
363		if req.iflags&itemTransfer != 0 && !f.isReg() {
364			return s.fatal(exitProtocol, fmt.Errorf("received request to transfer non-regular file: %d", req.ndx))
365		}
366		if req.iflags&itemTransfer == 0 {
367			if err := s.writeNdxAndAttrs(req); err != nil {
368				return err
369			}
370			continue
371		}
372		if phase == 2 {
373			return s.fatal(exitProtocol, errors.New("got transfer request in phase 2"))
374		}
375		if s.opts.dryRun {
376			if err := s.writeNdxAndAttrs(req); err != nil {
377				return err
378			}
379			continue
380		}
381
382		head, sums, err := s.receiveSums()
383		if err != nil {
384			return s.fatal(exitProtocol, err)
385		}
386		if err := s.sendFile(f, req, head, sums, tok); err != nil {
387			return err
388		}
389	}
390	if s.ioError != savedIOError && s.c.protocol >= 30 {
391		if err := s.c.writeMsgInt(msgIOError, s.ioError); err != nil {
392			return err
393		}
394	}
395	return s.c.writeNdx(ndxDone)
396}
397
398func (s *session) sendFile(f *fileEntry, req ndxRequest, head sumHead, sums []sumBuf, tok tokenSender) error {
399	file, info, err := s.fs.Open(f.src)
400	if err != nil {
401		if errors.Is(err, fs.ErrNotExist) {
402			s.ioError |= ioErrVanished
403			s.warning("file has vanished: %q", "/"+f.src)
404		} else {
405			s.ioError |= ioErrGeneral
406			s.xferError("send_files failed to open %q: %v", "/"+f.src, err)
407		}
408		if s.c.protocol >= 30 {
409			return s.c.writeMsgInt(msgNoSend, req.ndx)
410		}
411		return nil
412	}
413	defer func() { _ = file.Close() }()
414
415	if err := s.writeNdxAndAttrs(req); err != nil {
416		return err
417	}
418	if err := s.c.writeSumHead(head); err != nil {
419		return err
420	}
421	inplace := (s.inplacePartial && req.fnamecmpType == fnamecmpPartialDir) ||
422		(s.opts.inplace && req.fnamecmpType == fnamecmpFname)
423	readSize := max(3*int64(head.blength), maxMapSize)
424	m := &matcher{
425		head:    head,
426		sums:    sums,
427		cs:      &s.cs,
428		tok:     tok,
429		buf:     newMapFile(file, info.Size, readSize, head.blength),
430		sum:     s.cs.newXferSum(),
431		inplace: inplace,
432	}
433	if err := m.run(info.Size); err != nil {
434		return err
435	}
436	sum := m.sum.Sum(nil)
437	if m.buf.err != nil {
438		s.ioError |= ioErrGeneral
439		s.xferError("read errors mapping %q: %v", "/"+f.src, m.buf.err)
440		// A checksum the receiver can't match makes it discard the file.
441		allZero := !slices.ContainsFunc(sum, func(b byte) bool { return b != 0 })
442		clear(sum)
443		if allZero {
444			sum[len(sum)-1]++
445		}
446	}
447	s.stats.literal += m.literal
448	s.stats.matched += m.matched
449	return s.c.write(sum)
450}
451
452// receiveSums reads the block signatures the generator computed for its
453// copy of a file.
454func (s *session) receiveSums() (sumHead, []sumBuf, error) {
455	head, err := s.c.readSumHead(s.cs.xfer.size())
456	if err != nil {
457		return head, nil, err
458	}
459	sums := make([]sumBuf, head.count)
460	sum2 := make([]byte, int(head.count)*int(head.s2length))
461	var offset int64
462	for i := range sums {
463		v, err := s.c.readInt32()
464		if err != nil {
465			return head, nil, err
466		}
467		b := sum2[i*int(head.s2length) : (i+1)*int(head.s2length)]
468		if err := s.c.readFull(b); err != nil {
469			return head, nil, err
470		}
471		l := head.blength
472		if int32(i) == head.count-1 && head.remainder != 0 {
473			l = head.remainder
474		}
475		sums[i] = sumBuf{offset: offset, len: l, sum1: uint32(v), sum2: b}
476		offset += int64(l)
477	}
478	return head, sums, nil
479}
480
481func (s *session) fileChecksum(name string) ([]byte, error) {
482	f, info, err := s.fs.Open(name)
483	if err != nil {
484		return nil, err
485	}
486	defer func() { _ = f.Close() }()
487	h := s.cs.newFileSum()
488	if _, err := io.Copy(h, io.NewSectionReader(f, 0, info.Size)); err != nil {
489		return nil, err
490	}
491	return h.Sum(nil), nil
492}