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}