package hostfiles import ( "context" "encoding/json" "errors" "os" "path/filepath" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) const pfx = "instances/shop.test/abc/files/g1/" func open(t *testing.T, s release.Store) *Publisher { t.Helper() p, err := Open(context.Background(), s, pfx) if err != nil { t.Fatal(err) } return p } func stage(t *testing.T, p *Publisher, path, body string) { t.Helper() if err := p.Stage(context.Background(), path, []byte(body), 0o600, false); err != nil { t.Fatal(err) } } func commit(t *testing.T, p *Publisher) int64 { t.Helper() seq, err := p.Commit(context.Background()) if err != nil { t.Fatal(err) } return seq } func read(t *testing.T, dir, rel string) string { t.Helper() b, err := os.ReadFile(filepath.Join(dir, rel)) if err != nil { t.Fatal(err) } return string(b) } func TestPublishThenRestoreRoundTrips(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "instance.key", "k1") stage(t, p, "uploads/a/b.txt", "hello") if seq := commit(t, p); seq != 2 { t.Fatalf("seq = %d, want 2", seq) } dir := t.TempDir() m, st, err := Restore(ctx, s, pfx, dir, nil) if err != nil { t.Fatal(err) } if m.Seq != 2 || st.Written != 2 { t.Fatalf("manifest seq %d, written %d", m.Seq, st.Written) } if read(t, dir, "instance.key") != "k1" || read(t, dir, "uploads/a/b.txt") != "hello" { t.Fatal("content differs") } fi, _ := os.Stat(filepath.Join(dir, "instance.key")) if fi.Mode().Perm() != 0o600 { t.Fatalf("mode = %v, want 0600", fi.Mode().Perm()) } } func TestALaterWriteWinsAndAnUnchangedOneTakesNoSequence(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "instance.key", "old") commit(t, p) stage(t, p, "instance.key", "new") stage(t, p, "instance.key", "new") // unchanged: no new sequence number if seq := commit(t, p); seq != 2 { t.Fatalf("seq = %d, want 2", seq) } dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } if got := read(t, dir, "instance.key"); got != "new" { t.Fatalf("restored %q, want new", got) } } // A1-4: a stalled upload of the OLD content that lands after the new // content was published must not come back. func TestALateUploadCannotResurrectOldContent(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "instance.key", "new") commit(t, p) // The late arrival: any object at all under objects/, with a higher number. if _, err := s.Put(ctx, pfx+"objects/0000000000000099-deadbeef", []byte("old"), ""); err != nil { t.Fatal(err) } dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } if got := read(t, dir, "instance.key"); got != "new" { t.Fatalf("restored %q, want new", got) } } func TestADeleteIsATombstoneAndRestoreRemovesTheFile(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "a.txt", "a") stage(t, p, "b.txt", "b") commit(t, p) dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } if err := p.StageDelete("a.txt"); err != nil { t.Fatal(err) } commit(t, p) _, st, err := Restore(ctx, s, pfx, dir, nil) if err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "a.txt")); !os.IsNotExist(err) { t.Fatalf("a.txt still there: %v", err) } if st.Removed != 1 || read(t, dir, "b.txt") != "b" { t.Fatalf("removed %d", st.Removed) } if got := p.Paths(); len(got) != 1 || got[0] != "b.txt" { t.Fatalf("Paths = %v", got) } } func TestASecondWriterIsSupersededNotMerged(t *testing.T) { s := release.NewDirStore(t.TempDir()) a, b := open(t, s), open(t, s) stage(t, a, "x", "from a") commit(t, a) stage(t, b, "x", "from b") if _, err := b.Commit(context.Background()); !errors.Is(err, ErrSuperseded) { t.Fatalf("err = %v, want ErrSuperseded", err) } dir := t.TempDir() if _, _, err := Restore(context.Background(), s, pfx, dir, nil); err != nil { t.Fatal(err) } if got := read(t, dir, "x"); got != "from a" { t.Fatalf("restored %q, want from a", got) } } func TestAReopenedPublisherContinuesTheSequence(t *testing.T) { s := release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "x", "1") commit(t, p) q := open(t, s) stage(t, q, "y", "2") if seq := commit(t, q); seq != 2 { t.Fatalf("seq = %d, want 2", seq) } } // Review Focus 1. func TestCleanPathRefusesEscapes(t *testing.T) { for _, bad := range []string{"", "/etc/passwd", "../x", "a/../../x", "a/../b", "./a", "a//b", "a/", ".", "..", "a\x00b"} { if _, err := CleanPath(bad); err == nil { t.Errorf("CleanPath(%q) accepted", bad) } } for _, ok := range []string{"a", "a/b.txt", ".hidden", "a b/c", `back\slash`} { if got, err := CleanPath(ok); err != nil || got != ok { t.Errorf("CleanPath(%q) = %q, %v", ok, got, err) } } } func TestStageAndRestoreRefuseAnEscapingPath(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) if err := p.Stage(ctx, "../evil", []byte("x"), 0o600, false); err == nil { t.Fatal("Stage accepted ../evil") } // A manifest written by something else with a hostile path. body := `{"v":2,"seq":1,"entries":{"../evil":{"object":"objects/0000000000000001-aa","sha256":"aa","seq":1}},"platform":{}}` hostile := "instances/h/i/files/g/" if _, err := s.Put(ctx, ManifestKey(hostile), []byte(body), ""); err != nil { t.Fatal(err) } parent := t.TempDir() dir := filepath.Join(parent, "files") if err := os.Mkdir(dir, 0o755); err != nil { t.Fatal(err) } if _, _, err := Restore(ctx, s, hostile, dir, nil); err == nil { t.Fatal("Restore accepted a manifest with ../evil") } if _, err := os.Stat(filepath.Join(parent, "evil")); !os.IsNotExist(err) { t.Fatal("../evil was written") } } func TestRestoreRefusesContentThatDoesNotMatchItsHash(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "x", "good") commit(t, p) m, _, err := Load(ctx, s, pfx) if err != nil { t.Fatal(err) } key := m.Entries["x"].Object _, etag, _ := s.Get(ctx, key) if _, err := s.Put(ctx, key, []byte("tampered"), etag); err != nil { t.Fatal(err) } if _, _, err := Restore(ctx, s, pfx, t.TempDir(), nil); err == nil || !strings.Contains(err.Error(), "sha256") { t.Fatalf("err = %v, want a sha256 mismatch", err) } } func TestAnAbsentManifestRestoresNothing(t *testing.T) { m, st, err := Restore(context.Background(), release.NewDirStore(t.TempDir()), pfx, t.TempDir(), nil) if err != nil || m.Seq != 0 || st.Written != 0 { t.Fatalf("m=%+v st=%+v err=%v", m, st, err) } } func TestPlatformItemsAreNotWrittenIntoTheFilesDirectory(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) if err := p.StagePlatform(ctx, "wake.json", []byte(`{"v":1}`)); err != nil { t.Fatal(err) } commit(t, p) dir := t.TempDir() m, _, err := Restore(ctx, s, pfx, dir, nil) if err != nil { t.Fatal(err) } if entries, _ := os.ReadDir(dir); len(entries) != 0 { t.Fatalf("files directory has %d entries, want 0", len(entries)) } got, err := ReadPlatform(ctx, s, pfx, m, "wake.json", nil) if err != nil || string(got) != `{"v":1}` { t.Fatalf("ReadPlatform = %q, %v", got, err) } if _, err := ReadPlatform(ctx, s, pfx, m, "absent", nil); !errors.Is(err, os.ErrNotExist) { t.Fatalf("absent item: err = %v, want os.ErrNotExist", err) } } // Review Focus 3: a skipped file stays in the manifest, visibly. func TestASkippedFileIsRecordedAndReportedOnRestore(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) if err := p.StageSkipped("huge.bin", "too-large"); err != nil { t.Fatal(err) } commit(t, p) _, st, err := Restore(ctx, s, pfx, t.TempDir(), nil) if err != nil { t.Fatal(err) } if len(st.Skipped) != 1 || st.Skipped[0] != "huge.bin" { t.Fatalf("Skipped = %v", st.Skipped) } } // A destination that already holds a symlink pointing elsewhere must not // let a write or a delete reach through it. func TestRestoreNeverWritesOrDeletesThroughASymlink(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "sub/x", "ours") stage(t, p, "gone/y", "temp") commit(t, p) if err := p.StageDelete("gone/y"); err != nil { t.Fatal(err) } commit(t, p) outside, dir := t.TempDir(), t.TempDir() if err := os.WriteFile(filepath.Join(outside, "y"), []byte("not ours"), 0o600); err != nil { t.Fatal(err) } for _, link := range []string{"sub", "gone"} { if err := os.Symlink(outside, filepath.Join(dir, link)); err != nil { t.Fatal(err) } } if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(outside, "x")); !os.IsNotExist(err) { t.Fatal("a file was written through the symlink") } if got := read(t, outside, "y"); got != "not ours" { t.Fatalf("a file outside was changed to %q", got) } if fi, err := os.Lstat(filepath.Join(dir, "sub")); err != nil || !fi.IsDir() { t.Fatalf("sub is not a real directory: %v %v", fi, err) } if got := read(t, dir, "sub/x"); got != "ours" { t.Fatalf("sub/x = %q", got) } } func TestRestoreHandlesADirectoryBecomingAFileAndBack(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "a/b", "child") commit(t, p) prior := t.TempDir() // a copy restored while "a" was a directory if _, _, err := Restore(ctx, s, pfx, prior, nil); err != nil { t.Fatal(err) } // "a" becomes a file. if err := p.StageDelete("a/b"); err != nil { t.Fatal(err) } stage(t, p, "a", "now a file") commit(t, p) for name, dir := range map[string]string{"an empty destination": t.TempDir(), "a previously restored one": prior} { if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatalf("%s: %v", name, err) } if got := read(t, dir, "a"); got != "now a file" { t.Fatalf("%s: a = %q", name, got) } } // And back: "a" becomes a directory again. if err := p.StageDelete("a"); err != nil { t.Fatal(err) } stage(t, p, "a/c", "child again") commit(t, p) for name, dir := range map[string]string{"an empty destination": t.TempDir(), "a previously restored one": prior} { if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatalf("%s: %v", name, err) } if got := read(t, dir, "a/c"); got != "child again" { t.Fatalf("%s: a/c = %q", name, got) } } } func TestRestoreIntoAMissingDirectoryIsAnError(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "x", "x") commit(t, p) if _, _, err := Restore(ctx, s, pfx, filepath.Join(t.TempDir(), "absent"), nil); err == nil { t.Fatal("restored into a directory that does not exist") } } // Plan round 2, finding 3: a restore that returns nil has made its names // durable, and one that could not must say so. func TestRestoreSyncsEveryDirectoryItChangedBeforeReturning(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "top.txt", "t") stage(t, p, "a/b/deep.txt", "d") stage(t, p, "gone/old.txt", "o") commit(t, p) dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } if err := p.StageDelete("gone/old.txt"); err != nil { t.Fatal(err) } commit(t, p) var synced []string orig := syncDir t.Cleanup(func() { syncDir = orig }) syncDir = func(root *os.Root, rel string) error { // Everything must already be in place when the first sync runs. if len(synced) == 0 { if _, err := os.Stat(filepath.Join(dir, "gone", "old.txt")); !os.IsNotExist(err) { t.Error("a directory was synced before the tombstone was applied") } } synced = append(synced, rel) return orig(root, rel) } if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } want := []string{"a/b", "a", "gone", "."} if !reflect.DeepEqual(synced, want) { t.Fatalf("synced %v, want %v (deepest first, the root last)", synced, want) } syncDir = func(*os.Root, string) error { return errors.New("disk says no") } if _, _, err := Restore(ctx, s, pfx, dir, nil); err == nil || !strings.Contains(err.Error(), "disk says no") { t.Fatalf("err = %v; a failed directory sync must fail the restore", err) } } // Plan round 3: a restore that failed at its directory sync has already // removed the file. The retry finds nothing to remove, and must still // make that removal durable before it reports success. func TestARetriedRestoreStillSyncsADeletionAnEarlierAttemptMade(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "gone/old.txt", "o") commit(t, p) dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } if err := p.StageDelete("gone/old.txt"); err != nil { t.Fatal(err) } commit(t, p) orig := syncDir t.Cleanup(func() { syncDir = orig }) syncDir = func(*os.Root, string) error { return errors.New("disk says no") } if _, _, err := Restore(ctx, s, pfx, dir, nil); err == nil { t.Fatal("the first attempt must fail") } if _, err := os.Stat(filepath.Join(dir, "gone", "old.txt")); !os.IsNotExist(err) { t.Fatal("the first attempt should have removed the file before failing") } var synced []string syncDir = func(root *os.Root, rel string) error { synced = append(synced, rel); return orig(root, rel) } _, st, err := Restore(ctx, s, pfx, dir, nil) if err != nil { t.Fatal(err) } if st.Removed != 0 { t.Fatalf("Removed = %d; the retry had nothing left to remove", st.Removed) } if want := []string{"gone", "."}; !reflect.DeepEqual(synced, want) { t.Fatalf("the retry synced %v, want %v", synced, want) } } // A deeper directory replaced by a file: the directories that vanish // under the new file are not synced (they do not exist), and the restore // still succeeds and syncs what survives. func TestRestoreHandlesADeepDirectoryBecomingAFile(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "a/b/c", "leaf") commit(t, p) dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } if err := p.StageDelete("a/b/c"); err != nil { t.Fatal(err) } stage(t, p, "a", "now a file") commit(t, p) var synced []string orig := syncDir t.Cleanup(func() { syncDir = orig }) syncDir = func(root *os.Root, rel string) error { synced = append(synced, rel); return orig(root, rel) } for name, d := range map[string]string{"a previously restored destination": dir, "an empty one": t.TempDir()} { synced = nil if _, _, err := Restore(ctx, s, pfx, d, nil); err != nil { t.Fatalf("%s: %v", name, err) } if got := read(t, d, "a"); got != "now a file" { t.Fatalf("%s: a = %q", name, got) } if want := []string{"."}; !reflect.DeepEqual(synced, want) { t.Fatalf("%s: synced %v, want %v: a/b and a are gone, the root records it", name, synced, want) } } } // A store whose manifest Put is applied and then reports a lost response, once. type lostResponseStore struct { release.Store armed bool getFail bool // the next Get of the manifest fails once putFail bool // the next Put of the manifest fails once, unapplied } func (l *lostResponseStore) Get(ctx context.Context, key string) ([]byte, string, error) { if l.getFail && strings.HasSuffix(key, "manifest.json") { l.getFail = false return nil, "", errors.New("read timed out") } return l.Store.Get(ctx, key) } func (l *lostResponseStore) Put(ctx context.Context, key string, body []byte, etag string) (string, error) { if l.putFail && strings.HasSuffix(key, "manifest.json") { l.putFail = false return "", errors.New("connection refused") } got, err := l.Store.Put(ctx, key, body, etag) if err == nil && l.armed && strings.HasSuffix(key, "manifest.json") { l.armed = false return "", errors.New("connection reset") } return got, err } func TestALostCommitResponseIsNotASupersede(t *testing.T) { ctx := context.Background() s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())} p := open(t, s) stage(t, p, "x", "1") s.armed = true if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) { t.Fatalf("first commit err = %v, want the transport error", err) } seq, err := p.Commit(ctx) if err != nil || seq != 1 { t.Fatalf("second commit = %d, %v", seq, err) } stage(t, p, "y", "2") if seq := commit(t, p); seq != 2 { t.Fatalf("seq = %d, want 2", seq) } } func TestRestoreMasksTheModeToPermissionBits(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "x", "x") commit(t, p) m, etag, _, err := load(ctx, s, pfx) if err != nil { t.Fatal(err) } e := m.Entries["x"] e.Mode = 0o4755 m.Entries["x"] = e body, _ := json.Marshal(m) if _, err := s.Put(ctx, ManifestKey(pfx), body, etag); err != nil { t.Fatal(err) } dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { t.Fatal(err) } fi, err := os.Stat(filepath.Join(dir, "x")) if err != nil { t.Fatal(err) } if fi.Mode() != 0o755 { t.Fatalf("mode = %v, want -rwxr-xr-x with no special bits", fi.Mode()) } } func TestOverlappingLiveEntriesKeepTheNewerOne(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "a/b", "child") stage(t, p, "a", "file") // newer than the child commit(t, p) dir := t.TempDir() _, st, err := Restore(ctx, s, pfx, dir, nil) if err != nil { t.Fatal(err) } if got := read(t, dir, "a"); got != "file" || !reflect.DeepEqual(st.Superseded, []string{"a/b"}) || st.Written != 1 { t.Fatalf("a = %q, superseded %v, written %d", got, st.Superseded, st.Written) } q := open(t, release.NewDirStore(t.TempDir())) stage(t, q, "a", "file") stage(t, q, "a/b", "child") // newer than the file commit(t, q) dir2 := t.TempDir() _, st, err = Restore(ctx, q.s, pfx, dir2, nil) if err != nil { t.Fatal(err) } if got := read(t, dir2, "a/b"); got != "child" || !reflect.DeepEqual(st.Superseded, []string{"a"}) || st.Written != 1 { t.Fatalf("a/b = %q, superseded %v, written %d", got, st.Superseded, st.Written) } } func TestAFailedReReadAfterALostResponseIsNotASupersede(t *testing.T) { ctx := context.Background() s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())} p := open(t, s) stage(t, p, "x", "1") s.armed = true if _, err := p.Commit(ctx); err == nil { t.Fatal("first commit should fail") } s.getFail = true if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) { t.Fatalf("second commit err = %v, want the read error, not ErrSuperseded", err) } if seq, err := p.Commit(ctx); err != nil || seq != 1 { t.Fatalf("third commit = %d, %v", seq, err) } } func TestThreeDeepOverlapKeepsTheNewestEntry(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "a", "one") stage(t, p, "a/b/c", "three") stage(t, p, "a/b", "two") // the newest commit(t, p) dir := t.TempDir() _, st, err := Restore(ctx, s, pfx, dir, nil) if err != nil { t.Fatal(err) } if got := read(t, dir, "a/b"); got != "two" || st.Written != 1 || !reflect.DeepEqual(st.Superseded, []string{"a", "a/b/c"}) { t.Fatalf("a/b = %q, written %d, superseded %v", got, st.Written, st.Superseded) } } // A Stage between a commit whose response was lost and its retry changes // the body; the retry must still recognise the stored manifest as its own. func TestALostCommitResponseFollowedByAStageIsNotASupersede(t *testing.T) { ctx := context.Background() s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())} p := open(t, s) stage(t, p, "x", "1") s.armed = true if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) { t.Fatalf("first commit err = %v, want the transport error", err) } stage(t, p, "y", "2") seq, err := p.Commit(ctx) if err != nil || seq != 2 { t.Fatalf("second commit = %d, %v; want 2, nil", seq, err) } m, _, err := Load(ctx, s, pfx) if err != nil { t.Fatal(err) } if m.Seq != 2 || m.Entries["x"].Seq != 1 || m.Entries["y"].Seq != 2 { t.Fatalf("stored manifest = %+v, want x@1 and y@2 at seq 2", m) } stage(t, p, "z", "3") if seq := commit(t, p); seq != 3 { t.Fatalf("third commit seq = %d, want 3", seq) } } // Two failed attempts against the same stale etag: only one can have been // applied, and whichever it was, the publisher recognises it. func TestTwoFailedCommitsThenAStageRecognisesTheAppliedOne(t *testing.T) { ctx := context.Background() s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())} p := open(t, s) stage(t, p, "x", "1") s.armed = true if _, err := p.Commit(ctx); err == nil { t.Fatal("first commit should fail") } stage(t, p, "y", "2") s.putFail = true // the second attempt never reaches the store if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) { t.Fatalf("second commit err = %v, want the transport error", err) } stage(t, p, "z", "3") if seq, err := p.Commit(ctx); err != nil || seq != 3 { t.Fatalf("third commit = %d, %v; want 3, nil", seq, err) } } // putHostile publishes a good manifest with one file "a", lets bend alter // the stored manifest, and returns a files directory that already holds a // file Restore would remove or replace if it acted. func putHostile(t *testing.T, bend func(m *Manifest)) (release.Store, string) { t.Helper() ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) stage(t, p, "a", "good") stage(t, p, "gone", "soon") commit(t, p) if err := p.StageDelete("gone"); err != nil { t.Fatal(err) } if err := p.StagePlatform(ctx, "item", []byte("plat")); err != nil { t.Fatal(err) } commit(t, p) m, etag, _, err := load(ctx, s, pfx) if err != nil { t.Fatal(err) } bend(&m) body, _ := json.Marshal(m) if _, err := s.Put(ctx, ManifestKey(pfx), body, etag); err != nil { t.Fatal(err) } dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "gone"), []byte("still here"), 0o600); err != nil { t.Fatal(err) } return s, dir } func TestRestoreRefusesAnEntryWhoseObjectOrHashIsMalformedAndTouchesNothing(t *testing.T) { cases := map[string]func(m *Manifest){ "uppercase sha": func(m *Manifest) { e := m.Entries["a"] e.SHA256 = strings.ToUpper(e.SHA256) e.Object = pfx + objectKey(e.Seq, e.SHA256) m.Entries["a"] = e }, "short sha": func(m *Manifest) { e := m.Entries["a"] e.SHA256, e.Object = "aa", pfx+objectKey(e.Seq, "aa") m.Entries["a"] = e }, "object elsewhere": func(m *Manifest) { e := m.Entries["a"] e.Object = "instances/other/files/g1/" + objectKey(e.Seq, e.SHA256) m.Entries["a"] = e }, "object with another seq": func(m *Manifest) { e := m.Entries["a"] e.Object = pfx + objectKey(e.Seq+1, e.SHA256) m.Entries["a"] = e }, "platform object elsewhere": func(m *Manifest) { e := m.Platform["item"] e.Object = "objects/elsewhere" m.Platform["item"] = e }, "platform sha not hex": func(m *Manifest) { e := m.Platform["item"] e.SHA256 = strings.Repeat("g", 64) e.Object = pfx + objectKey(e.Seq, e.SHA256) m.Platform["item"] = e }, } for name, bend := range cases { s, dir := putHostile(t, bend) if _, _, err := Restore(context.Background(), s, pfx, dir, nil); err == nil { t.Fatalf("%s: Restore accepted the manifest", name) } if got := read(t, dir, "gone"); got != "still here" { t.Fatalf("%s: gone = %q, want it untouched", name, got) } if _, err := os.Stat(filepath.Join(dir, "a")); !os.IsNotExist(err) { t.Fatalf("%s: a was written", name) } } } func TestReadPlatformRefusesAMalformedEntry(t *testing.T) { s, _ := putHostile(t, func(m *Manifest) { e := m.Platform["item"] e.Object = pfx + "objects/elsewhere" m.Platform["item"] = e }) ctx := context.Background() // The right bytes at the wrong key: only the key check can refuse it. if _, err := s.Put(ctx, pfx+"objects/elsewhere", []byte("plat"), ""); err != nil { t.Fatal(err) } m, _, err := Load(ctx, s, pfx) if err != nil { t.Fatal(err) } if _, err := ReadPlatform(ctx, s, pfx, m, "item", nil); err == nil { t.Fatal("ReadPlatform accepted an object key that is not its own") } } func TestRestoreRefusesAnEntryWhoseSizeIsOutOfRangeAndTouchesNothing(t *testing.T) { for _, size := range []int64{-1, MaxFileBytes + 1} { s, dir := putHostile(t, func(m *Manifest) { e := m.Entries["a"] e.Size = size m.Entries["a"] = e }) if _, _, err := Restore(context.Background(), s, pfx, dir, nil); err == nil { t.Fatalf("size %d: Restore accepted the manifest", size) } if got := read(t, dir, "gone"); got != "still here" { t.Fatalf("size %d: gone = %q, want it untouched", size, got) } } } func TestFetchRefusesAnObjectWhoseLengthIsNotTheRecordedSize(t *testing.T) { s, dir := putHostile(t, func(m *Manifest) { e := m.Entries["a"] e.Size = 3 // the object is four bytes, and its hash is right m.Entries["a"] = e p := m.Platform["item"] p.Size = 5 m.Platform["item"] = p }) ctx := context.Background() if _, _, err := Restore(ctx, s, pfx, dir, nil); err == nil || !strings.Contains(err.Error(), "size") { t.Fatalf("Restore err = %v, want a size mismatch", err) } m, _, err := Load(ctx, s, pfx) if err != nil { t.Fatal(err) } if _, err := ReadPlatform(ctx, s, pfx, m, "item", nil); err == nil || !strings.Contains(err.Error(), "size") { t.Fatalf("ReadPlatform err = %v, want a size mismatch", err) } }