github.com/anthropics/anthropic-sdk-go@v1.70.1/internal/filestore/filestore_test.go (about) 1 //go:build !windows 2 3 package filestore 4 5 // Tests for FileStore — the confined-folder filesystem layer. 6 7 import ( 8 "archive/zip" 9 "bytes" 10 "crypto/sha256" 11 "encoding/hex" 12 "errors" 13 "io/fs" 14 "maps" 15 "os" 16 "path/filepath" 17 "slices" 18 "testing" 19 "time" 20 ) 21 22 func sha(s string) string { 23 sum := sha256.Sum256([]byte(s)) 24 return hex.EncodeToString(sum[:]) 25 } 26 27 func mustOpen(t *testing.T, root string, opts ...Option) *FileStore { 28 t.Helper() 29 store, err := Open(root, opts...) 30 if err != nil { 31 t.Fatalf("Open: %v", err) 32 } 33 return store 34 } 35 36 func createdStore(t *testing.T, root string, opts ...Option) *FileStore { 37 t.Helper() 38 store := mustOpen(t, root, opts...) 39 if err := store.CreateRoot(); err != nil { 40 t.Fatalf("CreateRoot: %v", err) 41 } 42 return store 43 } 44 45 // newStore opens a FileStore on a fresh root, creates the root, and returns 46 // the store with the root path. 47 func newStore(t *testing.T) (*FileStore, string) { 48 t.Helper() 49 root := filepath.Join(t.TempDir(), "store") 50 return createdStore(t, root), root 51 } 52 53 func put(t *testing.T, store *FileStore, rel, data string) { 54 t.Helper() 55 if err := store.Put(rel, []byte(data)); err != nil { 56 t.Fatalf("Put(%q): %v", rel, err) 57 } 58 } 59 60 func requireRefused(t *testing.T, err error) { 61 t.Helper() 62 var fse *FileStoreError 63 if !errors.As(err, &fse) { 64 t.Fatalf("expected *FileStoreError, got %T: %v", err, err) 65 } 66 } 67 68 // requireNotCreated fails the test unless err matches fs.ErrNotExist and dir 69 // is still absent: the write failed instead of creating dir. 70 func requireNotCreated(t *testing.T, err error, dir string) { 71 t.Helper() 72 if !errors.Is(err, fs.ErrNotExist) { 73 t.Fatalf("err = %v; want one matching fs.ErrNotExist", err) 74 } 75 if _, statErr := os.Lstat(dir); !errors.Is(statErr, fs.ErrNotExist) { 76 t.Fatalf("%s exists after the failed write (lstat: %v)", dir, statErr) 77 } 78 } 79 80 func TestFileStorePutWritesTextAndCreatesOwnerOnlyParents(t *testing.T) { 81 store, root := newStore(t) 82 put(t, store, "projects/foo/notes.md", "héllo") 83 84 dest := filepath.Join(root, "projects", "foo", "notes.md") 85 got, err := os.ReadFile(dest) 86 if err != nil || string(got) != "héllo" { 87 t.Fatalf("read back %q, %v", got, err) 88 } 89 if st, _ := os.Stat(filepath.Dir(dest)); st.Mode().Perm() != 0o700 { 90 t.Fatalf("parent mode = %v, want 0o700", st.Mode().Perm()) 91 } 92 if st, _ := os.Stat(dest); st.Mode().Perm() != 0o600 { 93 t.Fatalf("file mode = %v, want 0o600", st.Mode().Perm()) 94 } 95 } 96 97 func TestFileStorePutExecutableSetsOwnerExec(t *testing.T) { 98 store, root := newStore(t) 99 if err := store.Put("run.sh", []byte("#!/bin/sh\necho hi\n"), Executable()); err != nil { 100 t.Fatalf("Put: %v", err) 101 } 102 if st, _ := os.Stat(filepath.Join(root, "run.sh")); st.Mode().Perm() != 0o700 { 103 t.Fatalf("mode = %v, want 0o700", st.Mode().Perm()) 104 } 105 } 106 107 func TestFileStorePutIsAtomicAndLeavesNoTempOnSuccess(t *testing.T) { 108 store, root := newStore(t) 109 put(t, store, "a.md", "first") 110 put(t, store, "a.md", "second") 111 112 if got, _ := os.ReadFile(filepath.Join(root, "a.md")); string(got) != "second" { 113 t.Fatalf("got %q, want second", got) 114 } 115 if entries, _ := os.ReadDir(root); len(entries) != 1 || entries[0].Name() != "a.md" { 116 t.Fatalf("dir entries = %v", entries) 117 } 118 } 119 120 func TestFileStoreEveryVerbRefusesAnEscapingPath(t *testing.T) { 121 store, root := newStore(t) 122 secret := filepath.Join(filepath.Dir(root), "secret.md") 123 if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil { 124 t.Fatal(err) 125 } 126 put(t, store, "ok.md", "ok") 127 128 for _, bad := range []string{"../secret.md", "a/../../secret.md"} { 129 requireRefused(t, store.Put(bad, []byte("nope"))) 130 _, err := store.Get(bad) 131 requireRefused(t, err) 132 _, err = store.Ls(bad) 133 requireRefused(t, err) 134 _, err = store.HashTree(bad) 135 requireRefused(t, err) 136 _, err = store.HashFile(bad) 137 requireRefused(t, err) 138 _, err = store.FindSymlinks(bad) 139 requireRefused(t, err) 140 requireRefused(t, store.Move("ok.md", bad)) 141 requireRefused(t, store.Move(bad, "landed.md")) 142 requireRefused(t, store.Remove(bad)) 143 } 144 145 if got, _ := os.ReadFile(secret); string(got) != "top secret" { 146 t.Fatalf("secret was modified: %q", got) 147 } 148 // Nothing materialised outside the root either — not even a temp file. 149 entries, _ := os.ReadDir(filepath.Dir(root)) 150 names := make([]string, len(entries)) 151 for i, e := range entries { 152 names[i] = e.Name() 153 } 154 if !slices.Equal(names, []string{"secret.md", "store"}) { 155 t.Fatalf("outside the root: %v", names) 156 } 157 } 158 159 func TestFileStoreGetDoesNotFollowASymlinkLeaf(t *testing.T) { 160 store, root := newStore(t) 161 put(t, store, "real.md", "original") 162 if err := os.Symlink(filepath.Join(root, "real.md"), filepath.Join(root, "link.md")); err != nil { 163 t.Fatal(err) 164 } 165 166 _, err := store.Get("link.md") 167 requireRefused(t, err) 168 // Put replaces the symlink atomically rather than following it. 169 put(t, store, "link.md", "new") 170 if got, _ := os.ReadFile(filepath.Join(root, "real.md")); string(got) != "original" { 171 t.Fatalf("real.md was overwritten: %q", got) 172 } 173 if st, _ := os.Lstat(filepath.Join(root, "link.md")); st.Mode()&fs.ModeSymlink != 0 { 174 t.Fatal("link.md is still a symlink") 175 } 176 if got, _ := os.ReadFile(filepath.Join(root, "link.md")); string(got) != "new" { 177 t.Fatalf("link.md = %q", got) 178 } 179 } 180 181 func TestFileStoreGetReturnsNilWhenAbsent(t *testing.T) { 182 store, _ := newStore(t) 183 got, err := store.Get("absent.md") 184 if got != nil || err != nil { 185 t.Fatalf("absent: got %v, %v", got, err) 186 } 187 put(t, store, "present.md", "here") 188 got, err = store.Get("present.md") 189 if err != nil || string(got) != "here" { 190 t.Fatalf("present: got %q, %v", got, err) 191 } 192 } 193 194 func TestFileStoreLsListsRecursivelyAndNeverDescendsSymlinks(t *testing.T) { 195 store, root := newStore(t) 196 put(t, store, "a.md", "alpha") 197 put(t, store, "sub/b.md", "bravo") 198 outside := filepath.Join(filepath.Dir(root), "outside") 199 if err := os.Mkdir(outside, 0o755); err != nil { 200 t.Fatal(err) 201 } 202 if err := os.WriteFile(filepath.Join(outside, "s.md"), []byte("secret"), 0o644); err != nil { 203 t.Fatal(err) 204 } 205 if err := os.Symlink(outside, filepath.Join(root, "lnkdir")); err != nil { 206 t.Fatal(err) 207 } 208 if err := os.Symlink(filepath.Join(outside, "s.md"), filepath.Join(root, "lnk.md")); err != nil { 209 t.Fatal(err) 210 } 211 212 want := map[string]bool{"a.md": true, "sub/b.md": true} 213 if got, err := store.Ls("/"); err != nil || !maps.Equal(got, want) { 214 t.Fatalf("Ls(/) = %v, %v; want %v", got, err, want) 215 } 216 if got, err := store.Ls("sub"); err != nil || !maps.Equal(got, map[string]bool{"sub/b.md": true}) { 217 t.Fatalf("Ls(sub) = %v, %v", got, err) 218 } 219 if got, err := store.Ls("absent"); err != nil || len(got) != 0 { 220 t.Fatalf("Ls(absent) = %v, %v; want empty map", got, err) 221 } 222 if got, err := store.Ls("a.md/under"); err != nil || len(got) != 0 { 223 t.Fatalf("Ls(a.md/under) = %v, %v; want empty map", got, err) 224 } 225 _, err := store.Ls("a.md") 226 requireRefused(t, err) 227 } 228 229 func TestFileStoreFindSymlinksReportsEverySymlink(t *testing.T) { 230 store, root := newStore(t) 231 put(t, store, "a.md", "alpha") 232 put(t, store, "sub/b.md", "bravo") 233 if got, err := store.FindSymlinks("/"); err != nil || len(got) != 0 { 234 t.Fatalf("FindSymlinks on clean store = %v, %v; want empty map", got, err) 235 } 236 237 outside := filepath.Join(filepath.Dir(root), "outside") 238 if err := os.Mkdir(outside, 0o755); err != nil { 239 t.Fatal(err) 240 } 241 if err := os.WriteFile(filepath.Join(outside, "s.md"), []byte("secret"), 0o644); err != nil { 242 t.Fatal(err) 243 } 244 if err := os.Symlink(outside, filepath.Join(root, "lnkdir")); err != nil { 245 t.Fatal(err) 246 } 247 if err := os.Symlink(filepath.Join(outside, "s.md"), filepath.Join(root, "sub", "lnk.md")); err != nil { 248 t.Fatal(err) 249 } 250 if err := os.Symlink(filepath.Join(root, "gone.md"), filepath.Join(root, "dangling.md")); err != nil { 251 t.Fatal(err) 252 } 253 254 want := map[string]bool{"lnkdir": true, "sub/lnk.md": true, "dangling.md": true} 255 if got, err := store.FindSymlinks("/"); err != nil || !maps.Equal(got, want) { 256 t.Fatalf("FindSymlinks(/) = %v, %v; want %v", got, err, want) 257 } 258 if got, err := store.FindSymlinks("sub"); err != nil || !maps.Equal(got, map[string]bool{"sub/lnk.md": true}) { 259 t.Fatalf("FindSymlinks(sub) = %v, %v", got, err) 260 } 261 if got, err := store.FindSymlinks("absent"); err != nil || len(got) != 0 { 262 t.Fatalf("FindSymlinks(absent) = %v, %v; want empty map", got, err) 263 } 264 // The base itself being a symlink is reported, not descended. 265 if got, err := store.FindSymlinks("lnkdir"); err != nil || !maps.Equal(got, map[string]bool{"lnkdir": true}) { 266 t.Fatalf("FindSymlinks(lnkdir) = %v, %v", got, err) 267 } 268 // A regular-file base is refused like any non-directory. 269 _, err := store.FindSymlinks("a.md") 270 requireRefused(t, err) 271 } 272 273 func TestFileStoreHashTreeHashesRegularFilesAndSkipsSymlinks(t *testing.T) { 274 store, root := newStore(t) 275 put(t, store, "a.md", "alpha") 276 put(t, store, "sub/b.md", "bravo") 277 outside := filepath.Join(filepath.Dir(root), "secret.txt") 278 _ = os.WriteFile(outside, []byte("secret"), 0o644) 279 _ = os.Symlink(outside, filepath.Join(root, "link.md")) 280 281 want := map[string]string{"a.md": sha("alpha"), "sub/b.md": sha("bravo")} 282 if got, err := store.HashTree("/"); err != nil || !maps.Equal(got, want) { 283 t.Fatalf("HashTree(/) = %v, %v; want %v", got, err, want) 284 } 285 wantSub := map[string]string{"sub/b.md": sha("bravo")} 286 if got, err := store.HashTree("sub/"); err != nil || !maps.Equal(got, wantSub) { 287 t.Fatalf("HashTree(sub/) = %v, %v; want %v", got, err, wantSub) 288 } 289 } 290 291 func TestFileStoreListingsErrorOnAnUnreadableSubdirectory(t *testing.T) { 292 // An unreadable subdirectory fails the listing rather than dropping out 293 // of it — a caller diffing listings would otherwise read its files as gone. 294 if os.Geteuid() == 0 { 295 t.Skip("permission checks do not bind root") 296 } 297 store, root := newStore(t) 298 put(t, store, "a.md", "alpha") 299 put(t, store, "locked/b.md", "bravo") 300 locked := filepath.Join(root, "locked") 301 if err := os.Chmod(locked, 0o000); err != nil { 302 t.Fatal(err) 303 } 304 defer os.Chmod(locked, 0o700) 305 306 if _, err := store.HashTree("/"); !errors.Is(err, fs.ErrPermission) { 307 t.Fatalf("HashTree = %v; want a permission error", err) 308 } 309 if _, err := store.Ls("/"); !errors.Is(err, fs.ErrPermission) { 310 t.Fatalf("Ls = %v; want a permission error", err) 311 } 312 if _, err := store.FindSymlinks("/"); !errors.Is(err, fs.ErrPermission) { 313 t.Fatalf("FindSymlinks = %v; want a permission error", err) 314 } 315 316 if err := os.Chmod(locked, 0o700); err != nil { 317 t.Fatal(err) 318 } 319 want := map[string]string{"a.md": sha("alpha"), "locked/b.md": sha("bravo")} 320 if got, err := store.HashTree("/"); err != nil || !maps.Equal(got, want) { 321 t.Fatalf("HashTree(/) after chmod = %v, %v; want %v", got, err, want) 322 } 323 } 324 325 func TestFileStoreHashTreeRefusesASymlinkedPrefix(t *testing.T) { 326 // A prefix that exists but is a symlink is refused rather than listed 327 // (or silently empty); a plain file prefix is refused too. 328 store, root := newStore(t) 329 put(t, store, "real/a.md", "alpha") 330 if err := os.Symlink(filepath.Join(root, "real"), filepath.Join(root, "alias")); err != nil { 331 t.Fatal(err) 332 } 333 334 _, err := store.HashTree("alias") 335 requireRefused(t, err) 336 _, err = store.HashTree("real/a.md") 337 requireRefused(t, err) 338 if got, err := store.HashTree("absent"); err != nil || len(got) != 0 { 339 t.Fatalf("HashTree(absent) = %v, %v; want empty map", got, err) 340 } 341 } 342 343 func TestFileStoreHashFileHashesOneFileAndSharesTheCache(t *testing.T) { 344 store, root := newStore(t) 345 realMargin := timestampTrustMarginNs 346 timestampTrustMarginNs = -1_000_000_000_000_000 347 t.Cleanup(func() { timestampTrustMarginNs = realMargin }) 348 hashed := countHashes(t) 349 350 put(t, store, "a.md", "alpha") 351 if got, err := store.HashFile("a.md"); err != nil || got != sha("alpha") { 352 t.Fatalf("HashFile(a.md) = %q, %v", got, err) 353 } 354 if got, err := store.HashFile("absent.md"); err != nil || got != "" { 355 t.Fatalf("HashFile(absent.md) = %q, %v; want empty", got, err) 356 } 357 // HashTree reuses the entry HashFile just cached. 358 want := map[string]string{"a.md": sha("alpha")} 359 if got, err := store.HashTree("/"); err != nil || !maps.Equal(got, want) { 360 t.Fatalf("HashTree = %v, %v; want %v", got, err, want) 361 } 362 if !slices.Equal(*hashed, []string{"a.md"}) { 363 t.Fatalf("hashed %v, want one hash shared across both verbs", *hashed) 364 } 365 366 if err := os.Symlink(filepath.Join(root, "a.md"), filepath.Join(root, "lnk.md")); err != nil { 367 t.Fatal(err) 368 } 369 _, err := store.HashFile("lnk.md") 370 requireRefused(t, err) 371 var fse *FileStoreError 372 if !errors.As(err, &fse) || fse.Reason != ReasonIsASymlink { 373 t.Fatalf("HashFile(lnk.md) = %v; want an is-a-symlink refusal", err) 374 } 375 } 376 377 func TestFileStoreMoveRenamesWithinRootAndRefusesExistingDest(t *testing.T) { 378 store, _ := newStore(t) 379 put(t, store, "a.md", "alpha") 380 if err := store.Move("a.md", "nested/b.md"); err != nil { 381 t.Fatalf("Move: %v", err) 382 } 383 if got, _ := store.Get("nested/b.md"); string(got) != "alpha" { 384 t.Fatalf("Get nested/b.md = %q", got) 385 } 386 if got, _ := store.Get("a.md"); got != nil { 387 t.Fatal("a.md still present") 388 } 389 put(t, store, "c.md", "charlie") 390 requireRefused(t, store.Move("nested/b.md", "c.md")) 391 } 392 393 func TestFileStoreRootPathIsBannedFromTheInterface(t *testing.T) { 394 // A rel resolving to the root: Put/Get refuse, Move/Remove do nothing. 395 store, _ := newStore(t) 396 put(t, store, "x.md", "x") 397 398 if err := store.Remove("/"); err != nil { 399 t.Fatalf("Remove(/): %v", err) 400 } 401 if got, err := store.Get("x.md"); err != nil || string(got) != "x" { 402 t.Fatalf("x.md after Remove(/) = %q, %v", got, err) 403 } 404 if err := store.Move("/", "elsewhere"); err != nil { 405 t.Fatalf("Move(/, elsewhere): %v", err) 406 } 407 if err := store.Move("x.md", "/"); err != nil { 408 t.Fatalf("Move(x.md, /): %v", err) 409 } 410 if got, err := store.Ls("/"); err != nil || !maps.Equal(got, map[string]bool{"x.md": true}) { 411 t.Fatalf("Ls = %v, %v; want only x.md", got, err) 412 } 413 414 requireRefused(t, store.Put("/", []byte("data"))) 415 _, err := store.Get("/") 416 requireRefused(t, err) 417 var fse *FileStoreError 418 if !errors.As(err, &fse) || fse.Reason != ReasonNotAFile { 419 t.Fatalf("Get(/) = %v; want a not-a-regular-file refusal", err) 420 } 421 } 422 423 func TestFileStoreRemoveDeletesFileAndSubtree(t *testing.T) { 424 store, root := newStore(t) 425 put(t, store, "a.md", "a") 426 put(t, store, "sub/b.md", "b") 427 428 if err := store.Remove("a.md"); err != nil { 429 t.Fatal(err) 430 } 431 if _, err := os.Stat(filepath.Join(root, "a.md")); !errors.Is(err, fs.ErrNotExist) { 432 t.Fatal("a.md still exists") 433 } 434 if err := store.Remove("sub"); err != nil { 435 t.Fatal(err) 436 } 437 if _, err := os.Stat(filepath.Join(root, "sub")); !errors.Is(err, fs.ErrNotExist) { 438 t.Fatal("sub/ still exists") 439 } 440 if err := store.Remove("absent.md"); err != nil { 441 t.Fatalf("Remove(absent.md): %v", err) 442 } 443 } 444 445 func TestFileStoreRemoveUnlinksADanglingSymlink(t *testing.T) { 446 store, root := newStore(t) 447 if err := os.Symlink(filepath.Join(root, "nowhere"), filepath.Join(root, "dangle")); err != nil { 448 t.Fatal(err) 449 } 450 451 if err := store.Remove("dangle"); err != nil { 452 t.Fatalf("Remove: %v", err) 453 } 454 if _, err := os.Lstat(filepath.Join(root, "dangle")); !errors.Is(err, fs.ErrNotExist) { 455 t.Fatalf("dangling symlink still present: %v", err) 456 } 457 } 458 459 func TestFileStoreOpenErrorsOnAnUnreadableMountPath(t *testing.T) { 460 // A mount path the process cannot stat errors at Open — it is never read 461 // as absent, which would mark a real directory ours to remove on Dispose. 462 if os.Geteuid() == 0 { 463 t.Skip("permission checks do not bind root") 464 } 465 locked := filepath.Join(t.TempDir(), "locked") 466 if err := os.MkdirAll(filepath.Join(locked, "store"), 0o755); err != nil { 467 t.Fatal(err) 468 } 469 if err := os.Chmod(locked, 0o000); err != nil { 470 t.Fatal(err) 471 } 472 defer os.Chmod(locked, 0o700) 473 474 _, err := Open(filepath.Join(locked, "store")) 475 if !errors.Is(err, fs.ErrPermission) { 476 t.Fatalf("Open = %v; want a permission error", err) 477 } 478 var fse *FileStoreError 479 if errors.As(err, &fse) { 480 t.Fatalf("Open = %v; a deployment failure must not be a *FileStoreError", err) 481 } 482 } 483 484 func TestFileStoreDisposeRemovesRootOnlyIfCreated(t *testing.T) { 485 store, fresh := newStore(t) 486 put(t, store, "x.md", "x") 487 if r := store.Root(); !r.RemovedOnDispose || r.Path != fresh { 488 t.Fatalf("Root = %+v", r) 489 } 490 if err := store.Dispose(); err != nil { 491 t.Fatal(err) 492 } 493 if _, err := os.Stat(fresh); !errors.Is(err, fs.ErrNotExist) { 494 t.Fatalf("root %s not removed", fresh) 495 } 496 497 old := filepath.Join(t.TempDir(), "old") 498 if err := os.Mkdir(old, 0o755); err != nil { 499 t.Fatal(err) 500 } 501 store = mustOpen(t, old) 502 put(t, store, "y.md", "y") 503 if r := store.Root(); r.RemovedOnDispose { 504 t.Fatal("Root marked a pre-existing root for removal") 505 } 506 if err := store.Dispose(); err != nil { 507 t.Fatal(err) 508 } 509 if got, err := os.ReadFile(filepath.Join(old, "y.md")); err != nil || string(got) != "y" { 510 t.Fatalf("y.md = %q, %v", got, err) 511 } 512 } 513 514 func TestFileStorePutWritesArchiveBytesVerbatim(t *testing.T) { 515 // Archive-shaped bytes are just bytes — never extracted. 516 store, _ := newStore(t) 517 var buf bytes.Buffer 518 zw := zip.NewWriter(&buf) 519 w, _ := zw.Create("pdf/SKILL.md") 520 _, _ = w.Write([]byte("# pdf")) 521 _ = zw.Close() 522 blob := buf.Bytes() 523 524 if err := store.Put("bundle.zip", blob); err != nil { 525 t.Fatalf("Put: %v", err) 526 } 527 got, err := store.Get("bundle.zip") 528 if err != nil || !bytes.Equal(got, blob) { 529 t.Fatalf("bundle.zip is not byte-identical to the input: %v", err) 530 } 531 want := map[string]string{"bundle.zip": sha(string(blob))} 532 if tree, _ := store.HashTree("/"); !maps.Equal(tree, want) { 533 t.Fatalf("HashTree = %v, want %v", tree, want) 534 } 535 } 536 537 func TestFileStorePutRefusesDirectoryShapedPaths(t *testing.T) { 538 store, _ := newStore(t) 539 for _, dir := range []string{"bundles/v1/", "dir/.", ".", "/", ""} { 540 requireRefused(t, store.Put(dir, []byte("data"))) 541 } 542 if got, err := store.HashTree("/"); err != nil || len(got) != 0 { 543 t.Fatalf("HashTree = %v, %v; want nothing written", got, err) 544 } 545 } 546 547 func TestFileStoreUTF8RestrictionRefusesBinaryContent(t *testing.T) { 548 root := filepath.Join(t.TempDir(), "store") 549 store := createdStore(t, root, WithUTF8()) 550 binary := []byte{0xff, 0xfe, 0x00, ' ', 'n', 'o', 't', ' ', 'u', 't', 'f', '8'} 551 552 // A Put of invalid bytes is refused and writes nothing. 553 requireRefused(t, store.Put("blob.bin", binary)) 554 if _, err := os.Stat(filepath.Join(root, "blob.bin")); !errors.Is(err, fs.ErrNotExist) { 555 t.Fatal("blob.bin materialised despite the refusal") 556 } 557 558 // Conforming content is unaffected. 559 put(t, store, "note.md", "héllo") 560 if got, err := store.Get("note.md"); err != nil || string(got) != "héllo" { 561 t.Fatalf("Get note.md = %q, %v", got, err) 562 } 563 564 // A file smuggled in behind the store's back (written directly to disk) 565 // is refused at Get. 566 if err := os.WriteFile(filepath.Join(root, "smuggled.bin"), binary, 0o600); err != nil { 567 t.Fatal(err) 568 } 569 _, err := store.Get("smuggled.bin") 570 requireRefused(t, err) 571 572 // An unrestricted store keeps returning raw bytes. 573 loose := createdStore(t, filepath.Join(t.TempDir(), "store2")) 574 if err := loose.Put("blob.bin", binary); err != nil { 575 t.Fatalf("Put on unrestricted store: %v", err) 576 } 577 if got, err := loose.Get("blob.bin"); err != nil || !bytes.Equal(got, binary) { 578 t.Fatalf("Get blob.bin = %q, %v", got, err) 579 } 580 } 581 582 func TestFileStoreIsPathLegal(t *testing.T) { 583 for path, want := range map[string]bool{ 584 "/mnt/memory/notes": true, 585 "mnt/relative": false, 586 "/mnt/../../etc/cron.d": false, 587 "": false, 588 } { 589 if got := IsPathLegal(path); got != want { 590 t.Errorf("IsPathLegal(%q) = %v, want %v", path, got, want) 591 } 592 } 593 } 594 595 // countHashes replaces hashFile with a counting wrapper for the duration of 596 // the test and returns the slice of hashed base names. 597 func countHashes(t *testing.T) *[]string { 598 t.Helper() 599 var hashed []string 600 realHash := hashFile 601 hashFile = func(path string) (string, error) { 602 hashed = append(hashed, filepath.Base(path)) 603 return realHash(path) 604 } 605 t.Cleanup(func() { hashFile = realHash }) 606 return &hashed 607 } 608 609 func TestFileStoreHashTreeRehashesOnlyWhatChanged(t *testing.T) { 610 // A file whose stat identity (mtime, ctime, size) is unchanged since the 611 // last walk is served from the cache; a changed file is re-read. The 612 // trust margin is disabled here so fresh files cache immediately; a 613 // writer that restores mtime and size (rsync -t style) is still 614 // re-hashed, because userspace cannot restore ctime. 615 store, root := newStore(t) 616 realMargin := timestampTrustMarginNs 617 timestampTrustMarginNs = -1_000_000_000_000_000 618 t.Cleanup(func() { timestampTrustMarginNs = realMargin }) 619 hashed := countHashes(t) 620 621 put(t, store, "a.md", "alpha") 622 put(t, store, "b.md", "bravo") 623 want := map[string]string{"a.md": sha("alpha"), "b.md": sha("bravo")} 624 if got, err := store.HashTree("/"); err != nil || !maps.Equal(got, want) { 625 t.Fatalf("HashTree = %v, %v; want %v", got, err, want) 626 } 627 slices.Sort(*hashed) 628 if !slices.Equal(*hashed, []string{"a.md", "b.md"}) { 629 t.Fatalf("first walk hashed %v, want both files", *hashed) 630 } 631 632 *hashed = nil 633 if got, err := store.HashTree("/"); err != nil || !maps.Equal(got, want) { 634 t.Fatalf("HashTree = %v, %v; want %v", got, err, want) 635 } 636 if len(*hashed) != 0 { 637 t.Fatalf("unchanged walk hashed %v, want nothing", *hashed) 638 } 639 640 // An mtime-and-size-preserving rewrite: same length, mtime restored. The 641 // sleep guarantees the rewrite's ctime lands on a later coarse-clock tick 642 // than the original's — ctime is the one stamp the rewrite cannot restore. 643 time.Sleep(50 * time.Millisecond) 644 b := filepath.Join(root, "b.md") 645 st, err := os.Stat(b) 646 if err != nil { 647 t.Fatal(err) 648 } 649 if err := os.WriteFile(b, []byte("BRAVO"), 0o600); err != nil { 650 t.Fatal(err) 651 } 652 if err := os.Chtimes(b, st.ModTime(), st.ModTime()); err != nil { 653 t.Fatal(err) 654 } 655 got, err := store.HashTree("/") 656 if err != nil || got["b.md"] != sha("BRAVO") { 657 t.Fatalf("HashTree after rewrite = %v, %v; want b.md re-read", got, err) 658 } 659 if !slices.Equal(*hashed, []string{"b.md"}) { 660 t.Fatalf("rewrite walk hashed %v, want only b.md", *hashed) 661 } 662 } 663 664 func TestFileStoreHashTreeDistrustsFreshStamps(t *testing.T) { 665 // Filesystems stamp times with coarse clocks, so a same-tick rewrite can 666 // reuse the exact stat identity of the version just hashed. A file whose 667 // stamps are within the trust margin of the walk is never cached — 668 // pinned by freezing the walk clock at the file's own stamp time. 669 store, root := newStore(t) 670 hashed := countHashes(t) 671 672 put(t, store, "racy.md", "AAAAAA") 673 racy := filepath.Join(root, "racy.md") 674 st, err := os.Lstat(racy) 675 if err != nil { 676 t.Fatal(err) 677 } 678 stamp := ctimeNs(st) 679 realNow := nowNs 680 nowNs = func() int64 { return stamp } 681 t.Cleanup(func() { nowNs = realNow }) 682 683 if got, err := store.HashTree("/"); err != nil || got["racy.md"] != sha("AAAAAA") { 684 t.Fatalf("HashTree = %v, %v", got, err) 685 } 686 if got, err := store.HashTree("/"); err != nil || got["racy.md"] != sha("AAAAAA") { 687 t.Fatalf("HashTree = %v, %v", got, err) 688 } 689 // Both walks hashed: nothing this fresh is ever cached. 690 if !slices.Equal(*hashed, []string{"racy.md", "racy.md"}) { 691 t.Fatalf("hashed = %v, want both walks to hash", *hashed) 692 } 693 694 // What the margin defends: a same-tick rewrite that preserves the whole 695 // stat identity must be seen by the next walk. 696 if err := os.WriteFile(racy, []byte("BBBBBB"), 0o600); err != nil { 697 t.Fatal(err) 698 } 699 if got, err := store.HashTree("/"); err != nil || got["racy.md"] != sha("BBBBBB") { 700 t.Fatalf("HashTree after rewrite = %v, %v; want the new bytes", got, err) 701 } 702 703 // The margin's width is part of the contract: stamps 1.5s old are still 704 // distrusted (coarse filesystems stamp in whole seconds); 10s-old stamps 705 // are safely past it. 706 *hashed = nil 707 st, err = os.Lstat(racy) 708 if err != nil { 709 t.Fatal(err) 710 } 711 now := ctimeNs(st) 712 nowNs = func() int64 { return now + 1_500_000_000 } 713 if _, err := store.HashTree("/"); err != nil { 714 t.Fatal(err) 715 } 716 if _, err := store.HashTree("/"); err != nil { 717 t.Fatal(err) 718 } 719 if !slices.Equal(*hashed, []string{"racy.md", "racy.md"}) { 720 t.Fatalf("hashed inside the margin = %v, want both walks to hash", *hashed) 721 } 722 nowNs = func() int64 { return now + 10_000_000_000 } 723 if _, err := store.HashTree("/"); err != nil { 724 t.Fatal(err) 725 } 726 if _, err := store.HashTree("/"); err != nil { 727 t.Fatal(err) 728 } 729 if !slices.Equal(*hashed, []string{"racy.md", "racy.md", "racy.md"}) { 730 t.Fatalf("hashed past the margin = %v, want exactly one more hash", *hashed) 731 } 732 } 733 734 func TestCreateRootMakesTheFolderOwnerOnly(t *testing.T) { 735 root := filepath.Join(t.TempDir(), "store") 736 store := mustOpen(t, root) 737 if err := store.CreateRoot(); err != nil { 738 t.Fatalf("CreateRoot: %v", err) 739 } 740 st, err := os.Stat(root) 741 if err != nil { 742 t.Fatal(err) 743 } 744 if got := st.Mode().Perm(); got != 0o700 { 745 t.Fatalf("root mode = %v, want 0700", got) 746 } 747 if err := store.CreateRoot(); err != nil { // already existing is fine 748 t.Fatalf("second CreateRoot: %v", err) 749 } 750 if !store.Root().RemovedOnDispose { 751 t.Fatal("RemovedOnDispose = false, want true: Open found no root, so this store created it") 752 } 753 } 754 755 func TestFileStorePutNeverCreatesAMissingRoot(t *testing.T) { 756 // Only CreateRoot makes the folder: a Put into a store that was never 757 // created, or whose folder was removed, fails and re-creates nothing — 758 // not the root and nothing above it. 759 mnt := filepath.Join(t.TempDir(), "mnt") 760 root := filepath.Join(mnt, "store") 761 store := mustOpen(t, root) 762 763 requireNotCreated(t, store.Put("a.md", []byte("a")), mnt) 764 requireNotCreated(t, store.Put("sub/a.md", []byte("a")), mnt) 765 766 if err := store.CreateRoot(); err != nil { 767 t.Fatalf("CreateRoot: %v", err) 768 } 769 put(t, store, "a.md", "a") 770 if err := os.RemoveAll(root); err != nil { 771 t.Fatal(err) 772 } 773 requireNotCreated(t, store.Put("b.md", []byte("b")), root) 774 requireNotCreated(t, store.Put("sub/b.md", []byte("b")), root) 775 776 if err := os.RemoveAll(mnt); err != nil { 777 t.Fatal(err) 778 } 779 requireNotCreated(t, store.Put("z.md", []byte("z")), mnt) 780 781 if err := store.CreateRoot(); err != nil { 782 t.Fatalf("CreateRoot: %v", err) 783 } 784 put(t, store, "z.md", "z") 785 if got, err := store.Get("z.md"); err != nil || string(got) != "z" { 786 t.Fatalf("Get z.md = %q, %v", got, err) 787 } 788 } 789 790 func TestFileStoreMoveNeverCreatesAMissingRoot(t *testing.T) { 791 store, root := newStore(t) 792 put(t, store, "a.md", "a") 793 if err := os.RemoveAll(root); err != nil { 794 t.Fatal(err) 795 } 796 797 requireNotCreated(t, store.Move("a.md", "sub/b.md"), root) 798 }