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 ( | |
| 22 | 22 | github.com/alecthomas/chroma/v2 v2.27.0 | |
| 23 | 23 | github.com/antoniomika/syncmap v1.0.0 | |
| 24 | 24 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de | |
| 25 | + | github.com/cespare/xxhash/v2 v2.3.0 | |
| 25 | 26 | github.com/containerd/console v1.0.5 | |
| 26 | 27 | github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 | |
| 27 | 28 | github.com/emersion/go-smtp v0.25.0 |
| ... | ... | @@ -34,6 +35,7 @@ require ( | |
| 34 | 35 | github.com/gorilla/websocket v1.5.3 | |
| 35 | 36 | github.com/hashicorp/golang-lru/v2 v2.0.7 | |
| 36 | 37 | github.com/jmoiron/sqlx v1.4.0 | |
| 38 | + | github.com/klauspost/compress v1.19.1 | |
| 37 | 39 | github.com/lib/pq v1.12.3 | |
| 38 | 40 | github.com/matryer/is v1.4.1 | |
| 39 | 41 | github.com/microcosm-cc/bluemonday v1.0.27 |
| ... | ... | @@ -56,7 +58,6 @@ require ( | |
| 56 | 58 | go.abhg.dev/goldmark/toc v0.12.0 | |
| 57 | 59 | go.rockorager.dev/vaxis v0.17.1 | |
| 58 | 60 | golang.org/x/crypto v0.57.0 | |
| 59 | - | golang.org/x/sync v0.23.0 | |
| 60 | 61 | gopkg.in/yaml.v2 v2.4.0 | |
| 61 | 62 | modernc.org/sqlite v1.60.1 | |
| 62 | 63 | ) |
| ... | ... | @@ -69,7 +70,6 @@ require ( | |
| 69 | 70 | github.com/aymerick/douceur v0.2.0 // indirect | |
| 70 | 71 | github.com/beorn7/perks v1.0.1 // indirect | |
| 71 | 72 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect | |
| 72 | - | github.com/cespare/xxhash/v2 v2.3.0 // indirect | |
| 73 | 73 | github.com/containerd/errdefs v1.0.0 // indirect | |
| 74 | 74 | github.com/containerd/errdefs/pkg v0.3.0 // indirect | |
| 75 | 75 | github.com/containerd/log v0.1.0 // indirect |
| ... | ... | @@ -111,7 +111,6 @@ require ( | |
| 111 | 111 | github.com/gorilla/css v1.0.1 // indirect | |
| 112 | 112 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect | |
| 113 | 113 | github.com/jackc/pgx/v5 v5.8.0 // indirect | |
| 114 | - | github.com/klauspost/compress v1.19.1 // indirect | |
| 115 | 114 | github.com/kr/fs v0.1.0 // indirect | |
| 116 | 115 | github.com/kr/pretty v0.3.1 // indirect | |
| 117 | 116 | github.com/kr/text v0.2.0 // indirect |
| ... | ... | @@ -164,6 +163,7 @@ require ( | |
| 164 | 163 | golang.org/x/image v0.46.0 // indirect | |
| 165 | 164 | golang.org/x/mod v0.41.0 // indirect | |
| 166 | 165 | golang.org/x/net v0.59.0 // indirect | |
| 166 | + | golang.org/x/sync v0.23.0 // indirect | |
| 167 | 167 | golang.org/x/sys v0.48.0 // indirect | |
| 168 | 168 | golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect | |
| 169 | 169 | 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 | + | } |
| ... | ... | @@ -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 | - | } |
| ... | ... | @@ -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 | - | } |
| ... | ... | @@ -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 |
| ... | ... | @@ -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 | - | } |
| ... | ... | @@ -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 | - | } |
| ... | ... | @@ -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 | - | } |
| ... | ... | @@ -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 == "" } |
| ... | ... | @@ -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 | - | } |
| ... | ... | @@ -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 == "" } |
| ... | ... | @@ -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). | |
| 5 | 5 | // | |
| 6 | 6 | // If we encounter arguments that rsync(1) parses differently compared to this | |
| 7 | 7 | // package, then this package should be adjusted to match rsync(1). | |
| 8 | 8 | package rsyncopts | |
| 9 | 9 | ||
| 10 | 10 | import ( | |
| 11 | - | "errors" | |
| 12 | 11 | "fmt" | |
| 13 | - | "log/slog" | |
| 14 | 12 | "math" | |
| 15 | - | "slices" | |
| 16 | 13 | "strconv" | |
| 17 | 14 | "strings" | |
| 18 | 15 | "syscall" |
| ... | ... | @@ -91,42 +88,27 @@ func NewOptions() *Options { | |
| 91 | 88 | relative_paths: -1, | |
| 92 | 89 | implied_dirs: 1, | |
| 93 | 90 | max_delete: math.MinInt32, | |
| 91 | + | max_size: -1, | |
| 92 | + | min_size: -1, | |
| 94 | 93 | whole_file: -1, | |
| 95 | 94 | do_compression_level: math.MinInt32, | |
| 96 | 95 | rsync_path: "rsync", | |
| 97 | 96 | default_af_hint: syscall.AF_INET6, | |
| 98 | 97 | 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}, | |
| 121 | 98 | } | |
| 122 | 99 | } | |
| 123 | 100 | ||
| 124 | 101 | type Options struct { | |
| 125 | - | Gokrazy GokrazyOptions | |
| 126 | - | ||
| 127 | 102 | // 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 | |
| 130 | 112 | ||
| 131 | 113 | // order matches long_options order | |
| 132 | 114 | verbose int |
| ... | ... | @@ -247,12 +229,6 @@ type Options struct { | |
| 247 | 229 | checksum_seed int | |
| 248 | 230 | am_server int | |
| 249 | 231 | am_sender int | |
| 250 | - | am_daemon int | |
| 251 | - | ||
| 252 | - | daemon_bwlimit int | |
| 253 | - | config_file string | |
| 254 | - | daemon_opt int | |
| 255 | - | no_detach int | |
| 256 | 232 | } | |
| 257 | 233 | ||
| 258 | 234 | type priority int |
| ... | ... | @@ -354,48 +330,6 @@ func (o *Options) setOutputVerbosity(prio priority) { | |
| 354 | 330 | } | |
| 355 | 331 | } | |
| 356 | 332 | ||
| 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 | - | ||
| 399 | 333 | func (o *Options) table() []poptOption { | |
| 400 | 334 | return []poptOption{ | |
| 401 | 335 | /* longName, shortName, argInfo, arg, val */ |
| ... | ... | @@ -631,6 +565,7 @@ func (o *Options) table() []poptOption { | |
| 631 | 565 | {"mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 1}, | |
| 632 | 566 | {"no-mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 0}, | |
| 633 | 567 | {"qsort", "", POPT_ARG_NONE, &o.use_qsort, 0}, | |
| 568 | + | {"use-qsort", "", POPT_ARG_NONE, &o.use_qsort, 0}, | |
| 634 | 569 | {"copy-as", "", POPT_ARG_STRING, &o.copy_as, 0}, | |
| 635 | 570 | {"address", "", POPT_ARG_STRING, &o.bind_address, 0}, | |
| 636 | 571 | {"port", "", POPT_ARG_INT, &o.rsync_port, 0}, |
| ... | ... | @@ -654,25 +589,13 @@ func (o *Options) table() []poptOption { | |
| 654 | 589 | } | |
| 655 | 590 | } | |
| 656 | 591 | ||
| 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) { | |
| 666 | 595 | 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 | - | } | |
| 673 | 596 | pc := Context{ | |
| 674 | 597 | Options: opts, | |
| 675 | - | table: table, | |
| 598 | + | table: opts.table(), | |
| 676 | 599 | args: args, | |
| 677 | 600 | } | |
| 678 | 601 |
| ... | ... | @@ -688,7 +611,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 688 | 611 | // are returned and handled here. | |
| 689 | 612 | switch opt { | |
| 690 | 613 | case 'V': | |
| 691 | - | version_opt_cnt++ | |
| 614 | + | opts.version_opt_cnt++ | |
| 692 | 615 | ||
| 693 | 616 | case OPT_SERVER: | |
| 694 | 617 | opts.am_server = 1 |
| ... | ... | @@ -700,47 +623,11 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 700 | 623 | opts.am_sender = 1 | |
| 701 | 624 | ||
| 702 | 625 | 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") | |
| 713 | 627 | ||
| 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") | |
| 744 | 631 | ||
| 745 | 632 | case 'a': | |
| 746 | 633 | if opts.recurse == 0 { |
| ... | ... | @@ -766,7 +653,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 766 | 653 | opts.human_readable++ | |
| 767 | 654 | ||
| 768 | 655 | case 'H': | |
| 769 | - | opts.preserve_hard_links = 1 | |
| 656 | + | opts.preserve_hard_links++ | |
| 770 | 657 | ||
| 771 | 658 | case 'i': | |
| 772 | 659 | opts.itemize_changes++ |
| ... | ... | @@ -781,7 +668,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 781 | 668 | opts.verbose++ | |
| 782 | 669 | ||
| 783 | 670 | case 'y': | |
| 784 | - | return nil, errNotYetImplemented | |
| 671 | + | opts.fuzzy_basis++ | |
| 785 | 672 | ||
| 786 | 673 | case 'q': | |
| 787 | 674 | opts.quiet++ |
| ... | ... | @@ -789,9 +676,6 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 789 | 676 | case 'x': | |
| 790 | 677 | opts.one_file_system++ | |
| 791 | 678 | ||
| 792 | - | case 'F': | |
| 793 | - | return nil, errNotYetImplemented | |
| 794 | - | ||
| 795 | 679 | case 'P': | |
| 796 | 680 | opts.do_progress = 1 | |
| 797 | 681 | opts.keep_partial = 1 |
| ... | ... | @@ -810,57 +694,57 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 810 | 694 | opts.compress_choice = "" | |
| 811 | 695 | ||
| 812 | 696 | case OPT_OLD_ARGS: | |
| 813 | - | return nil, errNotYetImplemented | |
| 697 | + | opts.old_style_args++ | |
| 814 | 698 | ||
| 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") | |
| 817 | 701 | ||
| 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 | |
| 822 | 704 | ||
| 823 | 705 | 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 | |
| 825 | 711 | ||
| 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 | |
| 830 | 718 | ||
| 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 | |
| 833 | 725 | ||
| 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++ | |
| 838 | 728 | ||
| 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()) | |
| 841 | 731 | ||
| 842 | 732 | case OPT_INFO: | |
| 843 | 733 | parseOutputWords(infoWords[:], opts.info[:], pc.poptGetOptArg(), USER_PRIORITY) | |
| 844 | 734 | ||
| 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 | - | ||
| 854 | 735 | case 'A': | |
| 855 | - | return nil, fmt.Errorf("ACLs are not supported by gokrazy/rsync") | |
| 736 | + | opts.preserve_acls++ | |
| 856 | 737 | ||
| 857 | 738 | case 'X': | |
| 858 | 739 | opts.preserve_xattrs++ | |
| 859 | 740 | ||
| 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() | |
| 864 | 748 | ||
| 865 | 749 | default: | |
| 866 | 750 | return nil, fmt.Errorf("unhandled special case opt: %v", opt) |
| ... | ... | @@ -872,6 +756,15 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 872 | 756 | ||
| 873 | 757 | opts.setOutputVerbosity(DEFAULT_PRIORITY) | |
| 874 | 758 | ||
| 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 | + | ||
| 875 | 768 | if opts.recurse != 0 { | |
| 876 | 769 | opts.xfer_dirs = 1 | |
| 877 | 770 | } |
| ... | ... | @@ -911,7 +804,7 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 911 | 804 | opts.make_backups = 1 // --backup-dir implies --backup | |
| 912 | 805 | } | |
| 913 | 806 | ||
| 914 | - | if opts.do_progress != 0 /* && !opts.am_server */ { | |
| 807 | + | if opts.do_progress != 0 { | |
| 915 | 808 | if opts.info[INFO_NAME] == 0 { | |
| 916 | 809 | opts.info[INFO_NAME] = 1 | |
| 917 | 810 | } |
| ... | ... | @@ -923,3 +816,122 @@ func ParseArguments(args []string, gokrazyTable bool) (*Context, error) { | |
| 923 | 816 | ||
| 924 | 817 | return &pc, nil | |
| 925 | 818 | } | |
| 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 | |
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | 5 | "fmt" | |
| 6 | + | "io" | |
| 6 | 7 | "io/fs" | |
| 7 | 8 | "os" | |
| 8 | 9 | "path" | |
| 9 | - | "slices" | |
| 10 | 10 | "strings" | |
| 11 | 11 | ||
| 12 | 12 | "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" | |
| 17 | 14 | "github.com/picosh/pico/pkg/send/utils" | |
| 18 | 15 | ) | |
| 19 | 16 | ||
| 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 | |
| 26 | 26 | } | |
| 27 | 27 | ||
| 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, "/") } | |
| 34 | 29 | ||
| 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() | |
| 38 | 36 | } | |
| 37 | + | return false | |
| 38 | + | } | |
| 39 | 39 | ||
| 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 | |
| 79 | 43 | } | |
| 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 | |
| 86 | 47 | } | |
| 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 | |
| 94 | 51 | } | |
| 95 | 52 | } | |
| 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 | |
| 111 | 62 | } | |
| 112 | - | ||
| 113 | - | return h.writeHandler.Read(h.session, &utils.FileEntry{Filepath: filePath}) | |
| 114 | 63 | } | |
| 115 | 64 | ||
| 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) | |
| 127 | 67 | if err != nil { | |
| 128 | - | errMsg := fmt.Sprintf("%s\r\n", err.Error()) | |
| 129 | - | _, err = h.session.Stderr().Write([]byte(errMsg)) | |
| 68 | + | return nil, err | |
| 130 | 69 | } | |
| 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()}) | |
| 134 | 80 | } | |
| 135 | - | return 0, err | |
| 81 | + | return out, nil | |
| 136 | 82 | } | |
| 137 | 83 | ||
| 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)}) | |
| 140 | 86 | if err != nil { | |
| 141 | - | return err | |
| 87 | + | return nil, rsync.FileInfo{}, err | |
| 142 | 88 | } | |
| 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) | |
| 154 | 91 | } | |
| 92 | + | return r, rsync.FileInfo{Name: name, Size: info.Size(), ModTime: info.ModTime()}, nil | |
| 93 | + | } | |
| 155 | 94 | ||
| 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, | |
| 162 | 103 | }) | |
| 104 | + | } | |
| 163 | 105 | ||
| 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)}) | |
| 173 | 108 | } | |
| 174 | 109 | ||
| 110 | + | // Middleware serves rsync clients ("rsync --server ...") from the handler's | |
| 111 | + | // storage. | |
| 175 | 112 | func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware { | |
| 176 | 113 | return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler { | |
| 177 | 114 | return func(session *pssh.SSHServerConnSession) error { |
| ... | ... | @@ -192,66 +129,26 @@ func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddlewa | |
| 192 | 129 | } | |
| 193 | 130 | }() | |
| 194 | 131 | ||
| 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 | |
| 200 | 139 | } | |
| 201 | 140 | ||
| 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 | |
| 206 | 147 | } | |
| 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 | |
| 255 | 152 | } | |
| 256 | 153 | } | |
| 257 | 154 | } |
+103,
-129
| ... | ... | @@ -3,15 +3,18 @@ package rsync | |
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | 5 | "context" | |
| 6 | + | "errors" | |
| 6 | 7 | "io" | |
| 7 | 8 | "io/fs" | |
| 8 | 9 | "log/slog" | |
| 9 | 10 | "os" | |
| 11 | + | "slices" | |
| 12 | + | "strings" | |
| 10 | 13 | "testing" | |
| 11 | 14 | "time" | |
| 12 | 15 | ||
| 13 | 16 | "github.com/picosh/pico/pkg/pssh" | |
| 14 | - | rsyncutils "github.com/picosh/pico/pkg/rsync-receiver/utils" | |
| 17 | + | "github.com/picosh/pico/pkg/rsync" | |
| 15 | 18 | "github.com/picosh/pico/pkg/send/utils" | |
| 16 | 19 | "golang.org/x/crypto/ssh" | |
| 17 | 20 | ) |
| ... | ... | @@ -20,46 +23,62 @@ import ( | |
| 20 | 23 | type mockFileInfo struct { | |
| 21 | 24 | name string | |
| 22 | 25 | size int64 | |
| 23 | - | mode fs.FileMode | |
| 24 | 26 | modTime time.Time | |
| 25 | 27 | isDir bool | |
| 26 | 28 | } | |
| 27 | 29 | ||
| 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 | + | } | |
| 31 | 38 | func (m *mockFileInfo) ModTime() time.Time { return m.modTime } | |
| 32 | 39 | func (m *mockFileInfo) IsDir() bool { return m.isDir } | |
| 33 | 40 | func (m *mockFileInfo) Sys() any { return nil } | |
| 34 | 41 | ||
| 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. | |
| 37 | 44 | 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 | |
| 41 | 49 | } | |
| 42 | 50 | ||
| 43 | 51 | 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) | |
| 50 | 53 | return nil | |
| 51 | 54 | } | |
| 52 | 55 | ||
| 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) | |
| 54 | 66 | return "", nil | |
| 55 | 67 | } | |
| 56 | 68 | ||
| 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 | |
| 59 | 77 | } | |
| 60 | 78 | ||
| 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 | |
| 63 | 82 | } | |
| 64 | 83 | ||
| 65 | 84 | func (m *mockWriteHandler) GetLogger(_ *pssh.SSHServerConnSession) *slog.Logger { |
| ... | ... | @@ -87,9 +106,8 @@ func (m *mockChannel) Stderr() io.ReadWriter { return m.stderr } | |
| 87 | 106 | var _ ssh.Channel = (*mockChannel)(nil) | |
| 88 | 107 | ||
| 89 | 108 | // 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{}} | |
| 93 | 111 | ||
| 94 | 112 | ctx, cancel := context.WithCancel(context.Background()) | |
| 95 | 113 | logger := slog.Default() |
| ... | ... | @@ -100,138 +118,94 @@ func newMockSession() (*pssh.SSHServerConnSession, *bytes.Buffer) { | |
| 100 | 118 | }, | |
| 101 | 119 | }, server) | |
| 102 | 120 | ||
| 103 | - | session := &pssh.SSHServerConnSession{ | |
| 121 | + | return &pssh.SSHServerConnSession{ | |
| 104 | 122 | Channel: channel, | |
| 105 | 123 | SSHServerConn: serverConn, | |
| 106 | 124 | Ctx: ctx, | |
| 107 | 125 | CancelFunc: cancel, | |
| 108 | 126 | } | |
| 109 | - | ||
| 110 | - | return session, stderr | |
| 111 | 127 | } | |
| 112 | 128 | ||
| 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"}, | |
| 122 | 138 | }, | |
| 123 | - | } | |
| 139 | + | }} | |
| 140 | + | fsys := &storageFS{session: newMockSession(), handler: handler} | |
| 124 | 141 | ||
| 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) | |
| 132 | 143 | if err != nil { | |
| 133 | - | t.Fatalf("Remove() returned error: %v", err) | |
| 144 | + | t.Fatal(err) | |
| 134 | 145 | } | |
| 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) | |
| 138 | 153 | } | |
| 154 | + | } | |
| 139 | 155 | ||
| 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} | |
| 145 | 164 | ||
| 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) | |
| 159 | 167 | } | |
| 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) | |
| 163 | 170 | } | |
| 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) | |
| 166 | 173 | } | |
| 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) | |
| 169 | 176 | } | |
| 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) | |
| 180 | 179 | } | |
| 180 | + | } | |
| 181 | 181 | ||
| 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} | |
| 187 | 185 | ||
| 188 | - | err := h.Remove([]*rsyncutils.ReceiverFile{}) | |
| 186 | + | f, info, err := fsys.Open("site/a.txt") | |
| 189 | 187 | if err != nil { | |
| 190 | - | t.Fatalf("Remove() returned error: %v", err) | |
| 188 | + | t.Fatal(err) | |
| 191 | 189 | } | |
| 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) | |
| 197 | 193 | } | |
| 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) | |
| 201 | 196 | } | |
| 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) | |
| 212 | 199 | } | |
| 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) | |
| 218 | 203 | } | |
| 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) | |
| 232 | 206 | } | |
| 207 | + | } | |
| 233 | 208 | ||
| 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)} | |
| 237 | 211 | } |
+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 } | |
| 21 | 21 | ||
| 22 | 22 | type StorageMemory struct { | |
| 23 | 23 | storage map[string]map[string]string | |
| 24 | + | mtimes map[string]map[string]time.Time | |
| 24 | 25 | mu sync.RWMutex | |
| 25 | 26 | } | |
| 26 | 27 |
| ... | ... | @@ -28,11 +29,27 @@ var _ StorageServe = &StorageMemory{} | |
| 28 | 29 | var _ StorageServe = (*StorageMemory)(nil) | |
| 29 | 30 | ||
| 30 | 31 | 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 | + | } | |
| 31 | 40 | return &StorageMemory{ | |
| 32 | 41 | storage: st, | |
| 42 | + | mtimes: mtimes, | |
| 33 | 43 | }, nil | |
| 34 | 44 | } | |
| 35 | 45 | ||
| 46 | + | func memoryKey(fpath string) string { | |
| 47 | + | if !strings.HasPrefix(fpath, "/") { | |
| 48 | + | return "/" + fpath | |
| 49 | + | } | |
| 50 | + | return fpath | |
| 51 | + | } | |
| 52 | + | ||
| 36 | 53 | func (s *StorageMemory) GetBucket(name string) (Bucket, error) { | |
| 37 | 54 | s.mu.RLock() | |
| 38 | 55 | defer s.mu.RUnlock() |
| ... | ... | @@ -60,6 +77,7 @@ func (s *StorageMemory) UpsertBucket(name string) (Bucket, error) { | |
| 60 | 77 | defer s.mu.Unlock() | |
| 61 | 78 | ||
| 62 | 79 | s.storage[name] = map[string]string{} | |
| 80 | + | s.mtimes[name] = map[string]time.Time{} | |
| 63 | 81 | return bucket, nil | |
| 64 | 82 | } | |
| 65 | 83 |
| ... | ... | @@ -80,6 +98,7 @@ func (s *StorageMemory) DeleteBucket(bucket Bucket) error { | |
| 80 | 98 | defer s.mu.Unlock() | |
| 81 | 99 | ||
| 82 | 100 | delete(s.storage, bucket.Path) | |
| 101 | + | delete(s.mtimes, bucket.Path) | |
| 83 | 102 | return nil | |
| 84 | 103 | } | |
| 85 | 104 |
| ... | ... | @@ -87,13 +106,8 @@ func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndRea | |
| 87 | 106 | s.mu.RLock() | |
| 88 | 107 | defer s.mu.RUnlock() | |
| 89 | 108 | ||
| 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{} | |
| 97 | 111 | ||
| 98 | 112 | dat, ok := s.storage[bucket.Path][fpath] | |
| 99 | 113 | if !ok { |
| ... | ... | @@ -101,6 +115,7 @@ func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndRea | |
| 101 | 115 | } | |
| 102 | 116 | ||
| 103 | 117 | objInfo.Size = int64(len([]byte(dat))) | |
| 118 | + | objInfo.LastModified = s.mtimes[bucket.Path][fpath] | |
| 104 | 119 | return &seekableReader{bytes.NewReader([]byte(dat))}, objInfo, nil | |
| 105 | 120 | } | |
| 106 | 121 |
| ... | ... | @@ -113,7 +128,16 @@ func (s *StorageMemory) PutObject(bucket Bucket, fpath string, contents io.Reade | |
| 113 | 128 | return "", 0, err | |
| 114 | 129 | } | |
| 115 | 130 | ||
| 131 | + | fpath = memoryKey(fpath) | |
| 132 | + | mtime := info.LastModified | |
| 133 | + | if mtime.IsZero() { | |
| 134 | + | mtime = time.Now() | |
| 135 | + | } | |
| 116 | 136 | 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 | |
| 117 | 141 | return fmt.Sprintf("%s%s", bucket.Path, fpath), int64(len(d)), nil | |
| 118 | 142 | } | |
| 119 | 143 |
| ... | ... | @@ -121,7 +145,9 @@ func (s *StorageMemory) DeleteObject(bucket Bucket, fpath string) error { | |
| 121 | 145 | s.mu.Lock() | |
| 122 | 146 | defer s.mu.Unlock() | |
| 123 | 147 | ||
| 148 | + | fpath = memoryKey(fpath) | |
| 124 | 149 | delete(s.storage[bucket.Path], fpath) | |
| 150 | + | delete(s.mtimes[bucket.Path], fpath) | |
| 125 | 151 | return nil | |
| 126 | 152 | } | |
| 127 | 153 |
| ... | ... | @@ -136,76 +162,63 @@ func (s *StorageMemory) ListBuckets() ([]string, error) { | |
| 136 | 162 | return buckets, nil | |
| 137 | 163 | } | |
| 138 | 164 | ||
| 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. | |
| 139 | 168 | func (s *StorageMemory) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) { | |
| 140 | 169 | s.mu.RLock() | |
| 141 | 170 | defer s.mu.RUnlock() | |
| 142 | 171 | ||
| 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) | |
| 151 | 174 | 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 { | |
| 155 | 178 | fileList = append(fileList, &utils.VirtualFile{ | |
| 156 | 179 | 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], | |
| 160 | 182 | }) | |
| 161 | 183 | return fileList, nil | |
| 162 | 184 | } | |
| 163 | 185 | ||
| 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 | + | } | |
| 167 | 192 | } | |
| 193 | + | return fileList, nil | |
| 194 | + | } | |
| 168 | 195 | ||
| 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}) | |
| 180 | 201 | } | |
| 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) | |
| 184 | 211 | continue | |
| 185 | 212 | } | |
| 186 | - | fileList = append(fileList, &utils.VirtualFile{ | |
| 187 | - | FName: p, | |
| 188 | - | FIsDir: true, | |
| 189 | - | }) | |
| 190 | 213 | } | |
| 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) | |
| 208 | 216 | } | |
| 217 | + | fileList = append(fileList, &utils.VirtualFile{ | |
| 218 | + | FName: rel, | |
| 219 | + | FSize: int64(len(val)), | |
| 220 | + | FModTime: mtimes[key], | |
| 221 | + | }) | |
| 209 | 222 | } | |
| 210 | 223 | ||
| 211 | 224 | 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 | + | } |