Commit 5947599

Antonio Mika  ·  2026-10-09 09:04:34 -0400 EDT
parent b3705ff
feat(rsync): implement the rsync protocol natively
54 files changed,  +7544, -3166
M go.mod
M go.mod
+3, -3
......@@ -22,6 +22,7 @@ require (
2222 github.com/alecthomas/chroma/v2 v2.27.0
2323 github.com/antoniomika/syncmap v1.0.0
2424 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
25+ github.com/cespare/xxhash/v2 v2.3.0
2526 github.com/containerd/console v1.0.5
2627 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
2728 github.com/emersion/go-smtp v0.25.0
......@@ -34,6 +35,7 @@ require (
3435 github.com/gorilla/websocket v1.5.3
3536 github.com/hashicorp/golang-lru/v2 v2.0.7
3637 github.com/jmoiron/sqlx v1.4.0
38+ github.com/klauspost/compress v1.19.1
3739 github.com/lib/pq v1.12.3
3840 github.com/matryer/is v1.4.1
3941 github.com/microcosm-cc/bluemonday v1.0.27
......@@ -56,7 +58,6 @@ require (
5658 go.abhg.dev/goldmark/toc v0.12.0
5759 go.rockorager.dev/vaxis v0.17.1
5860 golang.org/x/crypto v0.57.0
59- golang.org/x/sync v0.23.0
6061 gopkg.in/yaml.v2 v2.4.0
6162 modernc.org/sqlite v1.60.1
6263 )
......@@ -69,7 +70,6 @@ require (
6970 github.com/aymerick/douceur v0.2.0 // indirect
7071 github.com/beorn7/perks v1.0.1 // indirect
7172 github.com/cenkalti/backoff/v4 v4.3.0 // indirect
72- github.com/cespare/xxhash/v2 v2.3.0 // indirect
7373 github.com/containerd/errdefs v1.0.0 // indirect
7474 github.com/containerd/errdefs/pkg v0.3.0 // indirect
7575 github.com/containerd/log v0.1.0 // indirect
......@@ -111,7 +111,6 @@ require (
111111 github.com/gorilla/css v1.0.1 // indirect
112112 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
113113 github.com/jackc/pgx/v5 v5.8.0 // indirect
114- github.com/klauspost/compress v1.19.1 // indirect
115114 github.com/kr/fs v0.1.0 // indirect
116115 github.com/kr/pretty v0.3.1 // indirect
117116 github.com/kr/text v0.2.0 // indirect
......@@ -164,6 +163,7 @@ require (
164163 golang.org/x/image v0.46.0 // indirect
165164 golang.org/x/mod v0.41.0 // indirect
166165 golang.org/x/net v0.59.0 // indirect
166+ golang.org/x/sync v0.23.0 // indirect
167167 golang.org/x/sys v0.48.0 // indirect
168168 golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect
169169 golang.org/x/term v0.46.0 // indirect
+397, -0
......@@ -0,0 +1,397 @@
1+package pgs
2+
3+import (
4+ "bytes"
5+ "crypto/rand"
6+ "encoding/pem"
7+ "fmt"
8+ "io"
9+ "log/slog"
10+ "net"
11+ "os"
12+ "os/exec"
13+ "path/filepath"
14+ "regexp"
15+ "strconv"
16+ "strings"
17+ "testing"
18+ "time"
19+
20+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
21+ "github.com/picosh/pico/pkg/db"
22+ "github.com/picosh/pico/pkg/pssh"
23+ "github.com/picosh/pico/pkg/shared"
24+ "github.com/picosh/pico/pkg/storage"
25+ "github.com/picosh/pico/pkg/storage/storagetest"
26+ "github.com/prometheus/client_golang/prometheus"
27+)
28+
29+// rsyncFileMax lifts pgs's per-file cap so the test can move files larger
30+// than the default allows.
31+const rsyncFileMax = 24 << 20
32+
33+// rsyncEnv is a pgs SSH server backed by one storage implementation, and a
34+// real rsync client set up to talk to it.
35+type rsyncEnv struct {
36+ t *testing.T
37+ st storage.StorageServe
38+ bucket storage.Bucket
39+ rsh string
40+}
41+
42+func newRsyncEnv(t *testing.T, st storage.StorageServe) *rsyncEnv {
43+ if _, err := exec.LookPath("rsync"); err != nil {
44+ t.Skip("rsync not installed")
45+ }
46+ logger := slog.New(slog.DiscardHandler)
47+ dbpool := pgsdb.NewDBMemory(logger)
48+ dbpool.SetupTestData()
49+ dbpool.Feature.Data.FileMax = rsyncFileMax
50+ dbpool.Feature.Data.StorageMax = 256 << 20
51+ bucket := storagetest.Bucket(t, st, shared.GetAssetBucketName(dbpool.Users[0].ID))
52+
53+ t.Setenv("PGS_SSH_PORT", "0")
54+ cfg := NewPgsConfig(logger, dbpool, st, discardPubsub{})
55+ done := make(chan error)
56+ readyCh := make(chan *pssh.SSHServer)
57+ prometheus.DefaultRegisterer = prometheus.NewRegistry()
58+ go StartSshServerForTesting(cfg, done, readyCh)
59+ t.Cleanup(func() { close(done) })
60+
61+ server := <-readyCh
62+ if server == nil {
63+ t.Fatal("failed to create ssh server")
64+ }
65+ var addr string
66+ for range 100 {
67+ server.Mu.Lock()
68+ if server.Listener != nil {
69+ addr = server.Listener.Addr().String()
70+ }
71+ server.Mu.Unlock()
72+ if addr != "" {
73+ break
74+ }
75+ time.Sleep(10 * time.Millisecond)
76+ }
77+ if addr == "" {
78+ t.Fatal("server listener not ready")
79+ }
80+
81+ user := GenerateUser()
82+ dbpool.Pubkeys = append(dbpool.Pubkeys, &db.PublicKey{
83+ ID: "rsync-pubkey",
84+ UserID: dbpool.Users[0].ID,
85+ Key: shared.KeyForKeyText(user.signer.PublicKey()),
86+ })
87+ keyFile := filepath.Join(t.TempDir(), "id_ed25519")
88+ block := &pem.Block{Type: "OPENSSH PRIVATE KEY", Bytes: user.privateKey}
89+ if err := os.WriteFile(keyFile, pem.EncodeToMemory(block), 0o600); err != nil {
90+ t.Fatal(err)
91+ }
92+ _, port, _ := net.SplitHostPort(addr)
93+ rsh := fmt.Sprintf(
94+ "ssh -p %s -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR",
95+ port, keyFile,
96+ )
97+ return &rsyncEnv{t: t, st: st, bucket: bucket, rsh: rsh}
98+}
99+
100+// discardPubsub drops cache purges, which pgs sends from goroutines that can
101+// outlive the test.
102+type discardPubsub struct{}
103+
104+func (discardPubsub) Read([]byte) (int, error) { return 0, io.EOF }
105+func (discardPubsub) Write(b []byte) (int, error) { return len(b), nil }
106+func (discardPubsub) Close() error { return nil }
107+
108+type rsyncStats struct {
109+ output string
110+ transferred int
111+ deleted int
112+ literal int
113+ matched int
114+}
115+
116+var statPatterns = map[string]*regexp.Regexp{
117+ "transferred": regexp.MustCompile(`Number of regular files transferred: ([\d,]+)`),
118+ "deleted": regexp.MustCompile(`Number of deleted files: ([\d,]+)`),
119+ "literal": regexp.MustCompile(`Literal data: ([\d,]+)`),
120+ "matched": regexp.MustCompile(`Matched data: ([\d,]+)`),
121+}
122+
123+func (e *rsyncEnv) rsync(args ...string) rsyncStats {
124+ e.t.Helper()
125+ args = append([]string{"-e", e.rsh, "--stats"}, args...)
126+ out, err := exec.Command("rsync", args...).CombinedOutput()
127+ if err != nil {
128+ e.t.Fatalf("rsync %q: %v\n%s", args, err, out)
129+ }
130+ stat := func(name string) int {
131+ m := statPatterns[name].FindSubmatch(out)
132+ if m == nil {
133+ e.t.Fatalf("no %s in rsync output:\n%s", name, out)
134+ }
135+ n, _ := strconv.Atoi(strings.ReplaceAll(string(m[1]), ",", ""))
136+ return n
137+ }
138+ return rsyncStats{
139+ output: string(out),
140+ transferred: stat("transferred"),
141+ deleted: stat("deleted"),
142+ literal: stat("literal"),
143+ matched: stat("matched"),
144+ }
145+}
146+
147+func (e *rsyncEnv) stored(name string) ([]byte, time.Time, bool) {
148+ e.t.Helper()
149+ r, info, err := e.st.GetObject(e.bucket, name)
150+ if err != nil {
151+ return nil, time.Time{}, false
152+ }
153+ defer func() { _ = r.Close() }()
154+ data, err := io.ReadAll(r)
155+ if err != nil {
156+ e.t.Fatalf("read %s: %v", name, err)
157+ }
158+ return data, info.LastModified, true
159+}
160+
161+// tree is a local directory of files with fixed contents and mtimes.
162+type tree struct {
163+ t *testing.T
164+ dir string
165+}
166+
167+func (tr tree) write(name string, data []byte, mtime time.Time) {
168+ tr.t.Helper()
169+ p := filepath.Join(tr.dir, name)
170+ if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
171+ tr.t.Fatal(err)
172+ }
173+ if err := os.WriteFile(p, data, 0o644); err != nil {
174+ tr.t.Fatal(err)
175+ }
176+ tr.touch(name, mtime)
177+}
178+
179+func (tr tree) touch(name string, mtime time.Time) {
180+ tr.t.Helper()
181+ if err := os.Chtimes(filepath.Join(tr.dir, name), mtime, mtime); err != nil {
182+ tr.t.Fatal(err)
183+ }
184+}
185+
186+func (tr tree) read(name string) ([]byte, time.Time) {
187+ tr.t.Helper()
188+ p := filepath.Join(tr.dir, name)
189+ data, err := os.ReadFile(p)
190+ if err != nil {
191+ tr.t.Fatal(err)
192+ }
193+ info, err := os.Stat(p)
194+ if err != nil {
195+ tr.t.Fatal(err)
196+ }
197+ return data, info.ModTime()
198+}
199+
200+func randomBytes(n int) []byte {
201+ b := make([]byte, n)
202+ _, _ = rand.Read(b)
203+ return b
204+}
205+
206+func rsyncStorageBackends() map[string]func(t *testing.T) storage.StorageServe {
207+ return map[string]func(t *testing.T) storage.StorageServe{
208+ "fs": func(t *testing.T) storage.StorageServe {
209+ st, err := storage.NewStorageFS(slog.New(slog.DiscardHandler), t.TempDir())
210+ if err != nil {
211+ t.Fatal(err)
212+ }
213+ return st
214+ },
215+ "memory": func(t *testing.T) storage.StorageServe {
216+ st, err := storage.NewStorageMemory(map[string]map[string]string{})
217+ if err != nil {
218+ t.Fatal(err)
219+ }
220+ return st
221+ },
222+ }
223+}
224+
225+// TestRsyncStorage runs a real rsync client against pgs on each storage
226+// backend and checks that quick checks, deltas, checksums and deletes are
227+// driven by what storage holds.
228+func TestRsyncStorage(t *testing.T) {
229+ for name, newStorage := range rsyncStorageBackends() {
230+ t.Run(name, func(t *testing.T) {
231+ env := newRsyncEnv(t, newStorage(t))
232+ src := tree{t, t.TempDir()}
233+ remote := "localhost:/site"
234+
235+ t0 := time.Unix(1_600_000_000, 0)
236+ t1 := time.Unix(1_650_000_000, 0)
237+ big := randomBytes(20 << 20)
238+ src.write("index.html", []byte("<html>index</html>"), t0)
239+ src.write("big.bin", big, t0)
240+ src.write("css/site.css", []byte("body{}"), t0)
241+ src.write("css/deep/x.css", []byte("a{}"), t1)
242+ src.write("img/logo.svg", []byte("<svg/>"), t1)
243+ src.write("keep.txt", []byte("excluded from deletes"), t0)
244+
245+ s := env.rsync("-rtz", src.dir+"/", remote)
246+ if s.transferred != 6 {
247+ t.Fatalf("initial upload transferred %d files\n%s", s.transferred, s.output)
248+ }
249+ for name, mtime := range map[string]time.Time{"index.html": t0, "big.bin": t0, "css/deep/x.css": t1} {
250+ data, stored, ok := env.stored("/site/" + name)
251+ local, _ := src.read(name)
252+ if !ok || !bytes.Equal(data, local) {
253+ t.Fatalf("%s not stored correctly\n%s", name, s.output)
254+ }
255+ if !stored.Equal(mtime) {
256+ t.Errorf("%s stored with mtime %v, want %v", name, stored, mtime)
257+ }
258+ }
259+
260+ t.Run("unchanged upload sends nothing", func(t *testing.T) {
261+ s := env.rsync("-rtz", src.dir+"/", remote)
262+ if s.transferred != 0 || s.literal != 0 {
263+ t.Fatalf("transferred %d files, %d literal bytes\n%s", s.transferred, s.literal, s.output)
264+ }
265+ })
266+
267+ t.Run("changed file is sent as a delta", func(t *testing.T) {
268+ copy(big[1<<20:], "a small change in the middle of the file")
269+ src.write("big.bin", big, t1)
270+ s := env.rsync("-rtz", src.dir+"/", remote)
271+ if s.transferred != 1 || s.literal > 16<<10 || s.matched < len(big)-16<<10 {
272+ t.Fatalf("transferred %d, literal %d, matched %d\n%s", s.transferred, s.literal, s.matched, s.output)
273+ }
274+ data, mtime, _ := env.stored("/site/big.bin")
275+ if !bytes.Equal(data, big) || !mtime.Equal(t1) {
276+ t.Fatalf("stored big.bin wrong after delta (mtime %v)", mtime)
277+ }
278+ })
279+
280+ t.Run("grown and shrunk files", func(t *testing.T) {
281+ src.write("index.html", []byte("<html>index, now with more content</html>"), t1)
282+ src.write("css/site.css", []byte("b{}"), t1)
283+ env.rsync("-rtz", src.dir+"/", remote)
284+ for _, name := range []string{"index.html", "css/site.css"} {
285+ data, _, _ := env.stored("/site/" + name)
286+ local, _ := src.read(name)
287+ if !bytes.Equal(data, local) {
288+ t.Errorf("%s stored as %q, want %q", name, data, local)
289+ }
290+ }
291+ })
292+
293+ t.Run("checksum mode ignores mtimes", func(t *testing.T) {
294+ src.touch("css/deep/x.css", t0)
295+ s := env.rsync("-rcz", src.dir+"/", remote)
296+ if s.transferred != 0 {
297+ t.Fatalf("-c transferred %d files with unchanged contents\n%s", s.transferred, s.output)
298+ }
299+ s = env.rsync("-rtz", src.dir+"/", remote)
300+ if s.transferred != 1 || s.literal != 0 {
301+ t.Fatalf("mtime-only change transferred %d files, %d literal bytes\n%s", s.transferred, s.literal, s.output)
302+ }
303+ if _, mtime, _ := env.stored("/site/css/deep/x.css"); !mtime.Equal(t0) {
304+ t.Fatalf("mtime-only change stored mtime %v, want %v", mtime, t0)
305+ }
306+ })
307+
308+ t.Run("oversized file is rejected", func(t *testing.T) {
309+ src.write("huge.bin", randomBytes(rsyncFileMax+1), t0)
310+ s := env.rsync("-rt", src.dir+"/", remote)
311+ if !strings.Contains(s.output, "huge.bin") {
312+ t.Fatalf("no warning about huge.bin\n%s", s.output)
313+ }
314+ if _, _, ok := env.stored("/site/huge.bin"); ok {
315+ t.Fatal("rejected file was stored")
316+ }
317+ if err := os.Remove(filepath.Join(src.dir, "huge.bin")); err != nil {
318+ t.Fatal(err)
319+ }
320+ })
321+
322+ t.Run("delete removes what the source lacks", func(t *testing.T) {
323+ if err := os.RemoveAll(filepath.Join(src.dir, "css/deep")); err != nil {
324+ t.Fatal(err)
325+ }
326+ if err := os.Remove(filepath.Join(src.dir, "img/logo.svg")); err != nil {
327+ t.Fatal(err)
328+ }
329+ if err := os.Remove(filepath.Join(src.dir, "keep.txt")); err != nil {
330+ t.Fatal(err)
331+ }
332+ s := env.rsync("-rt", "--delete", "--exclude=keep.txt", src.dir+"/", remote)
333+ if s.deleted < 2 {
334+ t.Fatalf("deleted %d files\n%s", s.deleted, s.output)
335+ }
336+ for name, want := range map[string]bool{
337+ "css/deep/x.css": false,
338+ "img/logo.svg": false,
339+ "keep.txt": true,
340+ "css/site.css": true,
341+ "big.bin": true,
342+ } {
343+ if _, _, ok := env.stored("/site/" + name); ok != want {
344+ t.Errorf("%s stored=%v after delete, want %v", name, ok, want)
345+ }
346+ }
347+ })
348+
349+ dst := tree{t, t.TempDir()}
350+
351+ t.Run("download", func(t *testing.T) {
352+ s := env.rsync("-rtz", remote+"/", dst.dir+"/")
353+ if s.transferred != 4 {
354+ t.Fatalf("download transferred %d files\n%s", s.transferred, s.output)
355+ }
356+ for _, name := range []string{"index.html", "big.bin", "css/site.css", "keep.txt"} {
357+ got, mtime := dst.read(name)
358+ want, storedMtime, _ := env.stored("/site/" + name)
359+ if !bytes.Equal(got, want) {
360+ t.Errorf("downloaded %s differs from storage", name)
361+ }
362+ if !mtime.Equal(storedMtime) {
363+ t.Errorf("downloaded %s has mtime %v, storage has %v", name, mtime, storedMtime)
364+ }
365+ }
366+ })
367+
368+ t.Run("unchanged download sends nothing", func(t *testing.T) {
369+ s := env.rsync("-rtz", remote+"/", dst.dir+"/")
370+ if s.transferred != 0 || s.literal != 0 {
371+ t.Fatalf("transferred %d files, %d literal bytes\n%s", s.transferred, s.literal, s.output)
372+ }
373+ })
374+
375+ t.Run("download delta against a local copy", func(t *testing.T) {
376+ local, _ := dst.read("big.bin")
377+ copy(local[2<<20:], "local edit")
378+ dst.write("big.bin", local, t0)
379+ s := env.rsync("-rtz", remote+"/", dst.dir+"/")
380+ if s.transferred != 1 || s.literal > 16<<10 || s.matched < len(big)-16<<10 {
381+ t.Fatalf("transferred %d, literal %d, matched %d\n%s", s.transferred, s.literal, s.matched, s.output)
382+ }
383+ if got, _ := dst.read("big.bin"); !bytes.Equal(got, big) {
384+ t.Fatal("big.bin wrong after delta download")
385+ }
386+ })
387+
388+ t.Run("download single file", func(t *testing.T) {
389+ one := tree{t, t.TempDir()}
390+ env.rsync("-t", remote+"/css/site.css", one.dir+"/")
391+ if got, _ := one.read("site.css"); string(got) != "b{}" {
392+ t.Fatalf("site.css is %q", got)
393+ }
394+ })
395+ })
396+ }
397+}
+0, -5
......@@ -1,5 +0,0 @@
1-package nofollow
2-
3-// Maybe resolves to unix.O_NOFOLLOW on unix systems,
4-// 0 on other platforms. TODO(go1.24): use os.Root.
5-const Maybe = 0
+0, -36
......@@ -1,36 +0,0 @@
1-package rsync
2-
3-// rsync.h.
4-const (
5- XMIT_TOP_DIR = (1 << 0)
6- XMIT_SAME_MODE = (1 << 1)
7- XMIT_EXTENDED_FLAGS = (1 << 2)
8- XMIT_SAME_RDEV_pre28 = XMIT_EXTENDED_FLAGS /* Only in protocols < 28 */
9- XMIT_SAME_UID = (1 << 3)
10- XMIT_SAME_GID = (1 << 4)
11- XMIT_SAME_NAME = (1 << 5)
12- XMIT_LONG_NAME = (1 << 6)
13- XMIT_SAME_TIME = (1 << 7)
14- XMIT_SAME_RDEV_MAJOR = (1 << 8)
15- XMIT_HAS_IDEV_DATA = (1 << 9)
16- XMIT_SAME_DEV = (1 << 10)
17- XMIT_RDEV_MINOR_IS_SMALL = (1 << 11)
18-)
19-
20-// as per /usr/include/bits/stat.h.
21-const (
22- S_IFMT = 0o0170000 // bits determining the file type
23- S_IFDIR = 0o0040000 // Directory
24- S_IFCHR = 0o0020000 // Character device
25- S_IFBLK = 0o0060000 // Block device
26- S_IFREG = 0o0100000 // Regular file
27- S_IFIFO = 0o0010000 // FIFO
28- S_IFLNK = 0o0120000 // Symbolic link
29- S_IFSOCK = 0o0140000 // Socket
30-)
31-
32-// ProtocolVersion defines the currently implemented rsync protocol
33-// version. Protocol version 27 seems to be the safest bet for wide
34-// compatibility: version 27 was introduced by rsync 2.6.0 (released 2004), and
35-// is supported by openrsync and rsyn.
36-const ProtocolVersion = 27
+0, -6
......@@ -1,6 +0,0 @@
1-// Package rsync contains a native Go rsync implementation.
2-//
3-// The only component currently is gokr-rsyncd, a read-only rsync daemon
4-// sender-only Go implementation of rsyncd. rsync daemon is a custom
5-// (un-standardized) network protocol, running on port 873 by default.
6-package rsync
+0, -7
......@@ -1,7 +0,0 @@
1-package rsync
2-
3-import "log/slog"
4-
5-// Logger is an interface that allows specifying your own logger.
6-// By default, the Go log package is used, which prints to stderr.
7-type Logger = slog.Logger
+0, -86
......@@ -1,86 +0,0 @@
1-package rsync
2-
3-import (
4- "fmt"
5-
6- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
7-)
8-
9-// rsync/rsync.h:struct sum_buf.
10-type SumBuf struct {
11- Offset int64
12- Len int64
13- Index int32
14- Sum1 uint32
15- Sum2 [16]byte
16-}
17-
18-// TODO: remove connection.go:sumHead in favor of this type.
19-type SumHead struct {
20- // “number of blocks” (openrsync)
21- // “how many chunks” (rsync)
22- ChecksumCount int32
23-
24- // “block length in the file” (openrsync)
25- // maximum (1 << 29) for older rsync, (1 << 17) for newer
26- BlockLength int32
27-
28- // “long checksum length” (openrsync)
29- ChecksumLength int32
30-
31- // “terminal (remainder) block length” (openrsync)
32- // RemainderLength is flength % BlockLength
33- RemainderLength int32
34-
35- Sums []SumBuf
36-}
37-
38-func (sh *SumHead) ReadFrom(c *rsyncwire.Conn) error {
39- // TODO(protocol>=30): update maxBlockLen
40- const maxBlockLen = 1 << 29 // see rsync.h:OLD_MAX_BLOCK_SIZE
41-
42- var err error
43- sh.ChecksumCount, err = c.ReadInt32()
44- if err != nil {
45- return err
46- }
47- if sh.ChecksumCount < 0 {
48- return fmt.Errorf("invalid checksum count %d", sh.ChecksumCount)
49- }
50-
51- sh.BlockLength, err = c.ReadInt32()
52- if err != nil {
53- return err
54- }
55- if sh.BlockLength < 0 || sh.BlockLength > maxBlockLen {
56- return fmt.Errorf("invalid block length %d", sh.BlockLength)
57- }
58-
59- sh.ChecksumLength, err = c.ReadInt32()
60- if err != nil {
61- return err
62- }
63- // TODO(protocol>=27): update max sh.ChecksumLength check
64- if sh.ChecksumLength < 0 || sh.ChecksumLength > 16 {
65- return fmt.Errorf("invalid checksum length %d", sh.ChecksumLength)
66- }
67-
68- sh.RemainderLength, err = c.ReadInt32()
69- if err != nil {
70- return err
71- }
72- if sh.RemainderLength < 0 || sh.RemainderLength > sh.BlockLength {
73- return fmt.Errorf("invalid remainder length %d", sh.RemainderLength)
74- }
75-
76- return nil
77-}
78-
79-func (sh *SumHead) WriteTo(c *rsyncwire.Conn) error {
80- var buf rsyncwire.Buffer
81- buf.WriteInt32(sh.ChecksumCount)
82- buf.WriteInt32(sh.BlockLength)
83- buf.WriteInt32(sh.ChecksumLength)
84- buf.WriteInt32(sh.RemainderLength)
85- return c.WriteString(buf.String())
86-}
+0, -73
......@@ -1,73 +0,0 @@
1-package rsyncchecksum_test
2-
3-import (
4- "bytes"
5- "os"
6- "path/filepath"
7- "testing"
8-
9- "github.com/picosh/pico/pkg/rsync-receiver/rsyncchecksum"
10-)
11-
12-func constructLargeDataFile(headPattern, bodyPattern, endPattern []byte) []byte {
13- // create large data file in source directory to be copied
14- head := bytes.Repeat(headPattern, 1*1024*1024)
15- body := bytes.Repeat(bodyPattern, 1*1024*1024)
16- end := bytes.Repeat(endPattern, 1*1024*1024)
17- return append(append(head, body...), end...)
18-}
19-
20-func writeLargeDataFile(t *testing.T, source string, headPattern, bodyPattern, endPattern []byte) {
21- // create large data file in source directory to be copied
22- content := constructLargeDataFile(headPattern, bodyPattern, endPattern)
23- large := filepath.Join(source, "large-data-file")
24- if err := os.MkdirAll(filepath.Dir(large), 0755); err != nil {
25- t.Fatal(err)
26- }
27- if err := os.WriteFile(large, content, 0644); err != nil {
28- t.Fatal(err)
29- }
30-}
31-
32-func TestSyncExtended(t *testing.T) {
33- tmp := t.TempDir()
34- source := filepath.Join(tmp, "source")
35-
36- writeLargeDataFile(t, source, []byte{0x11}, []byte{0xbb}, []byte{0xee})
37-
38- // These values are taken from the rsync debug output:
39- const k = 1768
40- want := make([]uint32, 1780)
41- for i := 0; i <= 592; i++ {
42- want[i] = 0xa5d47568
43- }
44- want[593] = 0x23645688
45- for i := 594; i <= 1185; i++ {
46- want[i] = 0x8c1c2378
47- }
48- want[1186] = 0x12504720
49- for i := 1187; i <= 1778; i++ {
50- want[i] = 0x7d9883b0
51- }
52- want[1779] = 0x61b8dff0
53-
54- sourceLarge := filepath.Join(source, "large-data-file")
55- f, err := os.Open(sourceLarge)
56- if err != nil {
57- t.Fatal(err)
58- }
59- defer func() { _ = f.Close() }()
60- buf := make([]byte, k)
61- for idx, wantChecksum := range want {
62- n, err := f.Read(buf)
63- if err != nil {
64- t.Fatal(err)
65- }
66-
67- chunk := buf[:n]
68- sum := rsyncchecksum.Checksum1(chunk)
69- if sum != wantChecksum {
70- t.Fatalf("checksum calculation error: got %08x, want %08x (idx %d), chunk: %#v", sum, wantChecksum, idx, chunk)
71- }
72- }
73-}
+0, -73
......@@ -1,73 +0,0 @@
1-package rsyncchecksum
2-
3-import (
4- "encoding/binary"
5- "io"
6- "os"
7-
8- "github.com/mmcloughlin/md4"
9-)
10-
11-func Tag2(s1, s2 uint16) uint16 {
12- return (((s1) + (s2)) & 0xFFFF)
13-}
14-
15-func Tag(sum uint32) uint16 {
16- return Tag2(uint16(sum&0xFFFF), uint16(sum>>16))
17-}
18-
19-// SignExtend mirrors how C converts from (signed char) to uint32, i.e. using
20-// sign extension. get_checksum1 treats the buffer as (signed char*) instead of
21-// (unsigned char*), which likely was not a conscious choice, but here we are.
22-//
23-// This function is exported for use in the rolling checksum in match.go.
24-func SignExtend(b byte) uint32 {
25- val := uint32(b)
26- return uint32(int32(val<<24) >> 24)
27-}
28-
29-func Checksum1(buf []byte) uint32 {
30- bufLen := len(buf)
31- var s1, s2 uint32
32- var i int
33-
34- if bufLen > 4 {
35- for i = 0; i < (bufLen - 4); i += 4 {
36- s2 += 4*(s1+SignExtend(buf[i])) +
37- 3*SignExtend(buf[i+1]) +
38- 2*SignExtend(buf[i+2]) +
39- SignExtend(buf[i+3])
40- s1 += SignExtend(buf[i+0]) +
41- SignExtend(buf[i+1]) +
42- SignExtend(buf[i+2]) +
43- SignExtend(buf[i+3])
44- }
45- }
46- for ; i < bufLen; i++ {
47- s1 += SignExtend(buf[i])
48- s2 += s1
49- }
50- return (s1 & 0xffff) + (s2 << 16)
51-}
52-
53-func Checksum2(seed int32, buf []byte) []byte {
54- h := md4.New()
55- h.Write(buf)
56- _ = binary.Write(h, binary.LittleEndian, seed) // hash.Hash.Write never fails
57- return h.Sum(nil)
58-}
59-
60-func FileChecksum(fn string) ([]byte, error) {
61- f, err := os.Open(fn)
62- if err != nil {
63- return nil, err
64- }
65- defer func() { _ = f.Close() }()
66- h := md4.New()
67- if _, err := io.Copy(h, f); err != nil {
68- return nil, err
69- }
70- return h.Sum(nil), nil
71-}
72-
73-const Size = md4.Size
+0, -37
......@@ -1,37 +0,0 @@
1-// Package rsynccommon contains functionality that both the sender and the
2-// receiver implementation need.
3-package rsynccommon
4-
5-import (
6- "math"
7-
8- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
9-)
10-
11-const blockSize = 700 // rsync/rsync.h
12-
13-// Corresponds to rsync/generator.c:sum_sizes_sqroot.
14-func SumSizesSqroot(contentLen int64) rsync.SumHead {
15- // * The block size is a rounded square root of file length.
16-
17- // The block size algorithm plays a crucial role in the protocol efficiency. In general, the block size is the rounded square root of the total file size. The minimum block size, however, is 700 B. Otherwise, the square root computation is simply sqrt(3) followed by ceil(3)
18-
19- // For reasons unknown, the square root result is rounded up to the nearest multiple of eight.
20-
21- // TODO: round this
22- blockLength := max(int32(math.Sqrt(float64(contentLen))), blockSize)
23-
24- // * The checksum size is determined according to:
25- // * blocksum_bits = BLOCKSUM_EXP + 2*log2(file_len) - log2(block_len)
26- // * provided by Donovan Baarda which gives a probability of rsync
27- // * algorithm corrupting data and falling back using the whole md4
28- // * checksums.
29- const checksumLength = 16 // TODO?
30-
31- return rsync.SumHead{
32- ChecksumCount: int32((contentLen + (int64(blockLength) - 1)) / int64(blockLength)),
33- RemainderLength: int32(contentLen % int64(blockLength)),
34- BlockLength: blockLength,
35- ChecksumLength: checksumLength,
36- }
37-}
+0, -109
......@@ -1,109 +0,0 @@
1-package rsyncreceiver
2-
3-import (
4- "fmt"
5- "io"
6- "log/slog"
7- "os"
8-
9- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
10- "github.com/picosh/pico/pkg/rsync-receiver/rsyncopts"
11- "github.com/picosh/pico/pkg/rsync-receiver/rsyncsender"
12- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
13- "github.com/picosh/pico/pkg/rsync-receiver/utils"
14-)
15-
16-func ClientRun(logger *slog.Logger, opts *rsyncopts.Options, conn io.ReadWriter, filesystem utils.FS, paths []string, negotiate bool) error {
17- var err error
18-
19- crd, cwr := rsyncwire.CounterPair(conn, conn)
20-
21- const sessionChecksumSeed = 666
22-
23- c := &rsyncwire.Conn{
24- Reader: crd,
25- Writer: cwr,
26- }
27-
28- if negotiate {
29- remoteProtocol, err := c.ReadInt32()
30- if err != nil {
31- return err
32- }
33- logger.Debug("remote protocol", "protocol", remoteProtocol)
34- if err := c.WriteInt32(rsync.ProtocolVersion); err != nil {
35- return err
36- }
37- }
38-
39- if err := c.WriteInt32(sessionChecksumSeed); err != nil {
40- return err
41- }
42-
43- // Switch to multiplexing protocol, but only for server-side transmissions.
44- // Transmissions received from the client are not multiplexed.
45- mpx := &rsyncwire.MultiplexWriter{Writer: c.Writer}
46- c.Writer = mpx
47-
48- defer func() {
49- if err != nil {
50- _, _ = mpx.WriteMsg(rsyncwire.MsgError, fmt.Appendf(nil, "gokr-rsync [receiver]: %v\n", err))
51- }
52- }()
53-
54- rt := &Transfer{
55- Opts: &TransferOpts{
56- DryRun: opts.DryRun(),
57-
58- DeleteMode: opts.DeleteMode(),
59- PreserveGid: opts.PreserveGid(),
60- PreserveUid: opts.PreserveUid(),
61- PreserveLinks: opts.PreserveLinks(),
62- PreservePerms: opts.PreservePerms(),
63- PreserveDevices: opts.PreserveDevices(),
64- PreserveSpecials: opts.PreserveSpecials(),
65- PreserveTimes: opts.PreserveMTimes(),
66- IgnoreTimes: opts.IgnoreTimes(),
67- SizeOnly: opts.SizeOnly(),
68- AlwaysChecksum: opts.AlwaysChecksum(),
69- // TODO: PreserveHardlinks: opts.PreserveHardlinks,
70- },
71- Dest: "/",
72- // TODO: what is Env used for and can we get rid of it?
73- Env: Osenv{
74- Stdout: os.Stdout,
75- Stderr: os.Stderr,
76- Stdin: os.Stdin,
77- },
78- Conn: c,
79- Seed: sessionChecksumSeed,
80-
81- Files: filesystem,
82-
83- Logger: logger,
84- }
85-
86- if opts.DeleteMode() {
87- // receive the exclusion list (openrsync’s is always empty)
88- exclusionList, err := rsyncsender.RecvFilterList(c)
89- if err != nil {
90- return err
91- }
92- logger.Debug("exclusion list read", "filters", exclusionList.Filters)
93- }
94-
95- // receive file list
96- logger.Debug("receiving file list")
97- fileList, err := rt.ReceiveFileList()
98- if err != nil {
99- return err
100- }
101- logger.Debug("received names", "files", fileList)
102- stats, err := rt.Do(c, fileList, true)
103- if err != nil {
104- return err
105- }
106-
107- logger.Debug("stats", "stats", stats)
108- return nil
109-}
+0, -94
......@@ -1,94 +0,0 @@
1-package rsyncreceiver
2-
3-import (
4- "context"
5-
6- "github.com/picosh/pico/pkg/rsync-receiver/rsyncstats"
7- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
8- "github.com/picosh/pico/pkg/rsync-receiver/utils"
9- "golang.org/x/sync/errgroup"
10-)
11-
12-func (rt *Transfer) deleteFiles(fileList []*utils.ReceiverFile) error {
13- if rt.IOErrors > 0 {
14- rt.Logger.Debug("IO error encountered, skipping file deletion")
15- return nil
16- }
17-
18- return rt.Files.Remove(fileList)
19-}
20-
21-// rsync/main.c:do_recv.
22-func (rt *Transfer) Do(c *rsyncwire.Conn, fileList []*utils.ReceiverFile, noReport bool) (*rsyncstats.TransferStats, error) {
23- if rt.Opts.DeleteMode {
24- if err := rt.deleteFiles(fileList); err != nil {
25- return nil, err
26- }
27- }
28-
29- ctx := context.Background()
30- eg, ctx := errgroup.WithContext(ctx)
31- eg.Go(func() error {
32- return rt.GenerateFiles(fileList)
33- })
34- eg.Go(func() error {
35- // Ensure we don’t block on the receiver when the generator returns an
36- // error.
37- errChan := make(chan error)
38- go func() {
39- errChan <- rt.RecvFiles(fileList)
40- }()
41- select {
42- case <-ctx.Done():
43- return ctx.Err()
44- case err := <-errChan:
45- return err
46- }
47- })
48- if err := eg.Wait(); err != nil {
49- return nil, err
50- }
51-
52- var stats *rsyncstats.TransferStats
53- if !noReport {
54- var err error
55- stats, err = report(c)
56- rt.Logger.Debug("report", "stats", stats)
57- if err != nil {
58- return nil, err
59- }
60- }
61-
62- // send final goodbye message
63- if err := c.WriteInt32(-1); err != nil {
64- return nil, err
65- }
66-
67- return stats, nil
68-}
69-
70-// rsync/main.c:report.
71-func report(c *rsyncwire.Conn) (*rsyncstats.TransferStats, error) {
72- // read statistics:
73- // total bytes read (from network connection)
74- read, err := c.ReadInt64()
75- if err != nil {
76- return nil, err
77- }
78- // total bytes written (to network connection)
79- written, err := c.ReadInt64()
80- if err != nil {
81- return nil, err
82- }
83- // total size of files
84- size, err := c.ReadInt64()
85- if err != nil {
86- return nil, err
87- }
88-
89- return &rsyncstats.TransferStats{
90- Read: read,
91- Written: written,
92- Size: size,
93- }, nil
94-}
+0, -191
......@@ -1,191 +0,0 @@
1-package rsyncreceiver
2-
3-import (
4- "fmt"
5- "io"
6- "path/filepath"
7- "time"
8-
9- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
10- "github.com/picosh/pico/pkg/rsync-receiver/utils"
11-)
12-
13-// rsync/flist.c:receive_file_entry.
14-func (rt *Transfer) receiveFileEntry(flags uint16, last *utils.ReceiverFile) (*utils.ReceiverFile, error) {
15- f := &utils.ReceiverFile{}
16-
17- var l1 int
18- if flags&rsync.XMIT_SAME_NAME != 0 {
19- l, err := rt.Conn.ReadByte()
20- if err != nil {
21- return nil, err
22- }
23- l1 = int(l)
24- }
25-
26- var l2 int
27- if flags&rsync.XMIT_LONG_NAME != 0 {
28- l, err := rt.Conn.ReadInt32()
29- if err != nil {
30- return nil, err
31- }
32- l2 = int(l)
33- } else {
34- l, err := rt.Conn.ReadByte()
35- if err != nil {
36- return nil, err
37- }
38- l2 = int(l)
39- }
40- // linux/limits.h
41- const PATH_MAX = 4096
42- if l2 >= PATH_MAX-l1 {
43- const lastname = ""
44- return nil, fmt.Errorf("overflow: flags=0x%x l1=%d l2=%d lastname=%s",
45- flags, l1, l2, lastname)
46- }
47- b := make([]byte, l1+l2)
48- readb := b
49- if l1 > 0 {
50- copy(b, []byte(last.Name))
51- readb = b[l1:]
52- }
53- if _, err := io.ReadFull(rt.Conn.Reader, readb); err != nil {
54- return nil, err
55- }
56- // TODO: does rsync’s clean_fname() and sanitize_path() combination do
57- // anything more than Go’s filepath.Clean()?
58- f.Name = filepath.Clean(string(b))
59-
60- length, err := rt.Conn.ReadInt64()
61- if err != nil {
62- return nil, err
63- }
64- f.Length = length
65-
66- if flags&rsync.XMIT_SAME_TIME != 0 {
67- f.ModTime = last.ModTime
68- } else {
69- modTime, err := rt.Conn.ReadInt32()
70- if err != nil {
71- return nil, err
72- }
73- f.ModTime = time.Unix(int64(modTime), 0)
74- }
75-
76- if flags&rsync.XMIT_SAME_MODE != 0 {
77- f.Mode = last.Mode
78- } else {
79- mode, err := rt.Conn.ReadInt32()
80- if err != nil {
81- return nil, err
82- }
83- f.Mode = mode
84- }
85-
86- if rt.Opts.PreserveUid {
87- if flags&rsync.XMIT_SAME_UID != 0 {
88- f.Uid = last.Uid
89- } else {
90- uid, err := rt.Conn.ReadInt32()
91- if err != nil {
92- return nil, err
93- }
94- f.Uid = uid
95- }
96- }
97-
98- if rt.Opts.PreserveGid {
99- if flags&rsync.XMIT_SAME_GID != 0 {
100- f.Gid = last.Gid
101- } else {
102- gid, err := rt.Conn.ReadInt32()
103- if err != nil {
104- return nil, err
105- }
106- f.Gid = gid
107- }
108- }
109-
110- mode := f.Mode & rsync.S_IFMT
111- isDev := mode == rsync.S_IFCHR || mode == rsync.S_IFBLK
112- isSpecial := mode == rsync.S_IFIFO || mode == rsync.S_IFSOCK
113- isLink := mode == rsync.S_IFLNK
114-
115- if rt.Opts.PreserveDevices && (isDev || isSpecial) {
116- // TODO(protocol >= 28): rdev/major/minor handling
117- if flags&rsync.XMIT_SAME_RDEV_pre28 != 0 {
118- f.Rdev = last.Rdev
119- } else {
120- rdev, err := rt.Conn.ReadInt32()
121- if err != nil {
122- return nil, err
123- }
124- f.Rdev = rdev
125- }
126- }
127-
128- if rt.Opts.PreserveLinks && isLink {
129- length, err := rt.Conn.ReadInt32()
130- if err != nil {
131- return nil, err
132- }
133- b := make([]byte, length)
134- if _, err := io.ReadFull(rt.Conn.Reader, b); err != nil {
135- return nil, err
136- }
137- f.LinkTarget = string(b)
138- }
139-
140- return f, nil
141-}
142-
143-// rsync/flist.c:recv_file_list.
144-func (rt *Transfer) ReceiveFileList() ([]*utils.ReceiverFile, error) {
145- lastFileEntry := new(utils.ReceiverFile)
146- var fileList []*utils.ReceiverFile
147- for {
148- b, err := rt.Conn.ReadByte()
149- if err != nil {
150- return nil, err
151- }
152- if b == 0 {
153- break
154- }
155- flags := uint16(b)
156- // log.Printf("flags: %x", flags)
157- // TODO(protocol >= 28): extended flags
158-
159- f, err := rt.receiveFileEntry(flags, lastFileEntry)
160- if err != nil {
161- return nil, err
162- }
163- lastFileEntry = f
164- // TODO: include depth in output?
165- rt.Logger.Debug("recv_file_list", "file", f.Name, "length", f.Length, "mode", f.Mode, "uid", f.Uid, "gid", f.Gid, "flags", flags)
166-
167- fileList = append(fileList, f)
168- }
169-
170- utils.SortFileList(fileList)
171-
172- if rt.Opts.PreserveUid || rt.Opts.PreserveGid {
173- // receive the uid/gid list
174- users, groups, err := rt.RecvIdList()
175- if err != nil {
176- return nil, err
177- }
178- _ = users
179- _ = groups
180- }
181-
182- // read the i/o error flag
183- ioErrors, err := rt.Conn.ReadInt32()
184- if err != nil {
185- return nil, err
186- }
187- rt.Logger.Debug("ioErrors", "errs", ioErrors)
188- rt.IOErrors = ioErrors
189-
190- return fileList, nil
191-}
+0, -156
......@@ -1,156 +0,0 @@
1-package rsyncreceiver
2-
3-import (
4- "fmt"
5- "io"
6- "os"
7-
8- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
9- "github.com/picosh/pico/pkg/rsync-receiver/rsyncchecksum"
10- "github.com/picosh/pico/pkg/rsync-receiver/rsynccommon"
11- "github.com/picosh/pico/pkg/rsync-receiver/utils"
12-)
13-
14-// rsync/generator.c:generate_files().
15-func (rt *Transfer) GenerateFiles(fileList []*utils.ReceiverFile) error {
16- phase := 0
17- for idx, f := range fileList {
18- // TODO: use a copy of f with .Mode |= S_IWUSR for directories, so
19- // that we can create files within all directories.
20- if err := rt.recvGenerator(idx, f); err != nil {
21- return err
22- }
23- }
24- phase++
25- rt.Logger.Debug("generateFiles", "phase", phase)
26- if err := rt.Conn.WriteInt32(-1); err != nil {
27- return err
28- }
29-
30- // TODO: re-do any files that failed
31- phase++
32- rt.Logger.Debug("generateFiles", "phase", phase)
33- if err := rt.Conn.WriteInt32(-1); err != nil {
34- return err
35- }
36-
37- rt.Logger.Debug("generateFiles finished")
38- return nil
39-}
40-
41-// rsync/generator.c:skip_file.
42-func (rt *Transfer) skipFile(f *utils.ReceiverFile, st os.FileInfo) (bool, error) {
43- if rt.Opts.AlwaysChecksum || rt.Opts.IgnoreTimes {
44- return false, nil
45- }
46-
47- sizeMatch := st.Size() == f.Length
48- if rt.Opts.SizeOnly {
49- return sizeMatch, nil
50- }
51-
52- timeMatch := st.ModTime().Equal(f.ModTime)
53- return sizeMatch && timeMatch, nil
54-}
55-
56-// rsync/generator.c:recv_generator.
57-func (rt *Transfer) recvGenerator(idx int, f *utils.ReceiverFile) error {
58- if rt.listOnly() {
59- if _, err := fmt.Fprintf(rt.Env.Stdout, "%s %11.0f %s %s\n",
60- f.FileMode().String(),
61- float64(f.Length), // TODO: rsync prints decimal separators
62- f.ModTime.Format("2006/01/02 15:04:05"),
63- f.Name); err != nil {
64- return err
65- }
66- return nil
67- }
68- rt.Logger.Debug("recv_generator", "file", f)
69-
70- if !f.FileMode().IsRegular() {
71- // None of the Preserve* options is enabled, so just skip over
72- // non-regular files.
73- return nil
74- }
75-
76- requestFullFile := func() error {
77- rt.Logger.Debug("requesting", "file", f)
78- if err := rt.Conn.WriteInt32(int32(idx)); err != nil {
79- return err
80- }
81- if rt.Opts.DryRun {
82- return nil
83- }
84- var sh rsync.SumHead
85- if err := sh.WriteTo(rt.Conn); err != nil {
86- return err
87- }
88- return nil
89- }
90-
91- st, in, err := rt.Files.Read(&utils.SenderFile{WPath: f.Name})
92- if err != nil {
93- rt.Logger.Error("failed to open file", "st", st, "file", f, "err", err)
94- return requestFullFile()
95- }
96-
97- defer func() { _ = in.Close() }()
98-
99- skip, err := rt.skipFile(f, st)
100- if err != nil {
101- return err
102- }
103-
104- if skip {
105- rt.Logger.Debug("skipping", "file", f)
106- return nil
107- }
108-
109- if rt.Opts.DryRun {
110- if err := rt.Conn.WriteInt32(int32(idx)); err != nil {
111- return err
112- }
113-
114- return nil
115- }
116-
117- rt.Logger.Debug("sending sums", "file", f, "st", st)
118- if err := rt.Conn.WriteInt32(int32(idx)); err != nil {
119- return err
120- }
121-
122- err = rt.generateAndSendSums(in, st.Size())
123- if err != nil {
124- rt.Logger.Error("failed to send sums", "file", f, "err", err)
125- }
126-
127- return err
128-}
129-
130-// rsync/generator.c:generate_and_send_sums.
131-func (rt *Transfer) generateAndSendSums(in utils.ReaderAtCloser, fileLen int64) error {
132- sh := rsynccommon.SumSizesSqroot(fileLen)
133- if err := sh.WriteTo(rt.Conn); err != nil {
134- return err
135- }
136- buf := make([]byte, int(sh.BlockLength))
137- remaining := fileLen
138- for i := int32(0); i < sh.ChecksumCount; i++ {
139- n1 := min(int64(sh.BlockLength), remaining)
140- b := buf[:n1]
141- if _, err := io.ReadFull(in, b); err != nil {
142- return err
143- }
144-
145- sum1 := rsyncchecksum.Checksum1(b)
146- sum2 := rsyncchecksum.Checksum2(rt.Seed, b)
147- if err := rt.Conn.WriteInt32(int32(sum1)); err != nil {
148- return err
149- }
150- if _, err := rt.Conn.Writer.Write(sum2); err != nil {
151- return err
152- }
153- remaining -= n1
154- }
155- return nil
156-}
+0, -170
......@@ -1,170 +0,0 @@
1-package rsyncreceiver
2-
3-import (
4- "bytes"
5- "encoding/binary"
6- "errors"
7- "fmt"
8- "io"
9- "sync"
10-
11- "github.com/mmcloughlin/md4"
12- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
13- "github.com/picosh/pico/pkg/rsync-receiver/utils"
14-)
15-
16-// rsync/receiver.c:recv_files.
17-func (rt *Transfer) RecvFiles(fileList []*utils.ReceiverFile) error {
18- phase := 0
19- for {
20- idx, err := rt.Conn.ReadInt32()
21- if err != nil {
22- return err
23- }
24- if idx == -1 {
25- if phase == 0 {
26- phase++
27- rt.Logger.Debug("recvFiles phase", "phase", phase)
28- // TODO: send done message
29- continue
30- }
31- break
32- }
33- rt.Logger.Debug("receiving file", "idx", idx, "file", fileList[idx])
34- if err := rt.recvFile1(fileList[idx]); err != nil {
35- return err
36- }
37- }
38- rt.Logger.Debug("recvFiles finished")
39- return nil
40-}
41-
42-func (rt *Transfer) recvFile1(f *utils.ReceiverFile) error {
43- if rt.Opts.DryRun {
44- fmt.Println(f.Name)
45- return nil
46- }
47-
48- localFile, err := rt.openLocalFile(f)
49- if err != nil {
50- rt.Logger.Error("opening local file failed, continuing", "err", err, "file", f)
51- } else {
52- defer func() { _ = localFile.Close() }()
53- }
54-
55- err = rt.receiveData(f, localFile)
56- if err != nil {
57- rt.Logger.Error("receiving data failed, continuing", "err", err, "file", f)
58- }
59- return err
60-}
61-
62-func (rt *Transfer) openLocalFile(f *utils.ReceiverFile) (utils.ReaderAtCloser, error) {
63- _, r, err := rt.Files.Read(&utils.SenderFile{
64- WPath: f.Name,
65- Regular: true,
66- })
67-
68- if err != nil {
69- return nil, err
70- }
71-
72- return r, nil
73-}
74-
75-// rsync/receiver.c:receive_data.
76-func (rt *Transfer) receiveData(f *utils.ReceiverFile, localFile utils.ReaderAtCloser) error {
77- var sh rsync.SumHead
78- if err := sh.ReadFrom(rt.Conn); err != nil {
79- return err
80- }
81-
82- r, w := io.Pipe()
83-
84- f.Reader = r
85-
86- var wg sync.WaitGroup
87- wg.Add(1)
88-
89- go func() {
90- defer func() {
91- wg.Done()
92- if err := r.Close(); err != nil {
93- return
94- }
95- }()
96-
97- _, err := rt.Files.Put(f)
98- if err != nil {
99- return
100- }
101- }()
102-
103- h := md4.New()
104- _ = binary.Write(h, binary.LittleEndian, rt.Seed) // hash.Hash.Write never fails
105-
106- for {
107- token, data, err := rt.recvToken()
108- if err != nil {
109- return err
110- }
111- if token == 0 {
112- break
113- }
114- if token > 0 {
115- if _, err := h.Write(data); err != nil {
116- return err
117- }
118-
119- if _, err := w.Write(data); err != nil {
120- if errors.Is(err, io.ErrClosedPipe) {
121- continue
122- }
123- return err
124- }
125- continue
126- }
127- if localFile == nil {
128- return fmt.Errorf("BUG: local file %s not open for copying chunk", localFile)
129- }
130- token = -(token + 1)
131- offset2 := int64(token) * int64(sh.BlockLength)
132- dataLen := sh.BlockLength
133- if token == sh.ChecksumCount-1 && sh.RemainderLength != 0 {
134- dataLen = sh.RemainderLength
135- }
136- data = make([]byte, dataLen)
137- if _, err := localFile.ReadAt(data, offset2); err != nil {
138- return err
139- }
140-
141- if _, err := h.Write(data); err != nil {
142- return err
143- }
144-
145- if _, err := w.Write(data); err != nil {
146- if errors.Is(err, io.ErrClosedPipe) {
147- continue
148- }
149- return err
150- }
151- }
152-
153- if err := w.Close(); err != nil {
154- return err
155- }
156-
157- wg.Wait()
158-
159- localSum := h.Sum(nil)
160- remoteSum := make([]byte, len(localSum))
161- if _, err := io.ReadFull(rt.Conn.Reader, remoteSum); err != nil {
162- return err
163- }
164- if !bytes.Equal(localSum, remoteSum) {
165- return fmt.Errorf("file corruption in %s", f.Name)
166- }
167- rt.Logger.Debug("checksum matches!", "localSum", localSum)
168-
169- return nil
170-}
+0, -20
......@@ -1,20 +0,0 @@
1-package rsyncreceiver
2-
3-import "io"
4-
5-// rsync/token.c:recvToken.
6-func (rt *Transfer) recvToken() (token int32, data []byte, _ error) {
7- var err error
8- token, err = rt.Conn.ReadInt32()
9- if err != nil {
10- return 0, nil, err
11- }
12- if token <= 0 {
13- return token, nil, nil
14- }
15- data = make([]byte, int(token))
16- if _, err := io.ReadFull(rt.Conn.Reader, data); err != nil {
17- return 0, nil, err
18- }
19- return token, data, nil
20-}
+0, -53
......@@ -1,53 +0,0 @@
1-package rsyncreceiver
2-
3-import (
4- "io"
5- "log/slog"
6-
7- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
8- "github.com/picosh/pico/pkg/rsync-receiver/utils"
9-)
10-
11-type Osenv struct {
12- Stdin io.Reader
13- Stdout io.Writer
14- Stderr io.Writer
15-}
16-
17-// TransferOpts is a subset of Opts which is required for implementing a receiver.
18-type TransferOpts struct {
19- Verbose bool
20- DryRun bool
21-
22- DeleteMode bool
23- PreserveGid bool
24- PreserveUid bool
25- PreserveLinks bool
26- PreservePerms bool
27- PreserveDevices bool
28- PreserveSpecials bool
29- PreserveTimes bool
30- PreserveHardlinks bool
31- IgnoreTimes bool
32- SizeOnly bool
33- AlwaysChecksum bool
34-}
35-
36-type Transfer struct {
37- // config
38- // Opts *Opts
39- Opts *TransferOpts
40- Dest string
41- Env Osenv
42-
43- // state
44- Conn *rsyncwire.Conn
45- Seed int32
46- IOErrors int32
47-
48- Files utils.FS
49-
50- Logger *slog.Logger
51-}
52-
53-func (rt *Transfer) listOnly() bool { return rt.Dest == "" }
+0, -69
......@@ -1,69 +0,0 @@
1-package rsyncreceiver
2-
3-import (
4- "io"
5-)
6-
7-type mapping struct {
8- Name string
9- LocalId int32
10-}
11-
12-func (rt *Transfer) recvIdMapping1(localId func(id int32, name string) int32) (map[int32]mapping, error) {
13- idMapping := make(map[int32]mapping)
14- for {
15- id, err := rt.Conn.ReadInt32()
16- if err != nil {
17- return nil, err
18- }
19- if id == 0 {
20- break
21- }
22- length, err := rt.Conn.ReadByte()
23- if err != nil {
24- return nil, err
25- }
26- name := make([]byte, length)
27- if _, err := io.ReadFull(rt.Conn.Reader, name); err != nil {
28- return nil, err
29- }
30- idMapping[id] = mapping{
31- Name: string(name),
32- LocalId: localId(id, string(name)),
33- }
34- }
35- return idMapping, nil
36-}
37-
38-// rsync/uidlist.c:recv_id_list.
39-func (rt *Transfer) RecvIdList() (users map[int32]mapping, groups map[int32]mapping, _ error) {
40- if rt.Opts.PreserveUid {
41- var err error
42- users, err = rt.recvIdMapping1(func(remoteUid int32, remoteUsername string) int32 {
43- // TODO: look up local uid by username
44- return remoteUid
45- })
46- if err != nil {
47- return nil, nil, err
48- }
49- for remoteUid, mapping := range users {
50- rt.Logger.Debug("remote uid maps to local uid", "remoteUid", remoteUid, "name", mapping.Name, "localUid", mapping.LocalId)
51- }
52- }
53-
54- if rt.Opts.PreserveGid {
55- var err error
56- groups, err = rt.recvIdMapping1(func(remoteGid int32, remoteGroupname string) int32 {
57- // TODO: look up local gid by groupname
58- return remoteGid
59- })
60- if err != nil {
61- return nil, nil, err
62- }
63- for remoteGid, mapping := range groups {
64- rt.Logger.Debug("remote gid maps to local gid", "remoteGid", remoteGid, "name", mapping.Name, "localGid", mapping.LocalId)
65- }
66- }
67-
68- return users, groups, nil
69-}
+0, -75
......@@ -1,75 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "fmt"
5- "io"
6- "log/slog"
7-
8- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
9- "github.com/picosh/pico/pkg/rsync-receiver/rsyncopts"
10- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
11- "github.com/picosh/pico/pkg/rsync-receiver/utils"
12-)
13-
14-func ClientRun(logger *slog.Logger, opts *rsyncopts.Options, conn io.ReadWriter, filesystem utils.FS, paths []string, negotiate bool) error {
15- var err error
16-
17- crd, cwr := rsyncwire.CounterPair(conn, conn)
18-
19- const sessionChecksumSeed = 666
20-
21- c := &rsyncwire.Conn{
22- Reader: crd,
23- Writer: cwr,
24- }
25-
26- if negotiate {
27- remoteProtocol, err := c.ReadInt32()
28- if err != nil {
29- return err
30- }
31- logger.Debug("remote protocol", "remoteProtocol", remoteProtocol)
32- if err := c.WriteInt32(rsync.ProtocolVersion); err != nil {
33- return err
34- }
35- }
36-
37- if err := c.WriteInt32(sessionChecksumSeed); err != nil {
38- return err
39- }
40-
41- // Switch to multiplexing protocol, but only for server-side transmissions.
42- // Transmissions received from the client are not multiplexed.
43- mpx := &rsyncwire.MultiplexWriter{Writer: c.Writer}
44- c.Writer = mpx
45-
46- defer func() {
47- if err != nil {
48- _, _ = mpx.WriteMsg(rsyncwire.MsgError, fmt.Appendf(nil, "gokr-rsync [sender]: %v\n", err))
49- }
50- }()
51-
52- st := &Transfer{
53- Opts: opts,
54- Conn: c,
55- Seed: sessionChecksumSeed,
56- Files: filesystem,
57-
58- Logger: logger,
59- }
60- // receive the exclusion list (openrsync’s is always empty)
61- exclusionList, err := RecvFilterList(st.Conn)
62- if err != nil {
63- return err
64- }
65- logger.Debug("exclusion list read", "filters", exclusionList.Filters)
66-
67- stats, err := st.Do(crd, cwr, paths, exclusionList)
68- if err != nil {
69- return err
70- }
71-
72- logger.Debug("handleConnSender done. stats", "stats", stats)
73-
74- return err
75-}
+0, -68
......@@ -1,68 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "fmt"
5- "sort"
6-
7- "github.com/picosh/pico/pkg/rsync-receiver/rsyncstats"
8- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
9-)
10-
11-// rsync/main.c:client_run am_sender.
12-func (st *Transfer) Do(crd *rsyncwire.CountingReader, cwr *rsyncwire.CountingWriter, paths []string, exclusionList *filterRuleList) (*rsyncstats.TransferStats, error) {
13- if exclusionList == nil {
14- exclusionList = &filterRuleList{}
15- }
16-
17- // “Update exchange” as per
18- // https://github.com/kristapsdz/openrsync/blob/master/rsync.5
19-
20- // send file list
21- fileList, err := st.SendFileList(st.Opts, paths, exclusionList)
22- if err != nil {
23- return nil, err
24- }
25-
26- st.Logger.Debug("file list sent")
27-
28- // Sort the file list. The client sorts, so we need to sort, too (in the
29- // same way!), otherwise our indices do not match what the client will
30- // request.
31- sort.Slice(fileList.Files, func(i, j int) bool {
32- return fileList.Files[i].WPath < fileList.Files[j].WPath
33- })
34-
35- if err := st.SendFiles(fileList); err != nil {
36- return nil, err
37- }
38-
39- // send statistics:
40- // total bytes read (from network connection)
41- if err := st.Conn.WriteInt64(crd.BytesRead); err != nil {
42- return nil, err
43- }
44- // total bytes written (to network connection)
45- if err := st.Conn.WriteInt64(cwr.BytesWritten); err != nil {
46- return nil, err
47- }
48- // total size of files
49- if err := st.Conn.WriteInt64(fileList.TotalSize); err != nil {
50- return nil, err
51- }
52-
53- st.Logger.Debug("reading final int32")
54-
55- finish, err := st.Conn.ReadInt32()
56- if err != nil {
57- return nil, err
58- }
59- if finish != -1 {
60- return nil, fmt.Errorf("protocol error: expected final -1, got %d", finish)
61- }
62-
63- return &rsyncstats.TransferStats{
64- Read: crd.BytesRead,
65- Written: cwr.BytesWritten,
66- Size: fileList.TotalSize,
67- }, nil
68-}
+0, -109
......@@ -1,109 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "io"
5- "path/filepath"
6- "strings"
7-
8- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
9-)
10-
11-type filterRuleList struct {
12- Filters []*filterRule
13-}
14-
15-// exclude.c:add_rule.
16-func (l *filterRuleList) addRule(fr *filterRule) {
17- if strings.HasSuffix(fr.pattern, "/") {
18- fr.flag |= filtruleDirectory
19- fr.pattern = strings.TrimSuffix(fr.pattern, "/")
20- }
21- if strings.ContainsFunc(fr.pattern, func(r rune) bool {
22- return r == '*' || r == '[' || r == '?'
23- }) {
24- fr.flag |= filtruleWild
25- }
26- l.Filters = append(l.Filters, fr)
27-}
28-
29-// exclude.c:check_filter.
30-func (l *filterRuleList) matches(name string) bool {
31- for _, fr := range l.Filters {
32- if fr.matches(name) {
33- return true
34- }
35- }
36- return false
37-}
38-
39-// exclude.c:recv_filter_list.
40-func RecvFilterList(c *rsyncwire.Conn) (*filterRuleList, error) {
41- var l filterRuleList
42- const exclusionListEnd = 0
43- for {
44- length, err := c.ReadInt32()
45- if err != nil {
46- return nil, err
47- }
48- if length == exclusionListEnd {
49- break
50- }
51- line := make([]byte, length)
52- if _, err := io.ReadFull(c.Reader, line); err != nil {
53- return nil, err
54- }
55- fr, err := parseFilter(string(line))
56- if err != nil {
57- return nil, err
58- }
59- l.addRule(fr)
60- }
61- return &l, nil
62-}
63-
64-const (
65- filtruleInclude = 1 << iota
66- filtruleClearList
67- filtruleDirectory
68- filtruleWild
69-)
70-
71-type filterRule struct {
72- flag int
73- pattern string
74-}
75-
76-// exclude.c:rule_matches.
77-func (fr *filterRule) matches(name string) bool {
78- if fr.flag&filtruleWild != 0 {
79- panic("wildcard filter rules not yet implemented")
80- }
81- if !strings.ContainsRune(fr.pattern, '/') &&
82- fr.flag&filtruleWild == 0 {
83- name = filepath.Base(name)
84- }
85- return fr.pattern == name
86-}
87-
88-// exclude.c:parse_filter_str / exclude.c:parse_rule_tok.
89-func parseFilter(line string) (*filterRule, error) {
90- rule := new(filterRule)
91-
92- // We only support what rsync calls XFLG_OLD_PREFIXES
93- if strings.HasPrefix(line, "- ") {
94- // clear include flag
95- rule.flag &= ^filtruleInclude
96- line = strings.TrimPrefix(line, "- ")
97- } else if strings.HasPrefix(line, "+ ") {
98- // set include flag
99- rule.flag |= filtruleInclude
100- line = strings.TrimPrefix(line, "+ ")
101- } else if strings.HasPrefix(line, "!") {
102- // set clear_list flag
103- rule.flag |= filtruleClearList
104- }
105-
106- rule.pattern = line
107-
108- return rule, nil
109-}
+0, -116
......@@ -1,116 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "io"
5- "log/slog"
6- "os"
7-)
8-
9-// rsync.h:map_struct.
10-type mapStruct struct {
11- fileSize int64 // file size (from stat)
12- pOffset int64 // window start
13- pFdOffset int64 // offset of cursor in fd ala lseek
14- window []byte // window pointer
15- pSize int64 // largest window we allocated
16- pLen int64 // latest (rounded) window size
17- defWindowSize int64 // default window size
18- f *os.File // file descriptor
19- err error // first read error
20-}
21-
22-const alignBoundary = 1024
23-
24-func alignedLength(l int64) int64 {
25- return ((l - 1) | (alignBoundary - 1)) + 1
26-}
27-
28-func alignedOvershoot(off int64) int64 {
29- return off & (alignBoundary - 1)
30-}
31-
32-func mapFile(f *os.File, len int64, readSize int32, blkSize int32) *mapStruct {
33- if blkSize > 0 && readSize%blkSize != 0 {
34- readSize += blkSize - (readSize % blkSize)
35- }
36- return &mapStruct{
37- fileSize: len,
38- defWindowSize: alignedLength(int64(readSize)),
39- f: f,
40- }
41-}
42-
43-func (ms *mapStruct) ptr(offset int64, l int32) []byte {
44- //log.Printf("ptr(offset=%d, l=%d)", offset, l)
45- len := int64(l)
46- if len == 0 {
47- return nil
48- }
49- if len < 0 {
50- slog.Debug("BUG: invalid len", "len", len)
51- return nil
52- }
53-
54- if offset >= ms.pOffset && offset+int64(len) <= ms.pOffset+int64(ms.pLen) {
55- //log.Printf("-> already available")
56- // region already available
57- off := offset - ms.pOffset
58- return ms.window[off : off+int64(len)]
59- }
60-
61- alignFudge := alignedOvershoot(offset)
62- windowStart := offset - alignFudge
63- windowSize := int64(ms.defWindowSize)
64- if windowStart+windowSize > ms.fileSize {
65- windowSize = ms.fileSize - windowStart
66- }
67- if windowSize < len+alignFudge {
68- windowSize = alignedLength(len + alignFudge)
69- }
70- if windowSize > ms.pSize {
71- win := make([]byte, windowSize)
72- copy(win, ms.window)
73- ms.window = win
74- ms.pSize = windowSize
75- }
76- readStart := windowStart
77- readSize := windowSize
78- readOffset := int64(0)
79-
80- //log.Printf("windowSize: %d, ms=%+v", windowSize, ms)
81- if windowStart >= ms.pOffset && windowStart < ms.pOffset+ms.pLen &&
82- windowStart+windowSize >= ms.pOffset+ms.pLen {
83- readStart = ms.pOffset + ms.pLen
84- readOffset = readStart - windowStart
85- readSize = windowSize - readOffset
86- off := ms.pLen - readOffset
87- copy(ms.window[:], ms.window[off:off+readOffset])
88- }
89- if readSize <= 0 {
90- slog.Debug("BUG: invalid readSize", "readSize", readSize)
91- return nil
92- }
93- if ms.pFdOffset != readStart {
94- if _, err := ms.f.Seek(readStart, io.SeekStart); err != nil {
95- slog.Error("seek error", "err", err)
96- return nil
97- }
98- ms.pFdOffset = readStart
99- }
100- ms.pOffset = windowStart
101- ms.pLen = windowSize
102- //log.Printf("-> reading %d bytes from %d into buffer at offset=%d", readSize, readStart, readOffset)
103- for readSize > 0 {
104- n, err := ms.f.Read(ms.window[readOffset : readOffset+readSize])
105- if err != nil {
106- ms.err = err
107- // TODO: zero the buffer, file has changed mid-transfer
108- slog.Debug("file has changed mid-transfer")
109- return nil
110- }
111- ms.pFdOffset += int64(n)
112- readOffset += int64(n)
113- readSize -= int64(n)
114- }
115- return ms.window[alignFudge : alignFudge+len]
116-}
+0, -236
......@@ -1,236 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "os"
5- "os/user"
6- "strconv"
7- "sync"
8-
9- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
10- "github.com/picosh/pico/pkg/rsync-receiver/rsyncchecksum"
11- "github.com/picosh/pico/pkg/rsync-receiver/rsyncopts"
12- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
13- "github.com/picosh/pico/pkg/rsync-receiver/utils"
14-)
15-
16-type fileList struct {
17- TotalSize int64
18- Files []utils.SenderFile
19-}
20-
21-// rsync/rsync.h defines chunkSize as 32 * 1024, but increasing it to 256K
22-// increases throughput with “tridge” rsync as client by 50 Mbit/s.
23-const chunkSize = 256 * 1024
24-
25-var (
26- lookupOnce sync.Once
27- lookupGroupOnce sync.Once
28-)
29-
30-// rsync/flist.c:send_file_list.
31-func (st *Transfer) SendFileList(opts *rsyncopts.Options, paths []string, excl *filterRuleList) (*fileList, error) {
32- var fileList fileList
33- fec := &rsyncwire.Buffer{}
34-
35- uidMap := make(map[int32]string)
36- gidMap := make(map[int32]string)
37-
38- // TODO: flush in between to keep the pipes filled when traversal takes long
39-
40- // TODO: handle info == nil case (permission denied?): should set an i/o
41- // error flag, but traversal should continue
42-
43- st.Logger.Debug("sendFileList()")
44- // TODO: handle |root| referring to an individual file, symlink or special (skip)
45- for _, requested := range paths {
46- files, err := st.Files.List(requested)
47- if err != nil {
48- return nil, err
49- }
50-
51- for _, info := range files {
52- // Only ever transmit long names, like openrsync
53- flags := byte(rsync.XMIT_LONG_NAME)
54-
55- // log.Printf("Trim(path=%q, %q) = %q", path, strip, name)
56- name := info.Name()
57- path := name
58- if name == "/" {
59- name = "."
60- flags |= rsync.XMIT_TOP_DIR
61- }
62- // log.Printf("flags for %q: %v", name, flags)
63-
64- if excl.matches(name) {
65- continue
66- }
67-
68- fileList.Files = append(fileList.Files, utils.SenderFile{
69- Path: "/",
70- Regular: info.Mode().IsRegular(),
71- WPath: name,
72- })
73-
74- // 1. status byte (integer)
75- _ = fec.WriteByte(flags)
76-
77- // 2. inherited filename length (optional, byte)
78- // 3. filename length (integer or byte)
79- fec.WriteInt32(int32(len(name)))
80-
81- // 4. file (byte array)
82- fec.WriteString(name)
83-
84- // 5. file length (long)
85- size := info.Size()
86- if info.Mode().IsDir() {
87- // tmpfs returns non-4K sizes for directories. Override with
88- // 4096 to make the tests succeed regardless of the /tmp file
89- // system type.
90- size = 4096
91- }
92- fec.WriteInt64(size)
93-
94- fileList.TotalSize += size
95-
96- // 6. file modification time (optional, integer)
97- // TODO: this will overflow in 2038! :(
98- fec.WriteInt32(int32(info.ModTime().Unix()))
99-
100- // 7. file mode (optional, mode_t, integer)
101- mode := int32(info.Mode() & os.ModePerm)
102- isDev := false
103- isSpecial := false
104- if info.Mode().IsDir() {
105- mode |= rsync.S_IFDIR
106- } else if info.Mode().IsRegular() {
107- mode |= rsync.S_IFREG
108- } else if info.Mode().Type()&os.ModeSymlink != 0 {
109- mode |= rsync.S_IFLNK
110- // TODO: skip symlink if PreserveSymlinks is not set
111- }
112-
113- if info.Mode().Type()&os.ModeCharDevice != 0 {
114- mode |= rsync.S_IFCHR
115- isDev = true
116- } else if info.Mode().Type()&os.ModeDevice != 0 {
117- mode |= rsync.S_IFBLK
118- isDev = true
119- }
120-
121- if info.Mode().Type()&os.ModeNamedPipe != 0 {
122- mode |= rsync.S_IFIFO
123- isSpecial = true
124- }
125-
126- if info.Mode().Type()&os.ModeSocket != 0 {
127- mode |= rsync.S_IFSOCK
128- isSpecial = true
129- }
130-
131- fec.WriteInt32(mode)
132-
133- if opts.PreserveUid() {
134- uid, ok := uidFromFileInfo(info)
135- if ok {
136- if _, ok := uidMap[uid]; !ok && uid != 0 {
137- u, err := user.LookupId(strconv.Itoa(int(uid)))
138- if err != nil {
139- lookupOnce.Do(func() {
140- st.Logger.Error("lookup", "uid", uid, "err", err)
141- })
142- } else {
143- uidMap[uid] = u.Username
144- }
145- }
146- }
147- // 8. if -o, the user id (integer)
148- fec.WriteInt32(uid)
149- }
150-
151- if opts.PreserveGid() {
152- gid, ok := gidFromFileInfo(info)
153- if ok {
154- if _, ok := gidMap[gid]; !ok && gid != 0 {
155- g, err := user.LookupGroupId(strconv.Itoa(int(gid)))
156- if err != nil {
157- lookupGroupOnce.Do(func() {
158- st.Logger.Error("lookupgroup", "gid", gid, "err", err)
159- })
160- } else {
161- gidMap[gid] = g.Name
162- }
163- }
164- }
165- // 9. if -g, the group id (integer)
166- fec.WriteInt32(gid)
167- }
168-
169- if (opts.PreserveDevices() && isDev) ||
170- (opts.PreserveSpecials() && isSpecial) {
171- // 10. if a special file and -D, the device “rdev” type (integer)
172- rdev, _ := rdevFromFileInfo(info)
173- fec.WriteInt32(rdev)
174- }
175-
176- if opts.PreserveLinks() && info.Mode().Type()&os.ModeSymlink != 0 {
177- // 11. if a symbolic link and -l, the link target's length (integer)
178- // 12. if a symbolic link and -l, the link target (byte array)
179- target, err := os.Readlink(path)
180- if err != nil {
181- continue
182- }
183- fec.WriteInt32(int32(len(target)))
184- fec.WriteString(target)
185- }
186-
187- if opts.AlwaysChecksum() {
188- var emptyChecksum [rsyncchecksum.Size]byte
189- checksum := emptyChecksum[:]
190- if info.Mode().IsRegular() {
191- // TODO: send md4 checksum of this file
192- checksum, err = rsyncchecksum.FileChecksum(path)
193- if err != nil {
194- continue
195- }
196- }
197- // For non-regular files, send empty md4 checksum
198- fec.WriteString(string(checksum))
199- }
200- }
201- if err != nil {
202- return nil, err
203- }
204- }
205-
206- const endOfFileList = 0
207- _ = fec.WriteByte(endOfFileList)
208-
209- const endOfSet = 0
210- if opts.PreserveUid() {
211- for uid, name := range uidMap {
212- fec.WriteInt32(uid)
213- _ = fec.WriteByte(byte(len(name)))
214- fec.WriteString(name)
215- }
216- fec.WriteInt32(endOfSet)
217- }
218-
219- if opts.PreserveGid() {
220- for gid, name := range gidMap {
221- fec.WriteInt32(gid)
222- _ = fec.WriteByte(byte(len(name)))
223- fec.WriteString(name)
224- }
225- fec.WriteInt32(endOfSet)
226- }
227-
228- const ioErrors = 0
229- fec.WriteInt32(ioErrors)
230-
231- if err := st.Conn.WriteString(fec.String()); err != nil {
232- return nil, err
233- }
234-
235- return &fileList, nil
236-}
+0, -257
......@@ -1,257 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "bytes"
5- "encoding/binary"
6- "fmt"
7- "hash"
8- "os"
9-
10- "github.com/mmcloughlin/md4"
11- "github.com/picosh/pico/pkg/rsync-receiver/nofollow"
12- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
13- "github.com/picosh/pico/pkg/rsync-receiver/rsyncchecksum"
14- "github.com/picosh/pico/pkg/rsync-receiver/utils"
15-)
16-
17-type target struct {
18- index int32
19- tag uint16
20-}
21-
22-// rsync/match.c:hash_search.
23-func (st *Transfer) hashSearch(targets []target, tagTable map[uint16]int, head rsync.SumHead, fileIndex int32, fl utils.SenderFile) error {
24- st.Logger.Debug("hashSearch", "file", fl, "head", head)
25- f, err := os.OpenFile(fl.Path, os.O_RDONLY|nofollow.Maybe, 0)
26- if err != nil {
27- return err
28- }
29- defer func() { _ = f.Close() }()
30-
31- fi, err := f.Stat()
32- if err != nil {
33- return err
34- }
35-
36- readSize := max(3*head.BlockLength, 256*1024)
37- ms := mapFile(f, fi.Size(), readSize, head.BlockLength)
38-
39- if err := st.Conn.WriteInt32(fileIndex); err != nil {
40- return err
41- }
42-
43- if err := head.WriteTo(st.Conn); err != nil {
44- return err
45- }
46-
47- // sum_init()
48- h := md4.New()
49- _ = binary.Write(h, binary.LittleEndian, st.Seed) // hash.Hash.Write never fails
50-
51- // The following quotes are citations from
52- // https://www.samba.org/~tridge/phd_thesis.pdf, section 3.2.6 The
53- // signature search algorithm (PDF page 64).
54-
55- // “Once the sorted signature table and the index table have been formed the
56- // signature search process can begin. For each byte offset in a_i the fast
57- // signature is computed, along with the 16 bit hash of the fast
58- // signature. The 16 bit hash is then used to lookup the signature index,
59- // giving the index in the signature table of the first fast signature with
60- // that hash.”
61-
62- var k int
63- var sum uint32
64- var s1, s2 uint32
65- var offset int64
66- end := fi.Size() + 1 - head.Sums[len(head.Sums)-1].Len
67- st.Logger.Debug("last block", "len", head.Sums[len(head.Sums)-1].Len, "end", end)
68-
69- readChunk := func() error {
70- k = int(head.BlockLength)
71- if remaining := int(fi.Size() - offset); remaining < k {
72- k = remaining
73- }
74-
75- chunk := ms.ptr(offset, int32(k))
76- sum = rsyncchecksum.Checksum1(chunk)
77- s1 = uint32(sum & 0xFFFF)
78- s2 = uint32(sum >> 16)
79- return nil
80- }
81- if err := readChunk(); err != nil {
82- return err
83- }
84-
85- tagHits := 0
86-Outer:
87- for {
88- tag := rsyncchecksum.Tag2(uint16(s1), uint16(s2))
89- var sum2 []byte
90- doneCsum2 := false
91- j, ok := tagTable[tag]
92- if ok {
93- // “A linear search is then performed through the signature table, stopping
94- // when an entry is found with a 16 bit hash which doesn’t match. For each
95- // entry the current 32 bit fast signature is compared to the entry in the
96- // signature table, and if that matches then the full 128 bit strong
97- // signature is computed at the current byte offset and compared to the
98- // strong signature in the signature table”
99- sum = (uint32(s1) & 0xFFFF) | (uint32(s2) << 16)
100- tagHits++
101- for ; j < int(head.ChecksumCount) && targets[j].tag == tag; j++ {
102- i := targets[j].index
103- if sum != head.Sums[i].Sum1 {
104- continue
105- }
106-
107- l := int64(head.BlockLength)
108- if v := fi.Size() - offset; v < l {
109- l = v
110- }
111- if l != head.Sums[i].Len {
112- continue
113- }
114-
115- // log.Printf("potential match at %d target=%d %d sum=%08x", offset, j, i, sum)
116-
117- if !doneCsum2 {
118- buf := ms.ptr(offset, int32(l))
119- sum2 = rsyncchecksum.Checksum2(st.Seed, buf[:])
120- doneCsum2 = true
121- }
122-
123- if local, remote := sum2[:head.ChecksumLength], head.Sums[i].Sum2[:head.ChecksumLength]; !bytes.Equal(local, remote) {
124- st.Logger.Debug("false alarm", "local", local, "remote", remote)
125- //falseAlarms++
126- continue
127- }
128-
129- // TODO(optimization): tridge rsync locates adjacent matches
130- // here for better run-length encoding, but I’m not sure where
131- // (if at all) we currently use run-length encoding:
132- // https://github.com/WayneD/rsync/commit/923fa978088f4c044eec528d9472962d9c9d13c3
133-
134- // “If the strong signature is found to match then A emits a
135- // token telling B that a match was found and which block in bi
136- // was matched12. The search then continues at the byte after
137- // the matching block.”
138-
139- if err := st.matched(h, ms, head, offset, i); err != nil {
140- return err
141- }
142-
143- // rsync doesn’t read the next chunk (offset+sums[i].len),
144- // rsync starts reading one byte before the next chunk
145- // (offset+sums[i].len-1), because the code path starting at
146- // “null_tag” removes the chunk’s first byte and adds the
147- // next byte after the chunk.
148- offset += head.Sums[i].Len - 1
149- if err := readChunk(); err != nil {
150- return fmt.Errorf("readChunk: %v", err)
151- }
152-
153- if offset >= end {
154- break Outer
155- }
156-
157- break
158- }
159- }
160-
161- // Update the rolling checksum by removing the oldest byte (update[0])
162- // and adding the newest byte (update[k]).
163- backup := max(offset-st.lastMatch, 0)
164-
165- more := offset+int64(k) < fi.Size()
166- mmore := int64(0)
167- if more {
168- mmore = 1
169- }
170- update := ms.ptr(offset-backup, int32(int64(k)+mmore+backup))
171- update = update[backup:]
172-
173- s1 -= rsyncchecksum.SignExtend(update[0])
174- s2 -= uint32(k) * rsyncchecksum.SignExtend(update[0])
175-
176- if more {
177- s1 += rsyncchecksum.SignExtend(update[k])
178- s2 += s1
179- } else {
180- k--
181- }
182- s1 = uint32(uint16(s1))
183- s2 = uint32(uint16(s2))
184-
185- if backup >= int64(head.BlockLength)+chunkSize && end-offset > chunkSize {
186- // Prevent offset-st.lastMatch from growing too large by flushing
187- // intermediate chunks.
188- if err := st.matched(h, ms, head, offset-int64(head.BlockLength), -2); err != nil {
189- return err
190- }
191- }
192-
193- offset++
194- if offset >= end {
195- break
196- }
197- }
198-
199- if err := st.matched(h, ms, head, fi.Size(), -1); err != nil {
200- return err
201- }
202-
203- {
204- sum := h.Sum(nil)
205- st.Logger.Debug("sum info", "sum", sum, "len", len(sum))
206- if _, err := st.Conn.Writer.Write(sum); err != nil {
207- return err
208- }
209- }
210-
211- return nil
212-
213-}
214-
215-// rsync/match.c:matched.
216-func (st *Transfer) matched(h hash.Hash, ms *mapStruct, head rsync.SumHead, offset int64, i int32) error {
217- n := offset - st.lastMatch
218-
219- transmitAccumulated := i < 0
220-
221- // if !transmitAccumulated {
222- // log.Printf("match at offset=%d last_match=%d i=%d len=%d n=%d",
223- // offset, st.lastMatch, i, head.Sums[i].Len, n)
224- // } else {
225- // log.Printf("transmit accumulated at offset=%d", offset)
226- // }
227-
228- /* FIXME: this is not used
229- l := int64(0)
230- if !transmitAccumulated {
231- l = head.Sums[i].Len
232- }
233- */
234-
235- if err := st.sendToken(ms, i, st.lastMatch, n); err != nil {
236- return fmt.Errorf("sendToken: %v", err)
237- }
238- // TODO: data_transfer += n;
239-
240- if !transmitAccumulated {
241- // stats.matched_data += s->sums[i].len;
242- n += head.Sums[i].Len
243- }
244-
245- for j := int64(0); j < n; j += chunkSize {
246- n1 := min(int64(chunkSize), n-j)
247- chunk := ms.ptr(st.lastMatch+j, int32(n1))
248- h.Write(chunk)
249- }
250-
251- if !transmitAccumulated {
252- st.lastMatch = offset + head.Sums[i].Len
253- } else {
254- st.lastMatch = offset
255- }
256- return nil
257-}
+0, -216
......@@ -1,216 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "encoding/binary"
5- "io"
6- "os"
7- "sort"
8-
9- "github.com/mmcloughlin/md4"
10- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
11- "github.com/picosh/pico/pkg/rsync-receiver/rsyncchecksum"
12- "github.com/picosh/pico/pkg/rsync-receiver/rsynccommon"
13- "github.com/picosh/pico/pkg/rsync-receiver/utils"
14-)
15-
16-// rsync/sender.c:send_files().
17-func (st *Transfer) SendFiles(fileList *fileList) error {
18- phase := 0
19- for {
20- // receive data about receiver’s copy of the file list contents (not
21- // ordered)
22- // see (*rsync.Receiver).Generator()
23- fileIndex, err := st.Conn.ReadInt32()
24- if err != nil {
25- return err
26- }
27- if fileIndex == -1 {
28- if phase == 0 {
29- phase++
30- // acknowledge phase change by sending -1
31- if err := st.Conn.WriteInt32(-1); err != nil {
32- return err
33- }
34- continue
35- }
36- break
37- }
38-
39- if st.Opts.DryRun() {
40- if err := st.Conn.WriteInt32(fileIndex); err != nil {
41- return err
42- }
43- continue
44- }
45-
46- head, err := st.receiveSums()
47- if err != nil {
48- return err
49- }
50-
51- // The following quotes are citations from
52- // https://www.samba.org/~tridge/phd_thesis.pdf, section 3.2.6 The
53- // signature search algorithm (PDF page 64).
54-
55- // rsync/match.c:build_hash_table
56- targets := make([]target, len(head.Sums))
57- tagTable := make(map[uint16]int) // TODO: or int32 more specifically?
58- {
59- // “The first step in the algorithm is to sort the received
60- // signatures by a 16 bit hash of the fast signature.”
61- for idx, sum := range head.Sums {
62- targets[idx] = target{
63- index: int32(idx),
64- tag: rsyncchecksum.Tag(sum.Sum1),
65- }
66- }
67- sort.Slice(targets, func(i, j int) bool {
68- return targets[i].tag < targets[j].tag
69- })
70-
71- // “A 16 bit index table is then formed which takes a 16 bit hash
72- // value and gives an index into the sorted signature table which
73- // points to the first entry in the table which has a matching
74- // hash.”
75- for idx := len(head.Sums) - 1; idx >= 0; idx-- {
76- tagTable[targets[idx].tag] = idx
77- }
78- }
79-
80- st.lastMatch = 0
81- if len(head.Sums) == 0 {
82- // fast path: send the whole file
83- err = st.sendFile(fileIndex, fileList.Files[fileIndex])
84- } else {
85- err = st.hashSearch(targets, tagTable, head, fileIndex, fileList.Files[fileIndex])
86- }
87- if err != nil {
88- if _, ok := err.(*os.PathError); ok {
89- // OpenFile() failed. Log the error (server side only) and
90- // proceed. Only starting with protocol 30, an I/O error flag is
91- // sent after the file transfer phase.
92- if os.IsNotExist(err) {
93- st.Logger.Debug("file has vanished", "file", fileList.Files[fileIndex])
94- } else {
95- st.Logger.Error("sendFiles", "err", err)
96- }
97- continue
98- } else {
99- return err
100- }
101- }
102- }
103-
104- // phase done
105- if err := st.Conn.WriteInt32(-1); err != nil {
106- return err
107- }
108-
109- return nil
110-}
111-
112-// rsync/sender.c:receive_sums().
113-func (st *Transfer) receiveSums() (rsync.SumHead, error) {
114- var head rsync.SumHead
115- if err := head.ReadFrom(st.Conn); err != nil {
116- return head, err
117- }
118- var offset int64
119- head.Sums = make([]rsync.SumBuf, int(head.ChecksumCount))
120- for i := int32(0); i < head.ChecksumCount; i++ {
121- shortChecksum, err := st.Conn.ReadInt32()
122- if err != nil {
123- return head, err
124- }
125- sb := rsync.SumBuf{
126- Index: i,
127- Offset: offset,
128- Sum1: uint32(shortChecksum),
129- }
130- if i == head.ChecksumCount-1 && head.RemainderLength != 0 {
131- sb.Len = int64(head.RemainderLength)
132- } else {
133- sb.Len = int64(head.BlockLength)
134- }
135- offset += sb.Len
136- n, err := io.ReadFull(st.Conn.Reader, sb.Sum2[:head.ChecksumLength])
137- if err != nil {
138- return head, err
139- }
140- _ = n
141- // log.Printf("chunk[%d] len=%d offset=%.0f sum1=%08x, sum2=%x",
142- // i, sb.len, float64(sb.offset), sb.sum1, sb.sum2[:n])
143- head.Sums[i] = sb
144- }
145- return head, nil
146-}
147-
148-func (st *Transfer) sendFile(fileIndex int32, fl utils.SenderFile) error {
149- // rsync/rsync.h defines CHUNK_SIZE as 32 * 1024. openrsync (tridge)
150- // uses 256K, but standard rsync rejects tokens larger than 32K.
151- const chunkSize = 32 * 1024
152-
153- fi, r, err := st.Files.Read(&fl)
154- if err != nil {
155- return err
156- }
157- defer func() { _ = r.Close() }()
158-
159- if err := st.Conn.WriteInt32(fileIndex); err != nil {
160- return err
161- }
162-
163- sh := rsynccommon.SumSizesSqroot(fi.Size())
164- // log.Printf("sh = %+v", sh)
165- if err := sh.WriteTo(st.Conn); err != nil {
166- return err
167- }
168-
169- h := md4.New()
170- _ = binary.Write(h, binary.LittleEndian, st.Seed) // hash.Hash.Write never fails
171-
172- buf := make([]byte, chunkSize)
173- for {
174- shouldBreak := false
175- n, err := r.Read(buf)
176- if err != nil {
177- if err == io.EOF {
178- shouldBreak = true
179- } else {
180- return err
181- }
182- }
183- chunk := buf[:n]
184-
185- if len(chunk) == 0 {
186- break
187- }
188-
189- _, err = h.Write(chunk)
190- if err != nil {
191- return err
192- }
193- // chunk size (“rawtok” variable in openrsync)
194- if err := st.Conn.WriteInt32(int32(len(chunk))); err != nil {
195- return err
196- }
197- if _, err := st.Conn.Writer.Write(chunk); err != nil {
198- return err
199- }
200-
201- if shouldBreak {
202- break
203- }
204- }
205- // transfer finished:
206- if err := st.Conn.WriteInt32(0); err != nil {
207- return err
208- }
209-
210- sum := h.Sum(nil)
211- // log.Printf("sum: %x (len = %d)", sum, len(sum))
212- if _, err := st.Conn.Writer.Write(sum); err != nil {
213- return err
214- }
215- return nil
216-}
+0, -15
......@@ -1,15 +0,0 @@
1-package rsyncsender
2-
3-import "io/fs"
4-
5-func uidFromFileInfo(fs.FileInfo) (int32, bool) {
6- return 1000, false
7-}
8-
9-func gidFromFileInfo(fs.FileInfo) (int32, bool) {
10- return 1000, false
11-}
12-
13-func rdevFromFileInfo(fs.FileInfo) (int32, bool) {
14- return 1000, false
15-}
+0, -34
......@@ -1,34 +0,0 @@
1-package rsyncsender
2-
3-// rsync/token.c:simple_send_token.
4-func (st *Transfer) simpleSendToken(ms *mapStruct, token int32, offset int64, n int64) error {
5- if n > 0 {
6- st.Logger.Debug("sending unmatched chunks", "offset", offset, "n", n)
7- l := int64(0)
8- for l < n {
9- n1 := min(int64(chunkSize), n-l)
10-
11- chunk := ms.ptr(offset+l, int32(n1))
12-
13- if err := st.Conn.WriteInt32(int32(n1)); err != nil {
14- return err
15- }
16-
17- if _, err := st.Conn.Writer.Write(chunk); err != nil {
18- return err
19- }
20-
21- l += n1
22- }
23- }
24- if token != -2 {
25- return st.Conn.WriteInt32(-(token + 1))
26- }
27- return nil
28-}
29-
30-// rsync/token.c:send_token.
31-func (st *Transfer) sendToken(ms *mapStruct, i int32, offset int64, n int64) error {
32- // TODO(compression): send deflated token
33- return st.simpleSendToken(ms, i, offset, n)
34-}
+0, -49
......@@ -1,49 +0,0 @@
1-package rsyncsender
2-
3-import (
4- "io"
5- "log/slog"
6-
7- "github.com/picosh/pico/pkg/rsync-receiver/rsyncopts"
8- "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
9- "github.com/picosh/pico/pkg/rsync-receiver/utils"
10-)
11-
12-type Osenv struct {
13- Stdin io.Reader
14- Stdout io.Writer
15- Stderr io.Writer
16-}
17-
18-// TransferOpts is a subset of Opts which is required for implementing a receiver.
19-type TransferOpts struct {
20- Verbose bool
21- DryRun bool
22-
23- DeleteMode bool
24- PreserveGid bool
25- PreserveUid bool
26- PreserveLinks bool
27- PreservePerms bool
28- PreserveDevices bool
29- PreserveSpecials bool
30- PreserveTimes bool
31- PreserveHardlinks bool
32-}
33-
34-type Transfer struct {
35- // config
36- // Opts *Opts
37- Opts *rsyncopts.Options
38-
39- // state
40- Conn *rsyncwire.Conn
41- Seed int32
42- lastMatch int64
43-
44- Files utils.FS
45-
46- Logger *slog.Logger
47-}
48-
49-//func (rt *Transfer) listOnly() bool { return rt.Dest == "" }
+0, -7
......@@ -1,7 +0,0 @@
1-package rsyncstats
2-
3-type TransferStats struct {
4- Read int64 // total bytes read (from network connection)
5- Written int64 // total bytes written (to network connection)
6- Size int64 // total size of files
7-}
+0, -159
......@@ -1,159 +0,0 @@
1-package rsyncwire
2-
3-import (
4- "bytes"
5- "encoding/binary"
6- "io"
7-)
8-
9-const (
10- MsgData uint8 = 0
11- MsgInfo uint8 = 2
12- MsgError uint8 = 1
13-)
14-
15-const mplexBase = 7
16-
17-type MultiplexWriter struct {
18- Writer io.Writer
19-}
20-
21-func (w *MultiplexWriter) Write(p []byte) (n int, err error) {
22- return w.WriteMsg(MsgData, p)
23-}
24-
25-func (w *MultiplexWriter) WriteMsg(tag uint8, p []byte) (n int, err error) {
26- header := uint32(mplexBase+tag)<<24 | uint32(len(p))
27- // log.Printf("len %d (hex %x)", len(p), uint32(len(p)))
28- // log.Printf("header=%v (%x)", header, header)
29- if err := binary.Write(w.Writer, binary.LittleEndian, header); err != nil {
30- return 0, err
31- }
32- return w.Writer.Write(p)
33-}
34-
35-type Buffer struct {
36- // buf.Write() never fails, making for a convenient API.
37- buf bytes.Buffer
38-}
39-
40-func (b *Buffer) WriteByte(data byte) error {
41- return binary.Write(&b.buf, binary.LittleEndian, data)
42-}
43-
44-func (b *Buffer) WriteInt32(data int32) {
45- _ = binary.Write(&b.buf, binary.LittleEndian, data)
46-}
47-
48-func (b *Buffer) WriteInt64(data int64) {
49- // send as a 32-bit integer if possible
50- if data <= 0x7FFFFFFF && data >= 0 {
51- b.WriteInt32(int32(data))
52- return
53- }
54- // otherwise, send -1 followed by the 64-bit integer
55- b.WriteInt32(-1)
56- _ = binary.Write(&b.buf, binary.LittleEndian, data)
57-}
58-
59-func (b *Buffer) WriteString(data string) {
60- _, _ = io.WriteString(&b.buf, data)
61-}
62-
63-func (b *Buffer) String() string {
64- return b.buf.String()
65-}
66-
67-type Conn struct {
68- Writer io.Writer
69- Reader io.Reader
70-}
71-
72-func (c *Conn) WriteByte(data byte) error {
73- return binary.Write(c.Writer, binary.LittleEndian, data)
74-}
75-
76-func (c *Conn) WriteInt32(data int32) error {
77- return binary.Write(c.Writer, binary.LittleEndian, data)
78-}
79-
80-func (c *Conn) WriteInt64(data int64) error {
81- // send as a 32-bit integer if possible
82- if data <= 0x7FFFFFFF && data >= 0 {
83- return c.WriteInt32(int32(data))
84- }
85- // otherwise, send -1 followed by the 64-bit integer
86- if err := c.WriteInt32(-1); err != nil {
87- return err
88- }
89- return binary.Write(c.Writer, binary.LittleEndian, data)
90-}
91-
92-func (c *Conn) WriteString(data string) error {
93- _, err := io.WriteString(c.Writer, data)
94- return err
95-}
96-
97-func (c *Conn) ReadByte() (byte, error) {
98- var buf [1]byte
99- if _, err := io.ReadFull(c.Reader, buf[:]); err != nil {
100- return 0, err
101- }
102- return buf[0], nil
103-}
104-
105-func (c *Conn) ReadInt32() (int32, error) {
106- var buf [4]byte
107- if _, err := io.ReadFull(c.Reader, buf[:]); err != nil {
108- return 0, err
109- }
110- return int32(binary.LittleEndian.Uint32(buf[:])), nil
111-}
112-
113-func (c *Conn) ReadInt64() (int64, error) {
114- {
115- data, err := c.ReadInt32()
116- if err != nil {
117- return 0, err
118- }
119- if data != -1 {
120- // The value was small enough to fit into a 32 bit int, so it was
121- // transferred directly.
122- return int64(data), nil
123- }
124- // Otherwise, -1 was transmitted, followed by the int64.
125- }
126- var data int64
127- if err := binary.Read(c.Reader, binary.LittleEndian, &data); err != nil {
128- return 0, err
129- }
130- return data, nil
131-}
132-
133-type CountingReader struct {
134- R io.Reader
135- BytesRead int64
136-}
137-
138-func (r *CountingReader) Read(p []byte) (n int, err error) {
139- n, err = r.R.Read(p)
140- r.BytesRead += int64(n)
141- return n, err
142-}
143-
144-type CountingWriter struct {
145- W io.Writer
146- BytesWritten int64
147-}
148-
149-func (w *CountingWriter) Write(p []byte) (n int, err error) {
150- n, err = w.W.Write(p)
151- w.BytesWritten += int64(n)
152- return n, err
153-}
154-
155-func CounterPair(r io.Reader, w io.Writer) (*CountingReader, *CountingWriter) {
156- crd := &CountingReader{R: r}
157- cwr := &CountingWriter{W: w}
158- return crd, cwr
159-}
+0, -59
......@@ -1,59 +0,0 @@
1-package utils
2-
3-import (
4- "io"
5- "io/fs"
6- "sort"
7- "time"
8-
9- "github.com/picosh/pico/pkg/rsync-receiver/rsync"
10-)
11-
12-type SenderFile struct {
13- // TODO: store relative to the root to conserve RAM
14- Path string
15- WPath string
16- Regular bool
17-}
18-
19-type ReceiverFile struct {
20- Name string
21- Length int64
22- ModTime time.Time
23- Mode int32
24- Uid int32
25- Gid int32
26- LinkTarget string
27- Rdev int32
28- Reader io.Reader
29-}
30-
31-// FileMode converts from the Linux permission bits to Go’s permission bits.
32-func (f *ReceiverFile) FileMode() fs.FileMode {
33- ret := fs.FileMode(f.Mode) & fs.ModePerm
34-
35- mode := f.Mode & rsync.S_IFMT
36- switch mode {
37- case rsync.S_IFCHR:
38- ret |= fs.ModeCharDevice
39- case rsync.S_IFBLK:
40- ret |= fs.ModeDevice
41- case rsync.S_IFIFO:
42- ret |= fs.ModeNamedPipe
43- case rsync.S_IFSOCK:
44- ret |= fs.ModeSocket
45- case rsync.S_IFLNK:
46- ret |= fs.ModeSymlink
47- case rsync.S_IFDIR:
48- ret |= fs.ModeDir
49- }
50-
51- return ret
52-}
53-
54-// rsync/flist.c:flist_sort_and_clean.
55-func SortFileList(fileList []*ReceiverFile) {
56- sort.Slice(fileList, func(i, j int) bool {
57- return fileList[i].Name < fileList[j].Name
58- })
59-}
+0, -20
......@@ -1,20 +0,0 @@
1-package utils
2-
3-import (
4- "io"
5- "os"
6-)
7-
8-type ReaderAtCloser interface {
9- io.Reader
10- io.ReaderAt
11- io.Closer
12-}
13-
14-// File System: need to handle all type of files: regular, folder, symlink, etc.
15-type FS interface {
16- Put(*ReceiverFile) (int64, error)
17- List(string) ([]os.FileInfo, error)
18- Read(*SenderFile) (os.FileInfo, ReaderAtCloser, error)
19- Remove([]*ReceiverFile) error
20-}
+172, -0
......@@ -0,0 +1,172 @@
1+package rsync
2+
3+import (
4+ "crypto/md5"
5+ "encoding/binary"
6+ "fmt"
7+ "hash"
8+
9+ "github.com/cespare/xxhash/v2"
10+ "github.com/mmcloughlin/md4"
11+)
12+
13+// csumType identifies a strong checksum. md4Old is the implied checksum of
14+// protocols 27-29, which seeds the whole-file sum; md4 is the same digest
15+// when negotiated by name in protocol 30+.
16+type csumType int
17+
18+const (
19+ csumNone csumType = iota
20+ csumMD4Old
21+ csumMD4
22+ csumMD5
23+ csumXXH64
24+)
25+
26+// checksumNames is our preference order for negotiation. It matches rsync's
27+// table order so both sides converge on the same choice.
28+var checksumNames = []struct {
29+ name string
30+ typ csumType
31+}{
32+ {"xxh64", csumXXH64},
33+ {"xxhash", csumXXH64},
34+ {"md5", csumMD5},
35+ {"md4", csumMD4},
36+ {"none", csumNone},
37+}
38+
39+func parseChecksumName(name string) (csumType, error) {
40+ for _, c := range checksumNames {
41+ if c.name == name {
42+ return c.typ, nil
43+ }
44+ }
45+ return 0, fmt.Errorf("unknown checksum name: %s", name)
46+}
47+
48+func (t csumType) size() int {
49+ switch t {
50+ case csumNone:
51+ return 1
52+ case csumXXH64:
53+ return 8
54+ default:
55+ return 16
56+ }
57+}
58+
59+// checksum1 is rsync's rolling weak checksum. The bytes are treated as
60+// signed chars, as rsync's C implementation does.
61+func checksum1(buf []byte) uint32 {
62+ var s1, s2 uint32
63+ i := 0
64+ for ; i < len(buf)-4; i += 4 {
65+ b0 := uint32(int8(buf[i]))
66+ b1 := uint32(int8(buf[i+1]))
67+ b2 := uint32(int8(buf[i+2]))
68+ b3 := uint32(int8(buf[i+3]))
69+ s2 += 4*(s1+b0) + 3*b1 + 2*b2 + b3
70+ s1 += b0 + b1 + b2 + b3
71+ }
72+ for ; i < len(buf); i++ {
73+ s1 += uint32(int8(buf[i]))
74+ s2 += s1
75+ }
76+ return (s1 & 0xffff) + (s2 << 16)
77+}
78+
79+// checksums holds the negotiated digests and the session seed.
80+type checksums struct {
81+ xfer csumType
82+ file csumType
83+ seed int32
84+ properSeedOrder bool
85+}
86+
87+func (c *checksums) seedBytes() []byte {
88+ var b [4]byte
89+ binary.LittleEndian.PutUint32(b[:], uint32(c.seed))
90+ return b[:]
91+}
92+
93+// blockSum is rsync's get_checksum2: the strong checksum of one block.
94+func (c *checksums) blockSum(buf []byte, out []byte) []byte {
95+ switch c.xfer {
96+ case csumXXH64:
97+ d := xxhash.NewWithSeed(uint64(int64(c.seed)))
98+ _, _ = d.Write(buf)
99+ return binary.LittleEndian.AppendUint64(out, d.Sum64())
100+ case csumMD5:
101+ h := md5.New()
102+ if c.properSeedOrder {
103+ if c.seed != 0 {
104+ h.Write(c.seedBytes())
105+ }
106+ h.Write(buf)
107+ } else {
108+ h.Write(buf)
109+ if c.seed != 0 {
110+ h.Write(c.seedBytes())
111+ }
112+ }
113+ return h.Sum(out)
114+ case csumMD4, csumMD4Old:
115+ h := md4.New()
116+ h.Write(buf)
117+ if c.seed != 0 {
118+ h.Write(c.seedBytes())
119+ }
120+ return h.Sum(out)
121+ default:
122+ return append(out, 0)
123+ }
124+}
125+
126+// newXferSum starts the whole-file checksum that follows each transferred
127+// file (rsync's sum_init with the transfer digest).
128+func (c *checksums) newXferSum() hash.Hash {
129+ switch c.xfer {
130+ case csumMD4Old:
131+ h := md4.New()
132+ h.Write(c.seedBytes())
133+ return h
134+ default:
135+ return newPlainHash(c.xfer)
136+ }
137+}
138+
139+// newFileSum starts the --checksum digest of a whole file.
140+func (c *checksums) newFileSum() hash.Hash {
141+ return newPlainHash(c.file)
142+}
143+
144+func newPlainHash(t csumType) hash.Hash {
145+ switch t {
146+ case csumMD4, csumMD4Old:
147+ return md4.New()
148+ case csumMD5:
149+ return md5.New()
150+ case csumXXH64:
151+ return &xxh64LE{d: xxhash.NewWithSeed(0)}
152+ default:
153+ return &noneHash{}
154+ }
155+}
156+
157+// xxh64LE emits the digest little-endian, as rsync's SIVAL64 does.
158+type xxh64LE struct{ d *xxhash.Digest }
159+
160+func (x *xxh64LE) Write(p []byte) (int, error) { return x.d.Write(p) }
161+func (x *xxh64LE) Sum(b []byte) []byte { return binary.LittleEndian.AppendUint64(b, x.d.Sum64()) }
162+func (x *xxh64LE) Reset() { x.d.Reset() }
163+func (x *xxh64LE) Size() int { return 8 }
164+func (x *xxh64LE) BlockSize() int { return 32 }
165+
166+type noneHash struct{}
167+
168+func (noneHash) Write(p []byte) (int, error) { return len(p), nil }
169+func (noneHash) Sum(b []byte) []byte { return append(b, 0) }
170+func (noneHash) Reset() {}
171+func (noneHash) Size() int { return 1 }
172+func (noneHash) BlockSize() int { return 1 }
+677, -0
......@@ -0,0 +1,677 @@
1+package rsync
2+
3+import (
4+ "bufio"
5+ "encoding/binary"
6+ "errors"
7+ "fmt"
8+ "io"
9+ "sync"
10+)
11+
12+// remoteExitError is returned when the peer aborts with MSG_ERROR_EXIT.
13+type remoteExitError struct{ code int }
14+
15+func (e *remoteExitError) Error() string {
16+ return fmt.Sprintf("remote side exited with code %d", e.code)
17+}
18+
19+// muxWriter queues protocol data and messages in order and writes them from
20+// its own goroutine. Once multiplexing is on, data goes out as MSG_DATA
21+// frames. Writers of data block when too much is queued; messages never
22+// block, so the receiver can report progress while the generator waits on
23+// the peer.
24+type muxWriter struct {
25+ mu sync.Mutex
26+ cond *sync.Cond
27+ w io.Writer
28+ queue []muxItem
29+ queued int
30+ kick bool
31+ busy bool
32+ multiplex bool
33+ written int64
34+ err error
35+ closed bool
36+}
37+
38+type muxItem struct {
39+ tag byte
40+ data []byte
41+}
42+
43+const (
44+ muxBatchSize = 32 * 1024
45+ muxMaxQueued = 256 * 1024
46+)
47+
48+func newMuxWriter(w io.Writer) *muxWriter {
49+ m := &muxWriter{w: w}
50+ m.cond = sync.NewCond(&m.mu)
51+ go m.pump()
52+ return m
53+}
54+
55+func (m *muxWriter) Write(p []byte) (int, error) {
56+ m.mu.Lock()
57+ defer m.mu.Unlock()
58+ for m.err == nil && m.queued >= muxMaxQueued {
59+ m.kick = true
60+ m.cond.Broadcast()
61+ m.cond.Wait()
62+ }
63+ if m.err != nil {
64+ return 0, m.err
65+ }
66+ if n := len(m.queue); n > 0 && m.queue[n-1].tag == msgData && len(m.queue[n-1].data) < muxBatchSize {
67+ m.queue[n-1].data = append(m.queue[n-1].data, p...)
68+ } else {
69+ m.queue = append(m.queue, muxItem{tag: msgData, data: append([]byte(nil), p...)})
70+ }
71+ m.queued += len(p)
72+ if m.queued >= muxBatchSize {
73+ m.kick = true
74+ m.cond.Broadcast()
75+ }
76+ return len(p), nil
77+}
78+
79+// WriteMsg queues an out-of-band message behind any pending data.
80+func (m *muxWriter) WriteMsg(tag byte, p []byte) error {
81+ m.mu.Lock()
82+ defer m.mu.Unlock()
83+ if m.err != nil {
84+ return m.err
85+ }
86+ if !m.multiplex {
87+ return errors.New("rsync: message sent before multiplexing started")
88+ }
89+ m.queue = append(m.queue, muxItem{tag: tag, data: append([]byte(nil), p...)})
90+ m.kick = true
91+ m.cond.Broadcast()
92+ return nil
93+}
94+
95+// Kick asks for queued output to go out without waiting for it.
96+func (m *muxWriter) Kick() error {
97+ m.mu.Lock()
98+ defer m.mu.Unlock()
99+ if len(m.queue) > 0 {
100+ m.kick = true
101+ m.cond.Broadcast()
102+ }
103+ return m.err
104+}
105+
106+// Flush waits until everything queued has been written.
107+func (m *muxWriter) Flush() error {
108+ m.mu.Lock()
109+ defer m.mu.Unlock()
110+ for m.err == nil && (len(m.queue) > 0 || m.busy) {
111+ m.kick = true
112+ m.cond.Broadcast()
113+ m.cond.Wait()
114+ }
115+ return m.err
116+}
117+
118+// Close flushes and stops the writer goroutine.
119+func (m *muxWriter) Close() error {
120+ err := m.Flush()
121+ m.mu.Lock()
122+ m.closed = true
123+ m.cond.Broadcast()
124+ m.mu.Unlock()
125+ return err
126+}
127+
128+func (m *muxWriter) setMultiplex(on bool) error {
129+ if err := m.Flush(); err != nil {
130+ return err
131+ }
132+ m.mu.Lock()
133+ m.multiplex = on
134+ m.mu.Unlock()
135+ return nil
136+}
137+
138+func (m *muxWriter) pump() {
139+ m.mu.Lock()
140+ defer m.mu.Unlock()
141+ for {
142+ for !m.closed && m.err == nil && (!m.kick || len(m.queue) == 0) {
143+ m.cond.Wait()
144+ }
145+ if m.closed || m.err != nil {
146+ m.queue = nil
147+ m.cond.Broadcast()
148+ return
149+ }
150+ items := m.queue
151+ m.queue = nil
152+ m.queued = 0
153+ m.kick = false
154+ m.busy = true
155+ multiplex := m.multiplex
156+ m.cond.Broadcast()
157+ m.mu.Unlock()
158+
159+ var n int64
160+ var err error
161+ for _, it := range items {
162+ var k int64
163+ k, err = writeItem(m.w, it, multiplex)
164+ n += k
165+ if err != nil {
166+ break
167+ }
168+ }
169+
170+ m.mu.Lock()
171+ m.busy = false
172+ m.written += n
173+ if err != nil {
174+ m.err = err
175+ }
176+ m.cond.Broadcast()
177+ }
178+}
179+
180+func writeItem(w io.Writer, it muxItem, multiplex bool) (int64, error) {
181+ if !multiplex {
182+ n, err := w.Write(it.data)
183+ return int64(n), err
184+ }
185+ var total int64
186+ data := it.data
187+ for {
188+ chunk := data[:min(len(data), 0xFFFFFF)]
189+ var hdr [4]byte
190+ binary.LittleEndian.PutUint32(hdr[:], uint32(mplexBase+int(it.tag))<<24|uint32(len(chunk)))
191+ n, err := w.Write(append(hdr[:], chunk...))
192+ total += int64(n)
193+ if err != nil {
194+ return total, err
195+ }
196+ data = data[len(chunk):]
197+ if len(data) == 0 {
198+ return total, nil
199+ }
200+ }
201+}
202+
203+func (m *muxWriter) bytesWritten() int64 {
204+ m.mu.Lock()
205+ defer m.mu.Unlock()
206+ n := m.written + int64(m.queued)
207+ for _, it := range m.queue {
208+ if it.tag != msgData {
209+ n += int64(len(it.data)) + 4
210+ }
211+ }
212+ return n
213+}
214+
215+// flushingReader sends our pending output before it blocks on the peer,
216+// which is what keeps request/response exchanges from stalling.
217+type flushingReader struct {
218+ r io.Reader
219+ flush func() error
220+ read int64
221+}
222+
223+func (f *flushingReader) Read(p []byte) (int, error) {
224+ if err := f.flush(); err != nil {
225+ return 0, err
226+ }
227+ n, err := f.r.Read(p)
228+ f.read += int64(n)
229+ return n, err
230+}
231+
232+// muxReader returns the data stream of a possibly multiplexed input and
233+// hands every other message to onMsg.
234+type muxReader struct {
235+ r *bufio.Reader
236+ src *flushingReader
237+ multiplex bool
238+ remaining int
239+ onMsg func(tag byte, data []byte) error
240+}
241+
242+func (m *muxReader) fill() error {
243+ for m.remaining == 0 {
244+ var hdr [4]byte
245+ if _, err := io.ReadFull(m.r, hdr[:]); err != nil {
246+ return err
247+ }
248+ v := binary.LittleEndian.Uint32(hdr[:])
249+ tag := int(v>>24) - mplexBase
250+ size := int(v & 0xFFFFFF)
251+ if tag == msgData {
252+ m.remaining = size
253+ continue
254+ }
255+ if tag < 0 {
256+ return fmt.Errorf("rsync: unexpected tag %d", tag)
257+ }
258+ data := make([]byte, size)
259+ if _, err := io.ReadFull(m.r, data); err != nil {
260+ return err
261+ }
262+ if err := m.onMsg(byte(tag), data); err != nil {
263+ return err
264+ }
265+ }
266+ return nil
267+}
268+
269+func (m *muxReader) Read(p []byte) (int, error) {
270+ if len(p) == 0 {
271+ return 0, nil
272+ }
273+ if !m.multiplex {
274+ return m.r.Read(p)
275+ }
276+ if err := m.fill(); err != nil {
277+ return 0, err
278+ }
279+ n, err := m.r.Read(p[:min(len(p), m.remaining)])
280+ m.remaining -= n
281+ return n, err
282+}
283+
284+func (m *muxReader) ReadByte() (byte, error) {
285+ if !m.multiplex {
286+ return m.r.ReadByte()
287+ }
288+ if err := m.fill(); err != nil {
289+ return 0, err
290+ }
291+ b, err := m.r.ReadByte()
292+ if err == nil {
293+ m.remaining--
294+ }
295+ return b, err
296+}
297+
298+// ndxState holds the delta-encoding state of one direction of write_ndx.
299+type ndxState struct {
300+ prevPositive int32
301+ prevNegative int32
302+}
303+
304+func newNdxState() ndxState { return ndxState{prevPositive: -1, prevNegative: 1} }
305+
306+// conn carries the rsync protocol over the session's stdin and stdout.
307+type conn struct {
308+ protocol int
309+ r *muxReader
310+ w *muxWriter
311+ ndxIn ndxState
312+ ndxOut ndxState
313+ rbuf [16]byte
314+ wbuf [16]byte
315+}
316+
317+func newConn(rw io.ReadWriter) *conn {
318+ w := newMuxWriter(rw)
319+ src := &flushingReader{r: rw, flush: w.Kick}
320+ c := &conn{
321+ r: &muxReader{r: bufio.NewReaderSize(src, 64*1024), src: src},
322+ w: w,
323+ ndxIn: newNdxState(),
324+ ndxOut: newNdxState(),
325+ }
326+ c.r.onMsg = func(tag byte, data []byte) error {
327+ return fmt.Errorf("rsync: unexpected message %d", tag)
328+ }
329+ return c
330+}
331+
332+func (c *conn) bytesRead() int64 { return c.r.src.read }
333+func (c *conn) bytesWritten() int64 { return c.w.bytesWritten() }
334+func (c *conn) flush() error { return c.w.Flush() }
335+func (c *conn) close() error { return c.w.Close() }
336+
337+func (c *conn) readFull(p []byte) error {
338+ _, err := io.ReadFull(c.r, p)
339+ if errors.Is(err, io.EOF) {
340+ return io.ErrUnexpectedEOF
341+ }
342+ return err
343+}
344+
345+func (c *conn) readByte() (byte, error) {
346+ b, err := c.r.ReadByte()
347+ if errors.Is(err, io.EOF) {
348+ return 0, io.ErrUnexpectedEOF
349+ }
350+ return b, err
351+}
352+
353+func (c *conn) readInt32() (int32, error) {
354+ if err := c.readFull(c.rbuf[:4]); err != nil {
355+ return 0, err
356+ }
357+ return int32(binary.LittleEndian.Uint32(c.rbuf[:4])), nil
358+}
359+
360+func (c *conn) readShortint() (uint16, error) {
361+ if err := c.readFull(c.rbuf[:2]); err != nil {
362+ return 0, err
363+ }
364+ return binary.LittleEndian.Uint16(c.rbuf[:2]), nil
365+}
366+
367+// readLongint reads the pre-30 64-bit encoding: an int32, or -1 followed by
368+// the full 64-bit value.
369+func (c *conn) readLongint() (int64, error) {
370+ v, err := c.readInt32()
371+ if err != nil || v != -1 {
372+ return int64(v), err
373+ }
374+ if err := c.readFull(c.rbuf[:8]); err != nil {
375+ return 0, err
376+ }
377+ return int64(binary.LittleEndian.Uint64(c.rbuf[:8])), nil
378+}
379+
380+// intByteExtra maps the high bits of a varint's first byte to the number of
381+// bytes that follow it.
382+var intByteExtra = [64]int{
383+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
384+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
385+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
386+ 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 6,
387+}
388+
389+func (c *conn) readVarint() (int32, error) {
390+ ch, err := c.readByte()
391+ if err != nil {
392+ return 0, err
393+ }
394+ var b [5]byte
395+ extra := intByteExtra[ch/4]
396+ if extra == 0 {
397+ return int32(ch), nil
398+ }
399+ if extra >= len(b) {
400+ return 0, errors.New("rsync: overflow in varint")
401+ }
402+ if err := c.readFull(b[:extra]); err != nil {
403+ return 0, err
404+ }
405+ bit := byte(1) << (8 - extra)
406+ b[extra] = ch & (bit - 1)
407+ return int32(binary.LittleEndian.Uint32(b[:4])), nil
408+}
409+
410+func (c *conn) readVarlong(minBytes int) (int64, error) {
411+ var b2 [8]byte
412+ if err := c.readFull(b2[:minBytes]); err != nil {
413+ return 0, err
414+ }
415+ var b [9]byte
416+ copy(b[:], b2[1:minBytes])
417+ extra := intByteExtra[b2[0]/4]
418+ if extra == 0 {
419+ b[minBytes-1] = b2[0]
420+ return int64(binary.LittleEndian.Uint64(b[:8])), nil
421+ }
422+ if minBytes+extra > len(b) {
423+ return 0, errors.New("rsync: overflow in varlong")
424+ }
425+ if err := c.readFull(b[minBytes-1 : minBytes-1+extra]); err != nil {
426+ return 0, err
427+ }
428+ bit := byte(1) << (8 - extra)
429+ b[minBytes+extra-1] = b2[0] & (bit - 1)
430+ return int64(binary.LittleEndian.Uint64(b[:8])), nil
431+}
432+
433+func (c *conn) readVarint30() (int32, error) {
434+ if c.protocol < 30 {
435+ return c.readInt32()
436+ }
437+ return c.readVarint()
438+}
439+
440+func (c *conn) readVarlong30(minBytes int) (int64, error) {
441+ if c.protocol < 30 {
442+ return c.readLongint()
443+ }
444+ return c.readVarlong(minBytes)
445+}
446+
447+func (c *conn) readVstring(max int) (string, error) {
448+ b, err := c.readByte()
449+ if err != nil {
450+ return "", err
451+ }
452+ n := int(b)
453+ if n&0x80 != 0 {
454+ lo, err := c.readByte()
455+ if err != nil {
456+ return "", err
457+ }
458+ n = (n&^0x80)*0x100 + int(lo)
459+ }
460+ if n >= max {
461+ return "", fmt.Errorf("rsync: over-long vstring received (%d > %d)", n, max-1)
462+ }
463+ buf := make([]byte, n)
464+ if err := c.readFull(buf); err != nil {
465+ return "", err
466+ }
467+ return string(buf), nil
468+}
469+
470+// readNdx reads a file-list index, which protocol 30 delta-encodes.
471+func (c *conn) readNdx() (int32, error) {
472+ if c.protocol < 30 {
473+ return c.readInt32()
474+ }
475+ b0, err := c.readByte()
476+ if err != nil {
477+ return 0, err
478+ }
479+ prev := &c.ndxIn.prevPositive
480+ switch b0 {
481+ case 0:
482+ return ndxDone, nil
483+ case 0xFF:
484+ prev = &c.ndxIn.prevNegative
485+ if b0, err = c.readByte(); err != nil {
486+ return 0, err
487+ }
488+ }
489+ var unum uint32
490+ if b0 == 0xFE {
491+ var b [4]byte
492+ if err := c.readFull(b[:2]); err != nil {
493+ return 0, err
494+ }
495+ if b[0]&0x80 != 0 {
496+ b[3] = b[0] &^ 0x80
497+ b[0] = b[1]
498+ if err := c.readFull(b[1:3]); err != nil {
499+ return 0, err
500+ }
501+ unum = binary.LittleEndian.Uint32(b[:])
502+ } else {
503+ unum = uint32(b[0])<<8 + uint32(b[1]) + uint32(*prev)
504+ }
505+ } else {
506+ unum = uint32(b0) + uint32(*prev)
507+ }
508+ if unum > 0x7FFFFFFF {
509+ return 0, fmt.Errorf("rsync: invalid file index %d", unum)
510+ }
511+ num := int32(unum)
512+ *prev = num
513+ if prev == &c.ndxIn.prevNegative {
514+ num = -num
515+ }
516+ return num, nil
517+}
518+
519+func (c *conn) write(p []byte) error {
520+ _, err := c.w.Write(p)
521+ return err
522+}
523+
524+func (c *conn) writeByte(b byte) error {
525+ return c.write([]byte{b})
526+}
527+
528+func (c *conn) writeInt32(v int32) error {
529+ var b [4]byte
530+ binary.LittleEndian.PutUint32(b[:], uint32(v))
531+ return c.write(b[:])
532+}
533+
534+func (c *conn) writeShortint(v uint16) error {
535+ var b [2]byte
536+ binary.LittleEndian.PutUint16(b[:], v)
537+ return c.write(b[:])
538+}
539+
540+func (c *conn) writeLongint(v int64) error {
541+ if v >= 0 && v <= 0x7FFFFFFF {
542+ return c.writeInt32(int32(v))
543+ }
544+ var b [12]byte
545+ binary.LittleEndian.PutUint32(b[:4], 0xFFFFFFFF)
546+ binary.LittleEndian.PutUint64(b[4:], uint64(v))
547+ return c.write(b[:])
548+}
549+
550+func appendVarint(dst []byte, x int32) []byte {
551+ var b [5]byte
552+ binary.LittleEndian.PutUint32(b[1:], uint32(x))
553+ cnt := 4
554+ for cnt > 1 && b[cnt] == 0 {
555+ cnt--
556+ }
557+ bit := byte(1) << (7 - cnt + 1)
558+ switch {
559+ case b[cnt] >= bit:
560+ cnt++
561+ b[0] = ^(bit - 1)
562+ case cnt > 1:
563+ b[0] = b[cnt] | ^(bit*2 - 1)
564+ default:
565+ b[0] = b[1]
566+ }
567+ return append(dst, b[:cnt]...)
568+}
569+
570+func appendVarlong(dst []byte, x int64, minBytes int) []byte {
571+ var b [9]byte
572+ binary.LittleEndian.PutUint64(b[1:], uint64(x))
573+ cnt := 8
574+ for cnt > minBytes && b[cnt] == 0 {
575+ cnt--
576+ }
577+ bit := byte(1) << (7 - cnt + minBytes)
578+ switch {
579+ case b[cnt] >= bit:
580+ cnt++
581+ b[0] = ^(bit - 1)
582+ case cnt > minBytes:
583+ b[0] = b[cnt] | ^(bit*2 - 1)
584+ default:
585+ b[0] = b[cnt]
586+ }
587+ return append(dst, b[:cnt]...)
588+}
589+
590+func (c *conn) writeVarint(v int32) error {
591+ return c.write(appendVarint(c.wbuf[:0], v))
592+}
593+
594+func (c *conn) writeVarlong(v int64, minBytes int) error {
595+ return c.write(appendVarlong(c.wbuf[:0], v, minBytes))
596+}
597+
598+func (c *conn) writeVarint30(v int32) error {
599+ if c.protocol < 30 {
600+ return c.writeInt32(v)
601+ }
602+ return c.writeVarint(v)
603+}
604+
605+func (c *conn) writeVarlong30(v int64, minBytes int) error {
606+ if c.protocol < 30 {
607+ return c.writeLongint(v)
608+ }
609+ return c.writeVarlong(v, minBytes)
610+}
611+
612+func (c *conn) writeVstring(s string) error {
613+ n := len(s)
614+ if n > 0x7FFF {
615+ return fmt.Errorf("rsync: attempting to send over-long vstring (%d > %d)", n, 0x7FFF)
616+ }
617+ var hdr []byte
618+ if n > 0x7F {
619+ hdr = []byte{byte(n/0x100 + 0x80), byte(n)}
620+ } else {
621+ hdr = []byte{byte(n)}
622+ }
623+ if err := c.write(hdr); err != nil {
624+ return err
625+ }
626+ return c.write([]byte(s))
627+}
628+
629+func (c *conn) writeNdx(ndx int32) error {
630+ if c.protocol < 30 {
631+ return c.writeInt32(ndx)
632+ }
633+ var b [6]byte
634+ cnt := 0
635+ var diff int32
636+ switch {
637+ case ndx >= 0:
638+ diff = ndx - c.ndxOut.prevPositive
639+ c.ndxOut.prevPositive = ndx
640+ case ndx == ndxDone:
641+ return c.writeByte(0)
642+ default:
643+ b[cnt] = 0xFF
644+ cnt++
645+ ndx = -ndx
646+ diff = ndx - c.ndxOut.prevNegative
647+ c.ndxOut.prevNegative = ndx
648+ }
649+ switch {
650+ case diff > 0 && diff < 0xFE:
651+ b[cnt] = byte(diff)
652+ cnt++
653+ case diff < 0 || diff > 0x7FFF:
654+ b[cnt] = 0xFE
655+ b[cnt+1] = byte(ndx>>24) | 0x80
656+ b[cnt+2] = byte(ndx)
657+ b[cnt+3] = byte(ndx >> 8)
658+ b[cnt+4] = byte(ndx >> 16)
659+ cnt += 5
660+ default:
661+ b[cnt] = 0xFE
662+ b[cnt+1] = byte(diff >> 8)
663+ b[cnt+2] = byte(diff)
664+ cnt += 3
665+ }
666+ return c.write(b[:cnt])
667+}
668+
669+func (c *conn) writeMsg(tag byte, data []byte) error {
670+ return c.w.WriteMsg(tag, data)
671+}
672+
673+func (c *conn) writeMsgInt(tag byte, v int32) error {
674+ var b [4]byte
675+ binary.LittleEndian.PutUint32(b[:], uint32(v))
676+ return c.w.WriteMsg(tag, b[:])
677+}
+133, -0
......@@ -0,0 +1,133 @@
1+package rsync
2+
3+// Protocol versions this implementation speaks. 27 is rsync 2.6.0 and 31 is
4+// what every rsync since 3.1.0 negotiates down to.
5+const (
6+ minProtocol = 27
7+ maxProtocol = 31
8+)
9+
10+// File-list transmission flags (rsync.h XMIT_*).
11+const (
12+ xmitTopDir = 1 << 0
13+ xmitSameMode = 1 << 1
14+ xmitSameRdevPre28 = 1 << 2
15+ xmitExtendedFlags = 1 << 2
16+ xmitSameUID = 1 << 3
17+ xmitSameGID = 1 << 4
18+ xmitSameName = 1 << 5
19+ xmitLongName = 1 << 6
20+ xmitSameTime = 1 << 7
21+ xmitSameRdevMajor = 1 << 8
22+ xmitNoContentDir = 1 << 8
23+ xmitHlinked = 1 << 9
24+ xmitUserNameFollows = 1 << 10
25+ xmitRdevMinor8Pre30 = 1 << 11
26+ xmitGroupNameFollows = 1 << 11
27+ xmitHlinkFirst = 1 << 12
28+ xmitIOErrorEndList = 1 << 12
29+ xmitModNsec = 1 << 13
30+ xmitSameAtime = 1 << 14
31+)
32+
33+// Sizes and limits (rsync.h).
34+const (
35+ maxPathLen = 4096
36+ maxWireNsec = 999999999
37+ blockSize = 700
38+ maxBlockSize = 1 << 17
39+ oldMaxBlockSize = 1 << 29
40+ chunkSize = 32 * 1024
41+ maxMapSize = 256 * 1024
42+ sumLength = 16
43+ shortSumLength = 2
44+ blocksumBias = 10
45+ maxDataCount = 16383
46+ mplexBase = 7
47+ maxChainLen = 1024
48+ traditionalTableSize = 1 << 16
49+)
50+
51+// Unix file type bits as sent on the wire.
52+const (
53+ sIFMT = 0o170000
54+ sIFSOCK = 0o140000
55+ sIFLNK = 0o120000
56+ sIFREG = 0o100000
57+ sIFBLK = 0o060000
58+ sIFDIR = 0o040000
59+ sIFCHR = 0o020000
60+ sIFIFO = 0o010000
61+)
62+
63+// Item flags sent alongside a file index (rsync.h ITEM_*).
64+const (
65+ itemReportChange = 1 << 1
66+ itemReportSize = 1 << 2
67+ itemReportTime = 1 << 3
68+ itemBasisTypeFollows = 1 << 11
69+ itemXnameFollows = 1 << 12
70+ itemIsNew = 1 << 13
71+ itemTransfer = 1 << 15
72+ itemMissingData = 1 << 16
73+)
74+
75+// Basis file types sent with itemBasisTypeFollows.
76+const (
77+ fnamecmpFname = 0x80
78+ fnamecmpPartialDir = 0x81
79+)
80+
81+// Multiplexed message tags (rsync.h enum msgcode).
82+const (
83+ msgData = 0
84+ msgErrorXfer = 1
85+ msgInfo = 2
86+ msgError = 3
87+ msgWarning = 4
88+ msgIOError = 22
89+ msgNoop = 42
90+ msgErrorExit = 86
91+ msgSuccess = 100
92+ msgDeleted = 101
93+ msgNoSend = 102
94+)
95+
96+// Special file-list index values.
97+const (
98+ ndxDone = -1
99+ ndxFlistEOF = -2
100+ ndxDelStats = -3
101+)
102+
103+// I/O error bits reported at the end of the file list.
104+const (
105+ ioErrGeneral = 1 << 0
106+ ioErrVanished = 1 << 1
107+ ioErrDelLimit = 1 << 2
108+ ioErrMask = ioErrGeneral | ioErrVanished | ioErrDelLimit
109+)
110+
111+// Compat flags a protocol 30+ server sends. We never offer incremental
112+// recursion, symlink times or iconv.
113+const (
114+ cfSafeFlist = 1 << 3
115+ cfAvoidXattrOptim = 1 << 4
116+ cfChksumSeedFix = 1 << 5
117+ cfInplacePartialDir = 1 << 6
118+ cfVarintFlistFlags = 1 << 7
119+ cfID0Names = 1 << 8
120+)
121+
122+// Exit codes (errcode.h).
123+const (
124+ exitSyntax = 1
125+ exitProtocol = 2
126+ exitFileSelect = 3
127+ exitUnsupported = 4
128+ exitFileIO = 11
129+ exitStreamIO = 12
130+ exitPartial = 23
131+ exitVanished = 24
132+ exitDelLimit = 25
133+)
+8, -0
......@@ -0,0 +1,8 @@
1+// Package rsync implements the server side of the rsync protocol, the end
2+// an rsync client talks to over SSH ("rsync --server ..."). It speaks
3+// protocol versions 27 through 31, negotiates xxh64, md5 or md4 block
4+// checksums and zlib or zlibx compression, sends and receives deltas
5+// against existing files, and supports --checksum and the --delete modes.
6+//
7+// Files live in an FS, which the pico services back with their storage.
8+package rsync
+507, -0
......@@ -0,0 +1,507 @@
1+package rsync
2+
3+import (
4+ "fmt"
5+ "strings"
6+)
7+
8+// Filter rule flags (rsync.h FILTRULE_*).
9+const (
10+ ruleWild = 1 << iota
11+ ruleWild2
12+ ruleWild2Prefix
13+ ruleWild3Suffix
14+ ruleAbsPath
15+ ruleInclude
16+ ruleDirectory
17+ ruleWordSplit
18+ ruleNoInherit
19+ ruleNoPrefixes
20+ ruleMergeFile
21+ rulePerdirMerge
22+ ruleExcludeSelf
23+ ruleFinishSetup
24+ ruleNegate
25+ ruleCVSIgnore
26+ ruleSenderSide
27+ ruleReceiverSide
28+ ruleClearList
29+ rulePerishable
30+ ruleXattr
31+)
32+
33+type filterRule struct {
34+ pattern string
35+ flags int
36+ slashCnt int
37+}
38+
39+// filterList holds the rules the client sent. The client only sends the
40+// rules that apply to our side of the transfer.
41+type filterList struct {
42+ rules []*filterRule
43+}
44+
45+// defaultCVSIgnore is the list rsync's -C adds.
46+const defaultCVSIgnore = "RCS SCCS CVS CVS.adm RCSLOG cvslog.* tags TAGS" +
47+ " .make.state .nse_depinfo *~ #* .#* ,* _$* *$" +
48+ " *.old *.bak *.BAK *.orig *.rej .del-*" +
49+ " *.a *.olb *.o *.obj *.so *.exe" +
50+ " *.Z *.elc *.ln core" +
51+ " .svn/ .git/ .hg/ .bzr/"
52+
53+// parse adds the rules in one line received from the client. Before
54+// protocol 29 only the "- " and "+ " prefixes exist.
55+func (l *filterList) parse(line string, oldPrefixes bool) error {
56+ rule := &filterRule{}
57+ s := line
58+ switch {
59+ case oldPrefixes:
60+ switch {
61+ case strings.HasPrefix(s, "- "):
62+ s = s[2:]
63+ case strings.HasPrefix(s, "+ "):
64+ rule.flags |= ruleInclude
65+ s = s[2:]
66+ case strings.HasPrefix(s, "!"):
67+ rule.flags |= ruleClearList
68+ }
69+ default:
70+ // Clients send the short form built by get_rule_prefix: a rule
71+ // character, optional modifiers, then a space or underscore.
72+ if s == "" {
73+ return nil
74+ }
75+ ch := s[0]
76+ i := 1
77+ if len(s) > 1 && s[1] == ',' {
78+ i = 2
79+ }
80+ sideFromPrefix := false
81+ switch ch {
82+ case ':':
83+ rule.flags |= rulePerdirMerge | ruleFinishSetup | ruleMergeFile
84+ case '.':
85+ rule.flags |= ruleMergeFile
86+ case '+':
87+ rule.flags |= ruleInclude
88+ case '-':
89+ case 'S':
90+ rule.flags |= ruleInclude | ruleSenderSide
91+ sideFromPrefix = true
92+ case 'H':
93+ rule.flags |= ruleSenderSide
94+ sideFromPrefix = true
95+ case 'R':
96+ rule.flags |= ruleInclude | ruleReceiverSide
97+ sideFromPrefix = true
98+ case 'P':
99+ rule.flags |= ruleReceiverSide
100+ sideFromPrefix = true
101+ case '!':
102+ rule.flags |= ruleClearList
103+ default:
104+ return fmt.Errorf("unknown filter rule: %q", line)
105+ }
106+ if ch != '!' {
107+ for ; i < len(s) && s[i] != ' ' && s[i] != '_'; i++ {
108+ switch s[i] {
109+ case '-':
110+ rule.flags |= ruleNoPrefixes
111+ case '+':
112+ rule.flags |= ruleNoPrefixes | ruleInclude
113+ case '/':
114+ rule.flags |= ruleAbsPath
115+ case '!':
116+ rule.flags |= ruleNegate
117+ case 'C':
118+ if sideFromPrefix {
119+ return fmt.Errorf("invalid modifier in filter rule: %q", line)
120+ }
121+ rule.flags |= ruleNoPrefixes | ruleWordSplit | ruleNoInherit | ruleCVSIgnore
122+ case 'e':
123+ rule.flags |= ruleExcludeSelf
124+ case 'n':
125+ rule.flags |= ruleNoInherit
126+ case 'p':
127+ rule.flags |= rulePerishable
128+ case 'r':
129+ rule.flags |= ruleReceiverSide
130+ case 's':
131+ rule.flags |= ruleSenderSide
132+ case 'w':
133+ rule.flags |= ruleWordSplit
134+ case 'x':
135+ rule.flags |= ruleXattr
136+ default:
137+ return fmt.Errorf("invalid modifier in filter rule: %q", line)
138+ }
139+ }
140+ if i < len(s) {
141+ i++
142+ }
143+ }
144+ s = s[min(i, len(s)):]
145+ }
146+
147+ if rule.flags&ruleClearList != 0 {
148+ if len(s) <= 1 {
149+ l.rules = nil
150+ return nil
151+ }
152+ rule.flags &^= ruleClearList
153+ }
154+
155+ switch {
156+ case rule.flags&ruleMergeFile != 0:
157+ // Merge files would have to be read from storage on our side.
158+ // They can only add rules for files we don't have, so skip them.
159+ return nil
160+ case rule.flags&ruleCVSIgnore != 0:
161+ for _, pat := range strings.Fields(defaultCVSIgnore) {
162+ l.add(pat, ruleNoPrefixes)
163+ }
164+ return nil
165+ case s == "":
166+ return fmt.Errorf("unexpected end of filter rule: %q", line)
167+ }
168+ l.add(s, rule.flags)
169+ return nil
170+}
171+
172+func (l *filterList) add(pat string, flags int) {
173+ rule := &filterRule{flags: flags}
174+ if len(pat) > 1 && strings.HasSuffix(pat, "/") {
175+ pat = pat[:len(pat)-1]
176+ rule.flags |= ruleDirectory
177+ }
178+ rule.slashCnt = strings.Count(pat, "/")
179+ rule.pattern = pat
180+ if strings.ContainsAny(pat, "*[?") {
181+ rule.flags |= ruleWild
182+ if i := strings.Index(pat, "**"); i >= 0 {
183+ rule.flags |= ruleWild2
184+ if i == 0 {
185+ rule.flags |= ruleWild2Prefix
186+ }
187+ if strings.HasSuffix(pat, "***") {
188+ rule.flags |= ruleWild3Suffix
189+ }
190+ }
191+ }
192+ l.rules = append(l.rules, rule)
193+}
194+
195+// check returns -1 if name is excluded, 1 if it is included and 0 when no
196+// rule matched. name is relative to the transfer root.
197+func (l *filterList) check(name string, isDir bool) int {
198+ for _, r := range l.rules {
199+ if r.matches(name, isDir) {
200+ if r.flags&ruleInclude != 0 {
201+ return 1
202+ }
203+ return -1
204+ }
205+ }
206+ return 0
207+}
208+
209+func (l *filterList) excluded(name string, isDir bool) bool {
210+ return l != nil && l.check(name, isDir) < 0
211+}
212+
213+// matches is rsync's rule_matches.
214+func (r *filterRule) matches(fname string, isDir bool) bool {
215+ ret := r.flags&ruleNegate == 0
216+ name := strings.TrimPrefix(fname, "/")
217+ if name == "" || r.flags&ruleXattr != 0 {
218+ return false
219+ }
220+
221+ var strs []string
222+ switch {
223+ case r.slashCnt == 0 && r.flags&ruleWild2 == 0:
224+ if i := strings.LastIndexByte(name, '/'); i >= 0 {
225+ name = name[i+1:]
226+ }
227+ case r.flags&ruleWild2Prefix != 0 && !strings.HasPrefix(fname, "/"):
228+ strs = append(strs, "/")
229+ }
230+ strs = append(strs, name)
231+ if isDir {
232+ if r.flags&ruleWild3Suffix != 0 {
233+ strs = append(strs, "/")
234+ }
235+ } else if r.flags&ruleDirectory != 0 {
236+ return !ret
237+ }
238+
239+ pattern := r.pattern
240+ anchored := false
241+ if strings.HasPrefix(pattern, "/") {
242+ anchored = true
243+ pattern = pattern[1:]
244+ }
245+
246+ var where int
247+ switch {
248+ case !anchored && r.slashCnt > 0 && r.flags&ruleWild2 == 0:
249+ where = r.slashCnt + 1
250+ case !anchored && r.flags&ruleWild2Prefix == 0 && r.flags&ruleWild2 != 0:
251+ where = -1
252+ }
253+
254+ text := strings.Join(strs, "")
255+ switch {
256+ case r.flags&ruleWild != 0:
257+ if wildmatchWhere(pattern, text, where) {
258+ return ret
259+ }
260+ case len(strs) > 1:
261+ if t, ok := trailingElements(text, where); ok && t == pattern {
262+ return ret
263+ }
264+ case anchored:
265+ if name == pattern {
266+ return ret
267+ }
268+ default:
269+ if strings.HasSuffix(name, pattern) &&
270+ (len(name) == len(pattern) || name[len(name)-len(pattern)-1] == '/') {
271+ return ret
272+ }
273+ }
274+ return !ret
275+}
276+
277+// trailingElements returns the last count slash-separated elements of s.
278+// A count of zero or less returns s unchanged.
279+func trailingElements(s string, count int) (string, bool) {
280+ if count <= 0 {
281+ return s, true
282+ }
283+ for i := len(s) - 1; i >= 0; i-- {
284+ if s[i] == '/' {
285+ count--
286+ if count == 0 {
287+ return s[i+1:], true
288+ }
289+ }
290+ }
291+ if count == 1 {
292+ return s, true
293+ }
294+ return "", false
295+}
296+
297+// wildmatchWhere matches pattern against the trailing where elements of
298+// text (where > 0), at the start of text (where == 0), or at the start or
299+// after any slash (where < 0).
300+func wildmatchWhere(pattern, text string, where int) bool {
301+ t, ok := trailingElements(text, where)
302+ if !ok {
303+ return false
304+ }
305+ m := dowild(pattern, t)
306+ if m == wildMatch || where >= 0 || m == wildAbortAll {
307+ return m == wildMatch
308+ }
309+ for i := 0; i < len(t); i++ {
310+ if t[i] != '/' {
311+ continue
312+ }
313+ m = dowild(pattern, t[i+1:])
314+ if m != wildNoMatch && m != wildAbortToStarStar {
315+ return m == wildMatch
316+ }
317+ }
318+ return false
319+}
320+
321+// wildmatch reports whether text matches the rsync wildcard pattern. "*"
322+// and "?" don't match "/", "**" does.
323+func wildmatch(pattern, text string) bool {
324+ return dowild(pattern, text) == wildMatch
325+}
326+
327+const (
328+ wildNoMatch = iota
329+ wildMatch
330+ wildAbortAll
331+ wildAbortToStarStar
332+)
333+
334+func dowild(p, text string) int {
335+ pi, ti := 0, 0
336+ for ; pi < len(p); pi, ti = pi+1, ti+1 {
337+ pch := p[pi]
338+ if ti >= len(text) && pch != '*' {
339+ return wildAbortAll
340+ }
341+ var tch byte
342+ if ti < len(text) {
343+ tch = text[ti]
344+ }
345+ switch pch {
346+ case '\\':
347+ pi++
348+ if pi >= len(p) || tch != p[pi] {
349+ return wildNoMatch
350+ }
351+ case '?':
352+ if tch == '/' {
353+ return wildNoMatch
354+ }
355+ case '*':
356+ special := false
357+ pi++
358+ if pi < len(p) && p[pi] == '*' {
359+ for pi < len(p) && p[pi] == '*' {
360+ pi++
361+ }
362+ special = true
363+ }
364+ if pi >= len(p) {
365+ // A trailing "**" matches everything, "*" only if
366+ // there are no more slashes.
367+ if !special && strings.IndexByte(text[ti:], '/') >= 0 {
368+ return wildNoMatch
369+ }
370+ return wildMatch
371+ }
372+ for ; ; ti++ {
373+ if ti >= len(text) {
374+ return wildAbortAll
375+ }
376+ m := dowild(p[pi:], text[ti:])
377+ if m != wildNoMatch {
378+ if !special || m != wildAbortToStarStar {
379+ return m
380+ }
381+ } else if !special && text[ti] == '/' {
382+ return wildAbortToStarStar
383+ }
384+ }
385+ case '[':
386+ pi++
387+ if pi >= len(p) {
388+ return wildAbortAll
389+ }
390+ negate := false
391+ if p[pi] == '!' || p[pi] == '^' {
392+ negate = true
393+ pi++
394+ }
395+ matched := false
396+ var prev byte
397+ for first := true; ; first = false {
398+ if pi >= len(p) {
399+ return wildAbortAll
400+ }
401+ c := p[pi]
402+ if c == ']' && !first {
403+ break
404+ }
405+ switch {
406+ case c == '\\':
407+ pi++
408+ if pi >= len(p) {
409+ return wildAbortAll
410+ }
411+ c = p[pi]
412+ if tch == c {
413+ matched = true
414+ }
415+ case c == '-' && prev != 0 && pi+1 < len(p) && p[pi+1] != ']':
416+ pi++
417+ c = p[pi]
418+ if c == '\\' {
419+ pi++
420+ if pi >= len(p) {
421+ return wildAbortAll
422+ }
423+ c = p[pi]
424+ }
425+ if tch <= c && tch >= prev {
426+ matched = true
427+ }
428+ c = 0
429+ case c == '[' && pi+1 < len(p) && p[pi+1] == ':':
430+ k := strings.IndexByte(p[pi+2:], ']')
431+ if k < 0 {
432+ return wildAbortAll
433+ }
434+ if k == 0 || p[pi+2+k-1] != ':' {
435+ // No ":]", so the '[' is literal.
436+ if tch == '[' {
437+ matched = true
438+ }
439+ break
440+ }
441+ ok, valid := charClass(p[pi+2:pi+2+k-1], tch)
442+ if !valid {
443+ return wildAbortAll
444+ }
445+ if ok {
446+ matched = true
447+ }
448+ pi += 2 + k
449+ c = 0
450+ default:
451+ if tch == c {
452+ matched = true
453+ }
454+ }
455+ prev = c
456+ pi++
457+ }
458+ if matched == negate || tch == '/' {
459+ return wildNoMatch
460+ }
461+ default:
462+ if tch != pch {
463+ return wildNoMatch
464+ }
465+ }
466+ }
467+ if ti < len(text) {
468+ return wildNoMatch
469+ }
470+ return wildMatch
471+}
472+
473+func charClass(class string, c byte) (match, valid bool) {
474+ isUpper := c >= 'A' && c <= 'Z'
475+ isLower := c >= 'a' && c <= 'z'
476+ isDigit := c >= '0' && c <= '9'
477+ isAlpha := isUpper || isLower
478+ isSpace := c == ' ' || (c >= '\t' && c <= '\r')
479+ isPrint := c >= 0x20 && c < 0x7f
480+ switch class {
481+ case "alnum":
482+ return isAlpha || isDigit, true
483+ case "alpha":
484+ return isAlpha, true
485+ case "blank":
486+ return c == ' ' || c == '\t', true
487+ case "cntrl":
488+ return c < 0x20 || c == 0x7f, true
489+ case "digit":
490+ return isDigit, true
491+ case "graph":
492+ return isPrint && c != ' ', true
493+ case "lower":
494+ return isLower, true
495+ case "print":
496+ return isPrint, true
497+ case "punct":
498+ return isPrint && c != ' ' && !isAlpha && !isDigit, true
499+ case "space":
500+ return isSpace, true
501+ case "upper":
502+ return isUpper, true
503+ case "xdigit":
504+ return isDigit || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'), true
505+ }
506+ return false, false
507+}
+752, -0
......@@ -0,0 +1,752 @@
1+package rsync
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "path"
7+ "slices"
8+ "strings"
9+)
10+
11+// Live flags of a file-list entry.
12+const (
13+ flagTopDir = 1 << 0
14+ flagContentDir = 1 << 2
15+ flagImpliedDir = 1 << 6
16+)
17+
18+// fileEntry is one file-list entry.
19+type 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+
37+func (f *fileEntry) name() string {
38+ if f.dirname == "" {
39+ return f.basename
40+ }
41+ return f.dirname + "/" + f.basename
42+}
43+
44+func (f *fileEntry) isDir() bool { return f.mode&sIFMT == sIFDIR }
45+func (f *fileEntry) isReg() bool { return f.mode&sIFMT == sIFREG }
46+func (f *fileEntry) isLink() bool { return f.mode&sIFMT == sIFLNK }
47+func (f *fileEntry) isDevice() bool {
48+ t := f.mode & sIFMT
49+ return t == sIFCHR || t == sIFBLK
50+}
51+func (f *fileEntry) isSpecial() bool {
52+ t := f.mode & sIFMT
53+ return t == sIFIFO || t == sIFSOCK
54+}
55+
56+func 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.
63+type 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+
82+func (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.
272+func (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.
291+func (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+
328+func (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+
504+func (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+
516+func (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 "..".
545+func 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.
569+const (
570+ sDir = iota
571+ sSlash
572+ sBase
573+ sTrailing
574+)
575+
576+const (
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 "/".
585+func 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+
650+type nameCursor struct {
651+ f *fileEntry
652+ tp int
653+ s string
654+ i int
655+ state int
656+ typ int
657+}
658+
659+func 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+
671+func (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+
686+func (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.
690+func (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+
709+func (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).
720+func 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+}
+98, -0
......@@ -0,0 +1,98 @@
1+package rsync
2+
3+import (
4+ "io"
5+ "time"
6+)
7+
8+// FileInfo describes an entry in an FS.
9+type FileInfo struct {
10+ // Name is slash-separated and relative to the directory being listed.
11+ Name string
12+ Size int64
13+ ModTime time.Time
14+ IsDir bool
15+}
16+
17+// File is a file opened for reading.
18+type File interface {
19+ io.ReaderAt
20+ io.Closer
21+}
22+
23+// FS is the storage the server reads from and writes to. Paths are
24+// slash-separated, relative to the root of the storage, and never start
25+// with a slash. The root itself is "".
26+type FS interface {
27+ // Stat returns the entry at name, or an error wrapping fs.ErrNotExist.
28+ Stat(name string) (FileInfo, error)
29+ // ReadDir returns the entries below dir with names relative to dir,
30+ // descending into subdirectories when recursive is set.
31+ ReadDir(dir string, recursive bool) ([]FileInfo, error)
32+ // Open opens a regular file for reading.
33+ Open(name string) (File, FileInfo, error)
34+ // Put stores a regular file with the data read from r. If the transfer
35+ // fails, r returns an error instead of io.EOF and Put must not keep
36+ // what it read. The returned message, if any, is shown to the user.
37+ Put(name string, info FileInfo, r io.Reader) (string, error)
38+ // Remove deletes a file or an empty directory.
39+ Remove(name string, isDir bool) error
40+}
41+
42+// mapFile gives windowed access to a file being sent, like rsync's
43+// map_ptr. Returned slices are valid until the next call. After a read error
44+// it returns zeroes and records the error, which the caller checks once the
45+// file is done.
46+type mapFile struct {
47+ r io.ReaderAt
48+ size int64
49+ window []byte
50+ start int64
51+ defSize int64
52+ err error
53+}
54+
55+func newMapFile(r io.ReaderAt, size int64, readSize int64, blength int32) *mapFile {
56+ if blength > 0 && readSize%int64(blength) != 0 {
57+ readSize += int64(blength) - readSize%int64(blength)
58+ }
59+ return &mapFile{r: r, size: size, defSize: alignUp(readSize)}
60+}
61+
62+func alignUp(n int64) int64 { return (n + 1023) &^ 1023 }
63+
64+func (m *mapFile) ptr(offset int64, n int) []byte {
65+ if n == 0 {
66+ return nil
67+ }
68+ if m.err != nil {
69+ return make([]byte, n)
70+ }
71+ end := offset + int64(n)
72+ if offset >= m.start && end <= m.start+int64(len(m.window)) {
73+ return m.window[offset-m.start : end-m.start]
74+ }
75+ start := offset &^ 1023
76+ size := max(m.defSize, alignUp(end-start))
77+ if start+size > m.size {
78+ size = m.size - start
79+ }
80+ buf := make([]byte, size)
81+ // Reuse the overlap with the current window.
82+ read := buf
83+ readAt := start
84+ if start >= m.start && start < m.start+int64(len(m.window)) {
85+ k := copy(buf, m.window[start-m.start:])
86+ read = buf[k:]
87+ readAt = start + int64(k)
88+ }
89+ if len(read) > 0 {
90+ if err := readFullAt(m.r, read, readAt); err != nil {
91+ m.err = err
92+ return make([]byte, n)
93+ }
94+ }
95+ m.window = buf
96+ m.start = start
97+ return m.window[offset-m.start : end-m.start]
98+}
+685, -0
......@@ -0,0 +1,685 @@
1+package rsync
2+
3+import (
4+ "bytes"
5+ "crypto/rand"
6+ "errors"
7+ "fmt"
8+ "io"
9+ "io/fs"
10+ "os"
11+ "os/exec"
12+ "path/filepath"
13+ "slices"
14+ "strings"
15+ "testing"
16+ "time"
17+)
18+
19+// dirFS serves a local directory.
20+type dirFS struct{ root string }
21+
22+func (d dirFS) full(name string) string { return filepath.Join(d.root, filepath.FromSlash(name)) }
23+
24+func (d dirFS) Stat(name string) (FileInfo, error) {
25+ st, err := os.Stat(d.full(name))
26+ if err != nil {
27+ return FileInfo{}, err
28+ }
29+ return FileInfo{Name: st.Name(), Size: st.Size(), ModTime: st.ModTime(), IsDir: st.IsDir()}, nil
30+}
31+
32+func (d dirFS) ReadDir(dir string, recursive bool) ([]FileInfo, error) {
33+ base := d.full(dir)
34+ var out []FileInfo
35+ err := filepath.WalkDir(base, func(p string, e fs.DirEntry, err error) error {
36+ if err != nil {
37+ return err
38+ }
39+ if p == base {
40+ return nil
41+ }
42+ st, err := e.Info()
43+ if err != nil {
44+ return err
45+ }
46+ rel, _ := filepath.Rel(base, p)
47+ out = append(out, FileInfo{Name: filepath.ToSlash(rel), Size: st.Size(), ModTime: st.ModTime(), IsDir: st.IsDir()})
48+ if e.IsDir() && !recursive {
49+ return filepath.SkipDir
50+ }
51+ return nil
52+ })
53+ return out, err
54+}
55+
56+func (d dirFS) Open(name string) (File, FileInfo, error) {
57+ f, err := os.Open(d.full(name))
58+ if err != nil {
59+ return nil, FileInfo{}, err
60+ }
61+ st, err := f.Stat()
62+ if err != nil {
63+ _ = f.Close()
64+ return nil, FileInfo{}, err
65+ }
66+ return f, FileInfo{Name: st.Name(), Size: st.Size(), ModTime: st.ModTime()}, nil
67+}
68+
69+func (d dirFS) Put(name string, info FileInfo, r io.Reader) (string, error) {
70+ p := d.full(name)
71+ if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
72+ return "", err
73+ }
74+ tmp, err := os.CreateTemp(filepath.Dir(p), ".tmp-")
75+ if err != nil {
76+ return "", err
77+ }
78+ if _, err := io.Copy(tmp, r); err != nil {
79+ _ = tmp.Close()
80+ _ = os.Remove(tmp.Name())
81+ return "", err
82+ }
83+ if err := tmp.Close(); err != nil {
84+ return "", err
85+ }
86+ if err := os.Rename(tmp.Name(), p); err != nil {
87+ return "", err
88+ }
89+ return "", os.Chtimes(p, info.ModTime, info.ModTime)
90+}
91+
92+func (d dirFS) Remove(name string, isDir bool) error { return os.Remove(d.full(name)) }
93+
94+// TestHelperProcess is the remote shell rsync runs in these tests. It serves
95+// the directory in RSYNC_TEST_ROOT on stdin and stdout.
96+func TestHelperProcess(t *testing.T) {
97+ root := os.Getenv("RSYNC_TEST_ROOT")
98+ if root == "" {
99+ t.Skip("helper process")
100+ }
101+ args := os.Args
102+ for len(args) > 0 && args[0] != "--" {
103+ args = args[1:]
104+ }
105+ // Skip "--", the host name and "rsync".
106+ if len(args) < 3 {
107+ os.Exit(2)
108+ }
109+ args = args[3:]
110+ rw := struct {
111+ io.Reader
112+ io.Writer
113+ }{os.Stdin, os.Stdout}
114+ err := Serve(Config{FS: dirFS{root: root}, Stderr: os.Stderr}, rw, args)
115+ code := 0
116+ var exit *ExitError
117+ if errors.As(err, &exit) {
118+ code = exit.Code
119+ } else if err != nil {
120+ code = 1
121+ }
122+ os.Exit(code)
123+}
124+
125+type rsyncEnv struct {
126+ t *testing.T
127+ remote string // directory our server serves
128+ local string
129+}
130+
131+func newRsyncEnv(t *testing.T) *rsyncEnv {
132+ t.Helper()
133+ if _, err := exec.LookPath("rsync"); err != nil {
134+ t.Skip("rsync binary not found")
135+ }
136+ return &rsyncEnv{t: t, remote: t.TempDir(), local: t.TempDir()}
137+}
138+
139+// run executes the rsync client with our server on the other end.
140+func (e *rsyncEnv) run(args ...string) (string, error) {
141+ e.t.Helper()
142+ rsh := fmt.Sprintf("%s -test.run=^TestHelperProcess$ --", os.Args[0])
143+ cmd := exec.Command("rsync", append([]string{"-e", rsh}, args...)...)
144+ cmd.Env = append(os.Environ(), "RSYNC_TEST_ROOT="+e.remote)
145+ out, err := cmd.CombinedOutput()
146+ return string(out), err
147+}
148+
149+func (e *rsyncEnv) mustRun(args ...string) string {
150+ e.t.Helper()
151+ out, err := e.run(args...)
152+ if err != nil {
153+ e.t.Fatalf("rsync %s: %v\n%s", strings.Join(args, " "), err, out)
154+ }
155+ return out
156+}
157+
158+func writeFile(t *testing.T, path string, data []byte) {
159+ t.Helper()
160+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
161+ t.Fatal(err)
162+ }
163+ if err := os.WriteFile(path, data, 0o644); err != nil {
164+ t.Fatal(err)
165+ }
166+}
167+
168+func randomBytes(t *testing.T, n int) []byte {
169+ t.Helper()
170+ b := make([]byte, n)
171+ if _, err := rand.Read(b); err != nil {
172+ t.Fatal(err)
173+ }
174+ return b
175+}
176+
177+// textBytes returns compressible data.
178+func textBytes(n int, seed string) []byte {
179+ var b bytes.Buffer
180+ for i := 0; b.Len() < n; i++ {
181+ fmt.Fprintf(&b, "%s line %d: the quick brown fox jumps over the lazy dog\n", seed, i)
182+ }
183+ return b.Bytes()[:n]
184+}
185+
186+// tree returns the regular files below root with their contents.
187+func tree(t *testing.T, root string) map[string]string {
188+ t.Helper()
189+ out := map[string]string{}
190+ err := filepath.WalkDir(root, func(p string, e fs.DirEntry, err error) error {
191+ if err != nil {
192+ return err
193+ }
194+ if e.Type().IsRegular() {
195+ b, err := os.ReadFile(p)
196+ if err != nil {
197+ return err
198+ }
199+ rel, _ := filepath.Rel(root, p)
200+ out[filepath.ToSlash(rel)] = string(b)
201+ }
202+ return nil
203+ })
204+ if err != nil {
205+ t.Fatal(err)
206+ }
207+ return out
208+}
209+
210+func assertSameTree(t *testing.T, want, got string) {
211+ t.Helper()
212+ a, b := tree(t, want), tree(t, got)
213+ var names []string
214+ for k := range a {
215+ names = append(names, k)
216+ }
217+ for k := range b {
218+ if _, ok := a[k]; !ok {
219+ names = append(names, k)
220+ }
221+ }
222+ slices.Sort(names)
223+ for _, n := range slices.Compact(names) {
224+ av, aok := a[n]
225+ bv, bok := b[n]
226+ switch {
227+ case !aok:
228+ t.Errorf("unexpected file %s", n)
229+ case !bok:
230+ t.Errorf("missing file %s", n)
231+ case av != bv:
232+ t.Errorf("file %s differs (%d bytes, want %d)", n, len(bv), len(av))
233+ }
234+ }
235+}
236+
237+func populate(t *testing.T, root string) {
238+ t.Helper()
239+ writeFile(t, filepath.Join(root, "index.html"), textBytes(3000, "index"))
240+ writeFile(t, filepath.Join(root, "empty.txt"), nil)
241+ writeFile(t, filepath.Join(root, "css/site.css"), textBytes(800, "css"))
242+ writeFile(t, filepath.Join(root, "img/logo.bin"), randomBytes(t, 200_000))
243+ writeFile(t, filepath.Join(root, "a/b/c/deep.txt"), textBytes(50, "deep"))
244+ writeFile(t, filepath.Join(root, "a.b"), []byte("dot file next to dir a"))
245+ writeFile(t, filepath.Join(root, "a-b"), []byte("dash file next to dir a"))
246+ writeFile(t, filepath.Join(root, "big.dat"), randomBytes(t, 3_000_000))
247+}
248+
249+var protocols = []string{"27", "28", "29", "30", "31", ""}
250+
251+func protoArgs(p string) []string {
252+ if p == "" {
253+ return nil
254+ }
255+ return []string{"--protocol=" + p}
256+}
257+
258+func TestRsyncUpload(t *testing.T) {
259+ for _, proto := range protocols {
260+ for _, compress := range [][]string{nil, {"-z"}, {"--compress-choice=zlib"}, {"--compress-choice=zlibx"}} {
261+ name := fmt.Sprintf("proto=%s/%s", proto, strings.Join(compress, ""))
262+ t.Run(name, func(t *testing.T) {
263+ if proto != "" && proto < "30" && len(compress) > 0 && compress[0] != "-z" {
264+ t.Skip("compression choice needs protocol 30")
265+ }
266+ e := newRsyncEnv(t)
267+ populate(t, e.local)
268+ args := append(protoArgs(proto), compress...)
269+ e.mustRun(append(args, "-rt", e.local+"/", "host:site")...)
270+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
271+
272+ // Change part of a big file and upload again: only the
273+ // changed region should cross the wire.
274+ big := filepath.Join(e.local, "big.dat")
275+ data, _ := os.ReadFile(big)
276+ copy(data[1_000_000:], randomBytes(t, 5000))
277+ data = append(data[:2_000_000], append(randomBytes(t, 1234), data[2_000_000:]...)...)
278+ writeFile(t, big, data)
279+ writeFile(t, filepath.Join(e.local, "css/site.css"), textBytes(900, "css2"))
280+ out := e.mustRun(append(args, "-rt", "--stats", e.local+"/", "host:site")...)
281+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
282+ literal := statValue(t, out, "Literal data")
283+ if literal > 100_000 {
284+ t.Errorf("literal data %d, want a delta transfer\n%s", literal, out)
285+ }
286+ if matched := statValue(t, out, "Matched data"); matched < 2_000_000 {
287+ t.Errorf("matched data %d, want most of big.dat\n%s", matched, out)
288+ }
289+ })
290+ }
291+ }
292+}
293+
294+func TestRsyncDownload(t *testing.T) {
295+ for _, proto := range protocols {
296+ for _, compress := range [][]string{nil, {"-z"}, {"--compress-choice=zlib"}} {
297+ name := fmt.Sprintf("proto=%s/%s", proto, strings.Join(compress, ""))
298+ t.Run(name, func(t *testing.T) {
299+ if proto != "" && proto < "30" && len(compress) > 0 && compress[0] != "-z" {
300+ t.Skip("compression choice needs protocol 30")
301+ }
302+ e := newRsyncEnv(t)
303+ site := filepath.Join(e.remote, "site")
304+ populate(t, site)
305+ args := append(protoArgs(proto), compress...)
306+ e.mustRun(append(args, "-rt", "host:site/", e.local+"/")...)
307+ assertSameTree(t, site, e.local)
308+
309+ // Change the server's copy and download again.
310+ big := filepath.Join(site, "big.dat")
311+ data, _ := os.ReadFile(big)
312+ copy(data[500_000:], randomBytes(t, 3000))
313+ writeFile(t, big, data)
314+ // rsync compares whole seconds, so make the change visible.
315+ later := time.Now().Add(time.Minute)
316+ if err := os.Chtimes(big, later, later); err != nil {
317+ t.Fatal(err)
318+ }
319+ out := e.mustRun(append(args, "-rt", "--stats", "host:site/", e.local+"/")...)
320+ assertSameTree(t, site, e.local)
321+ if literal := statValue(t, out, "Literal data"); literal > 100_000 {
322+ t.Errorf("literal data %d, want a delta transfer\n%s", literal, out)
323+ }
324+ })
325+ }
326+ }
327+}
328+
329+func TestRsyncChecksumChoices(t *testing.T) {
330+ for _, choice := range []string{"md4", "md5", "xxh64", "md5,md4"} {
331+ t.Run(choice, func(t *testing.T) {
332+ e := newRsyncEnv(t)
333+ populate(t, e.local)
334+ e.mustRun("-r", "--checksum-choice="+choice, e.local+"/", "host:site")
335+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
336+ e.mustRun("-r", "-c", "--checksum-choice="+choice, "host:site/", t.TempDir())
337+ })
338+ }
339+}
340+
341+func TestRsyncAlwaysChecksum(t *testing.T) {
342+ for _, proto := range protocols {
343+ t.Run("proto="+proto, func(t *testing.T) {
344+ e := newRsyncEnv(t)
345+ populate(t, e.local)
346+ args := protoArgs(proto)
347+ e.mustRun(append(args, "-r", e.local+"/", "host:site")...)
348+
349+ // Same size and time, different content: only -c notices.
350+ p := filepath.Join(e.local, "css/site.css")
351+ st, _ := os.Stat(filepath.Join(e.remote, "site/css/site.css"))
352+ data, _ := os.ReadFile(p)
353+ data[10] ^= 0xff
354+ writeFile(t, p, data)
355+ _ = os.Chtimes(p, st.ModTime(), st.ModTime())
356+
357+ out := e.mustRun(append(args, "-rv", e.local+"/", "host:site")...)
358+ if strings.Contains(out, "css/site.css") {
359+ t.Fatalf("quick check should have skipped the file\n%s", out)
360+ }
361+ out = e.mustRun(append(args, "-rvc", e.local+"/", "host:site")...)
362+ if !strings.Contains(out, "css/site.css") {
363+ t.Fatalf("checksum run did not transfer the file\n%s", out)
364+ }
365+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
366+
367+ // And the other direction.
368+ dl := t.TempDir()
369+ e.mustRun(append(args, "-rt", "host:site/", dl+"/")...)
370+ data, _ = os.ReadFile(filepath.Join(dl, "index.html"))
371+ st, _ = os.Stat(filepath.Join(dl, "index.html"))
372+ data[0] ^= 0xff
373+ writeFile(t, filepath.Join(dl, "index.html"), data)
374+ _ = os.Chtimes(filepath.Join(dl, "index.html"), st.ModTime(), st.ModTime())
375+ e.mustRun(append(args, "-rtc", "host:site/", dl+"/")...)
376+ assertSameTree(t, filepath.Join(e.remote, "site"), dl)
377+ })
378+ }
379+}
380+
381+func TestRsyncDelete(t *testing.T) {
382+ for _, proto := range protocols {
383+ for _, when := range []string{"--delete", "--delete-before", "--delete-during", "--delete-after", "--delete-delay"} {
384+ t.Run(fmt.Sprintf("proto=%s/%s", proto, when), func(t *testing.T) {
385+ if proto != "" && proto < "30" && when == "--delete-delay" {
386+ t.Skip("needs protocol 30")
387+ }
388+ e := newRsyncEnv(t)
389+ populate(t, e.local)
390+ args := protoArgs(proto)
391+ e.mustRun(append(args, "-r", e.local+"/", "host:site")...)
392+
393+ writeFile(t, filepath.Join(e.remote, "site/stale.txt"), []byte("old"))
394+ writeFile(t, filepath.Join(e.remote, "site/old/dir/file.txt"), []byte("old"))
395+ writeFile(t, filepath.Join(e.remote, "site/keep.log"), []byte("protected"))
396+ if err := os.RemoveAll(filepath.Join(e.local, "a")); err != nil {
397+ t.Fatal(err)
398+ }
399+
400+ out := e.mustRun(append(args, "-rv", when, "--exclude=*.log", e.local+"/", "host:site")...)
401+ if !strings.Contains(out, "deleting stale.txt") {
402+ t.Errorf("missing deletion message\n%s", out)
403+ }
404+ remote := tree(t, filepath.Join(e.remote, "site"))
405+ if _, ok := remote["keep.log"]; !ok {
406+ t.Error("excluded file was deleted")
407+ }
408+ delete(remote, "keep.log")
409+ if err := os.Remove(filepath.Join(e.remote, "site/keep.log")); err != nil {
410+ t.Fatal(err)
411+ }
412+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
413+ if _, err := os.Stat(filepath.Join(e.remote, "site/old")); !os.IsNotExist(err) {
414+ t.Errorf("stale directory survived: %v", err)
415+ }
416+ })
417+ }
418+ }
419+}
420+
421+func TestRsyncDownloadFilters(t *testing.T) {
422+ e := newRsyncEnv(t)
423+ site := filepath.Join(e.remote, "site")
424+ populate(t, site)
425+ e.mustRun("-r", "--exclude=*.bin", "--exclude=/a/", "host:site/", e.local+"/")
426+ got := tree(t, e.local)
427+ if _, ok := got["img/logo.bin"]; ok {
428+ t.Error("excluded file was sent")
429+ }
430+ if _, ok := got["a/b/c/deep.txt"]; ok {
431+ t.Error("excluded directory was sent")
432+ }
433+ if _, ok := got["index.html"]; !ok {
434+ t.Error("index.html missing")
435+ }
436+}
437+
438+func TestRsyncSingleFile(t *testing.T) {
439+ e := newRsyncEnv(t)
440+ writeFile(t, filepath.Join(e.local, "one.txt"), []byte("hello"))
441+
442+ e.mustRun(filepath.Join(e.local, "one.txt"), "host:site/renamed.txt")
443+ if b, err := os.ReadFile(filepath.Join(e.remote, "site/renamed.txt")); err != nil || string(b) != "hello" {
444+ t.Fatalf("single file upload: %q %v", b, err)
445+ }
446+ e.mustRun(filepath.Join(e.local, "one.txt"), "host:site/")
447+ if b, err := os.ReadFile(filepath.Join(e.remote, "site/one.txt")); err != nil || string(b) != "hello" {
448+ t.Fatalf("upload into dir: %q %v", b, err)
449+ }
450+
451+ dl := t.TempDir()
452+ e.mustRun("host:site/renamed.txt", dl+"/")
453+ if b, err := os.ReadFile(filepath.Join(dl, "renamed.txt")); err != nil || string(b) != "hello" {
454+ t.Fatalf("single file download: %q %v", b, err)
455+ }
456+}
457+
458+func TestRsyncListOnly(t *testing.T) {
459+ e := newRsyncEnv(t)
460+ populate(t, filepath.Join(e.remote, "site"))
461+ out := e.mustRun("host:site/")
462+ for _, want := range []string{"index.html", "css", "big.dat"} {
463+ if !strings.Contains(out, want) {
464+ t.Errorf("listing lacks %s\n%s", want, out)
465+ }
466+ }
467+ if strings.Contains(out, "site.css") {
468+ t.Errorf("non-recursive listing descended\n%s", out)
469+ }
470+ out = e.mustRun("-r", "host:site/")
471+ if !strings.Contains(out, "css/site.css") {
472+ t.Errorf("recursive listing lacks css/site.css\n%s", out)
473+ }
474+}
475+
476+func TestRsyncDryRun(t *testing.T) {
477+ e := newRsyncEnv(t)
478+ populate(t, e.local)
479+ out := e.mustRun("-rvn", e.local+"/", "host:site")
480+ if !strings.Contains(out, "big.dat") {
481+ t.Errorf("dry run did not list files\n%s", out)
482+ }
483+ if _, err := os.Stat(filepath.Join(e.remote, "site")); !os.IsNotExist(err) {
484+ t.Errorf("dry run wrote files: %v", err)
485+ }
486+}
487+
488+func TestRsyncMissingSource(t *testing.T) {
489+ e := newRsyncEnv(t)
490+ out, err := e.run("-r", "host:nope/", e.local+"/")
491+ var exit *exec.ExitError
492+ if !errors.As(err, &exit) || exit.ExitCode() != exitPartial {
493+ t.Fatalf("want exit code %d, got %v\n%s", exitPartial, err, out)
494+ }
495+ if !strings.Contains(out, "No such file or directory") {
496+ t.Errorf("missing error message\n%s", out)
497+ }
498+}
499+
500+func TestRsyncUnchangedSkipped(t *testing.T) {
501+ e := newRsyncEnv(t)
502+ populate(t, e.local)
503+ e.mustRun("-rt", e.local+"/", "host:site")
504+ out := e.mustRun("-rt", "--stats", e.local+"/", "host:site")
505+ if n := statValue(t, out, "Number of regular files transferred"); n != 0 {
506+ t.Errorf("transferred %d files, want 0\n%s", n, out)
507+ }
508+}
509+
510+func statValue(t *testing.T, out, label string) int64 {
511+ t.Helper()
512+ for _, line := range strings.Split(out, "\n") {
513+ rest, ok := strings.CutPrefix(line, label+": ")
514+ if !ok {
515+ continue
516+ }
517+ rest = strings.TrimSuffix(rest, " bytes")
518+ rest = strings.ReplaceAll(rest, ",", "")
519+ var v int64
520+ if _, err := fmt.Sscan(rest, &v); err == nil {
521+ return v
522+ }
523+ }
524+ t.Fatalf("no %q in output\n%s", label, out)
525+ return 0
526+}
527+
528+func TestRsyncCompression(t *testing.T) {
529+ type tc struct {
530+ proto string
531+ args []string
532+ }
533+ cases := []tc{
534+ {"27", []string{"-z"}},
535+ {"29", []string{"-z"}},
536+ {"30", []string{"-z"}},
537+ {"30", []string{"--compress-choice=zlib"}},
538+ {"31", []string{"--compress-choice=zlib"}},
539+ {"", []string{"-z"}},
540+ {"", []string{"--compress-choice=zlibx"}},
541+ {"", []string{"-z", "--compress-level=1"}},
542+ {"", []string{"-z", "--compress-level=9"}},
543+ }
544+ for _, c := range cases {
545+ for _, block := range []string{"", "--block-size=100000"} {
546+ t.Run(fmt.Sprintf("proto=%s/%s/%s", c.proto, strings.Join(c.args, ""), block), func(t *testing.T) {
547+ e := newRsyncEnv(t)
548+ text := textBytes(2_000_000, "compressible")
549+ writeFile(t, filepath.Join(e.local, "text.txt"), text)
550+ args := append(protoArgs(c.proto), c.args...)
551+ if block != "" {
552+ args = append(args, block)
553+ }
554+
555+ out := e.mustRun(append(args, "-rt", "--stats", e.local+"/", "host:site")...)
556+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
557+ if sent := statValue(t, out, "Total bytes sent"); sent > 500_000 {
558+ t.Errorf("upload sent %d bytes, compression is not in effect\n%s", sent, out)
559+ }
560+
561+ // A delta with matched blocks exercises the deflate history.
562+ text = append(textBytes(150_000, "prefix"), text...)
563+ text = append(text, textBytes(70_000, "suffix")...)
564+ writeFile(t, filepath.Join(e.local, "text.txt"), text)
565+ out = e.mustRun(append(args, "-rt", "--stats", e.local+"/", "host:site")...)
566+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
567+ if matched := statValue(t, out, "Matched data"); matched < 1_000_000 {
568+ t.Errorf("matched %d bytes, want a delta\n%s", matched, out)
569+ }
570+
571+ dl := t.TempDir()
572+ writeFile(t, filepath.Join(dl, "text.txt"), textBytes(2_000_000, "compressible"))
573+ old := time.Now().Add(-time.Hour)
574+ _ = os.Chtimes(filepath.Join(dl, "text.txt"), old, old)
575+ out = e.mustRun(append(args, "-rt", "--stats", "host:site/", dl+"/")...)
576+ assertSameTree(t, filepath.Join(e.remote, "site"), dl)
577+ if recv := statValue(t, out, "Total bytes received"); recv > 300_000 {
578+ t.Errorf("download received %d bytes, want a compressed delta\n%s", recv, out)
579+ }
580+ if matched := statValue(t, out, "Matched data"); matched < 1_000_000 {
581+ t.Errorf("download matched %d bytes, want a delta\n%s", matched, out)
582+ }
583+ })
584+ }
585+ }
586+}
587+
588+func TestRsyncArchiveMode(t *testing.T) {
589+ for _, proto := range protocols {
590+ t.Run("proto="+proto, func(t *testing.T) {
591+ e := newRsyncEnv(t)
592+ populate(t, e.local)
593+ if err := os.Symlink("index.html", filepath.Join(e.local, "link.html")); err != nil {
594+ t.Fatal(err)
595+ }
596+ args := protoArgs(proto)
597+ out := e.mustRun(append(args, "-av", e.local+"/", "host:site")...)
598+ if !strings.Contains(out, `skipping non-regular file "link.html"`) {
599+ t.Errorf("symlink not reported as skipped\n%s", out)
600+ }
601+ _ = os.Remove(filepath.Join(e.local, "link.html"))
602+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
603+
604+ dl := t.TempDir()
605+ e.mustRun(append(args, "-a", "host:site/", dl+"/")...)
606+ assertSameTree(t, e.local, dl)
607+ })
608+ }
609+}
610+
611+func TestRsyncTransferOptions(t *testing.T) {
612+ cases := [][]string{
613+ {"-s"},
614+ {"-W"},
615+ {"--inplace"},
616+ {"-u"},
617+ {"--size-only"},
618+ {"-I"},
619+ {"--block-size=2048"},
620+ {"--checksum-seed=12345"},
621+ {"--numeric-ids", "-og"},
622+ {"-U"},
623+ }
624+ for _, opts := range cases {
625+ t.Run(strings.Join(opts, " "), func(t *testing.T) {
626+ e := newRsyncEnv(t)
627+ populate(t, e.local)
628+ e.mustRun(append(slices.Clone(opts), "-rt", e.local+"/", "host:site")...)
629+ assertSameTree(t, e.local, filepath.Join(e.remote, "site"))
630+
631+ big := filepath.Join(e.remote, "site/big.dat")
632+ data, _ := os.ReadFile(big)
633+ copy(data[100_000:], randomBytes(t, 4000))
634+ copy(data[2_500_000:], data[10_000:20_000])
635+ writeFile(t, big, data)
636+ later := time.Now().Add(time.Minute)
637+ _ = os.Chtimes(big, later, later)
638+
639+ dl := t.TempDir()
640+ e.mustRun(append(slices.Clone(opts), "-rt", e.local+"/", dl+"/")...)
641+ e.mustRun(append(slices.Clone(opts), "-rt", "host:site/", dl+"/")...)
642+ if opts[0] != "--size-only" {
643+ assertSameTree(t, filepath.Join(e.remote, "site"), dl)
644+ }
645+ })
646+ }
647+}
648+
649+func TestRsyncSkipOptions(t *testing.T) {
650+ e := newRsyncEnv(t)
651+ writeFile(t, filepath.Join(e.local, "new.txt"), []byte("new"))
652+ writeFile(t, filepath.Join(e.local, "old.txt"), []byte("changed locally"))
653+ writeFile(t, filepath.Join(e.local, "huge.txt"), textBytes(50_000, "huge"))
654+ writeFile(t, filepath.Join(e.remote, "site/old.txt"), []byte("remote"))
655+
656+ e.mustRun("-r", "--existing", e.local+"/", "host:site")
657+ got := tree(t, filepath.Join(e.remote, "site"))
658+ if _, ok := got["new.txt"]; ok || got["old.txt"] != "changed locally" {
659+ t.Errorf("--existing: %v", got)
660+ }
661+
662+ writeFile(t, filepath.Join(e.remote, "site/old.txt"), []byte("remote"))
663+ e.mustRun("-r", "--ignore-existing", "--max-size=10k", e.local+"/", "host:site")
664+ got = tree(t, filepath.Join(e.remote, "site"))
665+ if got["old.txt"] != "remote" || got["new.txt"] != "new" {
666+ t.Errorf("--ignore-existing: %v", got)
667+ }
668+ if _, ok := got["huge.txt"]; ok {
669+ t.Error("--max-size let a large file through")
670+ }
671+}
672+
673+func TestRsyncUnsupportedOptions(t *testing.T) {
674+ e := newRsyncEnv(t)
675+ populate(t, e.local)
676+ for _, opt := range []string{"-H", "-A", "-X", "-R"} {
677+ out, err := e.run("-r", opt, e.local+"/", "host:site")
678+ if err == nil {
679+ t.Errorf("%s: want an error\n%s", opt, out)
680+ }
681+ if !strings.Contains(out, "not supported") {
682+ t.Errorf("%s: missing explanation\n%s", opt, out)
683+ }
684+ }
685+}
+233, -0
......@@ -0,0 +1,233 @@
1+package rsync
2+
3+import (
4+ "bytes"
5+ "hash"
6+)
7+
8+// matcher finds the receiver's blocks in the file being sent and emits the
9+// delta (rsync's match.c).
10+type matcher struct {
11+ head sumHead
12+ sums []sumBuf
13+ cs *checksums
14+ tok tokenSender
15+ buf *mapFile
16+ sum hash.Hash
17+ lastMatch int64
18+ inplace bool // the receiver updates its basis file in place
19+
20+ tablesize uint32
21+ table []int32
22+
23+ literal int64
24+ matched int64
25+}
26+
27+// matched sends the literal data before offset and, for i >= 0, the token of
28+// block i. i == -1 ends the file and i == -2 only flushes literal data.
29+func (m *matcher) emit(offset int64, i int32) error {
30+ n := offset - m.lastMatch
31+ var block []byte
32+ if i >= 0 {
33+ block = m.buf.ptr(offset, int(m.sums[i].len))
34+ // Copy, the literal ptr below may move the window.
35+ block = append([]byte(nil), block...)
36+ }
37+ literal := m.buf.ptr(m.lastMatch, int(n))
38+ if err := m.tok.send(i, literal, block); err != nil {
39+ return err
40+ }
41+ m.literal += n
42+ total := n
43+ if i >= 0 {
44+ m.matched += int64(m.sums[i].len)
45+ total += int64(m.sums[i].len)
46+ }
47+ for j := int64(0); j < total; j += chunkSize {
48+ n1 := min(chunkSize, total-j)
49+ m.sum.Write(m.buf.ptr(m.lastMatch+j, int(n1)))
50+ }
51+ if i >= 0 {
52+ m.lastMatch = offset + int64(m.sums[i].len)
53+ } else {
54+ m.lastMatch = offset
55+ }
56+ return nil
57+}
58+
59+func (m *matcher) buildTable() {
60+ m.tablesize = uint32(m.head.count/8)*10 + 11
61+ if m.tablesize < traditionalTableSize {
62+ m.tablesize = traditionalTableSize
63+ }
64+ m.table = make([]int32, m.tablesize)
65+ for i := range m.table {
66+ m.table[i] = -1
67+ }
68+ for i := range m.sums {
69+ var t uint32
70+ if m.tablesize == traditionalTableSize {
71+ s := m.sums[i].sum1
72+ t = (s&0xFFFF + s>>16) & 0xFFFF
73+ } else {
74+ t = m.sums[i].sum1 % m.tablesize
75+ }
76+ m.sums[i].chain = m.table[t]
77+ m.table[t] = int32(i)
78+ }
79+}
80+
81+// run sends a file of length size.
82+func (m *matcher) run(size int64) error {
83+ if size > 0 && m.head.count > 0 {
84+ m.buildTable()
85+ if err := m.hashSearch(size); err != nil {
86+ return err
87+ }
88+ } else {
89+ for j := m.lastMatch + chunkSize; j < size; j += chunkSize {
90+ if err := m.emit(j, -2); err != nil {
91+ return err
92+ }
93+ }
94+ if err := m.emit(size, -1); err != nil {
95+ return err
96+ }
97+ }
98+ return nil
99+}
100+
101+func (m *matcher) hashSearch(size int64) error {
102+ var sum2 []byte
103+ wantI := int32(0)
104+ blength := int64(m.head.blength)
105+ k := min(size, blength)
106+ sum := checksum1(m.buf.ptr(0, int(k)))
107+ s1, s2 := sum&0xFFFF, sum>>16
108+ var offset, alignedOffset int64
109+ alignedI := int32(0)
110+ end := size + 1 - int64(m.sums[len(m.sums)-1].len)
111+ s2len := int(m.head.s2length)
112+
113+ for {
114+ doneCsum2 := false
115+ var hashEntry uint32
116+ if m.tablesize == traditionalTableSize {
117+ hashEntry = (s1 + s2) & 0xFFFF
118+ sum = s1&0xffff | s2<<16
119+ } else {
120+ sum = s1&0xffff | s2<<16
121+ hashEntry = sum % m.tablesize
122+ }
123+ i := m.table[hashEntry]
124+ prev := &m.table[hashEntry]
125+ chainLen := 0
126+ for ; i >= 0; i = m.sums[i].chain {
127+ // An in-place receiver can only use blocks at or after the
128+ // current offset, or ones it has left untouched.
129+ if m.inplace && m.sums[i].offset < offset && !m.sums[i].same {
130+ *prev = m.sums[i].chain
131+ continue
132+ }
133+ prev = &m.sums[i].chain
134+ if sum != m.sums[i].sum1 {
135+ continue
136+ }
137+ chainLen++
138+ if chainLen > maxChainLen {
139+ break
140+ }
141+ l := int32(min(blength, size-offset))
142+ if l != m.sums[i].len {
143+ continue
144+ }
145+ if !doneCsum2 {
146+ sum2 = m.cs.blockSum(m.buf.ptr(offset, int(l)), sum2[:0])
147+ doneCsum2 = true
148+ }
149+ if !bytes.Equal(sum2[:s2len], m.sums[i].sum2) {
150+ continue
151+ }
152+
153+ if m.inplace {
154+ for alignedOffset < offset {
155+ alignedOffset += blength
156+ alignedI++
157+ }
158+ if (offset == alignedOffset || (sum == 0 && int64(l) == blength && alignedOffset+int64(l) <= size)) && alignedI < m.head.count {
159+ ok := true
160+ if i != alignedI {
161+ a := &m.sums[alignedI]
162+ if sum != a.sum1 || l != a.len || !bytes.Equal(sum2[:s2len], a.sum2) {
163+ ok = false
164+ } else {
165+ i = alignedI
166+ }
167+ }
168+ if ok && offset != alignedOffset {
169+ backup := max(alignedOffset-m.lastMatch, 0)
170+ block := m.buf.ptr(alignedOffset-backup, int(int64(l)+backup))[backup:]
171+ if checksum1(block) != m.sums[i].sum1 ||
172+ !bytes.Equal(m.cs.blockSum(block, nil)[:s2len], m.sums[i].sum2) {
173+ ok = false
174+ } else {
175+ offset = alignedOffset
176+ }
177+ }
178+ if ok {
179+ m.sums[i].same = true
180+ wantI = i
181+ }
182+ }
183+ }
184+
185+ if i != wantI && wantI < m.head.count &&
186+ (!m.inplace || m.sums[wantI].offset >= offset || m.sums[wantI].same) &&
187+ sum == m.sums[wantI].sum1 && l == m.sums[wantI].len &&
188+ bytes.Equal(sum2[:s2len], m.sums[wantI].sum2) {
189+ // Prefer the adjacent block so the token runs compress.
190+ i = wantI
191+ }
192+ wantI = i + 1
193+
194+ if err := m.emit(offset, i); err != nil {
195+ return err
196+ }
197+ offset += int64(m.sums[i].len) - 1
198+ k = min(blength, size-offset)
199+ sum = checksum1(m.buf.ptr(offset, int(k)))
200+ s1, s2 = sum&0xFFFF, sum>>16
201+ break
202+ }
203+
204+ backup := max(offset-m.lastMatch, 0)
205+ more := offset+k < size
206+ n := k + backup
207+ if more {
208+ n++
209+ }
210+ window := m.buf.ptr(offset-backup, int(n))[backup:]
211+ first := uint32(int8(window[0]))
212+ s1 -= first
213+ s2 -= uint32(k) * first
214+ if more {
215+ s1 += uint32(int8(window[k]))
216+ s2 += s1
217+ } else {
218+ k--
219+ }
220+
221+ if backup >= blength+chunkSize && end-offset > chunkSize {
222+ if err := m.emit(offset-blength, -2); err != nil {
223+ return err
224+ }
225+ }
226+
227+ offset++
228+ if offset >= end {
229+ break
230+ }
231+ }
232+ return m.emit(size, -1)
233+}
+848, -0
......@@ -0,0 +1,848 @@
1+package rsync
2+
3+import (
4+ "bytes"
5+ "context"
6+ "errors"
7+ "fmt"
8+ "io"
9+ "io/fs"
10+ "math"
11+ "path"
12+ "slices"
13+ "strings"
14+ "time"
15+)
16+
17+var errChecksumMismatch = errors.New("rsync: file failed verification")
18+
19+// recvEvent is what the receiver tells the generator, standing in for the
20+// pipe between rsync's two receiving processes.
21+type recvEvent struct {
22+ done bool
23+ redo int32
24+}
25+
26+// destination maps file-list names to storage paths (get_local_name).
27+type destination struct {
28+ dir string
29+ single bool // the transfer is one file written to dir itself
30+}
31+
32+func (d destination) path(f *fileEntry) string {
33+ if d.single {
34+ return d.dir
35+ }
36+ name := f.name()
37+ if name == "." {
38+ return d.dir
39+ }
40+ return path.Join(d.dir, name)
41+}
42+
43+// runReceiver is the server side of an upload (do_server_recv).
44+func (s *session) runReceiver(args []string) error {
45+ if err := s.recvFilterList(); err != nil {
46+ return err
47+ }
48+ files, err := s.recvFileList()
49+ if err != nil {
50+ return err
51+ }
52+ if len(files) == 0 {
53+ return s.fatal(exitFileSelect, errors.New("server_recv: recv_file_list error"))
54+ }
55+
56+ base := ""
57+ if len(args) > 0 {
58+ base = args[0]
59+ }
60+ destArg := ""
61+ if len(args) > 1 {
62+ destArg = args[1]
63+ }
64+ dest, err := s.resolveDestination(files, base, destArg)
65+ if err != nil {
66+ return s.fatal(exitFileSelect, err)
67+ }
68+
69+ ctx, cancel := context.WithCancel(context.Background())
70+ defer cancel()
71+ events := make(chan recvEvent, 64)
72+ g := &generator{s: s, files: files, dest: dest, events: events, ctx: ctx}
73+ r := &receiver{s: s, files: files, dest: dest, events: events, ctx: ctx, sent: make([]bool, len(files))}
74+
75+ errc := make(chan error, 2)
76+ go func() { errc <- g.run() }()
77+ go func() { errc <- r.run() }()
78+ for range 2 {
79+ if err := <-errc; err != nil {
80+ cancel()
81+ return err
82+ }
83+ }
84+ return s.finish()
85+}
86+
87+func (s *session) recvFileList() ([]*fileEntry, error) {
88+ codec := s.newFlistCodec()
89+ var files []*fileEntry
90+ for {
91+ xflags, ok, ioErr, err := codec.recvFlags(s.c)
92+ if err != nil {
93+ return nil, err
94+ }
95+ if !ok {
96+ if !s.opts.ignoreErrors {
97+ s.addIOError(ioErr & ioErrMask)
98+ }
99+ break
100+ }
101+ f, err := codec.recvEntry(s.c, xflags)
102+ if err != nil {
103+ return nil, s.fatal(exitProtocol, err)
104+ }
105+ files = append(files, f)
106+ }
107+ if !s.opts.numericIDs {
108+ for _, want := range []bool{s.opts.preserveUID, s.opts.preserveGID} {
109+ if !want {
110+ continue
111+ }
112+ for {
113+ id, err := s.c.readVarint30()
114+ if err != nil {
115+ return nil, err
116+ }
117+ if id == 0 {
118+ break
119+ }
120+ if _, err := codec.readIDName(s.c); err != nil {
121+ return nil, err
122+ }
123+ }
124+ if s.xmitID0Names {
125+ if _, err := codec.readIDName(s.c); err != nil {
126+ return nil, err
127+ }
128+ }
129+ }
130+ }
131+ sortFileList(files, s.c.protocol, true)
132+ if s.c.protocol < 30 {
133+ v, err := s.c.readInt32()
134+ if err != nil {
135+ return nil, err
136+ }
137+ if !s.opts.ignoreErrors {
138+ s.addIOError(v & ioErrMask)
139+ }
140+ }
141+ return files, nil
142+}
143+
144+func (s *session) resolveDestination(files []*fileEntry, base, destArg string) (destination, error) {
145+ dir, err := cleanSourcePath(strings.TrimLeft(path.Join(base, destArg), "/"))
146+ if err != nil {
147+ return destination{}, err
148+ }
149+ trailingSlash := strings.HasSuffix(destArg, "/")
150+ if dir == "" {
151+ return destination{dir: dir}, nil
152+ }
153+ info, err := s.fs.Stat(dir)
154+ exists := err == nil
155+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
156+ return destination{}, fmt.Errorf("ERROR: cannot stat destination %q: %w", "/"+dir, err)
157+ }
158+ if exists {
159+ if info.IsDir {
160+ return destination{dir: dir}, nil
161+ }
162+ if len(files) > 1 {
163+ return destination{}, errors.New("ERROR: destination must be a directory when copying more than 1 file")
164+ }
165+ if files[0].isDir() {
166+ return destination{}, errors.New("ERROR: cannot overwrite non-directory with a directory")
167+ }
168+ }
169+ if len(files) > 1 || trailingSlash {
170+ if exists {
171+ return destination{}, errors.New("ERROR: destination path is not a directory")
172+ }
173+ return destination{dir: dir}, nil
174+ }
175+ return destination{dir: dir, single: true}, nil
176+}
177+
178+// generator decides what the client has to send and sends the signatures
179+// of our copies (generator.c).
180+type generator struct {
181+ s *session
182+ files []*fileEntry
183+ dest destination
184+ events chan recvEvent
185+ ctx context.Context
186+ done int
187+
188+ deletedFiles int32
189+ deletedDirs int32
190+}
191+
192+func (g *generator) run() error {
193+ s := g.s
194+ c := s.c
195+ earlyDelayDone := !s.opts.delayUpdates
196+ earlyDeleteDone := s.opts.deleteDuring != 2 && !s.opts.deleteAfter
197+
198+ if s.opts.deleteMode && !g.dest.single && (s.opts.deleteBefore || s.opts.deleteDuring == 1) {
199+ if err := g.deletePass(); err != nil {
200+ return err
201+ }
202+ }
203+
204+ for i, f := range g.files {
205+ if f.inactive {
206+ continue
207+ }
208+ if err := g.recvGenerator(int32(i), f, false); err != nil {
209+ return err
210+ }
211+ }
212+ if err := c.writeNdx(ndxDone); err != nil {
213+ return err
214+ }
215+
216+ // Redo files that failed verification until the receiver is done
217+ // with the first pass.
218+ if err := g.wait(1, true); err != nil {
219+ return err
220+ }
221+
222+ if err := c.writeNdx(ndxDone); err != nil {
223+ return err
224+ }
225+ if c.protocol >= 29 && earlyDelayDone {
226+ if err := c.writeNdx(ndxDone); err != nil {
227+ return err
228+ }
229+ }
230+ if c.protocol >= 31 && earlyDeleteDone {
231+ if s.opts.deleteMode {
232+ if err := g.writeDelStats(); err != nil {
233+ return err
234+ }
235+ }
236+ if earlyDelayDone {
237+ if err := c.writeNdx(ndxDone); err != nil {
238+ return err
239+ }
240+ }
241+ }
242+ if err := g.wait(2, false); err != nil {
243+ return err
244+ }
245+
246+ if c.protocol >= 29 {
247+ if !earlyDelayDone {
248+ if err := c.writeNdx(ndxDone); err != nil {
249+ return err
250+ }
251+ if c.protocol >= 31 && earlyDeleteDone {
252+ if err := c.writeNdx(ndxDone); err != nil {
253+ return err
254+ }
255+ }
256+ }
257+ if err := g.wait(3, false); err != nil {
258+ return err
259+ }
260+ }
261+
262+ if s.opts.deleteMode && !g.dest.single && (s.opts.deleteDuring == 2 || s.opts.deleteAfter) {
263+ if err := g.deletePass(); err != nil {
264+ return err
265+ }
266+ }
267+
268+ if c.protocol >= 31 {
269+ if !earlyDeleteDone {
270+ if s.opts.deleteMode {
271+ if err := g.writeDelStats(); err != nil {
272+ return err
273+ }
274+ }
275+ if err := c.writeNdx(ndxDone); err != nil {
276+ return err
277+ }
278+ }
279+ if err := g.wait(4, false); err != nil {
280+ return err
281+ }
282+ }
283+
284+ // The final goodbye.
285+ if err := c.writeNdx(ndxDone); err != nil {
286+ return err
287+ }
288+ return c.flush()
289+}
290+
291+// wait handles receiver events until it has finished n phases.
292+func (g *generator) wait(n int, redo bool) error {
293+ if err := g.s.c.w.Kick(); err != nil {
294+ return err
295+ }
296+ for g.done < n {
297+ select {
298+ case <-g.ctx.Done():
299+ return g.ctx.Err()
300+ case ev := <-g.events:
301+ if ev.done {
302+ g.done++
303+ continue
304+ }
305+ if redo {
306+ if err := g.recvGenerator(ev.redo, g.files[ev.redo], true); err != nil {
307+ return err
308+ }
309+ if err := g.s.c.w.Kick(); err != nil {
310+ return err
311+ }
312+ }
313+ }
314+ }
315+ return nil
316+}
317+
318+// recvGenerator handles one file-list entry (generator.c:recv_generator).
319+// A redo asks again for a file that failed verification, with full-length
320+// block checksums.
321+func (g *generator) recvGenerator(ndx int32, f *fileEntry, redo bool) error {
322+ s := g.s
323+ o := &s.opts
324+ if !f.isReg() {
325+ if !f.isDir() && o.infoNonreg > 0 {
326+ s.info("skipping non-regular file %q", f.name())
327+ }
328+ return nil
329+ }
330+ if !redo {
331+ if o.maxSize >= 0 && f.size > o.maxSize {
332+ return nil
333+ }
334+ if o.minSize >= 0 && f.size < o.minSize {
335+ return nil
336+ }
337+ }
338+
339+ target := g.dest.path(f)
340+ st, err := s.fs.Stat(target)
341+ exists := err == nil
342+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
343+ s.xferError("recv_generator: failed to stat %q: %v", "/"+target, err)
344+ return nil
345+ }
346+ if !redo {
347+ if o.ignoreNonExist && !exists {
348+ return nil
349+ }
350+ if o.ignoreExisting && exists {
351+ return nil
352+ }
353+ if o.updateOnly && exists && !st.IsDir && f.mtime-st.ModTime.Unix() < 0 {
354+ return nil
355+ }
356+ }
357+ if exists && st.IsDir {
358+ s.xferError("cannot replace directory %q with a file", "/"+target)
359+ return nil
360+ }
361+
362+ if exists && !redo {
363+ same, err := g.quickCheck(f, target, st)
364+ if err != nil {
365+ s.xferError("failed to checksum %q: %v", "/"+target, err)
366+ } else if same {
367+ if o.removeSource && !o.dryRun {
368+ return s.c.writeMsgInt(msgSuccess, ndx)
369+ }
370+ return nil
371+ }
372+ }
373+
374+ // Build the signature of our copy before announcing the file, so a
375+ // failure to read it can fall back to sending the whole file.
376+ var head sumHead
377+ var sums []byte
378+ if exists && !o.wholeFile && !o.dryRun && st.Size > 0 {
379+ csumLength := shortSumLength
380+ if redo {
381+ csumLength = sumLength
382+ }
383+ head, sums, err = g.signature(target, csumLength)
384+ if err != nil {
385+ s.send(msgError, fmt.Sprintf("rsync: [generator] failed to read %q, continuing: %v\n", "/"+target, err))
386+ exists = false
387+ head, sums = sumHead{}, nil
388+ }
389+ }
390+
391+ if err := s.c.writeNdx(ndx); err != nil {
392+ return err
393+ }
394+ if s.c.protocol >= 29 {
395+ iflags := itemTransfer
396+ if o.alwaysChecksum {
397+ iflags |= itemReportChange
398+ }
399+ if exists {
400+ if st.Size != f.size {
401+ iflags |= itemReportSize
402+ }
403+ if !o.preserveTimes || st.ModTime.Unix() != f.mtime {
404+ iflags |= itemReportTime
405+ }
406+ } else {
407+ iflags |= itemIsNew
408+ }
409+ if err := s.c.writeShortint(uint16(iflags)); err != nil {
410+ return err
411+ }
412+ }
413+ if o.dryRun {
414+ return nil
415+ }
416+ if err := s.c.writeSumHead(head); err != nil {
417+ return err
418+ }
419+ return s.c.write(sums)
420+}
421+
422+// signature computes the block checksums of our copy of a file.
423+func (g *generator) signature(target string, csumLength int) (sumHead, []byte, error) {
424+ s := g.s
425+ basis, info, err := s.fs.Open(target)
426+ if err != nil {
427+ return sumHead{}, nil, err
428+ }
429+ defer func() { _ = basis.Close() }()
430+ if info.Size <= 0 {
431+ return sumHead{}, nil, nil
432+ }
433+ head, err := sumSizesSqroot(info.Size, s.c.protocol, s.opts.blockSize, csumLength, s.cs.xfer.size())
434+ if err != nil {
435+ return sumHead{}, nil, err
436+ }
437+ sums, err := encodeSums(basis, info.Size, head, &s.cs)
438+ if err != nil {
439+ return sumHead{}, nil, err
440+ }
441+ return head, sums, nil
442+}
443+
444+// quickCheck reports whether our copy already matches (quick_check_ok).
445+func (g *generator) quickCheck(f *fileEntry, target string, st FileInfo) (bool, error) {
446+ o := &g.s.opts
447+ if st.Size != f.size {
448+ return false, nil
449+ }
450+ if o.alwaysChecksum {
451+ sum, err := g.s.fileChecksum(target)
452+ if err != nil {
453+ return false, err
454+ }
455+ return bytes.Equal(sum, f.sum), nil
456+ }
457+ if o.sizeOnly {
458+ return true, nil
459+ }
460+ if o.ignoreTimes {
461+ return false, nil
462+ }
463+ // Storage keeps whole seconds, so nanoseconds are not compared.
464+ return st.ModTime.Unix() == f.mtime, nil
465+}
466+
467+func (g *generator) writeDelStats() error {
468+ return g.s.writeDelStats([5]int32{g.deletedFiles, g.deletedDirs, 0, 0, 0})
469+}
470+
471+// deletePass removes what the destination has beyond the file list.
472+func (g *generator) deletePass() error {
473+ s := g.s
474+ s.mu.Lock()
475+ ioError := s.ioError
476+ s.mu.Unlock()
477+ if ioError != 0 && !s.opts.ignoreErrors {
478+ s.info("IO error encountered -- skipping file deletion")
479+ return nil
480+ }
481+
482+ existing, err := s.fs.ReadDir(g.dest.dir, true)
483+ if errors.Is(err, fs.ErrNotExist) {
484+ return nil
485+ }
486+ if err != nil {
487+ s.xferError("delete: failed to list %q: %v", "/"+g.dest.dir, err)
488+ return nil
489+ }
490+
491+ listed := make(map[string]*fileEntry, len(g.files))
492+ for _, f := range g.files {
493+ if !f.inactive {
494+ listed[f.name()] = f
495+ }
496+ }
497+ contentDir := func(name string) bool {
498+ f, ok := listed[name]
499+ if !ok || !f.isDir() {
500+ return false
501+ }
502+ if g.s.c.protocol < 30 && s.opts.recurse {
503+ return true
504+ }
505+ return f.flags&flagContentDir != 0
506+ }
507+
508+ type victim struct {
509+ name string
510+ isDir bool
511+ }
512+ var victims []victim
513+ var protectedNames []string
514+ for _, e := range existing {
515+ name, err := cleanSourcePath(e.Name)
516+ if err != nil || name == "" {
517+ continue
518+ }
519+ if f, ok := listed[name]; ok && f.isDir() == e.IsDir {
520+ continue
521+ }
522+ // Find the closest listed ancestor. Everything between it and
523+ // the entry is unlisted and goes too, unless a filter protects
524+ // part of the way.
525+ anc := path.Dir(name)
526+ var chain []string
527+ chain = append(chain, name)
528+ for anc != "." {
529+ if _, ok := listed[anc]; ok {
530+ break
531+ }
532+ chain = append(chain, anc)
533+ anc = path.Dir(anc)
534+ }
535+ if !contentDir(anc) {
536+ continue
537+ }
538+ protected := false
539+ for i, p := range chain {
540+ if s.filters.excluded(p, i > 0 || e.IsDir) {
541+ protected = true
542+ break
543+ }
544+ }
545+ if protected {
546+ protectedNames = append(protectedNames, name)
547+ } else {
548+ victims = append(victims, victim{name: name, isDir: e.IsDir})
549+ }
550+ }
551+
552+ // Children before parents.
553+ slices.SortStableFunc(victims, func(a, b victim) int {
554+ return strings.Count(b.name, "/") - strings.Count(a.name, "/")
555+ })
556+
557+ maxDelete := s.opts.maxDelete
558+ if maxDelete < 0 && maxDelete != math.MinInt32 {
559+ maxDelete = 0
560+ }
561+ skipped := 0
562+ for _, v := range victims {
563+ if maxDelete >= 0 && int(g.deletedFiles+g.deletedDirs) >= maxDelete {
564+ skipped++
565+ continue
566+ }
567+ if v.isDir && slices.ContainsFunc(protectedNames, func(p string) bool { return strings.HasPrefix(p, v.name+"/") }) {
568+ s.info("cannot delete non-empty directory: %s", v.name)
569+ continue
570+ }
571+ full := path.Join(g.dest.dir, v.name)
572+ if !s.opts.dryRun {
573+ if err := s.fs.Remove(full, v.isDir); err != nil {
574+ op := "unlink"
575+ if v.isDir {
576+ op = "rmdir"
577+ }
578+ s.xferError("delete_file: %s(%s) failed: %v", op, v.name, err)
579+ continue
580+ }
581+ }
582+ if v.isDir {
583+ g.deletedDirs++
584+ } else {
585+ g.deletedFiles++
586+ }
587+ g.logDelete(v.name, v.isDir)
588+ }
589+ if skipped > 0 {
590+ s.warning("Deletions stopped due to --max-delete limit (%d skipped)", skipped)
591+ s.addIOError(ioErrDelLimit)
592+ }
593+ return nil
594+}
595+
596+func (g *generator) logDelete(name string, isDir bool) {
597+ s := g.s
598+ if s.c.protocol >= 29 {
599+ msg := []byte(name)
600+ if isDir {
601+ msg = append(msg, 0)
602+ }
603+ if err := s.c.writeMsg(msgDeleted, msg); err != nil {
604+ s.logger.Debug("rsync message not delivered", "err", err)
605+ }
606+ return
607+ }
608+ if s.opts.infoDel > 0 {
609+ if isDir {
610+ name += "/"
611+ }
612+ s.info("deleting %s", name)
613+ }
614+}
615+
616+// receiver applies the deltas the client sends (receiver.c).
617+type receiver struct {
618+ s *session
619+ files []*fileEntry
620+ dest destination
621+ events chan recvEvent
622+ ctx context.Context
623+ sent []bool
624+}
625+
626+func (r *receiver) notify(ev recvEvent) error {
627+ select {
628+ case r.events <- ev:
629+ return nil
630+ case <-r.ctx.Done():
631+ return r.ctx.Err()
632+ }
633+}
634+
635+func (r *receiver) run() error {
636+ s := r.s
637+ c := s.c
638+ phase := 0
639+ maxPhase := 1
640+ if c.protocol >= 29 {
641+ maxPhase = 2
642+ }
643+ tok := newTokenReceiver(c, s.compression)
644+ for {
645+ req, err := s.readNdxAndAttrs(len(r.files))
646+ if err != nil {
647+ return err
648+ }
649+ if req.ndx == ndxDone {
650+ phase++
651+ if phase > maxPhase {
652+ break
653+ }
654+ if err := r.notify(recvEvent{done: true}); err != nil {
655+ return err
656+ }
657+ continue
658+ }
659+ f := r.files[req.ndx]
660+ if f.inactive {
661+ return s.fatal(exitProtocol, fmt.Errorf("refusing transfer of cleared file index %d", req.ndx))
662+ }
663+ if req.iflags&itemTransfer == 0 {
664+ continue
665+ }
666+ if !f.isReg() {
667+ return s.fatal(exitProtocol, fmt.Errorf("received request to transfer non-regular file: %d", req.ndx))
668+ }
669+ if phase == 2 {
670+ return s.fatal(exitProtocol, errors.New("got transfer request in phase 2"))
671+ }
672+ redoing := r.sent[req.ndx]
673+ if s.opts.dryRun {
674+ continue
675+ }
676+
677+ err = r.receiveFile(f, tok)
678+ switch {
679+ case err == nil:
680+ if s.opts.removeSource {
681+ if err := c.writeMsgInt(msgSuccess, req.ndx); err != nil {
682+ return err
683+ }
684+ }
685+ case errors.Is(err, errChecksumMismatch):
686+ if redoing {
687+ s.xferError("ERROR: %s failed verification -- update discarded.", f.name())
688+ continue
689+ }
690+ if s.opts.infoName > 0 {
691+ s.warning("WARNING: %s failed verification -- update discarded (will try again).", f.name())
692+ }
693+ r.sent[req.ndx] = true
694+ if err := r.notify(recvEvent{redo: req.ndx}); err != nil {
695+ return err
696+ }
697+ default:
698+ var putErr *putError
699+ if !errors.As(err, &putErr) {
700+ return err
701+ }
702+ // Storage refusing a file (pgs rejecting a dotfile, say) is
703+ // reported without failing the whole run.
704+ s.warning("rsync: [receiver] %s not stored: %v", f.name(), putErr.err)
705+ }
706+ }
707+ if err := r.notify(recvEvent{done: true}); err != nil {
708+ return err
709+ }
710+
711+ if c.protocol >= 31 {
712+ // The sender confirms our final NDX_DONE with one of its own.
713+ req, err := s.readNdxAndAttrs(0)
714+ if err != nil {
715+ return err
716+ }
717+ if req.ndx != ndxDone {
718+ return s.fatal(exitProtocol, fmt.Errorf("invalid packet at end of run (%d)", req.ndx))
719+ }
720+ return r.notify(recvEvent{done: true})
721+ }
722+ return nil
723+}
724+
725+// putError is a failure of the storage backend to keep a file.
726+type putError struct{ err error }
727+
728+func (e *putError) Error() string { return e.err.Error() }
729+
730+// receiveFile reads one file's delta, rebuilds the file from it and our old
731+// copy, and streams the result to storage. The stream only ends cleanly
732+// once the whole-file checksum matched (receiver.c:receive_data).
733+func (r *receiver) receiveFile(f *fileEntry, tok tokenReceiver) error {
734+ s := r.s
735+ target := r.dest.path(f)
736+
737+ head, err := s.c.readSumHead(s.cs.xfer.size())
738+ if err != nil {
739+ return s.fatal(exitProtocol, err)
740+ }
741+
742+ var basis File
743+ var basisSize int64
744+ if head.count > 0 {
745+ b, info, err := s.fs.Open(target)
746+ if err == nil {
747+ basis, basisSize = b, info.Size
748+ defer func() { _ = basis.Close() }()
749+ }
750+ }
751+
752+ pr, pw := io.Pipe()
753+ type result struct {
754+ msg string
755+ err error
756+ }
757+ resc := make(chan result, 1)
758+ go func() {
759+ info := FileInfo{Name: target, Size: f.size, ModTime: time.Unix(f.mtime, int64(f.nsec))}
760+ msg, err := s.fs.Put(target, info, pr)
761+ if err == nil {
762+ err = errPutReturned
763+ }
764+ _ = pr.CloseWithError(err)
765+ if errors.Is(err, errPutReturned) {
766+ err = nil
767+ }
768+ resc <- result{msg, err}
769+ }()
770+
771+ sum := s.cs.newXferSum()
772+ write := func(p []byte) {
773+ sum.Write(p)
774+ // After a failed Put the pipe returns its error; keep reading the
775+ // protocol stream regardless.
776+ _, _ = pw.Write(p)
777+ }
778+
779+ var block []byte
780+ var tokErr error
781+ for {
782+ i, data, err := tok.recv()
783+ if err != nil {
784+ tokErr = err
785+ break
786+ }
787+ if i == 0 {
788+ break
789+ }
790+ if i > 0 {
791+ write(data)
792+ continue
793+ }
794+ idx := -(i + 1)
795+ if idx >= head.count {
796+ tokErr = s.fatal(exitProtocol, fmt.Errorf("invalid block index %d (count=%d)", idx, head.count))
797+ break
798+ }
799+ l := head.blength
800+ if idx == head.count-1 && head.remainder != 0 {
801+ l = head.remainder
802+ }
803+ offset := int64(idx) * int64(head.blength)
804+ if basis == nil || offset+int64(l) > basisSize {
805+ tokErr = s.fatal(exitProtocol, fmt.Errorf("got a block match with no basis file for %q", "/"+target))
806+ break
807+ }
808+ if cap(block) < int(l) {
809+ block = make([]byte, l)
810+ }
811+ block = block[:l]
812+ if err := readFullAt(basis, block, offset); err != nil {
813+ tokErr = s.fatal(exitFileIO, fmt.Errorf("failed to read basis %q: %w", "/"+target, err))
814+ break
815+ }
816+ tok.see(block)
817+ write(block)
818+ }
819+ if tokErr != nil {
820+ _ = pw.CloseWithError(tokErr)
821+ <-resc
822+ return tokErr
823+ }
824+
825+ remote := make([]byte, s.cs.xfer.size())
826+ if err := s.c.readFull(remote); err != nil {
827+ _ = pw.CloseWithError(err)
828+ <-resc
829+ return err
830+ }
831+ if !bytes.Equal(sum.Sum(nil), remote) {
832+ _ = pw.CloseWithError(errChecksumMismatch)
833+ <-resc
834+ return errChecksumMismatch
835+ }
836+ _ = pw.Close()
837+ res := <-resc
838+ if res.err != nil {
839+ return &putError{err: res.err}
840+ }
841+ if res.msg != "" {
842+ s.info("%s", res.msg)
843+ }
844+ return nil
845+}
846+
847+// errPutReturned unblocks writers to the pipe once Put is done reading.
848+var errPutReturned = errors.New("rsync: storage finished reading")
R pkg/rsync-receiver/rsyncopts/popt.go => pkg/rsync/rsyncopts/popt.go
+0, -0
R pkg/rsync-receiver/rsyncopts/rsyncopts.go => pkg/rsync/rsyncopts/rsyncopts.go
+190, -178
......@@ -1,18 +1,15 @@
1-// Package rsyncopts implements a parser for command-line options that
2-// implements a subset of popt(3) semantics; just enough to parse typical
3-// rsync(1) invocations without the advanced popt features like aliases
4-// or option prefix matching (not --del, only --delete).
1+// Package rsyncopts parses the command line an rsync client sends to the
2+// server. It implements just enough of popt(3) for typical rsync(1)
3+// invocations, without aliases or option prefix matching (not --del, only
4+// --delete).
55 //
66 // If we encounter arguments that rsync(1) parses differently compared to this
77 // package, then this package should be adjusted to match rsync(1).
88 package rsyncopts
99
1010 import (
11- "errors"
1211 "fmt"
13- "log/slog"
1412 "math"
15- "slices"
1613 "strconv"
1714 "strings"
1815 "syscall"
......@@ -91,42 +88,27 @@ func NewOptions() *Options {
9188 relative_paths: -1,
9289 implied_dirs: 1,
9390 max_delete: math.MinInt32,
91+ max_size: -1,
92+ min_size: -1,
9493 whole_file: -1,
9594 do_compression_level: math.MinInt32,
9695 rsync_path: "rsync",
9796 default_af_hint: syscall.AF_INET6,
9897 blocking_io: -1,
99- protocol_version: 27,
100- }
101-}
102-
103-// GokrazyOptions contains additional command-line flags, prefixed with
104-// gokr. (like --gokr.modulemap) to not clash with rsync flag names.
105-type GokrazyOptions struct {
106- Config string
107- Listen string
108- MonitoringListen string
109- AnonSSHListen string
110- ModuleMap string
111-}
112-
113-func (o *GokrazyOptions) table() []poptOption {
114- return []poptOption{
115- /* longName, shortName, argInfo, arg, val */
116- {"gokr.config", "", POPT_ARG_STRING, &o.Config, 0},
117- {"gokr.listen", "", POPT_ARG_STRING, &o.Listen, 0},
118- {"gokr.monitoring_listen", "", POPT_ARG_STRING, &o.MonitoringListen, 0},
119- {"gokr.anonssh_listen", "", POPT_ARG_STRING, &o.AnonSSHListen, 0},
120- {"gokr.modulemap", "", POPT_ARG_STRING, &o.ModuleMap, 0},
12198 }
12299 }
123100
124101 type Options struct {
125- Gokrazy GokrazyOptions
126-
127102 // not directly referenced in the table, but used in the special case code.
128- do_compression int
129- info [COUNT_INFO]uint16
103+ do_compression int
104+ info [COUNT_INFO]uint16
105+ version_opt_cnt int
106+ block_size int64
107+ max_size int64
108+ min_size int64
109+ batch_mode int
110+ basis_dirs []string
111+ modify_window_set int
130112
131113 // order matches long_options order
132114 verbose int
......@@ -247,12 +229,6 @@ type Options struct {
247229 checksum_seed int
248230 am_server int
249231 am_sender int
250- am_daemon int
251-
252- daemon_bwlimit int
253- config_file string
254- daemon_opt int
255- no_detach int
256232 }
257233
258234 type priority int
......@@ -354,48 +330,6 @@ func (o *Options) setOutputVerbosity(prio priority) {
354330 }
355331 }
356332
357-func (o *Options) DryRun() bool { return o.dry_run != 0 }
358-func (o *Options) PreserveLinks() bool { return o.preserve_links != 0 }
359-func (o *Options) PreserveUid() bool { return o.preserve_uid != 0 }
360-func (o *Options) PreserveGid() bool { return o.preserve_gid != 0 }
361-func (o *Options) PreserveDevices() bool { return o.preserve_devices != 0 }
362-func (o *Options) PreserveMTimes() bool { return o.preserve_mtimes != 0 }
363-func (o *Options) PreservePerms() bool { return o.preserve_perms != 0 }
364-func (o *Options) PreserveSpecials() bool { return o.preserve_specials != 0 }
365-func (o *Options) Recurse() bool { return o.recurse != 0 }
366-func (o *Options) DeleteMode() bool { return o.delete_mode != 0 }
367-func (o *Options) AlwaysChecksum() bool { return o.always_checksum != 0 }
368-func (o *Options) Compress() bool { return o.do_compression != 0 }
369-func (o *Options) IgnoreTimes() bool { return o.ignore_times == 1 }
370-func (o *Options) SizeOnly() bool { return o.size_only == 1 }
371-
372-func (o *Options) daemonTable() []poptOption {
373- return []poptOption{
374- /* longName, shortName, argInfo, arg, val */
375- {"help", "", POPT_ARG_NONE, nil, OPT_HELP},
376- {"address", "", POPT_ARG_STRING, &o.bind_address, 0},
377- {"bwlimit", "", POPT_ARG_INT, &o.daemon_bwlimit, 0},
378- {"config", "", POPT_ARG_STRING, &o.config_file, 0},
379- {"daemon", "", POPT_ARG_NONE, &o.daemon_opt, 0},
380- {"dparam", "M", POPT_ARG_STRING, nil, 'M'},
381- {"ipv4", "4", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET},
382- {"ipv6", "6", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET6},
383- {"detach", "", POPT_ARG_VAL, &o.no_detach, 0},
384- {"no-detach", "", POPT_ARG_VAL, &o.no_detach, 1},
385- {"log-file", "", POPT_ARG_STRING, &o.logfile_name, 0},
386- {"log-file-format", "", POPT_ARG_STRING, &o.logfile_format, 0},
387- {"port", "", POPT_ARG_INT, &o.rsync_port, 0},
388- {"sockopts", "", POPT_ARG_STRING, &o.sockopts, 0},
389- {"protocol", "", POPT_ARG_INT, &o.protocol_version, 0},
390- {"server", "", POPT_ARG_NONE, &o.am_server, 0},
391- {"temp-dir", "T", POPT_ARG_STRING, &o.tmpdir, 0},
392- {"verbose", "v", POPT_ARG_NONE, 0, 'v'},
393- {"no-verbose", "", POPT_ARG_VAL, &o.verbose, 0},
394- {"no-v", "", POPT_ARG_VAL, &o.verbose, 0},
395- {"help", "h", POPT_ARG_NONE, 0, 'h'},
396- }
397-}
398-
399333 func (o *Options) table() []poptOption {
400334 return []poptOption{
401335 /* longName, shortName, argInfo, arg, val */
......@@ -631,6 +565,7 @@ func (o *Options) table() []poptOption {
631565 {"mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 1},
632566 {"no-mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 0},
633567 {"qsort", "", POPT_ARG_NONE, &o.use_qsort, 0},
568+ {"use-qsort", "", POPT_ARG_NONE, &o.use_qsort, 0},
634569 {"copy-as", "", POPT_ARG_STRING, &o.copy_as, 0},
635570 {"address", "", POPT_ARG_STRING, &o.bind_address, 0},
636571 {"port", "", POPT_ARG_INT, &o.rsync_port, 0},
......@@ -654,25 +589,13 @@ func (o *Options) table() []poptOption {
654589 }
655590 }
656591
657-var errNotYetImplemented = errors.New("option not yet implemented in gokrazy/rsync")
658-
659-// rsync/options.c:parse_arguments.
660-func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
661- // NOTE: We do not implement support for refusing options per rsyncd.conf
662- // here, as we have our own configuration file.
663-
664- version_opt_cnt := 0
665-
592+// ParseArguments parses the arguments that follow "rsync" on the command
593+// line (rsync/options.c:parse_arguments).
594+func ParseArguments(args []string) (*Context, error) {
666595 opts := NewOptions()
667- table := opts.table()
668- if gokrazyTable {
669- // We need to make the --gokr.* flags known, otherwise the first parsing
670- // attempt fails and the daemon mode parsing is never run.
671- table = slices.Concat(opts.Gokrazy.table(), table)
672- }
673596 pc := Context{
674597 Options: opts,
675- table: table,
598+ table: opts.table(),
676599 args: args,
677600 }
678601
......@@ -688,7 +611,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
688611 // are returned and handled here.
689612 switch opt {
690613 case 'V':
691- version_opt_cnt++
614+ opts.version_opt_cnt++
692615
693616 case OPT_SERVER:
694617 opts.am_server = 1
......@@ -700,47 +623,11 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
700623 opts.am_sender = 1
701624
702625 case OPT_DAEMON:
703- // Parse the whole command-line using the daemon options table.
704- table := opts.daemonTable()
705- if gokrazyTable {
706- table = slices.Concat(opts.Gokrazy.table(), table)
707- }
708- pc := Context{
709- Options: opts,
710- table: table,
711- args: args,
712- }
626+ return nil, fmt.Errorf("daemon mode is not supported")
713627
714- for {
715- opt, err := pc.poptGetNextOpt()
716- if err != nil {
717- return nil, err
718- }
719- if opt == -1 {
720- break // done
721- }
722- // Most options are handled by poptGetNextOpt, only special cases
723- // are returned and handled here.
724- switch opt {
725- case 'M':
726- return nil, errNotYetImplemented
727- case 'v':
728- opts.verbose++
729- default:
730- return nil, fmt.Errorf("unhandled special case opt: %v", opt)
731- }
732- }
733-
734- opts.am_daemon = 1
735-
736- return &pc, nil
737-
738- case OPT_FILTER,
739- OPT_EXCLUDE,
740- OPT_INCLUDE,
741- OPT_INCLUDE_FROM,
742- OPT_EXCLUDE_FROM:
743- return nil, errNotYetImplemented
628+ case OPT_FILTER, OPT_EXCLUDE, OPT_INCLUDE, OPT_INCLUDE_FROM, OPT_EXCLUDE_FROM, 'F':
629+ // Clients send their filter rules over the protocol instead.
630+ return nil, fmt.Errorf("filter options are not accepted on the server side")
744631
745632 case 'a':
746633 if opts.recurse == 0 {
......@@ -766,7 +653,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
766653 opts.human_readable++
767654
768655 case 'H':
769- opts.preserve_hard_links = 1
656+ opts.preserve_hard_links++
770657
771658 case 'i':
772659 opts.itemize_changes++
......@@ -781,7 +668,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
781668 opts.verbose++
782669
783670 case 'y':
784- return nil, errNotYetImplemented
671+ opts.fuzzy_basis++
785672
786673 case 'q':
787674 opts.quiet++
......@@ -789,9 +676,6 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
789676 case 'x':
790677 opts.one_file_system++
791678
792- case 'F':
793- return nil, errNotYetImplemented
794-
795679 case 'P':
796680 opts.do_progress = 1
797681 opts.keep_partial = 1
......@@ -810,57 +694,57 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
810694 opts.compress_choice = ""
811695
812696 case OPT_OLD_ARGS:
813- return nil, errNotYetImplemented
697+ opts.old_style_args++
814698
815- case 'M': // --remote-option
816- return nil, errNotYetImplemented
699+ case 'M':
700+ return nil, fmt.Errorf("--remote-option is not accepted on the server side")
817701
818- case OPT_WRITE_BATCH,
819- OPT_ONLY_WRITE_BATCH,
820- OPT_READ_BATCH:
821- return nil, errNotYetImplemented
702+ case OPT_WRITE_BATCH, OPT_ONLY_WRITE_BATCH, OPT_READ_BATCH:
703+ opts.batch_mode = 1
822704
823705 case OPT_BLOCK_SIZE:
824- return nil, errNotYetImplemented
706+ size, err := parseSizeArg(pc.poptGetOptArg(), 'b')
707+ if err != nil || size < 0 {
708+ return nil, fmt.Errorf("invalid --block-size value")
709+ }
710+ opts.block_size = size
825711
826- case OPT_MAX_SIZE, // (needs parse_size_arg)
827- OPT_MIN_SIZE,
828- OPT_BWLIMIT:
829- return nil, errNotYetImplemented
712+ case OPT_MAX_SIZE:
713+ size, err := parseSizeArg(opts.max_size_arg, 'b')
714+ if err != nil {
715+ return nil, fmt.Errorf("invalid --max-size value: %s", opts.max_size_arg)
716+ }
717+ opts.max_size = size
830718
831- case OPT_APPEND:
832- return nil, errNotYetImplemented
719+ case OPT_MIN_SIZE:
720+ size, err := parseSizeArg(opts.min_size_arg, 'b')
721+ if err != nil {
722+ return nil, fmt.Errorf("invalid --min-size value: %s", opts.min_size_arg)
723+ }
724+ opts.min_size = size
833725
834- case OPT_LINK_DEST,
835- OPT_COPY_DEST,
836- OPT_COMPARE_DEST:
837- return nil, errNotYetImplemented
726+ case OPT_APPEND:
727+ opts.append_mode++
838728
839- case OPT_CHMOD: // (needs parse_chmod):
840- return nil, errNotYetImplemented
729+ case OPT_LINK_DEST, OPT_COPY_DEST, OPT_COMPARE_DEST:
730+ opts.basis_dirs = append(opts.basis_dirs, pc.poptGetOptArg())
841731
842732 case OPT_INFO:
843733 parseOutputWords(infoWords[:], opts.info[:], pc.poptGetOptArg(), USER_PRIORITY)
844734
845- case OPT_DEBUG:
846- // TODO: plumb the debug level that make sense for our implementation
847- slog.Info("TODO: set debug level", "to", pc.poptGetOptArg())
848-
849- case OPT_USERMAP,
850- OPT_GROUPMAP,
851- OPT_CHOWN:
852- return nil, errNotYetImplemented
853-
854735 case 'A':
855- return nil, fmt.Errorf("ACLs are not supported by gokrazy/rsync")
736+ opts.preserve_acls++
856737
857738 case 'X':
858739 opts.preserve_xattrs++
859740
860- case OPT_STOP_AFTER,
861- OPT_STOP_AT,
862- OPT_STDERR:
863- return nil, errNotYetImplemented
741+ case OPT_MODIFY_WINDOW:
742+ opts.modify_window_set = 1
743+
744+ case OPT_DEBUG, OPT_CHMOD, OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN,
745+ OPT_BWLIMIT, OPT_STOP_AFTER, OPT_STOP_AT, OPT_STDERR, OPT_NO_ICONV:
746+ // These don't change what the server does.
747+ _ = pc.poptGetOptArg()
864748
865749 default:
866750 return nil, fmt.Errorf("unhandled special case opt: %v", opt)
......@@ -872,6 +756,15 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
872756
873757 opts.setOutputVerbosity(DEFAULT_PRIORITY)
874758
759+ if opts.delete_before+min(opts.delete_during, 1)+opts.delete_after > 1 {
760+ return nil, fmt.Errorf("you may not combine multiple --delete-WHEN options")
761+ }
762+ if opts.delete_before != 0 || opts.delete_during != 0 || opts.delete_after != 0 {
763+ opts.delete_mode = 1
764+ } else if opts.delete_mode != 0 || opts.delete_excluded != 0 {
765+ opts.delete_mode = 1
766+ }
767+
875768 if opts.recurse != 0 {
876769 opts.xfer_dirs = 1
877770 }
......@@ -911,7 +804,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
911804 opts.make_backups = 1 // --backup-dir implies --backup
912805 }
913806
914- if opts.do_progress != 0 /* && !opts.am_server */ {
807+ if opts.do_progress != 0 {
915808 if opts.info[INFO_NAME] == 0 {
916809 opts.info[INFO_NAME] = 1
917810 }
......@@ -923,3 +816,122 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
923816
924817 return &pc, nil
925818 }
819+
820+// parseSizeArg parses a size like 100K or 1.5MB (rsync/options.c
821+// parse_size_arg). A bare number is in units of defSuffix.
822+func parseSizeArg(arg string, defSuffix byte) (int64, error) {
823+ s := arg
824+ i := 0
825+ for i < len(s) && (s[i] >= '0' && s[i] <= '9' || s[i] == '.') {
826+ i++
827+ }
828+ if i == 0 {
829+ return 0, fmt.Errorf("invalid size %q", arg)
830+ }
831+ num, err := strconv.ParseFloat(s[:i], 64)
832+ if err != nil {
833+ return 0, err
834+ }
835+ s = s[i:]
836+ suffix := defSuffix
837+ if s != "" {
838+ suffix = s[0]
839+ s = s[1:]
840+ }
841+ mult := float64(1024)
842+ switch {
843+ case strings.HasPrefix(strings.ToLower(s), "ib"):
844+ s = s[2:]
845+ case strings.HasPrefix(strings.ToLower(s), "b"):
846+ mult = 1000
847+ s = s[1:]
848+ }
849+ var size float64
850+ switch suffix | 0x20 {
851+ case 'b':
852+ size = num
853+ case 'k':
854+ size = num * mult
855+ case 'm':
856+ size = num * mult * mult
857+ case 'g':
858+ size = num * mult * mult * mult
859+ case 't':
860+ size = num * mult * mult * mult * mult
861+ case 'p':
862+ size = num * mult * mult * mult * mult * mult
863+ default:
864+ return 0, fmt.Errorf("invalid size %q", arg)
865+ }
866+ switch s {
867+ case "":
868+ case "+1":
869+ size++
870+ case "-1":
871+ size--
872+ default:
873+ return 0, fmt.Errorf("invalid size %q", arg)
874+ }
875+ return int64(size), nil
876+}
877+
878+func (o *Options) Verbose() int { return o.verbose }
879+func (o *Options) Info(level infoLevel) int { return int(o.info[level]) }
880+func (o *Options) DryRun() bool { return o.dry_run != 0 }
881+func (o *Options) Server() bool { return o.am_server != 0 }
882+func (o *Options) Sender() bool { return o.am_sender != 0 }
883+func (o *Options) Recurse() bool { return o.recurse != 0 }
884+func (o *Options) XferDirs() bool { return o.xfer_dirs != 0 }
885+func (o *Options) ListOnly() bool { return o.list_only != 0 }
886+func (o *Options) RelativePaths() bool { return o.relative_paths != 0 }
887+func (o *Options) PreserveLinks() bool { return o.preserve_links != 0 }
888+func (o *Options) PreserveHardLinks() bool { return o.preserve_hard_links != 0 }
889+func (o *Options) PreserveUid() bool { return o.preserve_uid != 0 }
890+func (o *Options) PreserveGid() bool { return o.preserve_gid != 0 }
891+func (o *Options) PreserveDevices() bool { return o.preserve_devices != 0 }
892+func (o *Options) PreserveSpecials() bool { return o.preserve_specials != 0 }
893+func (o *Options) PreserveMTimes() bool { return o.preserve_mtimes != 0 }
894+func (o *Options) PreservePerms() bool { return o.preserve_perms != 0 }
895+func (o *Options) PreserveACLs() bool { return o.preserve_acls != 0 }
896+func (o *Options) PreserveXattrs() bool { return o.preserve_xattrs != 0 }
897+func (o *Options) PreserveAtimes() bool { return o.preserve_atimes != 0 }
898+func (o *Options) PreserveCrtimes() bool { return o.preserve_crtimes != 0 }
899+func (o *Options) NumericIDs() bool { return o.numeric_ids != 0 }
900+func (o *Options) DeleteMode() bool { return o.delete_mode != 0 }
901+func (o *Options) DeleteBefore() bool { return o.delete_before != 0 }
902+func (o *Options) DeleteDuring() int { return o.delete_during }
903+func (o *Options) DeleteAfter() bool { return o.delete_after != 0 }
904+func (o *Options) DeleteExcluded() bool { return o.delete_excluded != 0 }
905+func (o *Options) ForceDelete() bool { return o.force_delete != 0 }
906+func (o *Options) MaxDelete() int { return o.max_delete }
907+func (o *Options) IgnoreErrors() bool { return o.ignore_errors != 0 }
908+func (o *Options) PruneEmptyDirs() bool { return o.prune_empty_dirs != 0 }
909+func (o *Options) AlwaysChecksum() bool { return o.always_checksum != 0 }
910+func (o *Options) ChecksumChoice() string { return o.checksum_choice }
911+func (o *Options) ChecksumSeed() int { return o.checksum_seed }
912+func (o *Options) Compress() bool { return o.do_compression != 0 }
913+func (o *Options) CompressChoice() string { return o.compress_choice }
914+func (o *Options) CompressLevel() int { return o.do_compression_level }
915+func (o *Options) IgnoreTimes() bool { return o.ignore_times != 0 }
916+func (o *Options) SizeOnly() bool { return o.size_only != 0 }
917+func (o *Options) UpdateOnly() bool { return o.update_only != 0 }
918+func (o *Options) IgnoreExisting() bool { return o.ignore_existing != 0 }
919+func (o *Options) IgnoreNonExisting() bool { return o.ignore_non_existing != 0 }
920+func (o *Options) WholeFile() bool { return o.whole_file > 0 }
921+func (o *Options) BlockSize() int64 { return o.block_size }
922+func (o *Options) MaxSize() int64 { return o.max_size }
923+func (o *Options) MinSize() int64 { return o.min_size }
924+func (o *Options) Inplace() bool { return o.inplace != 0 }
925+func (o *Options) AppendMode() int { return o.append_mode }
926+func (o *Options) DelayUpdates() bool { return o.delay_updates != 0 }
927+func (o *Options) RemoveSourceFiles() int { return o.remove_source_files }
928+func (o *Options) CvsExclude() bool { return o.cvs_exclude != 0 }
929+func (o *Options) ProtocolVersion() int { return o.protocol_version }
930+func (o *Options) ProtectArgs() bool { return o.protect_args != 0 }
931+func (o *Options) ShellCmd() string { return o.shell_cmd }
932+func (o *Options) FilesFrom() string { return o.files_from }
933+func (o *Options) MissingArgs() int { return o.missing_args }
934+func (o *Options) BatchMode() bool { return o.batch_mode != 0 }
935+func (o *Options) BasisDirs() []string { return o.basis_dirs }
936+func (o *Options) Iconv() string { return o.iconv_opt }
937+func (o *Options) ModifyWindowSet() bool { return o.modify_window_set != 0 }
+480, -0
......@@ -0,0 +1,480 @@
1+package rsync
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "io"
7+ "io/fs"
8+ "path"
9+ "slices"
10+ "strings"
11+ "time"
12+)
13+
14+const (
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.
22+func (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) {
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, flags)
85+ if err != nil {
86+ s.ioError |= ioErrGeneral
87+ s.xferError("opendir %q failed: %v", "/"+src, err)
88+ continue
89+ }
90+ files = append(files, children...)
91+ }
92+ } else {
93+ files = append(files, s.newEntry(fn, src, info, flags))
94+ }
95+ }
96+ return files, nil
97+}
98+
99+// listDir returns the entries below a source directory, with names under
100+// prefix. Directories missing from the listing are implied by their
101+// contents, as object stores don't keep them.
102+func (s *session) listDir(prefix, src string, flags int) ([]*fileEntry, error) {
103+ entries, err := s.fs.ReadDir(src, s.opts.recurse)
104+ if err != nil {
105+ return nil, err
106+ }
107+ join := func(name string) string {
108+ if prefix == "." {
109+ return name
110+ }
111+ return prefix + "/" + name
112+ }
113+
114+ byName := make(map[string]FileInfo, len(entries))
115+ for _, e := range entries {
116+ name, err := cleanSourcePath(e.Name)
117+ if err != nil || name == "" {
118+ continue
119+ }
120+ if !s.opts.recurse && strings.Contains(name, "/") {
121+ continue
122+ }
123+ e.Name = name
124+ byName[name] = e
125+ if s.opts.recurse {
126+ for d := path.Dir(name); d != "."; d = path.Dir(d) {
127+ if _, ok := byName[d]; ok {
128+ break
129+ }
130+ byName[d] = FileInfo{Name: d, IsDir: true, ModTime: e.ModTime}
131+ }
132+ }
133+ }
134+ names := make([]string, 0, len(byName))
135+ for name := range byName {
136+ names = append(names, name)
137+ }
138+ slices.Sort(names)
139+
140+ var files []*fileEntry
141+ var excludedDirs []string
142+ for _, name := range names {
143+ e := byName[name]
144+ if slices.ContainsFunc(excludedDirs, func(d string) bool { return strings.HasPrefix(name, d+"/") }) {
145+ continue
146+ }
147+ full := join(name)
148+ if s.filters.excluded(full, e.IsDir) {
149+ if e.IsDir {
150+ excludedDirs = append(excludedDirs, name)
151+ }
152+ continue
153+ }
154+ files = append(files, s.newEntry(full, path.Join(src, name), e, flags))
155+ }
156+ return files, nil
157+}
158+
159+func (s *session) newEntry(name, src string, info FileInfo, flags int) *fileEntry {
160+ f := newFileEntry(name)
161+ if name == "." {
162+ f.dirname, f.basename = "", "."
163+ }
164+ f.src = src
165+ f.mtime = info.ModTime.Unix()
166+ f.nsec = uint32(info.ModTime.Nanosecond())
167+ if info.ModTime.IsZero() {
168+ f.mtime, f.nsec = time.Now().Unix(), 0
169+ }
170+ if info.IsDir {
171+ f.mode = sIFDIR | 0o755
172+ f.size = 4096
173+ f.flags = flags
174+ } else {
175+ f.mode = sIFREG | 0o644
176+ f.size = info.Size
177+ }
178+ return f
179+}
180+
181+// cleanSourcePath normalizes a path inside the storage, refusing anything
182+// that would leave it.
183+func cleanSourcePath(p string) (string, error) {
184+ var parts []string
185+ for _, c := range strings.Split(p, "/") {
186+ switch c {
187+ case "", ".":
188+ case "..":
189+ return "", fmt.Errorf("path escapes the transfer root: %s", p)
190+ default:
191+ parts = append(parts, c)
192+ }
193+ }
194+ return strings.Join(parts, "/"), nil
195+}
196+
197+// runSender is the server side of a download (do_server_sender).
198+func (s *session) runSender(args []string) error {
199+ start := time.Now()
200+ if err := s.recvFilterList(); err != nil {
201+ return err
202+ }
203+
204+ srcs := args
205+ if len(srcs) > 0 {
206+ // The first argument is the "." the client uses as a placeholder
207+ // for the working directory.
208+ srcs = srcs[1:]
209+ }
210+ if len(srcs) == 0 && (s.opts.recurse || s.opts.xferDirs) {
211+ srcs = []string{"."}
212+ }
213+
214+ files, err := s.buildFileList(srcs)
215+ if err != nil {
216+ return s.fatal(exitFileSelect, err)
217+ }
218+ if s.opts.alwaysChecksum {
219+ for _, f := range files {
220+ if f.isReg() {
221+ if f.sum, err = s.fileChecksum(f.src); err != nil {
222+ s.ioError |= ioErrGeneral
223+ s.xferError("failed to checksum %q: %v", "/"+f.src, err)
224+ }
225+ }
226+ }
227+ }
228+ buildTime := time.Since(start)
229+
230+ start = time.Now()
231+ codec := s.newFlistCodec()
232+ var totalSize int64
233+ for _, f := range files {
234+ if err := codec.sendEntry(s.c, f); err != nil {
235+ return err
236+ }
237+ if f.isReg() || f.isLink() {
238+ totalSize += f.size
239+ }
240+ }
241+ ioError := s.ioError
242+ if s.opts.ignoreErrors {
243+ ioError = 0
244+ }
245+ if ioError != 0 && !s.safeFlist {
246+ if err := codec.sendEnd(s.c, 0); err != nil {
247+ return err
248+ }
249+ } else if err := codec.sendEnd(s.c, ioError); err != nil {
250+ return err
251+ }
252+ sortFileList(files, s.c.protocol, false)
253+ if err := s.sendIDLists(); err != nil {
254+ return err
255+ }
256+ if s.c.protocol < 30 {
257+ if err := s.c.writeInt32(ioError); err != nil {
258+ return err
259+ }
260+ } else if !s.safeFlist && ioError != 0 {
261+ if err := s.c.writeMsgInt(msgIOError, ioError); err != nil {
262+ return err
263+ }
264+ }
265+ if err := s.c.flush(); err != nil {
266+ return err
267+ }
268+ xferTime := time.Since(start)
269+
270+ if len(files) == 0 {
271+ return s.finish()
272+ }
273+
274+ if err := s.sendFiles(files); err != nil {
275+ return err
276+ }
277+
278+ if err := s.c.writeVarlong30(s.c.bytesRead(), 3); err != nil {
279+ return err
280+ }
281+ if err := s.c.writeVarlong30(s.c.bytesWritten(), 3); err != nil {
282+ return err
283+ }
284+ if err := s.c.writeVarlong30(totalSize, 3); err != nil {
285+ return err
286+ }
287+ if s.c.protocol >= 29 {
288+ if err := s.c.writeVarlong30(max(buildTime.Milliseconds(), 1), 3); err != nil {
289+ return err
290+ }
291+ if err := s.c.writeVarlong30(xferTime.Milliseconds(), 3); err != nil {
292+ return err
293+ }
294+ }
295+ if err := s.readFinalGoodbye(true); err != nil {
296+ return err
297+ }
298+ return s.finish()
299+}
300+
301+func (s *session) sendIDLists() error {
302+ if s.opts.numericIDs {
303+ return nil
304+ }
305+ for _, want := range []bool{s.opts.preserveUID, s.opts.preserveGID} {
306+ if !want {
307+ continue
308+ }
309+ // Every entry is owned by id 0, which is never listed by number.
310+ if err := s.c.writeVarint30(0); err != nil {
311+ return err
312+ }
313+ if s.xmitID0Names {
314+ if err := s.c.writeByte(4); err != nil {
315+ return err
316+ }
317+ if err := s.c.write([]byte("root")); err != nil {
318+ return err
319+ }
320+ }
321+ }
322+ return nil
323+}
324+
325+// sendFiles answers the receiver's requests until it is done
326+// (send_files).
327+func (s *session) sendFiles(files []*fileEntry) error {
328+ phase := 0
329+ maxPhase := 1
330+ if s.c.protocol >= 29 {
331+ maxPhase = 2
332+ }
333+ savedIOError := s.ioError
334+ tok := newTokenSender(s.c, s.compression, s.compressLevel)
335+ for {
336+ req, err := s.readNdxAndAttrs(len(files))
337+ if err != nil {
338+ return err
339+ }
340+ if req.ndx == ndxDone {
341+ phase++
342+ if phase > maxPhase {
343+ break
344+ }
345+ if err := s.c.writeNdx(ndxDone); err != nil {
346+ return err
347+ }
348+ continue
349+ }
350+ f := files[req.ndx]
351+ if req.iflags&itemTransfer != 0 && !f.isReg() {
352+ return s.fatal(exitProtocol, fmt.Errorf("received request to transfer non-regular file: %d", req.ndx))
353+ }
354+ if req.iflags&itemTransfer == 0 {
355+ if err := s.writeNdxAndAttrs(req); err != nil {
356+ return err
357+ }
358+ continue
359+ }
360+ if phase == 2 {
361+ return s.fatal(exitProtocol, errors.New("got transfer request in phase 2"))
362+ }
363+ if s.opts.dryRun {
364+ if err := s.writeNdxAndAttrs(req); err != nil {
365+ return err
366+ }
367+ continue
368+ }
369+
370+ head, sums, err := s.receiveSums()
371+ if err != nil {
372+ return s.fatal(exitProtocol, err)
373+ }
374+ if err := s.sendFile(f, req, head, sums, tok); err != nil {
375+ return err
376+ }
377+ }
378+ if s.ioError != savedIOError && s.c.protocol >= 30 {
379+ if err := s.c.writeMsgInt(msgIOError, s.ioError); err != nil {
380+ return err
381+ }
382+ }
383+ return s.c.writeNdx(ndxDone)
384+}
385+
386+func (s *session) sendFile(f *fileEntry, req ndxRequest, head sumHead, sums []sumBuf, tok tokenSender) error {
387+ file, info, err := s.fs.Open(f.src)
388+ if err != nil {
389+ if errors.Is(err, fs.ErrNotExist) {
390+ s.ioError |= ioErrVanished
391+ s.warning("file has vanished: %q", "/"+f.src)
392+ } else {
393+ s.ioError |= ioErrGeneral
394+ s.xferError("send_files failed to open %q: %v", "/"+f.src, err)
395+ }
396+ if s.c.protocol >= 30 {
397+ return s.c.writeMsgInt(msgNoSend, req.ndx)
398+ }
399+ return nil
400+ }
401+ defer func() { _ = file.Close() }()
402+
403+ if err := s.writeNdxAndAttrs(req); err != nil {
404+ return err
405+ }
406+ if err := s.c.writeSumHead(head); err != nil {
407+ return err
408+ }
409+ inplace := (s.inplacePartial && req.fnamecmpType == fnamecmpPartialDir) ||
410+ (s.opts.inplace && req.fnamecmpType == fnamecmpFname)
411+ readSize := max(3*int64(head.blength), maxMapSize)
412+ m := &matcher{
413+ head: head,
414+ sums: sums,
415+ cs: &s.cs,
416+ tok: tok,
417+ buf: newMapFile(file, info.Size, readSize, head.blength),
418+ sum: s.cs.newXferSum(),
419+ inplace: inplace,
420+ }
421+ if err := m.run(info.Size); err != nil {
422+ return err
423+ }
424+ sum := m.sum.Sum(nil)
425+ if m.buf.err != nil {
426+ s.ioError |= ioErrGeneral
427+ s.xferError("read errors mapping %q: %v", "/"+f.src, m.buf.err)
428+ // A checksum the receiver can't match makes it discard the file.
429+ allZero := !slices.ContainsFunc(sum, func(b byte) bool { return b != 0 })
430+ clear(sum)
431+ if allZero {
432+ sum[len(sum)-1]++
433+ }
434+ }
435+ s.stats.literal += m.literal
436+ s.stats.matched += m.matched
437+ return s.c.write(sum)
438+}
439+
440+// receiveSums reads the block signatures the generator computed for its
441+// copy of a file.
442+func (s *session) receiveSums() (sumHead, []sumBuf, error) {
443+ head, err := s.c.readSumHead(s.cs.xfer.size())
444+ if err != nil {
445+ return head, nil, err
446+ }
447+ sums := make([]sumBuf, head.count)
448+ sum2 := make([]byte, int(head.count)*int(head.s2length))
449+ var offset int64
450+ for i := range sums {
451+ v, err := s.c.readInt32()
452+ if err != nil {
453+ return head, nil, err
454+ }
455+ b := sum2[i*int(head.s2length) : (i+1)*int(head.s2length)]
456+ if err := s.c.readFull(b); err != nil {
457+ return head, nil, err
458+ }
459+ l := head.blength
460+ if int32(i) == head.count-1 && head.remainder != 0 {
461+ l = head.remainder
462+ }
463+ sums[i] = sumBuf{offset: offset, len: l, sum1: uint32(v), sum2: b}
464+ offset += int64(l)
465+ }
466+ return head, sums, nil
467+}
468+
469+func (s *session) fileChecksum(name string) ([]byte, error) {
470+ f, info, err := s.fs.Open(name)
471+ if err != nil {
472+ return nil, err
473+ }
474+ defer func() { _ = f.Close() }()
475+ h := s.cs.newFileSum()
476+ if _, err := io.Copy(h, io.NewSectionReader(f, 0, info.Size)); err != nil {
477+ return nil, err
478+ }
479+ return h.Sum(nil), nil
480+}
+803, -0
......@@ -0,0 +1,803 @@
1+package rsync
2+
3+import (
4+ "crypto/rand"
5+ "encoding/binary"
6+ "errors"
7+ "fmt"
8+ "io"
9+ "log/slog"
10+ "slices"
11+ "strings"
12+ "sync"
13+
14+ "github.com/picosh/pico/pkg/rsync/rsyncopts"
15+)
16+
17+// Config configures a server session.
18+type Config struct {
19+ FS FS
20+ Logger *slog.Logger
21+ // Stderr receives errors raised before the protocol is running.
22+ Stderr io.Writer
23+}
24+
25+// ExitError carries the exit code the client should see. Its message has
26+// already been delivered to the client.
27+type ExitError struct {
28+ Code int
29+ Err error
30+}
31+
32+func (e *ExitError) Error() string {
33+ if e.Err == nil {
34+ return fmt.Sprintf("rsync exited with code %d", e.Code)
35+ }
36+ return e.Err.Error()
37+}
38+
39+func (e *ExitError) Unwrap() error { return e.Err }
40+
41+// options holds the parsed command line in the form the session uses.
42+type options struct {
43+ sender bool
44+ recurse bool
45+ xferDirs bool
46+ dryRun bool
47+ preserveUID bool
48+ preserveGID bool
49+ preserveLinks bool
50+ preserveDevs bool
51+ preserveSpecs bool
52+ preserveTimes bool
53+ preserveAtimes bool
54+ numericIDs bool
55+ alwaysChecksum bool
56+ ignoreTimes bool
57+ sizeOnly bool
58+ updateOnly bool
59+ ignoreExisting bool
60+ ignoreNonExist bool
61+ wholeFile bool
62+ inplace bool
63+ delayUpdates bool
64+ removeSource bool
65+ deleteMode bool
66+ deleteBefore bool
67+ deleteDuring int
68+ deleteAfter bool
69+ deleteExcluded bool
70+ pruneEmptyDirs bool
71+ ignoreErrors bool
72+ cvsExclude bool
73+ maxDelete int
74+ maxSize int64
75+ minSize int64
76+ blockSize int32
77+ infoName int
78+ infoDel int
79+ infoNonreg int
80+}
81+
82+type stats struct {
83+ literal int64
84+ matched int64
85+}
86+
87+// session is one rsync server run.
88+type session struct {
89+ fs FS
90+ logger *slog.Logger
91+ c *conn
92+ opts options
93+
94+ cs checksums
95+ compression compression
96+ compressLevel int
97+ filters *filterList
98+ varintFlags bool
99+ safeFlist bool
100+ xmitID0Names bool
101+ inplacePartial bool
102+
103+ mu sync.Mutex
104+ ioError int32
105+ gotXferError bool
106+ stats stats
107+}
108+
109+// Serve runs the server side of an rsync transfer over rw, which carries the
110+// SSH session's stdin and stdout. args is the command line after "rsync".
111+func Serve(cfg Config, rw io.ReadWriter, args []string) error {
112+ logger := cfg.Logger
113+ if logger == nil {
114+ logger = slog.New(slog.DiscardHandler)
115+ }
116+ stderr := cfg.Stderr
117+ if stderr == nil {
118+ stderr = io.Discard
119+ }
120+ early := func(code int, err error) error {
121+ _, _ = fmt.Fprintf(stderr, "rsync: %v\n", err)
122+ return &ExitError{Code: code, Err: err}
123+ }
124+
125+ c := newConn(rw)
126+ defer func() { _ = c.close() }()
127+ pc, err := rsyncopts.ParseArguments(args)
128+ if err != nil {
129+ return early(exitSyntax, err)
130+ }
131+ if pc.Options.ProtectArgs() {
132+ // The real arguments follow on stdin, NUL-separated.
133+ extra, err := readProtectedArgs(c)
134+ if err != nil {
135+ return early(exitProtocol, err)
136+ }
137+ if pc, err = rsyncopts.ParseArguments(append(slices.Clone(args), extra...)); err != nil {
138+ return early(exitSyntax, err)
139+ }
140+ }
141+ o := pc.Options
142+ if !o.Server() {
143+ return early(exitSyntax, errors.New("only server mode is supported"))
144+ }
145+ if err := checkSupported(o); err != nil {
146+ return early(exitUnsupported, err)
147+ }
148+
149+ s := &session{
150+ fs: cfg.FS,
151+ logger: logger,
152+ c: c,
153+ filters: &filterList{},
154+ }
155+ s.opts = options{
156+ sender: o.Sender(),
157+ recurse: o.Recurse(),
158+ xferDirs: o.XferDirs(),
159+ dryRun: o.DryRun(),
160+ preserveUID: o.PreserveUid(),
161+ preserveGID: o.PreserveGid(),
162+ preserveLinks: o.PreserveLinks(),
163+ preserveDevs: o.PreserveDevices(),
164+ preserveSpecs: o.PreserveSpecials(),
165+ preserveTimes: o.PreserveMTimes(),
166+ preserveAtimes: o.PreserveAtimes(),
167+ numericIDs: o.NumericIDs(),
168+ alwaysChecksum: o.AlwaysChecksum(),
169+ ignoreTimes: o.IgnoreTimes(),
170+ sizeOnly: o.SizeOnly(),
171+ updateOnly: o.UpdateOnly(),
172+ ignoreExisting: o.IgnoreExisting(),
173+ ignoreNonExist: o.IgnoreNonExisting(),
174+ wholeFile: o.WholeFile(),
175+ inplace: o.Inplace(),
176+ delayUpdates: o.DelayUpdates(),
177+ removeSource: o.RemoveSourceFiles() != 0,
178+ deleteMode: o.DeleteMode(),
179+ deleteBefore: o.DeleteBefore(),
180+ deleteDuring: o.DeleteDuring(),
181+ deleteAfter: o.DeleteAfter(),
182+ deleteExcluded: o.DeleteExcluded(),
183+ pruneEmptyDirs: o.PruneEmptyDirs(),
184+ ignoreErrors: o.IgnoreErrors(),
185+ cvsExclude: o.CvsExclude(),
186+ maxDelete: o.MaxDelete(),
187+ maxSize: o.MaxSize(),
188+ minSize: o.MinSize(),
189+ blockSize: int32(o.BlockSize()),
190+ infoName: o.Info(rsyncopts.INFO_NAME),
191+ infoDel: o.Info(rsyncopts.INFO_DEL),
192+ infoNonreg: o.Info(rsyncopts.INFO_NONREG),
193+ }
194+
195+ if err := s.setupProtocol(o); err != nil {
196+ var exit *ExitError
197+ if errors.As(err, &exit) {
198+ return early(exit.Code, exit.Err)
199+ }
200+ return early(exitProtocol, err)
201+ }
202+
203+ if err := c.w.setMultiplex(true); err != nil {
204+ return &ExitError{Code: exitStreamIO, Err: err}
205+ }
206+ if s.c.protocol >= 30 {
207+ c.r.multiplex = true
208+ }
209+ c.r.onMsg = s.handleMsg
210+
211+ logger.Debug("rsync session starting",
212+ "protocol", c.protocol,
213+ "sender", s.opts.sender,
214+ "checksum", s.cs.xfer,
215+ "compression", s.compression,
216+ )
217+
218+ if s.opts.sender {
219+ err = s.runSender(pc.RemainingArgs)
220+ } else {
221+ err = s.runReceiver(pc.RemainingArgs)
222+ }
223+ if err == nil {
224+ return nil
225+ }
226+ var exit *ExitError
227+ if errors.As(err, &exit) {
228+ return exit
229+ }
230+ // An unexpected failure, most likely the connection going away.
231+ return s.fatal(exitStreamIO, err)
232+}
233+
234+// checkSupported refuses options that would change the wire format in ways
235+// we don't implement, or that make no sense for a storage backend.
236+func checkSupported(o *rsyncopts.Options) error {
237+ switch {
238+ case o.PreserveHardLinks():
239+ return errors.New("--hard-links is not supported")
240+ case o.PreserveACLs():
241+ return errors.New("--acls is not supported")
242+ case o.PreserveXattrs():
243+ return errors.New("--xattrs is not supported")
244+ case o.PreserveCrtimes():
245+ return errors.New("--crtimes is not supported")
246+ case o.RelativePaths():
247+ return errors.New("--relative is not supported")
248+ case o.AppendMode() != 0:
249+ return errors.New("--append is not supported")
250+ case len(o.BasisDirs()) > 0:
251+ return errors.New("--link-dest, --copy-dest and --compare-dest are not supported")
252+ case o.BatchMode():
253+ return errors.New("batch mode is not supported")
254+ case o.FilesFrom() != "":
255+ return errors.New("--files-from is not supported")
256+ case o.Iconv() != "":
257+ return errors.New("--iconv is not supported")
258+ case o.MissingArgs() == 2:
259+ return errors.New("--delete-missing-args is not supported")
260+ case o.Sender() && o.RemoveSourceFiles() != 0:
261+ return errors.New("--remove-source-files is not supported when downloading")
262+ }
263+ return nil
264+}
265+
266+// readProtectedArgs reads the arguments a --secluded-args client sends
267+// before the protocol starts. The first one is the program name.
268+func readProtectedArgs(c *conn) ([]string, error) {
269+ var args []string
270+ var cur []byte
271+ for {
272+ b, err := c.readByte()
273+ if err != nil {
274+ return nil, err
275+ }
276+ if b != 0 {
277+ cur = append(cur, b)
278+ continue
279+ }
280+ if len(cur) == 0 {
281+ break
282+ }
283+ args = append(args, string(cur))
284+ cur = cur[:0]
285+ }
286+ if len(args) > 0 {
287+ args = args[1:]
288+ }
289+ return args, nil
290+}
291+
292+// setupProtocol runs the handshake that precedes the transfer
293+// (compat.c:setup_protocol).
294+func (s *session) setupProtocol(o *rsyncopts.Options) error {
295+ c := s.c
296+ version := maxProtocol
297+ if v := o.ProtocolVersion(); v > 0 && v < version {
298+ version = v
299+ }
300+ if err := c.writeInt32(int32(version)); err != nil {
301+ return err
302+ }
303+ if err := c.flush(); err != nil {
304+ return err
305+ }
306+ remote, err := c.readInt32()
307+ if err != nil {
308+ return err
309+ }
310+ if int(remote) < version {
311+ version = int(remote)
312+ }
313+ if version < minProtocol {
314+ return &ExitError{Code: exitProtocol, Err: fmt.Errorf("protocol version %d is too old, %d or newer is required", remote, minProtocol)}
315+ }
316+ c.protocol = version
317+
318+ if s.opts.deleteMode && !s.opts.deleteBefore && s.opts.deleteDuring == 0 && !s.opts.deleteAfter {
319+ if version < 30 {
320+ s.opts.deleteBefore = true
321+ } else {
322+ s.opts.deleteDuring = 1
323+ }
324+ }
325+
326+ negotiate := false
327+ if version >= 30 {
328+ info := clientInfo(o.ShellCmd())
329+ flags := 0
330+ if strings.Contains(info, "f") {
331+ flags |= cfSafeFlist
332+ }
333+ if strings.Contains(info, "x") {
334+ flags |= cfAvoidXattrOptim
335+ }
336+ if strings.Contains(info, "C") {
337+ flags |= cfChksumSeedFix
338+ }
339+ if strings.Contains(info, "I") {
340+ flags |= cfInplacePartialDir
341+ }
342+ if strings.Contains(info, "u") {
343+ flags |= cfID0Names
344+ }
345+ if strings.Contains(info, "v") {
346+ negotiate = true
347+ flags |= cfVarintFlistFlags
348+ }
349+ if strings.Contains(info, "V") {
350+ flags |= cfVarintFlistFlags
351+ err = c.writeByte(byte(flags))
352+ } else {
353+ err = c.writeVarint(int32(flags))
354+ }
355+ if err != nil {
356+ return err
357+ }
358+ s.cs.properSeedOrder = flags&cfChksumSeedFix != 0
359+ s.varintFlags = flags&cfVarintFlistFlags != 0
360+ s.xmitID0Names = flags&cfID0Names != 0
361+ s.safeFlist = flags&cfSafeFlist != 0 || version >= 31
362+ s.inplacePartial = flags&cfInplacePartialDir != 0
363+ }
364+
365+ checksumChoice := o.ChecksumChoice()
366+ compressChoice := o.CompressChoice()
367+ wantCompress := o.Compress() || compressChoice != ""
368+ if negotiate {
369+ if checksumChoice == "" {
370+ if err := c.writeVstring(nameList(checksumNames)); err != nil {
371+ return err
372+ }
373+ }
374+ if wantCompress && compressChoice == "" {
375+ if err := c.writeVstring(nameList(compressNames)); err != nil {
376+ return err
377+ }
378+ }
379+ if err := c.flush(); err != nil {
380+ return err
381+ }
382+ }
383+
384+ // Pick the transfer checksum.
385+ switch {
386+ case checksumChoice != "":
387+ xfer, file, _ := strings.Cut(checksumChoice, ",")
388+ if file == "" {
389+ file = xfer
390+ }
391+ if s.cs.xfer, err = checksumByChoice(xfer, version); err != nil {
392+ return &ExitError{Code: exitUnsupported, Err: err}
393+ }
394+ if s.cs.file, err = checksumByChoice(file, version); err != nil {
395+ return &ExitError{Code: exitUnsupported, Err: err}
396+ }
397+ case negotiate:
398+ list, err := c.readVstring(256)
399+ if err != nil {
400+ return err
401+ }
402+ name, ok := negotiateName(checksumNames, list)
403+ if !ok {
404+ return &ExitError{Code: exitUnsupported, Err: fmt.Errorf("failed to negotiate a checksum choice, client list: %s", list)}
405+ }
406+ s.cs.xfer, _ = parseChecksumName(name)
407+ s.cs.file = s.cs.xfer
408+ default:
409+ s.cs.xfer, _ = checksumByChoice("auto", version)
410+ s.cs.file = s.cs.xfer
411+ }
412+
413+ // Pick the compression.
414+ s.compression = compressNone
415+ switch {
416+ case compressChoice != "":
417+ if s.compression, err = parseCompressName(compressChoice); err != nil {
418+ return &ExitError{Code: exitUnsupported, Err: err}
419+ }
420+ case wantCompress && negotiate:
421+ list, err := c.readVstring(256)
422+ if err != nil {
423+ return err
424+ }
425+ name, ok := negotiateName(compressNames, list)
426+ if !ok {
427+ return &ExitError{Code: exitUnsupported, Err: fmt.Errorf("failed to negotiate a compress choice, client list: %s", list)}
428+ }
429+ s.compression, _ = parseCompressName(name)
430+ case wantCompress:
431+ s.compression = compressZlib
432+ }
433+ s.compressLevel = 6
434+ if lvl := o.CompressLevel(); s.compression != compressNone && lvl >= 0 {
435+ if lvl == 0 {
436+ s.compression = compressNone
437+ } else {
438+ s.compressLevel = min(lvl, 9)
439+ }
440+ }
441+
442+ seed := int32(o.ChecksumSeed())
443+ if seed == 0 {
444+ var b [4]byte
445+ _, _ = rand.Read(b[:])
446+ seed = int32(binary.LittleEndian.Uint32(b[:]) & 0x7FFFFFFF)
447+ if seed == 0 {
448+ seed = 1
449+ }
450+ }
451+ s.cs.seed = seed
452+ if err := c.writeInt32(seed); err != nil {
453+ return err
454+ }
455+ if s.cs.xfer == csumNone {
456+ s.opts.wholeFile = true
457+ }
458+ return c.flush()
459+}
460+
461+// clientInfo extracts the capability letters the client appends to its -e
462+// option, as in "-e.LsfxCIvu".
463+func clientInfo(shellCmd string) string {
464+ if i := strings.IndexByte(shellCmd, '.'); i >= 0 {
465+ return shellCmd[i+1:]
466+ }
467+ return shellCmd
468+}
469+
470+func checksumByChoice(name string, protocol int) (csumType, error) {
471+ if name == "auto" {
472+ if protocol >= 30 {
473+ return csumMD5, nil
474+ }
475+ return csumMD4Old, nil
476+ }
477+ return parseChecksumName(name)
478+}
479+
480+func nameList[T any](items []struct {
481+ name string
482+ typ T
483+}) string {
484+ var names []string
485+ seen := map[any]bool{}
486+ for _, it := range items {
487+ if seen[it.typ] {
488+ continue
489+ }
490+ seen[it.typ] = true
491+ names = append(names, it.name)
492+ }
493+ return strings.Join(names, " ")
494+}
495+
496+// negotiateName picks our most preferred name that the client also listed.
497+func negotiateName[T any](items []struct {
498+ name string
499+ typ T
500+}, list string) (string, bool) {
501+ theirs := strings.Fields(list)
502+ for _, it := range items {
503+ if slices.Contains(theirs, it.name) {
504+ return it.name, true
505+ }
506+ }
507+ return "", false
508+}
509+
510+// handleMsg processes an out-of-band message from the client.
511+func (s *session) handleMsg(tag byte, data []byte) error {
512+ switch tag {
513+ case msgIOError:
514+ if len(data) != 4 {
515+ return fmt.Errorf("invalid multi-message %d:%d", tag, len(data))
516+ }
517+ s.addIOError(int32(binary.LittleEndian.Uint32(data)) & ioErrMask)
518+ case msgNoop:
519+ case msgErrorExit:
520+ code := 0
521+ if len(data) == 4 {
522+ code = int(int32(binary.LittleEndian.Uint32(data)))
523+ }
524+ return &remoteExitError{code: code}
525+ case msgInfo, msgError, msgErrorXfer, msgWarning:
526+ s.logger.Info("rsync client message", "msg", strings.TrimSpace(string(data)))
527+ case msgNoSend:
528+ // The sender couldn't open a file we asked for and has reported
529+ // why; nothing will arrive for it.
530+ if len(data) != 4 {
531+ return fmt.Errorf("invalid multi-message %d:%d", tag, len(data))
532+ }
533+ case msgSuccess, msgDeleted:
534+ default:
535+ return fmt.Errorf("unexpected tag %d", tag)
536+ }
537+ return nil
538+}
539+
540+func (s *session) addIOError(v int32) {
541+ s.mu.Lock()
542+ s.ioError |= v
543+ s.mu.Unlock()
544+}
545+
546+func (s *session) who() string {
547+ if s.opts.sender {
548+ return "sender"
549+ }
550+ return "receiver"
551+}
552+
553+func (s *session) send(tag byte, msg string) {
554+ if s.c.protocol < 30 {
555+ switch tag {
556+ case msgError:
557+ tag = msgErrorXfer
558+ case msgWarning:
559+ tag = msgInfo
560+ }
561+ }
562+ if err := s.c.writeMsg(tag, []byte(msg)); err != nil {
563+ s.logger.Debug("rsync message not delivered", "err", err)
564+ }
565+}
566+
567+func (s *session) info(format string, args ...any) {
568+ s.send(msgInfo, fmt.Sprintf(format, args...)+"\n")
569+}
570+
571+func (s *session) warning(format string, args ...any) {
572+ s.send(msgWarning, fmt.Sprintf(format, args...)+"\n")
573+}
574+
575+// xferError reports a failure affecting one file; the run continues and
576+// ends with a partial-transfer exit code.
577+func (s *session) xferError(format string, args ...any) {
578+ s.mu.Lock()
579+ s.gotXferError = true
580+ s.mu.Unlock()
581+ s.send(msgErrorXfer, fmt.Sprintf("rsync: [%s] %s\n", s.who(), fmt.Sprintf(format, args...)))
582+}
583+
584+// fatal reports err to the client and ends the run.
585+func (s *session) fatal(code int, err error) error {
586+ var remote *remoteExitError
587+ if errors.As(err, &remote) {
588+ return &ExitError{Code: remote.code, Err: err}
589+ }
590+ s.logger.Error("rsync session failed", "err", err)
591+ if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
592+ s.send(msgError, fmt.Sprintf("rsync: [%s] %v\n", s.who(), err))
593+ if s.c.protocol >= 31 {
594+ _ = s.c.writeMsgInt(msgErrorExit, int32(code))
595+ }
596+ _ = s.c.flush()
597+ }
598+ return &ExitError{Code: code, Err: err}
599+}
600+
601+// finish ends a run that completed, reporting a partial transfer if any
602+// file failed.
603+func (s *session) finish() error {
604+ if err := s.c.flush(); err != nil {
605+ return err
606+ }
607+ s.mu.Lock()
608+ ioError, xferErr := s.ioError, s.gotXferError
609+ s.mu.Unlock()
610+ code := 0
611+ switch {
612+ case ioError&ioErrGeneral != 0 || xferErr:
613+ code = exitPartial
614+ case ioError&ioErrVanished != 0:
615+ code = exitVanished
616+ case ioError&ioErrDelLimit != 0:
617+ code = exitDelLimit
618+ }
619+ if code == 0 {
620+ return nil
621+ }
622+ return &ExitError{Code: code, Err: fmt.Errorf("some files were not transferred (code %d)", code)}
623+}
624+
625+func (s *session) newFlistCodec() *flistCodec {
626+ return &flistCodec{
627+ protocol: s.c.protocol,
628+ varintFlags: s.varintFlags,
629+ safeFlist: s.safeFlist,
630+ preserveUID: s.opts.preserveUID,
631+ preserveGID: s.opts.preserveGID,
632+ preserveLinks: s.opts.preserveLinks,
633+ preserveDevs: s.opts.preserveDevs,
634+ preserveSpecs: s.opts.preserveSpecs,
635+ preserveAtime: s.opts.preserveAtimes,
636+ alwaysSum: s.opts.alwaysChecksum,
637+ sumLen: s.cs.file.size(),
638+ }
639+}
640+
641+// recvFilterList reads the client's filter rules (exclude.c:recv_filter_list).
642+func (s *session) recvFilterList() error {
643+ receiverWants := s.opts.pruneEmptyDirs || (s.opts.deleteMode && (!s.opts.deleteExcluded || s.c.protocol >= 29))
644+ if s.opts.sender || receiverWants {
645+ for {
646+ n, err := s.c.readInt32()
647+ if err != nil {
648+ return err
649+ }
650+ if n == 0 {
651+ break
652+ }
653+ if n < 0 || n >= maxPathLen*2 {
654+ return s.fatal(exitProtocol, fmt.Errorf("filter rule too long: %d", n))
655+ }
656+ line := make([]byte, n)
657+ if err := s.c.readFull(line); err != nil {
658+ return err
659+ }
660+ if err := s.filters.parse(string(line), s.c.protocol < 29); err != nil {
661+ return s.fatal(exitSyntax, err)
662+ }
663+ }
664+ }
665+ if s.opts.cvsExclude && (s.opts.sender || s.c.protocol < 29) {
666+ for _, pat := range strings.Fields(defaultCVSIgnore) {
667+ s.filters.add(pat, ruleNoPrefixes)
668+ }
669+ }
670+ return nil
671+}
672+
673+type ndxRequest struct {
674+ ndx int32
675+ iflags int
676+ fnamecmpType byte
677+ xname string
678+ hasXname bool
679+}
680+
681+// readNdxAndAttrs reads the next file index and its item flags
682+// (rsync.c:read_ndx_and_attrs).
683+func (s *session) readNdxAndAttrs(count int) (ndxRequest, error) {
684+ c := s.c
685+ for {
686+ ndx, err := c.readNdx()
687+ if err != nil {
688+ return ndxRequest{}, err
689+ }
690+ if ndx == ndxDone {
691+ return ndxRequest{ndx: ndx}, nil
692+ }
693+ if ndx == ndxDelStats {
694+ var counts [5]int32
695+ for i := range counts {
696+ if counts[i], err = c.readVarint(); err != nil {
697+ return ndxRequest{}, err
698+ }
699+ }
700+ if s.opts.sender {
701+ // Echo the receiver's deletion stats back to the client.
702+ if err := s.writeDelStats(counts); err != nil {
703+ return ndxRequest{}, err
704+ }
705+ }
706+ continue
707+ }
708+ if ndx < 0 || int(ndx) > count {
709+ return ndxRequest{}, s.fatal(exitProtocol, fmt.Errorf("invalid file index: %d (%d - %d)", ndx, ndxDone, count-1))
710+ }
711+ req := ndxRequest{ndx: ndx, iflags: itemTransfer | itemMissingData, fnamecmpType: fnamecmpFname}
712+ if c.protocol >= 29 {
713+ v, err := c.readShortint()
714+ if err != nil {
715+ return ndxRequest{}, err
716+ }
717+ req.iflags = int(v)
718+ }
719+ // The protocol 29 keep-alive.
720+ if c.protocol < 30 && int(ndx) == count && req.iflags == itemIsNew {
721+ continue
722+ }
723+ if int(ndx) == count {
724+ return ndxRequest{}, s.fatal(exitProtocol, fmt.Errorf("invalid file index: %d", ndx))
725+ }
726+ if req.iflags&itemBasisTypeFollows != 0 {
727+ b, err := c.readByte()
728+ if err != nil {
729+ return ndxRequest{}, err
730+ }
731+ req.fnamecmpType = b
732+ }
733+ if req.iflags&itemXnameFollows != 0 {
734+ if req.xname, err = c.readVstring(maxPathLen); err != nil {
735+ return ndxRequest{}, err
736+ }
737+ req.hasXname = true
738+ }
739+ return req, nil
740+ }
741+}
742+
743+func (s *session) writeNdxAndAttrs(req ndxRequest) error {
744+ if err := s.c.writeNdx(req.ndx); err != nil {
745+ return err
746+ }
747+ if s.c.protocol < 29 {
748+ return nil
749+ }
750+ if err := s.c.writeShortint(uint16(req.iflags)); err != nil {
751+ return err
752+ }
753+ if req.iflags&itemBasisTypeFollows != 0 {
754+ if err := s.c.writeByte(req.fnamecmpType); err != nil {
755+ return err
756+ }
757+ }
758+ if req.iflags&itemXnameFollows != 0 {
759+ return s.c.writeVstring(req.xname)
760+ }
761+ return nil
762+}
763+
764+func (s *session) writeDelStats(counts [5]int32) error {
765+ if err := s.c.writeNdx(ndxDelStats); err != nil {
766+ return err
767+ }
768+ for _, v := range counts {
769+ if err := s.c.writeVarint(v); err != nil {
770+ return err
771+ }
772+ }
773+ return nil
774+}
775+
776+// readFinalGoodbye waits for the client's end-of-run index
777+// (main.c:read_final_goodbye).
778+func (s *session) readFinalGoodbye(sender bool) error {
779+ c := s.c
780+ var ndx int32
781+ var err error
782+ if c.protocol < 29 {
783+ ndx, err = c.readInt32()
784+ } else {
785+ var req ndxRequest
786+ req, err = s.readNdxAndAttrs(0)
787+ ndx = req.ndx
788+ if err == nil && c.protocol >= 31 && ndx == ndxDone && sender {
789+ if err = c.writeNdx(ndxDone); err != nil {
790+ return err
791+ }
792+ req, err = s.readNdxAndAttrs(0)
793+ ndx = req.ndx
794+ }
795+ }
796+ if err != nil {
797+ return err
798+ }
799+ if ndx != ndxDone {
800+ return s.fatal(exitProtocol, fmt.Errorf("invalid packet at end of run (%d)", ndx))
801+ }
802+ return nil
803+}
+189, -0
......@@ -0,0 +1,189 @@
1+package rsync
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+ "io"
7+)
8+
9+// sumHead describes the block signatures of a basis file.
10+type sumHead struct {
11+ count int32
12+ blength int32
13+ s2length int32
14+ remainder int32
15+}
16+
17+type sumBuf struct {
18+ offset int64
19+ len int32
20+ sum1 uint32
21+ sum2 []byte
22+ chain int32
23+ same bool // matched at the same offset during an in-place update
24+}
25+
26+func (c *conn) writeSumHead(h sumHead) error {
27+ if err := c.writeInt32(h.count); err != nil {
28+ return err
29+ }
30+ if err := c.writeInt32(h.blength); err != nil {
31+ return err
32+ }
33+ if err := c.writeInt32(h.s2length); err != nil {
34+ return err
35+ }
36+ return c.writeInt32(h.remainder)
37+}
38+
39+func (c *conn) readSumHead(xferLen int) (sumHead, error) {
40+ var h sumHead
41+ var err error
42+ maxBlength := int32(maxBlockSize)
43+ if c.protocol < 30 {
44+ maxBlength = oldMaxBlockSize
45+ }
46+ if h.count, err = c.readInt32(); err != nil {
47+ return h, err
48+ }
49+ if h.count < 0 {
50+ return h, fmt.Errorf("invalid checksum count %d", h.count)
51+ }
52+ if h.blength, err = c.readInt32(); err != nil {
53+ return h, err
54+ }
55+ if h.blength < 0 || h.blength > maxBlength {
56+ return h, fmt.Errorf("invalid block length %d", h.blength)
57+ }
58+ if h.count > 0 && h.blength == 0 {
59+ return h, fmt.Errorf("invalid zero block length")
60+ }
61+ if h.s2length, err = c.readInt32(); err != nil {
62+ return h, err
63+ }
64+ if h.s2length < 0 || int(h.s2length) > xferLen {
65+ return h, fmt.Errorf("invalid checksum length %d", h.s2length)
66+ }
67+ if h.remainder, err = c.readInt32(); err != nil {
68+ return h, err
69+ }
70+ if h.remainder < 0 || h.remainder > h.blength {
71+ return h, fmt.Errorf("invalid remainder length %d", h.remainder)
72+ }
73+ return h, nil
74+}
75+
76+// sumSizesSqroot picks the block size (about the square root of the file
77+// length, rounded to a multiple of 8) and the strong checksum length.
78+func sumSizesSqroot(length int64, protocol int, fixedBlock int32, csumLength int, xferLen int) (sumHead, error) {
79+ maxS2 := min(sumLength, xferLen)
80+ var blength int32
81+ switch {
82+ case fixedBlock > 0:
83+ blength = fixedBlock
84+ case length <= blockSize*blockSize:
85+ blength = blockSize
86+ default:
87+ maxBlength := int32(maxBlockSize)
88+ if protocol < 30 {
89+ maxBlength = oldMaxBlockSize
90+ }
91+ c := int64(1)
92+ for l := length; ; {
93+ l >>= 2
94+ if l == 0 {
95+ break
96+ }
97+ c <<= 1
98+ }
99+ if c >= int64(maxBlength) {
100+ blength = maxBlength
101+ } else {
102+ var b int64
103+ for {
104+ b |= c
105+ if length < b*b {
106+ b &^= c
107+ }
108+ c >>= 1
109+ if c < 8 {
110+ break
111+ }
112+ }
113+ blength = int32(max(b, blockSize))
114+ }
115+ }
116+
117+ var s2length int
118+ if csumLength == sumLength {
119+ s2length = maxS2
120+ } else {
121+ b := blocksumBias
122+ for l := length; ; {
123+ l >>= 1
124+ if l == 0 {
125+ break
126+ }
127+ b += 2
128+ }
129+ for c := blength; ; {
130+ c >>= 1
131+ if c == 0 || b == 0 {
132+ break
133+ }
134+ b--
135+ }
136+ s2length = (b + 1 - 32 + 7) / 8
137+ s2length = max(s2length, csumLength)
138+ s2length = min(s2length, maxS2)
139+ }
140+
141+ remainder := int32(length % int64(blength))
142+ count := length / int64(blength)
143+ if remainder != 0 {
144+ count++
145+ }
146+ if count > 0x7FFFFFFF {
147+ return sumHead{}, fmt.Errorf("file is too large for checksum sending")
148+ }
149+ return sumHead{
150+ count: int32(count),
151+ blength: blength,
152+ s2length: int32(s2length),
153+ remainder: remainder,
154+ }, nil
155+}
156+
157+// encodeSums computes the block signatures of a basis file in their wire
158+// form. They are built up front so a read error can't leave a half-written
159+// signature on the stream.
160+func encodeSums(r io.ReaderAt, length int64, head sumHead, cs *checksums) ([]byte, error) {
161+ out := make([]byte, 0, int(head.count)*(4+int(head.s2length)))
162+ buf := make([]byte, head.blength)
163+ var sum2 []byte
164+ var offset int64
165+ for i := int32(0); i < head.count; i++ {
166+ n := min(int64(head.blength), length-offset)
167+ block := buf[:n]
168+ if err := readFullAt(r, block, offset); err != nil {
169+ return nil, err
170+ }
171+ offset += n
172+ out = binary.LittleEndian.AppendUint32(out, checksum1(block))
173+ sum2 = cs.blockSum(block, sum2[:0])
174+ out = append(out, sum2[:head.s2length]...)
175+ }
176+ return out, nil
177+}
178+
179+// readFullAt fills p from r at off, treating a short read as an error.
180+func readFullAt(r io.ReaderAt, p []byte, off int64) error {
181+ n, err := r.ReadAt(p, off)
182+ if n == len(p) {
183+ return nil
184+ }
185+ if err == nil || err == io.EOF {
186+ err = io.ErrUnexpectedEOF
187+ }
188+ return err
189+}
+532, -0
......@@ -0,0 +1,532 @@
1+package rsync
2+
3+import (
4+ "bytes"
5+ stdflate "compress/flate"
6+ "errors"
7+ "fmt"
8+ "io"
9+
10+ "github.com/klauspost/compress/flate"
11+)
12+
13+// compression identifies the negotiated token compression.
14+type compression int
15+
16+const (
17+ compressNone compression = iota
18+ compressZlib
19+ compressZlibx
20+)
21+
22+// compressNames is our preference order for negotiation. zlib adds matched
23+// blocks to the deflate history; zlibx does not.
24+var compressNames = []struct {
25+ name string
26+ typ compression
27+}{
28+ {"zlibx", compressZlibx},
29+ {"zlib", compressZlib},
30+ {"none", compressNone},
31+}
32+
33+func parseCompressName(name string) (compression, error) {
34+ for _, c := range compressNames {
35+ if c.name == name {
36+ return c.typ, nil
37+ }
38+ }
39+ return 0, fmt.Errorf("unknown compress name: %s", name)
40+}
41+
42+// tokenSender emits one file's delta. token is a block index for a match, -1
43+// at the end of the file, or -2 to send literal data without a token.
44+// matched holds the matched block's data.
45+type tokenSender interface {
46+ send(token int32, literal, matched []byte) error
47+}
48+
49+// tokenReceiver decodes one file's delta. recv returns a positive length
50+// with literal data, -(block+1) for a match, or 0 at the end of the file.
51+// see is called with each matched block's data.
52+type tokenReceiver interface {
53+ recv() (int32, []byte, error)
54+ see(data []byte)
55+}
56+
57+func newTokenSender(c *conn, comp compression, level int) tokenSender {
58+ if comp == compressNone {
59+ return &simpleTokenSender{c: c}
60+ }
61+ return &deflateTokenSender{c: c, zlib: comp == compressZlib, level: level, lastToken: -1}
62+}
63+
64+func newTokenReceiver(c *conn, comp compression) tokenReceiver {
65+ if comp == compressNone {
66+ return &simpleTokenReceiver{c: c, buf: make([]byte, chunkSize)}
67+ }
68+ return &deflateTokenReceiver{c: c, zlib: comp == compressZlib, out: make([]byte, chunkSize)}
69+}
70+
71+type simpleTokenSender struct{ c *conn }
72+
73+func (s *simpleTokenSender) send(token int32, literal, _ []byte) error {
74+ for len(literal) > 0 {
75+ n := min(len(literal), chunkSize)
76+ if err := s.c.writeInt32(int32(n)); err != nil {
77+ return err
78+ }
79+ if err := s.c.write(literal[:n]); err != nil {
80+ return err
81+ }
82+ literal = literal[n:]
83+ }
84+ if token == -2 {
85+ return nil
86+ }
87+ return s.c.writeInt32(-(token + 1))
88+}
89+
90+type simpleTokenReceiver struct {
91+ c *conn
92+ buf []byte
93+ residue int32
94+}
95+
96+func (s *simpleTokenReceiver) recv() (int32, []byte, error) {
97+ if s.residue == 0 {
98+ i, err := s.c.readInt32()
99+ if err != nil || i <= 0 {
100+ return i, nil, err
101+ }
102+ s.residue = i
103+ }
104+ n := min(int32(len(s.buf)), s.residue)
105+ s.residue -= n
106+ if err := s.c.readFull(s.buf[:n]); err != nil {
107+ return 0, nil, err
108+ }
109+ return n, s.buf[:n], nil
110+}
111+
112+func (s *simpleTokenReceiver) see([]byte) {}
113+
114+// Flag bytes of the compressed token stream.
115+const (
116+ tokEndFlag = 0
117+ tokLong = 0x20
118+ tokRunLong = 0x21
119+ tokDeflatedData = 0x40
120+ tokRel = 0x80
121+ tokRunRel = 0xc0
122+ maxTokenIndex = 0x7ffffffe
123+ deflateWindow = 32 * 1024
124+)
125+
126+var syncMarker = []byte{0, 0, 0xff, 0xff}
127+
128+// deflateTokenSender produces rsync's compressed token stream: literal data
129+// is raw deflate, sync-flushed before every token with the trailing
130+// 00 00 ff ff marker stripped.
131+type deflateTokenSender struct {
132+ c *conn
133+ zlib bool
134+ level int
135+
136+ w *flate.Writer
137+ out bytes.Buffer
138+
139+ lastToken, runStart, lastRunEnd int32
140+ flushPending bool
141+
142+ history []byte
143+ needDict bool
144+}
145+
146+func (d *deflateTokenSender) send(token int32, literal, matched []byte) error {
147+ switch {
148+ case d.lastToken == -1:
149+ if err := d.resetStream(); err != nil {
150+ return err
151+ }
152+ d.lastRunEnd = 0
153+ d.runStart = token
154+ d.flushPending = false
155+ case d.lastToken == -2:
156+ d.runStart = token
157+ case len(literal) != 0 || token != d.lastToken+1 || token >= d.runStart+65536:
158+ if err := d.writeRun(); err != nil {
159+ return err
160+ }
161+ d.lastRunEnd = d.lastToken
162+ d.runStart = token
163+ }
164+ d.lastToken = token
165+
166+ if len(literal) != 0 || d.flushPending {
167+ if err := d.deflate(literal, token != -2); err != nil {
168+ return err
169+ }
170+ d.flushPending = token == -2
171+ }
172+
173+ if token == -1 {
174+ return d.c.writeByte(tokEndFlag)
175+ }
176+ if token != -2 && d.zlib {
177+ d.addHistory(matched, d.c.protocol)
178+ d.needDict = true
179+ }
180+ return nil
181+}
182+
183+func (d *deflateTokenSender) resetStream() error {
184+ d.history = d.history[:0]
185+ d.needDict = false
186+ d.out.Reset()
187+ if d.w == nil {
188+ w, err := flate.NewWriter(&d.out, d.level)
189+ if err != nil {
190+ return err
191+ }
192+ d.w = w
193+ return nil
194+ }
195+ // Reset would reuse the last dictionary, so clear it explicitly.
196+ d.w.ResetDict(&d.out, nil)
197+ return nil
198+}
199+
200+func (d *deflateTokenSender) writeRun() error {
201+ r := d.runStart - d.lastRunEnd
202+ n := d.lastToken - d.runStart
203+ if r >= 0 && r <= 63 {
204+ flag := byte(tokRel)
205+ if n != 0 {
206+ flag = tokRunRel
207+ }
208+ if err := d.c.writeByte(flag + byte(r)); err != nil {
209+ return err
210+ }
211+ } else {
212+ flag := byte(tokLong)
213+ if n != 0 {
214+ flag = tokRunLong
215+ }
216+ if err := d.c.writeByte(flag); err != nil {
217+ return err
218+ }
219+ if err := d.c.writeInt32(d.runStart); err != nil {
220+ return err
221+ }
222+ }
223+ if n != 0 {
224+ return d.c.write([]byte{byte(n), byte(n >> 8)})
225+ }
226+ return nil
227+}
228+
229+func (d *deflateTokenSender) deflate(literal []byte, flush bool) error {
230+ if d.needDict {
231+ // The receiver's window holds the matched blocks too, so restart
232+ // the compressor with the same history.
233+ d.w.ResetDict(&d.out, d.window())
234+ d.needDict = false
235+ }
236+ if len(literal) > 0 {
237+ if _, err := d.w.Write(literal); err != nil {
238+ return err
239+ }
240+ d.addHistory(literal, 31)
241+ }
242+ if flush {
243+ if err := d.w.Flush(); err != nil {
244+ return err
245+ }
246+ if !bytes.HasSuffix(d.out.Bytes(), syncMarker) {
247+ return errors.New("rsync: deflate flush did not end in a sync marker")
248+ }
249+ d.out.Truncate(d.out.Len() - len(syncMarker))
250+ }
251+ data := d.out.Bytes()
252+ for len(data) > 0 {
253+ n := min(len(data), maxDataCount)
254+ if err := d.c.write([]byte{byte(tokDeflatedData + n>>8), byte(n)}); err != nil {
255+ return err
256+ }
257+ if err := d.c.write(data[:n]); err != nil {
258+ return err
259+ }
260+ data = data[n:]
261+ }
262+ d.out.Reset()
263+ return nil
264+}
265+
266+// addHistory appends data to the deflate history. Before protocol 31 rsync
267+// fed every 64K piece of a long matched block from its start, and both
268+// sides must reproduce that.
269+func (d *deflateTokenSender) addHistory(data []byte, protocol int) {
270+ d.history = appendHistory(d.history, data, protocol)
271+}
272+
273+func (d *deflateTokenSender) window() []byte {
274+ if len(d.history) > deflateWindow {
275+ return d.history[len(d.history)-deflateWindow:]
276+ }
277+ return d.history
278+}
279+
280+func appendHistory(history, data []byte, protocol int) []byte {
281+ toklen := len(data)
282+ src := data
283+ for toklen > 0 {
284+ n := min(toklen, 0xffff)
285+ history = append(history, src[:n]...)
286+ if protocol >= 31 {
287+ src = src[n:]
288+ }
289+ toklen -= n
290+ }
291+ if len(history) > 2*deflateWindow {
292+ history = append(history[:0], history[len(history)-deflateWindow:]...)
293+ }
294+ return history
295+}
296+
297+type deflateRecvState int
298+
299+const (
300+ rInit deflateRecvState = iota
301+ rIdle
302+ rRunning
303+ rInflating
304+)
305+
306+// deflateTokenReceiver decodes the compressed token stream. Each run of
307+// DEFLATED_DATA chunks is inflated as one segment: the source appends the
308+// stripped sync marker when the next flag arrives and then reports EOF, and
309+// the inflater is restarted with the current history for the next segment.
310+type deflateTokenReceiver struct {
311+ c *conn
312+ zlib bool
313+
314+ state deflateRecvState
315+ rxToken int32
316+ rxRun int32
317+ savedFlag int
318+ hasSaved bool
319+
320+ src segmentSource
321+ inflate io.ReadCloser
322+ history []byte
323+ out []byte
324+}
325+
326+func (d *deflateTokenReceiver) recv() (int32, []byte, error) {
327+ for {
328+ switch d.state {
329+ case rInit:
330+ d.history = d.history[:0]
331+ d.rxToken = 0
332+ d.state = rIdle
333+
334+ case rIdle:
335+ var flag int
336+ if d.hasSaved {
337+ flag, d.hasSaved = d.savedFlag, false
338+ } else {
339+ b, err := d.c.readByte()
340+ if err != nil {
341+ return 0, nil, err
342+ }
343+ flag = int(b)
344+ }
345+ if flag&0xC0 == tokDeflatedData {
346+ if err := d.src.start(d.c, flag); err != nil {
347+ return 0, nil, err
348+ }
349+ if d.inflate == nil {
350+ d.inflate = stdflate.NewReaderDict(&d.src, d.window())
351+ } else if err := d.inflate.(stdflate.Resetter).Reset(&d.src, d.window()); err != nil {
352+ return 0, nil, err
353+ }
354+ d.state = rInflating
355+ continue
356+ }
357+ if flag == tokEndFlag {
358+ d.state = rInit
359+ return 0, nil, nil
360+ }
361+ return d.tokenNum(flag)
362+
363+ case rInflating:
364+ n, err := d.inflate.Read(d.out)
365+ if n > 0 {
366+ d.history = appendHistory(d.history, d.out[:n], 31)
367+ return int32(n), d.out[:n], nil
368+ }
369+ if err == nil {
370+ continue
371+ }
372+ if d.src.err != nil {
373+ return 0, nil, d.src.err
374+ }
375+ if !errors.Is(err, io.ErrUnexpectedEOF) || !d.src.done() {
376+ return 0, nil, fmt.Errorf("rsync: inflate failed: %w", err)
377+ }
378+ d.savedFlag, d.hasSaved = d.src.next, true
379+ d.state = rIdle
380+
381+ case rRunning:
382+ if d.rxRun <= 0 || d.rxToken >= maxTokenIndex {
383+ return 0, nil, errors.New("rsync: invalid token number in compressed stream")
384+ }
385+ d.rxToken++
386+ d.rxRun--
387+ if d.rxRun == 0 {
388+ d.state = rIdle
389+ }
390+ return -1 - d.rxToken, nil, nil
391+ }
392+ }
393+}
394+
395+func (d *deflateTokenReceiver) tokenNum(flag int) (int32, []byte, error) {
396+ invalid := errors.New("rsync: invalid token number in compressed stream")
397+ if flag&tokRel != 0 {
398+ incr := int32(flag & 0x3f)
399+ if d.rxToken > maxTokenIndex-incr {
400+ return 0, nil, invalid
401+ }
402+ d.rxToken += incr
403+ flag >>= 6
404+ } else {
405+ v, err := d.c.readInt32()
406+ if err != nil {
407+ return 0, nil, err
408+ }
409+ if v < 0 || v > maxTokenIndex {
410+ return 0, nil, invalid
411+ }
412+ d.rxToken = v
413+ }
414+ if flag&1 != 0 {
415+ lo, err := d.c.readByte()
416+ if err != nil {
417+ return 0, nil, err
418+ }
419+ hi, err := d.c.readByte()
420+ if err != nil {
421+ return 0, nil, err
422+ }
423+ d.rxRun = int32(lo) + int32(hi)<<8
424+ if d.rxRun <= 0 || d.rxToken > maxTokenIndex-d.rxRun {
425+ return 0, nil, invalid
426+ }
427+ d.state = rRunning
428+ }
429+ return -1 - d.rxToken, nil, nil
430+}
431+
432+func (d *deflateTokenReceiver) see(data []byte) {
433+ if d.zlib {
434+ d.history = appendHistory(d.history, data, d.c.protocol)
435+ }
436+}
437+
438+func (d *deflateTokenReceiver) window() []byte {
439+ if len(d.history) > deflateWindow {
440+ return d.history[len(d.history)-deflateWindow:]
441+ }
442+ return d.history
443+}
444+
445+// segmentSource feeds one segment of DEFLATED_DATA chunks to the inflater,
446+// reading further chunks from the connection on demand.
447+type segmentSource struct {
448+ c *conn
449+ buf []byte
450+ pos int
451+ marker int // bytes of the sync marker handed out, -1 before the segment ends
452+ next int // flag byte that ended the segment
453+ err error
454+ scratch [maxDataCount]byte
455+}
456+
457+func (s *segmentSource) start(c *conn, flag int) error {
458+ s.c = c
459+ s.marker = -1
460+ s.err = nil
461+ return s.readChunk(flag)
462+}
463+
464+func (s *segmentSource) readChunk(flag int) error {
465+ lo, err := s.c.readByte()
466+ if err != nil {
467+ return err
468+ }
469+ n := (flag&0x3f)<<8 + int(lo)
470+ if err := s.c.readFull(s.scratch[:n]); err != nil {
471+ return err
472+ }
473+ s.buf = s.scratch[:n]
474+ s.pos = 0
475+ return nil
476+}
477+
478+func (s *segmentSource) done() bool { return s.marker == len(syncMarker) }
479+
480+func (s *segmentSource) ReadByte() (byte, error) {
481+ for {
482+ if s.marker >= 0 {
483+ if s.marker == len(syncMarker) {
484+ return 0, io.EOF
485+ }
486+ b := syncMarker[s.marker]
487+ s.marker++
488+ return b, nil
489+ }
490+ if s.pos < len(s.buf) {
491+ b := s.buf[s.pos]
492+ s.pos++
493+ return b, nil
494+ }
495+ b, err := s.c.readByte()
496+ if err != nil {
497+ s.err = err
498+ return 0, err
499+ }
500+ if int(b)&0xC0 == tokDeflatedData {
501+ if err := s.readChunk(int(b)); err != nil {
502+ s.err = err
503+ return 0, err
504+ }
505+ continue
506+ }
507+ s.next = int(b)
508+ s.marker = 0
509+ }
510+}
511+
512+func (s *segmentSource) Read(p []byte) (int, error) {
513+ n := 0
514+ for n < len(p) {
515+ if s.marker < 0 && s.pos < len(s.buf) {
516+ m := copy(p[n:], s.buf[s.pos:])
517+ s.pos += m
518+ n += m
519+ continue
520+ }
521+ if n > 0 {
522+ return n, nil
523+ }
524+ b, err := s.ReadByte()
525+ if err != nil {
526+ return n, err
527+ }
528+ p[n] = b
529+ n++
530+ }
531+ return n, nil
532+}
+309, -0
......@@ -0,0 +1,309 @@
1+package rsync
2+
3+import (
4+ "bytes"
5+ "io"
6+ "slices"
7+ "testing"
8+)
9+
10+type loopback struct {
11+ bytes.Buffer
12+}
13+
14+func newTestConn(protocol int) (*conn, *loopback) {
15+ var buf loopback
16+ c := newConn(&buf)
17+ c.protocol = protocol
18+ return c, &buf
19+}
20+
21+func TestVarintRoundTrip(t *testing.T) {
22+ values := []int32{0, 1, 0x7f, 0x80, 0xff, 0x3fff, 0x4000, 0x1fffff, 0x200000, 0x0fffffff, 0x10000000, 0x7fffffff, -1, -2, -0x80000000}
23+ for _, v := range values {
24+ c, _ := newTestConn(31)
25+ if err := c.writeVarint(v); err != nil {
26+ t.Fatal(err)
27+ }
28+ if err := c.flush(); err != nil {
29+ t.Fatal(err)
30+ }
31+ got, err := c.readVarint()
32+ if err != nil || got != v {
33+ t.Errorf("varint %d: got %d, %v", v, got, err)
34+ }
35+ }
36+ if got := appendVarint(nil, 0x80); !bytes.Equal(got, []byte{0x80, 0x80}) {
37+ t.Errorf("varint 0x80 encodes as % x", got)
38+ }
39+}
40+
41+func TestVarlongRoundTrip(t *testing.T) {
42+ values := []int64{0, 1, 0xffffff, 0x1000000, 0x7fffffff, 0x80000000, 1 << 40, 1<<62 + 12345, -1}
43+ for _, min := range []int{3, 4} {
44+ for _, v := range values {
45+ c, _ := newTestConn(31)
46+ if err := c.writeVarlong(v, min); err != nil {
47+ t.Fatal(err)
48+ }
49+ if err := c.flush(); err != nil {
50+ t.Fatal(err)
51+ }
52+ got, err := c.readVarlong(min)
53+ if err != nil || got != v {
54+ t.Errorf("varlong(%d) %d: got %d, %v", min, v, got, err)
55+ }
56+ }
57+ }
58+}
59+
60+func TestLongintRoundTrip(t *testing.T) {
61+ for _, v := range []int64{0, 0x7fffffff, 0x80000000, 1 << 40, -5} {
62+ c, _ := newTestConn(29)
63+ if err := c.writeLongint(v); err != nil {
64+ t.Fatal(err)
65+ }
66+ if err := c.flush(); err != nil {
67+ t.Fatal(err)
68+ }
69+ got, err := c.readLongint()
70+ if err != nil || got != v {
71+ t.Errorf("longint %d: got %d, %v", v, got, err)
72+ }
73+ }
74+}
75+
76+func TestNdxRoundTrip(t *testing.T) {
77+ seq := []int32{0, 1, 2, 5, 300, 299, 40000, 70000, 3, ndxDone, ndxDelStats, ndxFlistEOF, 7, ndxDone}
78+ for _, proto := range []int{29, 30} {
79+ c, _ := newTestConn(proto)
80+ for _, v := range seq {
81+ if err := c.writeNdx(v); err != nil {
82+ t.Fatal(err)
83+ }
84+ }
85+ if err := c.flush(); err != nil {
86+ t.Fatal(err)
87+ }
88+ for _, v := range seq {
89+ got, err := c.readNdx()
90+ if err != nil || got != v {
91+ t.Fatalf("protocol %d: ndx %d: got %d, %v", proto, v, got, err)
92+ }
93+ }
94+ }
95+}
96+
97+func TestMultiplexedMessages(t *testing.T) {
98+ c, buf := newTestConn(31)
99+ if err := c.w.setMultiplex(true); err != nil {
100+ t.Fatal(err)
101+ }
102+ _ = c.writeInt32(42)
103+ _ = c.writeMsg(msgInfo, []byte("hello\n"))
104+ _ = c.writeInt32(43)
105+ if err := c.flush(); err != nil {
106+ t.Fatal(err)
107+ }
108+
109+ r, _ := newTestConn(31)
110+ r.r.r.Reset(bytes.NewReader(buf.Bytes()))
111+ r.r.multiplex = true
112+ var msgs []string
113+ r.r.onMsg = func(tag byte, data []byte) error {
114+ msgs = append(msgs, string(data))
115+ return nil
116+ }
117+ a, err := r.readInt32()
118+ if err != nil {
119+ t.Fatal(err)
120+ }
121+ b, err := r.readInt32()
122+ if err != nil {
123+ t.Fatal(err)
124+ }
125+ if a != 42 || b != 43 || !slices.Equal(msgs, []string{"hello\n"}) {
126+ t.Fatalf("got %d %d %q", a, b, msgs)
127+ }
128+ if _, err := r.readByte(); err != io.ErrUnexpectedEOF {
129+ t.Fatalf("want EOF, got %v", err)
130+ }
131+}
132+
133+func TestChecksum1(t *testing.T) {
134+ // Bytes above 0x7f count as negative, as in rsync's C code.
135+ if got := checksum1([]byte{0xff, 1, 2, 3, 4, 5}); got != 0x001d000e {
136+ t.Errorf("checksum1 = %#x", got)
137+ }
138+ if got := checksum1(nil); got != 0 {
139+ t.Errorf("checksum1(nil) = %#x", got)
140+ }
141+}
142+
143+func TestSumSizes(t *testing.T) {
144+ tests := []struct {
145+ length int64
146+ proto int
147+ want sumHead
148+ }{
149+ {0, 31, sumHead{0, 700, 2, 0}},
150+ {1, 31, sumHead{1, 700, 2, 1}},
151+ {1_000_000, 31, sumHead{1000, 1000, 2, 0}},
152+ {100 << 20, 31, sumHead{10240, 10240, 3, 0}},
153+ {100<<20 + 1, 29, sumHead{10241, 10240, 3, 1}},
154+ }
155+ for _, tt := range tests {
156+ got, err := sumSizesSqroot(tt.length, tt.proto, 0, shortSumLength, 16)
157+ if err != nil || got != tt.want {
158+ t.Errorf("sumSizes(%d, %d) = %+v, %v; want %+v", tt.length, tt.proto, got, err, tt.want)
159+ }
160+ }
161+ got, _ := sumSizesSqroot(1_000_000, 31, 0, sumLength, 8)
162+ if got.s2length != 8 {
163+ t.Errorf("full checksum length capped at digest size: got %d", got.s2length)
164+ }
165+}
166+
167+func TestFileNameOrder(t *testing.T) {
168+ mk := func(name string, dir bool) *fileEntry {
169+ f := newFileEntry(name)
170+ if name == "." {
171+ f.dirname, f.basename = "", "."
172+ }
173+ f.mode = sIFREG
174+ if dir {
175+ f.mode = sIFDIR
176+ }
177+ return f
178+ }
179+ entries := func() []*fileEntry {
180+ return []*fileEntry{
181+ mk("b.txt", false), mk("a/x", false), mk("a", true), mk(".", true),
182+ mk("a.b", false), mk("a-b", false), mk("a/y/z", false), mk("a/y", true), mk("a/w", false),
183+ }
184+ }
185+ names := func(fs []*fileEntry) []string {
186+ var out []string
187+ for _, f := range fs {
188+ out = append(out, f.name())
189+ }
190+ return out
191+ }
192+
193+ modern := entries()
194+ sortFileList(modern, 31, false)
195+ want := []string{".", "a-b", "a.b", "b.txt", "a", "a/w", "a/x", "a/y", "a/y/z"}
196+ if got := names(modern); !slices.Equal(got, want) {
197+ t.Errorf("protocol 31 order:\n got %q\nwant %q", got, want)
198+ }
199+
200+ old := entries()
201+ sortFileList(old, 28, false)
202+ want = []string{".", "a", "a-b", "a.b", "a/w", "a/x", "a/y", "a/y/z", "b.txt"}
203+ if got := names(old); !slices.Equal(got, want) {
204+ t.Errorf("protocol 28 order:\n got %q\nwant %q", got, want)
205+ }
206+}
207+
208+func TestDuplicateNames(t *testing.T) {
209+ a := newFileEntry("x")
210+ a.mode = sIFREG
211+ b := newFileEntry("x")
212+ b.mode = sIFDIR
213+ c := newFileEntry("x")
214+ c.mode = sIFREG
215+ files := []*fileEntry{a, b, c}
216+ sortFileList(files, 31, true)
217+ var active []*fileEntry
218+ for _, f := range files {
219+ if !f.inactive {
220+ active = append(active, f)
221+ }
222+ }
223+ if len(active) != 1 || !active[0].isDir() {
224+ t.Fatalf("want only the directory kept, got %d entries", len(active))
225+ }
226+}
227+
228+func TestWildmatch(t *testing.T) {
229+ tests := []struct {
230+ pattern, text string
231+ want bool
232+ }{
233+ {"foo", "foo", true},
234+ {"foo", "bar", false},
235+ {"???", "foo", true},
236+ {"??", "foo", false},
237+ {"*", "foo", true},
238+ {"f*", "foo", true},
239+ {"*f", "foo", false},
240+ {"*ob*a*r*", "foobar", true},
241+ {"*ab", "aaaaaaabababab", true},
242+ {`foo\*`, "foo*", true},
243+ {`foo\*bar`, "foobar", false},
244+ {"*[al]?", "ball", true},
245+ {"[ten]", "ten", false},
246+ {"**[!te]", "ten", true},
247+ {"**[!ten]", "ten", false},
248+ {"t[a-g]n", "ten", true},
249+ {"t[!a-g]n", "ten", false},
250+ {"t[^a-g]n", "ton", true},
251+ {"a[]]b", "a]b", true},
252+ {"[[:digit:]]x", "7x", true},
253+ {"[[:alpha:]]x", "7x", false},
254+ {"[[:bogus:]]", "a", false},
255+ {"*", "a/b", false},
256+ {"**", "a/b", true},
257+ {"a/*", "a/b/c", false},
258+ {"a/**", "a/b/c", true},
259+ {"**/c", "a/b/c", true},
260+ {"a?b", "a/b", false},
261+ {"[a/]b", "/b", false},
262+ }
263+ for _, tt := range tests {
264+ if got := wildmatch(tt.pattern, tt.text); got != tt.want {
265+ t.Errorf("wildmatch(%q, %q) = %v, want %v", tt.pattern, tt.text, got, tt.want)
266+ }
267+ }
268+}
269+
270+func TestFilterRules(t *testing.T) {
271+ var l filterList
272+ for _, rule := range []string{"+ keep.log", "- *.log", "- /build/", "- node_modules/", "- docs/*.tmp", "-! *.html"} {
273+ if err := l.parse(rule, false); err != nil {
274+ t.Fatalf("parse %q: %v", rule, err)
275+ }
276+ }
277+ tests := []struct {
278+ name string
279+ isDir bool
280+ want int
281+ }{
282+ {"keep.log", false, 1},
283+ {"sub/keep.log", false, 1},
284+ {"x.log", false, -1},
285+ {"sub/x.log", false, -1},
286+ {"build", true, -1},
287+ {"build", false, -1}, // "-! *.html" excludes anything not ending in .html
288+ {"sub/build", true, -1},
289+ {"src/node_modules", true, -1},
290+ {"docs/a.tmp", false, -1},
291+ {"index.html", false, 0},
292+ }
293+ for _, tt := range tests {
294+ if got := l.check(tt.name, tt.isDir); got != tt.want {
295+ t.Errorf("check(%q, dir=%v) = %d, want %d", tt.name, tt.isDir, got, tt.want)
296+ }
297+ }
298+
299+ var old filterList
300+ _ = old.parse("*.o", true)
301+ _ = old.parse("+ keep.o", true)
302+ if old.check("x.o", false) != -1 {
303+ t.Error("protocol 28 bare pattern is an exclude")
304+ }
305+ _ = old.parse("!", true)
306+ if len(old.rules) != 0 {
307+ t.Error("! clears the list")
308+ }
309+}
+89, -192
......@@ -3,175 +3,112 @@ package rsync
33 import (
44 "errors"
55 "fmt"
6+ "io"
67 "io/fs"
78 "os"
89 "path"
9- "slices"
1010 "strings"
1111
1212 "github.com/picosh/pico/pkg/pssh"
13- "github.com/picosh/pico/pkg/rsync-receiver/rsyncopts"
14- "github.com/picosh/pico/pkg/rsync-receiver/rsyncreceiver"
15- "github.com/picosh/pico/pkg/rsync-receiver/rsyncsender"
16- rsyncutils "github.com/picosh/pico/pkg/rsync-receiver/utils"
13+ "github.com/picosh/pico/pkg/rsync"
1714 "github.com/picosh/pico/pkg/send/utils"
1815 )
1916
20-type handler struct {
21- session *pssh.SSHServerConnSession
22- writeHandler utils.CopyFromClientHandler
23- root string
24- recursive bool
25- ignoreTimes bool
17+// keepDirName marks an otherwise empty directory in object storage.
18+const keepDirName = "._pico_keep_dir"
19+
20+// storageFS exposes a CopyFromClientHandler as an rsync.FS. Handlers take
21+// absolute paths and differ in how they name listing results, so this
22+// normalizes both directions.
23+type storageFS struct {
24+ session *pssh.SSHServerConnSession
25+ handler utils.CopyFromClientHandler
2626 }
2727
28-func (h *handler) List(rPath string) ([]fs.FileInfo, error) {
29- isDir := false
30- if rPath == "." {
31- rPath = "/"
32- isDir = true
33- }
28+func abs(name string) string { return "/" + strings.TrimPrefix(name, "/") }
3429
35- list, err := h.writeHandler.List(h.session, rPath, isDir, h.recursive)
36- if err != nil {
37- return nil, err
30+// isSelf reports whether a listing entry stands for the listed path itself
31+// rather than something below it.
32+func isSelf(info os.FileInfo) bool {
33+ switch info.Name() {
34+ case "", ".", "/":
35+ return info.IsDir()
3836 }
37+ return false
38+}
3939
40- var dirs []string
41-
42- var newList []fs.FileInfo
43-
44- for _, f := range list {
45- if !f.IsDir() && f.Size() == 0 {
46- continue
47- }
48-
49- fname := f.Name()
50- if strings.HasPrefix(f.Name(), "/") {
51- fname = path.Join(rPath, f.Name())
52- }
53-
54- if fname == "" && !f.IsDir() {
55- fname = path.Base(rPath)
56- }
57-
58- newFile := &utils.VirtualFile{
59- FName: fname,
60- FIsDir: f.IsDir(),
61- FSize: f.Size(),
62- FModTime: f.ModTime(),
63- FSys: f.Sys(),
64- }
65-
66- newList = append(newList, newFile)
67-
68- parts := strings.Split(newFile.Name(), string(os.PathSeparator))
69- lastDir := newFile.Name()
70- for i := 0; i < len(parts); i++ {
71- lastDir, _ = path.Split(lastDir)
72- if lastDir == "" {
73- continue
74- }
75-
76- lastDir = lastDir[:len(lastDir)-1]
77- dirs = append(dirs, lastDir)
78- }
40+func (s *storageFS) Stat(name string) (rsync.FileInfo, error) {
41+ if name == "" {
42+ return rsync.FileInfo{IsDir: true}, nil
7943 }
80-
81- for _, dir := range dirs {
82- newList = append(newList, &utils.VirtualFile{
83- FName: dir,
84- FIsDir: true,
85- })
44+ entries, err := s.handler.List(s.session, abs(name), false, false)
45+ if err != nil {
46+ return rsync.FileInfo{}, err
8647 }
87-
88- slices.Reverse(newList)
89-
90- onlyEmpty := true
91- for _, f := range newList {
92- if f.Name() != "" {
93- onlyEmpty = false
48+ for _, e := range entries {
49+ if isSelf(e) {
50+ return rsync.FileInfo{Name: name, ModTime: e.ModTime(), IsDir: true}, nil
9451 }
9552 }
96-
97- if len(newList) == 0 || onlyEmpty {
98- return nil, errors.New("no files to send, the directory may not exist or could be empty")
99- }
100-
101- return newList, nil
102-}
103-
104-func (h *handler) Read(file *rsyncutils.SenderFile) (os.FileInfo, rsyncutils.ReaderAtCloser, error) {
105- filePath := file.WPath
106-
107- if strings.HasSuffix(h.root, file.WPath) {
108- filePath = h.root
109- } else if !strings.HasPrefix(filePath, h.root) {
110- filePath = path.Join(h.root, file.Path, file.WPath)
53+ switch {
54+ case len(entries) == 0:
55+ return rsync.FileInfo{}, fmt.Errorf("%s: %w", name, fs.ErrNotExist)
56+ case len(entries) == 1 && !entries[0].IsDir() && path.Base(entries[0].Name()) == path.Base(name):
57+ e := entries[0]
58+ return rsync.FileInfo{Name: name, Size: e.Size(), ModTime: e.ModTime()}, nil
59+ default:
60+ // The handler listed the contents of a directory.
61+ return rsync.FileInfo{Name: name, IsDir: true}, nil
11162 }
112-
113- return h.writeHandler.Read(h.session, &utils.FileEntry{Filepath: filePath})
11463 }
11564
116-func (h *handler) Put(file *rsyncutils.ReceiverFile) (int64, error) {
117- fileEntry := &utils.FileEntry{
118- Filepath: path.Join("/", h.root, file.Name),
119- Mode: fs.FileMode(0600),
120- Size: file.Length,
121- Mtime: file.ModTime.Unix(),
122- Atime: file.ModTime.Unix(),
123- }
124- fileEntry.Reader = file.Reader
125-
126- msg, err := h.writeHandler.Write(h.session, fileEntry)
65+func (s *storageFS) ReadDir(dir string, recursive bool) ([]rsync.FileInfo, error) {
66+ entries, err := s.handler.List(s.session, abs(dir), true, recursive)
12767 if err != nil {
128- errMsg := fmt.Sprintf("%s\r\n", err.Error())
129- _, err = h.session.Stderr().Write([]byte(errMsg))
68+ return nil, err
13069 }
131- if msg != "" {
132- nMsg := fmt.Sprintf("%s\r\n", msg)
133- _, err = h.session.Stderr().Write([]byte(nMsg))
70+ out := make([]rsync.FileInfo, 0, len(entries))
71+ for _, e := range entries {
72+ if isSelf(e) {
73+ continue
74+ }
75+ name := strings.Trim(e.Name(), "/")
76+ if name == "" || path.Base(name) == keepDirName {
77+ continue
78+ }
79+ out = append(out, rsync.FileInfo{Name: name, Size: e.Size(), ModTime: e.ModTime(), IsDir: e.IsDir()})
13480 }
135- return 0, err
81+ return out, nil
13682 }
13783
138-func (h *handler) Remove(willReceive []*rsyncutils.ReceiverFile) error {
139- entries, err := h.writeHandler.List(h.session, path.Join("/", h.root), true, true)
84+func (s *storageFS) Open(name string) (rsync.File, rsync.FileInfo, error) {
85+ info, r, err := s.handler.Read(s.session, &utils.FileEntry{Filepath: abs(name)})
14086 if err != nil {
141- return err
87+ return nil, rsync.FileInfo{}, err
14288 }
143-
144- var toDelete []string
145-
146- for _, entry := range entries {
147- exists := slices.ContainsFunc(willReceive, func(rf *rsyncutils.ReceiverFile) bool {
148- return rf.Name == entry.Name()
149- })
150-
151- if !exists && path.Base(entry.Name()) != "._pico_keep_dir" {
152- toDelete = append(toDelete, entry.Name())
153- }
89+ if r == nil {
90+ return nil, rsync.FileInfo{}, fmt.Errorf("%s: %w", name, fs.ErrNotExist)
15491 }
92+ return r, rsync.FileInfo{Name: name, Size: info.Size(), ModTime: info.ModTime()}, nil
93+}
15594
156- // Sort by path depth descending so children are deleted before parents.
157- // This ensures directories are empty before we try to remove them.
158- slices.SortFunc(toDelete, func(a, b string) int {
159- depthA := strings.Count(a, "/")
160- depthB := strings.Count(b, "/")
161- return depthB - depthA
95+func (s *storageFS) Put(name string, info rsync.FileInfo, r io.Reader) (string, error) {
96+ return s.handler.Write(s.session, &utils.FileEntry{
97+ Filepath: abs(name),
98+ Mode: fs.FileMode(0600),
99+ Size: info.Size,
100+ Mtime: info.ModTime.Unix(),
101+ Atime: info.ModTime.Unix(),
102+ Reader: r,
162103 })
104+}
163105
164- var errs []error
165-
166- for _, file := range toDelete {
167- errs = append(errs, h.writeHandler.Delete(h.session, &utils.FileEntry{Filepath: path.Join("/", h.root, file)}))
168- _, err = fmt.Fprintf(h.session.Stderr(), "deleting %s\r\n", file)
169- errs = append(errs, err)
170- }
171-
172- return errors.Join(errs...)
106+func (s *storageFS) Remove(name string, isDir bool) error {
107+ return s.handler.Delete(s.session, &utils.FileEntry{Filepath: abs(name)})
173108 }
174109
110+// Middleware serves rsync clients ("rsync --server ...") from the handler's
111+// storage.
175112 func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
176113 return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
177114 return func(session *pssh.SSHServerConnSession) error {
......@@ -192,66 +129,26 @@ func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddlewa
192129 }
193130 }()
194131
195- cmdFlags := session.Command()
196- flgs := cmdFlags[1:]
197- for idx, f := range flgs {
198- // openrsync sends "delete-before" when the client provided "delete"
199- flgs[idx] = strings.ReplaceAll(f, "delete-before", "delete")
132+ err := rsync.Serve(rsync.Config{
133+ FS: &storageFS{session: session, handler: writeHandler},
134+ Logger: logger,
135+ Stderr: session.Stderr(),
136+ }, session, cmd[1:])
137+ if err == nil {
138+ return nil
200139 }
201140
202- optsCtx, err := rsyncopts.ParseArguments(cmdFlags[1:], true)
203- if err != nil {
204- _, _ = fmt.Fprintf(session.Stderr(), "ERROR: error parsing rsync arguments: %s\r\n", err.Error())
205- return err
141+ // The client has already been told what went wrong; what is
142+ // left is to hand it rsync's exit code.
143+ code := 1
144+ var exit *rsync.ExitError
145+ if errors.As(err, &exit) {
146+ code = exit.Code
206147 }
207-
208- if optsCtx.Options.Compress() {
209- err := fmt.Errorf("compression is currently unsupported")
210- _, _ = fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
211- return err
212- }
213-
214- if optsCtx.Options.AlwaysChecksum() {
215- err := fmt.Errorf("checksum is currently unsupported")
216- _, _ = fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
217- return err
218- }
219-
220- if len(optsCtx.RemainingArgs) != 2 {
221- err := fmt.Errorf("missing source and destination arguments")
222- _, _ = fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
223- return err
224- }
225-
226- root := strings.TrimPrefix(optsCtx.RemainingArgs[len(optsCtx.RemainingArgs)-1], "/")
227- if root == "" {
228- root = "/"
229- }
230-
231- fileHandler := &handler{
232- session: session,
233- writeHandler: writeHandler,
234- root: root,
235- recursive: optsCtx.Options.Recurse(),
236- ignoreTimes: !optsCtx.Options.PreserveMTimes(),
237- }
238-
239- for _, arg := range cmd {
240- if arg == "--sender" {
241- err := rsyncsender.ClientRun(logger, optsCtx.Options, session, fileHandler, []string{fileHandler.root}, true)
242- if err != nil {
243- logger.Error("error running rsync sender", "err", err)
244- }
245- return err
246- }
247- }
248-
249- err = rsyncreceiver.ClientRun(logger, optsCtx.Options, session, fileHandler, []string{fileHandler.root}, true)
250- if err != nil {
251- logger.Error("error running rsync receiver", "err", err)
252- }
253-
254- return err
148+ logger.Error("rsync transfer failed", "err", err, "code", code)
149+ _ = session.Exit(code)
150+ _ = session.Close()
151+ return nil
255152 }
256153 }
257154 }
+103, -129
......@@ -3,15 +3,18 @@ package rsync
33 import (
44 "bytes"
55 "context"
6+ "errors"
67 "io"
78 "io/fs"
89 "log/slog"
910 "os"
11+ "slices"
12+ "strings"
1013 "testing"
1114 "time"
1215
1316 "github.com/picosh/pico/pkg/pssh"
14- rsyncutils "github.com/picosh/pico/pkg/rsync-receiver/utils"
17+ "github.com/picosh/pico/pkg/rsync"
1518 "github.com/picosh/pico/pkg/send/utils"
1619 "golang.org/x/crypto/ssh"
1720 )
......@@ -20,46 +23,62 @@ import (
2023 type mockFileInfo struct {
2124 name string
2225 size int64
23- mode fs.FileMode
2426 modTime time.Time
2527 isDir bool
2628 }
2729
28-func (m *mockFileInfo) Name() string { return m.name }
29-func (m *mockFileInfo) Size() int64 { return m.size }
30-func (m *mockFileInfo) Mode() fs.FileMode { return m.mode }
30+func (m *mockFileInfo) Name() string { return m.name }
31+func (m *mockFileInfo) Size() int64 { return m.size }
32+func (m *mockFileInfo) Mode() fs.FileMode {
33+ if m.isDir {
34+ return fs.ModeDir | 0o755
35+ }
36+ return 0o644
37+}
3138 func (m *mockFileInfo) ModTime() time.Time { return m.modTime }
3239 func (m *mockFileInfo) IsDir() bool { return m.isDir }
3340 func (m *mockFileInfo) Sys() any { return nil }
3441
35-// mockWriteHandler implements utils.CopyFromClientHandler for testing.
36-// It records the order of Delete calls to verify deletion order.
42+// mockWriteHandler implements utils.CopyFromClientHandler and records the
43+// paths it was called with.
3744 type mockWriteHandler struct {
38- entries []os.FileInfo
39- deleteCalls []string
40- deleteErrors map[string]error
45+ lists map[string][]os.FileInfo
46+ files map[string]string
47+ calls []string
48+ written map[string]string
4149 }
4250
4351 func (m *mockWriteHandler) Delete(_ *pssh.SSHServerConnSession, entry *utils.FileEntry) error {
44- m.deleteCalls = append(m.deleteCalls, entry.Filepath)
45- if m.deleteErrors != nil {
46- if err, ok := m.deleteErrors[entry.Filepath]; ok {
47- return err
48- }
49- }
52+ m.calls = append(m.calls, "delete "+entry.Filepath)
5053 return nil
5154 }
5255
53-func (m *mockWriteHandler) Write(_ *pssh.SSHServerConnSession, _ *utils.FileEntry) (string, error) {
56+func (m *mockWriteHandler) Write(_ *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error) {
57+ m.calls = append(m.calls, "write "+entry.Filepath)
58+ b, err := io.ReadAll(entry.Reader)
59+ if err != nil {
60+ return "", err
61+ }
62+ if m.written == nil {
63+ m.written = map[string]string{}
64+ }
65+ m.written[entry.Filepath] = string(b)
5466 return "", nil
5567 }
5668
57-func (m *mockWriteHandler) Read(_ *pssh.SSHServerConnSession, _ *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error) {
58- return nil, nil, nil
69+func (m *mockWriteHandler) Read(_ *pssh.SSHServerConnSession, entry *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error) {
70+ m.calls = append(m.calls, "read "+entry.Filepath)
71+ data, ok := m.files[entry.Filepath]
72+ if !ok {
73+ return nil, nil, os.ErrNotExist
74+ }
75+ info := &mockFileInfo{name: entry.Filepath, size: int64(len(data))}
76+ return info, utils.NopReadAndReaderAtCloser(strings.NewReader(data)), nil
5977 }
6078
61-func (m *mockWriteHandler) List(_ *pssh.SSHServerConnSession, _ string, _ bool, _ bool) ([]os.FileInfo, error) {
62- return m.entries, nil
79+func (m *mockWriteHandler) List(_ *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
80+ m.calls = append(m.calls, "list "+fpath)
81+ return m.lists[fpath], nil
6382 }
6483
6584 func (m *mockWriteHandler) GetLogger(_ *pssh.SSHServerConnSession) *slog.Logger {
......@@ -87,9 +106,8 @@ func (m *mockChannel) Stderr() io.ReadWriter { return m.stderr }
87106 var _ ssh.Channel = (*mockChannel)(nil)
88107
89108 // newMockSession creates a mock SSHServerConnSession for testing.
90-func newMockSession() (*pssh.SSHServerConnSession, *bytes.Buffer) {
91- stderr := &bytes.Buffer{}
92- channel := &mockChannel{stderr: stderr}
109+func newMockSession() *pssh.SSHServerConnSession {
110+ channel := &mockChannel{stderr: &bytes.Buffer{}}
93111
94112 ctx, cancel := context.WithCancel(context.Background())
95113 logger := slog.Default()
......@@ -100,138 +118,94 @@ func newMockSession() (*pssh.SSHServerConnSession, *bytes.Buffer) {
100118 },
101119 }, server)
102120
103- session := &pssh.SSHServerConnSession{
121+ return &pssh.SSHServerConnSession{
104122 Channel: channel,
105123 SSHServerConn: serverConn,
106124 Ctx: ctx,
107125 CancelFunc: cancel,
108126 }
109-
110- return session, stderr
111127 }
112128
113-func TestRemove_DeletesChildrenBeforeParents(t *testing.T) {
114- session, _ := newMockSession()
115- mockHandler := &mockWriteHandler{
116- entries: []os.FileInfo{
117- &mockFileInfo{name: "a", isDir: true},
118- &mockFileInfo{name: "a/file.txt", size: 100},
119- &mockFileInfo{name: "b/c", isDir: true},
120- &mockFileInfo{name: "b/c/deep.txt", size: 50},
121- &mockFileInfo{name: "b", isDir: true},
129+func TestReadDirNormalizesListing(t *testing.T) {
130+ handler := &mockWriteHandler{lists: map[string][]os.FileInfo{
131+ "/site": {
132+ &mockFileInfo{name: "", isDir: true},
133+ &mockFileInfo{name: "index.html", size: 10},
134+ &mockFileInfo{name: "/css/site.css", size: 5},
135+ &mockFileInfo{name: "css", isDir: true},
136+ &mockFileInfo{name: "empty/._pico_keep_dir"},
137+ &mockFileInfo{name: "empty.txt"},
122138 },
123- }
139+ }}
140+ fsys := &storageFS{session: newMockSession(), handler: handler}
124141
125- h := &handler{
126- session: session,
127- writeHandler: mockHandler,
128- root: "test",
129- }
130-
131- err := h.Remove([]*rsyncutils.ReceiverFile{})
142+ entries, err := fsys.ReadDir("site", true)
132143 if err != nil {
133- t.Fatalf("Remove() returned error: %v", err)
144+ t.Fatal(err)
134145 }
135-
136- if len(mockHandler.deleteCalls) != 5 {
137- t.Fatalf("expected 5 delete calls, got %d: %v", len(mockHandler.deleteCalls), mockHandler.deleteCalls)
146+ var names []string
147+ for _, e := range entries {
148+ names = append(names, e.Name)
149+ }
150+ want := []string{"index.html", "css/site.css", "css", "empty.txt"}
151+ if !slices.Equal(names, want) {
152+ t.Errorf("got %q, want %q", names, want)
138153 }
154+}
139155
140- indexOfA := -1
141- indexOfAFile := -1
142- indexOfB := -1
143- indexOfBC := -1
144- indexOfBCDeep := -1
156+func TestStat(t *testing.T) {
157+ handler := &mockWriteHandler{lists: map[string][]os.FileInfo{
158+ "/site": {&mockFileInfo{name: "", isDir: true}},
159+ "/site/index.html": {&mockFileInfo{name: "index.html", size: 42}},
160+ "/": {&mockFileInfo{name: "/", isDir: true}, &mockFileInfo{name: "hello.md"}},
161+ "/posts": {&mockFileInfo{name: "a.md"}, &mockFileInfo{name: "b.md"}},
162+ }}
163+ fsys := &storageFS{session: newMockSession(), handler: handler}
145164
146- for i, path := range mockHandler.deleteCalls {
147- switch path {
148- case "/test/a":
149- indexOfA = i
150- case "/test/a/file.txt":
151- indexOfAFile = i
152- case "/test/b":
153- indexOfB = i
154- case "/test/b/c":
155- indexOfBC = i
156- case "/test/b/c/deep.txt":
157- indexOfBCDeep = i
158- }
165+ if info, err := fsys.Stat("site"); err != nil || !info.IsDir {
166+ t.Errorf("site: %+v, %v", info, err)
159167 }
160-
161- if indexOfAFile > indexOfA {
162- t.Errorf("a/file.txt (index %d) should be deleted before a (index %d)", indexOfAFile, indexOfA)
168+ if info, err := fsys.Stat("site/index.html"); err != nil || info.IsDir || info.Size != 42 {
169+ t.Errorf("site/index.html: %+v, %v", info, err)
163170 }
164- if indexOfBCDeep > indexOfBC {
165- t.Errorf("b/c/deep.txt (index %d) should be deleted before b/c (index %d)", indexOfBCDeep, indexOfBC)
171+ if info, err := fsys.Stat(""); err != nil || !info.IsDir {
172+ t.Errorf("root: %+v, %v", info, err)
166173 }
167- if indexOfBC > indexOfB {
168- t.Errorf("b/c (index %d) should be deleted before b (index %d)", indexOfBC, indexOfB)
174+ if info, err := fsys.Stat("posts"); err != nil || !info.IsDir {
175+ t.Errorf("posts: %+v, %v", info, err)
169176 }
170-}
171-
172-func TestRemove_IgnoresPicoKeepDir(t *testing.T) {
173- session, _ := newMockSession()
174- mockHandler := &mockWriteHandler{
175- entries: []os.FileInfo{
176- &mockFileInfo{name: "dir", isDir: true},
177- &mockFileInfo{name: "dir/._pico_keep_dir", size: 0},
178- &mockFileInfo{name: "dir/file.txt", size: 100},
179- },
177+ if _, err := fsys.Stat("nope"); !errors.Is(err, fs.ErrNotExist) {
178+ t.Errorf("nope: %v", err)
180179 }
180+}
181181
182- h := &handler{
183- session: session,
184- writeHandler: mockHandler,
185- root: "test",
186- }
182+func TestHandlerPathsAreAbsolute(t *testing.T) {
183+ handler := &mockWriteHandler{files: map[string]string{"/site/a.txt": "hello"}}
184+ fsys := &storageFS{session: newMockSession(), handler: handler}
187185
188- err := h.Remove([]*rsyncutils.ReceiverFile{})
186+ f, info, err := fsys.Open("site/a.txt")
189187 if err != nil {
190- t.Fatalf("Remove() returned error: %v", err)
188+ t.Fatal(err)
191189 }
192-
193- for _, path := range mockHandler.deleteCalls {
194- if path == "/test/dir/._pico_keep_dir" {
195- t.Error("._pico_keep_dir should not be in delete list")
196- }
190+ buf := make([]byte, info.Size)
191+ if _, err := f.ReadAt(buf, 0); err != nil || string(buf) != "hello" {
192+ t.Fatalf("read %q, %v", buf, err)
197193 }
198-
199- if len(mockHandler.deleteCalls) != 2 {
200- t.Errorf("expected 2 delete calls (dir, dir/file.txt), got %d: %v", len(mockHandler.deleteCalls), mockHandler.deleteCalls)
194+ if _, err := fsys.Put("site/b.txt", fsInfo(5), strings.NewReader("world")); err != nil {
195+ t.Fatal(err)
201196 }
202-}
203-
204-func TestRemove_OnlyDeletesFilesNotInWillReceive(t *testing.T) {
205- session, _ := newMockSession()
206- mockHandler := &mockWriteHandler{
207- entries: []os.FileInfo{
208- &mockFileInfo{name: "a.txt", size: 100},
209- &mockFileInfo{name: "b.txt", size: 100},
210- &mockFileInfo{name: "c.txt", size: 100},
211- },
197+ if err := fsys.Remove("site/c.txt", false); err != nil {
198+ t.Fatal(err)
212199 }
213-
214- h := &handler{
215- session: session,
216- writeHandler: mockHandler,
217- root: "test",
200+ want := []string{"read /site/a.txt", "write /site/b.txt", "delete /site/c.txt"}
201+ if !slices.Equal(handler.calls, want) {
202+ t.Errorf("calls %q, want %q", handler.calls, want)
218203 }
219-
220- willReceive := []*rsyncutils.ReceiverFile{
221- {Name: "a.txt"},
222- {Name: "c.txt"},
223- }
224-
225- err := h.Remove(willReceive)
226- if err != nil {
227- t.Fatalf("Remove() returned error: %v", err)
228- }
229-
230- if len(mockHandler.deleteCalls) != 1 {
231- t.Fatalf("expected 1 delete call, got %d: %v", len(mockHandler.deleteCalls), mockHandler.deleteCalls)
204+ if handler.written["/site/b.txt"] != "world" {
205+ t.Errorf("written %q", handler.written)
232206 }
207+}
233208
234- if mockHandler.deleteCalls[0] != "/test/b.txt" {
235- t.Errorf("expected to delete /test/b.txt, got %s", mockHandler.deleteCalls[0])
236- }
209+func fsInfo(size int64) rsync.FileInfo {
210+ return rsync.FileInfo{Size: size, ModTime: time.Unix(1700000000, 0)}
237211 }
+240, -0
......@@ -0,0 +1,240 @@
1+package storage_test
2+
3+import (
4+ "bytes"
5+ "crypto/rand"
6+ "fmt"
7+ "io"
8+ "log/slog"
9+ "slices"
10+ "strings"
11+ "testing"
12+ "time"
13+
14+ "github.com/picosh/pico/pkg/storage"
15+ "github.com/picosh/pico/pkg/storage/storagetest"
16+)
17+
18+func backends(t *testing.T) map[string]func(t *testing.T) storage.StorageServe {
19+ return map[string]func(t *testing.T) storage.StorageServe{
20+ "fs": func(t *testing.T) storage.StorageServe {
21+ st, err := storage.NewStorageFS(slog.New(slog.DiscardHandler), t.TempDir())
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ return st
26+ },
27+ "memory": func(t *testing.T) storage.StorageServe {
28+ st, err := storage.NewStorageMemory(map[string]map[string]string{})
29+ if err != nil {
30+ t.Fatal(err)
31+ }
32+ return st
33+ },
34+ }
35+}
36+
37+func put(t *testing.T, st storage.StorageServe, b storage.Bucket, name string, data []byte, mtime time.Time) {
38+ t.Helper()
39+ _, size, err := st.PutObject(b, name, bytes.NewReader(data), &storage.ObjectInfo{LastModified: mtime})
40+ if err != nil {
41+ t.Fatalf("put %s: %v", name, err)
42+ }
43+ if size != int64(len(data)) {
44+ t.Fatalf("put %s: size %d, want %d", name, size, len(data))
45+ }
46+}
47+
48+// entry renders a listing entry so listings compare as sorted strings.
49+func entry(name string, isDir bool, size int64, mtime time.Time) string {
50+ if isDir {
51+ return name + "/"
52+ }
53+ return fmt.Sprintf("%s %d %d", name, size, mtime.Unix())
54+}
55+
56+func list(t *testing.T, st storage.StorageServe, b storage.Bucket, dir string, recursive bool) []string {
57+ t.Helper()
58+ infos, err := st.ListObjects(b, dir, recursive)
59+ if err != nil {
60+ t.Fatalf("list %s: %v", dir, err)
61+ }
62+ out := []string{}
63+ for _, info := range infos {
64+ out = append(out, entry(info.Name(), info.IsDir(), info.Size(), info.ModTime()))
65+ }
66+ slices.Sort(out)
67+ return out
68+}
69+
70+func TestStorageConformance(t *testing.T) {
71+ for name, newStorage := range backends(t) {
72+ t.Run(name, func(t *testing.T) {
73+ st := newStorage(t)
74+ b := storagetest.Bucket(t, st, "conformance")
75+
76+ t1 := time.Unix(1_700_000_000, 0)
77+ t2 := time.Unix(1_700_100_000, 0)
78+ put(t, st, b, "/proj/index.html", []byte("<html></html>"), t1)
79+ put(t, st, b, "/proj/css/site.css", []byte("body{}"), t2)
80+ put(t, st, b, "/proj/css/deep/x.css", []byte("a{}"), t1)
81+ put(t, st, b, "/other.txt", []byte("other"), t2)
82+ before := time.Now().Add(-time.Second)
83+ put(t, st, b, "/proj/now.txt", []byte("now"), time.Time{})
84+
85+ t.Run("get", func(t *testing.T) {
86+ r, info, err := st.GetObject(b, "/proj/index.html")
87+ if err != nil {
88+ t.Fatal(err)
89+ }
90+ defer func() { _ = r.Close() }()
91+ data, _ := io.ReadAll(r)
92+ if string(data) != "<html></html>" || info.Size != 13 || !info.LastModified.Equal(t1) {
93+ t.Fatalf("got %q size=%d mtime=%v", data, info.Size, info.LastModified)
94+ }
95+
96+ _, info, err = st.GetObject(b, "/proj/now.txt")
97+ if err != nil {
98+ t.Fatal(err)
99+ }
100+ if info.LastModified.Before(before) || info.LastModified.After(time.Now().Add(time.Second)) {
101+ t.Fatalf("object stored without an mtime has mtime %v", info.LastModified)
102+ }
103+
104+ if _, _, err := st.GetObject(b, "/proj/missing.txt"); err == nil {
105+ t.Fatal("missing object returned no error")
106+ }
107+ })
108+
109+ t.Run("list", func(t *testing.T) {
110+ now := list(t, st, b, "/proj/now.txt", false)[0]
111+ tests := []struct {
112+ dir string
113+ recursive bool
114+ want []string
115+ }{
116+ {"/proj/index.html", false, []string{entry("index.html", false, 13, t1)}},
117+ {"/proj", false, []string{"/"}},
118+ {"/proj/", false, []string{"css/", entry("index.html", false, 13, t1), now}},
119+ {"/proj/", true, []string{
120+ "css/",
121+ entry("css/deep/x.css", false, 3, t1),
122+ "css/deep/",
123+ entry("css/site.css", false, 6, t2),
124+ entry("index.html", false, 13, t1),
125+ now,
126+ }},
127+ {"/proj/css/", true, []string{entry("deep/x.css", false, 3, t1), "deep/", entry("site.css", false, 6, t2)}},
128+ {"/missing", false, []string{}},
129+ {"/missing/", true, []string{}},
130+ }
131+ for _, tt := range tests {
132+ want := slices.Clone(tt.want)
133+ slices.Sort(want)
134+ if got := list(t, st, b, tt.dir, tt.recursive); !slices.Equal(got, want) {
135+ t.Errorf("list(%q, recursive=%v)\n got %q\nwant %q", tt.dir, tt.recursive, got, want)
136+ }
137+ }
138+ })
139+
140+ t.Run("overwrite", func(t *testing.T) {
141+ put(t, st, b, "/proj/css/site.css", []byte("body{color:red}"), t1)
142+ r, info, err := st.GetObject(b, "/proj/css/site.css")
143+ if err != nil {
144+ t.Fatal(err)
145+ }
146+ data, _ := io.ReadAll(r)
147+ _ = r.Close()
148+ if string(data) != "body{color:red}" || !info.LastModified.Equal(t1) {
149+ t.Fatalf("got %q mtime=%v", data, info.LastModified)
150+ }
151+ })
152+
153+ t.Run("random access", func(t *testing.T) {
154+ data := make([]byte, 4<<20+12345)
155+ _, _ = rand.Read(data)
156+ put(t, st, b, "/proj/big.bin", data, t2)
157+
158+ r, info, err := st.GetObject(b, "/proj/big.bin")
159+ if err != nil {
160+ t.Fatal(err)
161+ }
162+ defer func() { _ = r.Close() }()
163+ if info.Size != int64(len(data)) || !info.LastModified.Equal(t2) {
164+ t.Fatalf("size=%d mtime=%v", info.Size, info.LastModified)
165+ }
166+ for _, off := range []int64{0, 1<<20 - 10, 5, int64(len(data)) - 100, 3 << 20} {
167+ buf := make([]byte, 64<<10)
168+ n, err := r.ReadAt(buf, off)
169+ want := data[off:min(off+int64(len(buf)), int64(len(data)))]
170+ if !bytes.Equal(buf[:n], want) {
171+ t.Fatalf("ReadAt(%d) returned wrong data", off)
172+ }
173+ if n < len(buf) && err != io.EOF {
174+ t.Fatalf("short ReadAt(%d) returned %v", off, err)
175+ }
176+ }
177+ if _, err := r.Seek(int64(len(data))-1000, io.SeekStart); err != nil {
178+ t.Fatal(err)
179+ }
180+ tail, err := io.ReadAll(r)
181+ if err != nil || !bytes.Equal(tail, data[len(data)-1000:]) {
182+ t.Fatalf("read after seek: %d bytes, %v", len(tail), err)
183+ }
184+ if _, err := r.Seek(0, io.SeekStart); err != nil {
185+ t.Fatal(err)
186+ }
187+ all, err := io.ReadAll(r)
188+ if err != nil || !bytes.Equal(all, data) {
189+ t.Fatalf("full read: %d bytes, %v", len(all), err)
190+ }
191+ })
192+
193+ t.Run("failed upload", func(t *testing.T) {
194+ body := io.MultiReader(strings.NewReader(strings.Repeat("x", 1000)), errReader{})
195+ _, _, err := st.PutObject(b, "/proj/broken.txt", body, &storage.ObjectInfo{})
196+ if err == nil {
197+ t.Fatal("upload from a failing reader succeeded")
198+ }
199+ r, _, err := st.GetObject(b, "/proj/broken.txt")
200+ if err == nil {
201+ data, _ := io.ReadAll(r)
202+ _ = r.Close()
203+ t.Fatalf("failed upload left an object of %d bytes", len(data))
204+ }
205+ })
206+
207+ t.Run("delete", func(t *testing.T) {
208+ if err := st.DeleteObject(b, "/proj/css/deep/x.css"); err != nil {
209+ t.Fatal(err)
210+ }
211+ if err := st.DeleteObject(b, "/proj/css/deep/x.css"); err != nil {
212+ t.Fatalf("deleting a missing object: %v", err)
213+ }
214+ if _, _, err := st.GetObject(b, "/proj/css/deep/x.css"); err == nil {
215+ t.Fatal("deleted object still readable")
216+ }
217+ got := list(t, st, b, "/proj/css/", true)
218+ want := []string{entry("site.css", false, 15, t1)}
219+ if !slices.Equal(got, want) {
220+ t.Fatalf("after delete got %q, want %q", got, want)
221+ }
222+ })
223+
224+ t.Run("quota", func(t *testing.T) {
225+ size, err := st.GetBucketQuota(b)
226+ if err != nil {
227+ t.Fatal(err)
228+ }
229+ want := uint64(13 + 15 + 5 + 3 + 4<<20 + 12345)
230+ if size != want {
231+ t.Fatalf("quota %d, want %d", size, want)
232+ }
233+ })
234+ })
235+ }
236+}
237+
238+type errReader struct{}
239+
240+func (errReader) Read([]byte) (int, error) { return 0, fmt.Errorf("connection reset") }
+72, -59
......@@ -21,6 +21,7 @@ func (s *seekableReader) Close() error { return nil }
2121
2222 type StorageMemory struct {
2323 storage map[string]map[string]string
24+ mtimes map[string]map[string]time.Time
2425 mu sync.RWMutex
2526 }
2627
......@@ -28,11 +29,27 @@ var _ StorageServe = &StorageMemory{}
2829 var _ StorageServe = (*StorageMemory)(nil)
2930
3031 func NewStorageMemory(st map[string]map[string]string) (*StorageMemory, error) {
32+ now := time.Now()
33+ mtimes := map[string]map[string]time.Time{}
34+ for bucket, objects := range st {
35+ mtimes[bucket] = map[string]time.Time{}
36+ for key := range objects {
37+ mtimes[bucket][key] = now
38+ }
39+ }
3140 return &StorageMemory{
3241 storage: st,
42+ mtimes: mtimes,
3343 }, nil
3444 }
3545
46+func memoryKey(fpath string) string {
47+ if !strings.HasPrefix(fpath, "/") {
48+ return "/" + fpath
49+ }
50+ return fpath
51+}
52+
3653 func (s *StorageMemory) GetBucket(name string) (Bucket, error) {
3754 s.mu.RLock()
3855 defer s.mu.RUnlock()
......@@ -60,6 +77,7 @@ func (s *StorageMemory) UpsertBucket(name string) (Bucket, error) {
6077 defer s.mu.Unlock()
6178
6279 s.storage[name] = map[string]string{}
80+ s.mtimes[name] = map[string]time.Time{}
6381 return bucket, nil
6482 }
6583
......@@ -80,6 +98,7 @@ func (s *StorageMemory) DeleteBucket(bucket Bucket) error {
8098 defer s.mu.Unlock()
8199
82100 delete(s.storage, bucket.Path)
101+ delete(s.mtimes, bucket.Path)
83102 return nil
84103 }
85104
......@@ -87,13 +106,8 @@ func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndRea
87106 s.mu.RLock()
88107 defer s.mu.RUnlock()
89108
90- if !strings.HasPrefix(fpath, "/") {
91- fpath = "/" + fpath
92- }
93-
94- objInfo := &ObjectInfo{
95- LastModified: time.Time{},
96- }
109+ fpath = memoryKey(fpath)
110+ objInfo := &ObjectInfo{}
97111
98112 dat, ok := s.storage[bucket.Path][fpath]
99113 if !ok {
......@@ -101,6 +115,7 @@ func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndRea
101115 }
102116
103117 objInfo.Size = int64(len([]byte(dat)))
118+ objInfo.LastModified = s.mtimes[bucket.Path][fpath]
104119 return &seekableReader{bytes.NewReader([]byte(dat))}, objInfo, nil
105120 }
106121
......@@ -113,7 +128,16 @@ func (s *StorageMemory) PutObject(bucket Bucket, fpath string, contents io.Reade
113128 return "", 0, err
114129 }
115130
131+ fpath = memoryKey(fpath)
132+ mtime := info.LastModified
133+ if mtime.IsZero() {
134+ mtime = time.Now()
135+ }
116136 s.storage[bucket.Path][fpath] = string(d)
137+ if s.mtimes[bucket.Path] == nil {
138+ s.mtimes[bucket.Path] = map[string]time.Time{}
139+ }
140+ s.mtimes[bucket.Path][fpath] = mtime
117141 return fmt.Sprintf("%s%s", bucket.Path, fpath), int64(len(d)), nil
118142 }
119143
......@@ -121,7 +145,9 @@ func (s *StorageMemory) DeleteObject(bucket Bucket, fpath string) error {
121145 s.mu.Lock()
122146 defer s.mu.Unlock()
123147
148+ fpath = memoryKey(fpath)
124149 delete(s.storage[bucket.Path], fpath)
150+ delete(s.mtimes[bucket.Path], fpath)
125151 return nil
126152 }
127153
......@@ -136,76 +162,63 @@ func (s *StorageMemory) ListBuckets() ([]string, error) {
136162 return buckets, nil
137163 }
138164
165+// ListObjects follows StorageFS: a path naming an object lists that object,
166+// a directory path without a trailing slash lists only the directory itself,
167+// and a path ending in a slash lists its contents relative to it.
139168 func (s *StorageMemory) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
140169 s.mu.RLock()
141170 defer s.mu.RUnlock()
142171
143- var fileList []os.FileInfo
144-
145- resolved := dir
146-
147- if !strings.HasPrefix(resolved, "/") {
148- resolved = "/" + resolved
149- }
150-
172+ fileList := []os.FileInfo{}
173+ resolved := memoryKey(dir)
151174 objects := s.storage[bucket.Path]
152- // dir is actually an object
153- oval, ok := objects[resolved]
154- if ok {
175+ mtimes := s.mtimes[bucket.Path]
176+
177+ if val, ok := objects[resolved]; ok {
155178 fileList = append(fileList, &utils.VirtualFile{
156179 FName: filepath.Base(resolved),
157- FIsDir: false,
158- FSize: int64(len([]byte(oval))),
159- FModTime: time.Time{},
180+ FSize: int64(len(val)),
181+ FModTime: mtimes[resolved],
160182 })
161183 return fileList, nil
162184 }
163185
164- for key, val := range objects {
165- if !strings.HasPrefix(key, resolved) {
166- continue
186+ if !strings.HasSuffix(resolved, "/") {
187+ for key := range objects {
188+ if strings.HasPrefix(key, resolved+"/") {
189+ fileList = append(fileList, &utils.VirtualFile{FName: "", FIsDir: true})
190+ break
191+ }
167192 }
193+ return fileList, nil
194+ }
168195
169- rep := strings.Replace(key, resolved, "", 1)
170- fdir := filepath.Dir(rep)
171- fname := filepath.Base(rep)
172- paths := strings.Split(fdir, "/")
173-
174- if fdir == "/" {
175- ffname := filepath.Base(resolved)
176- fileList = append(fileList, &utils.VirtualFile{
177- FName: ffname,
178- FIsDir: true,
179- })
196+ seen := map[string]bool{}
197+ addDir := func(name string) {
198+ if !seen[name] {
199+ seen[name] = true
200+ fileList = append(fileList, &utils.VirtualFile{FName: name, FIsDir: true})
180201 }
181-
182- for _, p := range paths {
183- if p == "" || p == "/" || p == "." {
202+ }
203+ for key, val := range objects {
204+ rel, ok := strings.CutPrefix(key, resolved)
205+ if !ok || rel == "" {
206+ continue
207+ }
208+ if !recursive {
209+ if d, _, nested := strings.Cut(rel, "/"); nested {
210+ addDir(d)
184211 continue
185212 }
186- fileList = append(fileList, &utils.VirtualFile{
187- FName: p,
188- FIsDir: true,
189- })
190213 }
191-
192- trimRes := strings.TrimSuffix(resolved, "/")
193- dirKey := filepath.Dir(key)
194- if recursive {
195- fileList = append(fileList, &utils.VirtualFile{
196- FName: fname,
197- FIsDir: false,
198- FSize: int64(len([]byte(val))),
199- FModTime: time.Time{},
200- })
201- } else if resolved == dirKey || trimRes == dirKey {
202- fileList = append(fileList, &utils.VirtualFile{
203- FName: fname,
204- FIsDir: false,
205- FSize: int64(len([]byte(val))),
206- FModTime: time.Time{},
207- })
214+ for d := filepath.Dir(rel); d != "." && !seen[d]; d = filepath.Dir(d) {
215+ addDir(d)
208216 }
217+ fileList = append(fileList, &utils.VirtualFile{
218+ FName: rel,
219+ FSize: int64(len(val)),
220+ FModTime: mtimes[key],
221+ })
209222 }
210223
211224 return fileList, nil
+24, -0
......@@ -0,0 +1,24 @@
1+// Package storagetest provides storage helpers for tests.
2+package storagetest
3+
4+import (
5+ "testing"
6+
7+ "github.com/picosh/pico/pkg/storage"
8+)
9+
10+// Bucket creates an empty bucket and removes it when the test ends.
11+func Bucket(t testing.TB, st storage.StorageServe, name string) storage.Bucket {
12+ t.Helper()
13+ if b, err := st.GetBucket(name); err == nil {
14+ if err := st.DeleteBucket(b); err != nil {
15+ t.Fatal(err)
16+ }
17+ }
18+ b, err := st.UpsertBucket(name)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ t.Cleanup(func() { _ = st.DeleteBucket(b) })
23+ return b
24+}