package hostfiles import ( "context" "encoding/json" "errors" "fmt" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) // Region HA stage B's rule (73bab739, relocationPlatformItem), carried // here so both sides share it: a live platform item is the host's; a // missing one is "none" only under a pin of a host that never published // files (no digest, no retained copy); a missing one under a pin of // published files, and a tombstone (which no publisher writes) under // either pin, refuse. func TestPlatformItemNeedsEvidenceForNone(t *testing.T) { published := PinnedFiles{SHA256: "ab", Retained: "retained/ab"} never := PinnedFiles{} live := Manifest{Platform: map[string]Entry{EnvItem: {Object: "o", SHA256: "cd", Size: 1, Seq: 1}}} missing := Manifest{Platform: map[string]Entry{}} tomb := Manifest{Platform: map[string]Entry{EnvItem: {Deleted: true, Seq: 2}}} for _, tc := range []struct { name string pin PinnedFiles m Manifest want bool err error }{ {"a live item", published, live, true, nil}, {"a live item, evidence or not", never, live, true, nil}, {"missing under published files", published, missing, false, ErrNoEvidence}, {"missing under a host that never published", never, missing, false, nil}, {"a tombstone under published files", published, tomb, false, ErrTombstone}, {"a tombstone under a host that never published", never, tomb, false, ErrTombstone}, } { got, err := PlatformItem(tc.pin, tc.m, EnvItem) if got != tc.want || !errors.Is(err, tc.err) || (tc.err == nil) != (err == nil) { t.Errorf("%s: %v %v, want %v %v", tc.name, got, err, tc.want, tc.err) } } // Half-made evidence is not a host that never published files: the // pinner writes both or neither, and ReadPin refuses the rest (H2). for _, half := range []PinnedFiles{{SHA256: "ab"}, {Retained: "retained/ab"}} { if half.NeverPublished() { t.Errorf("%+v reads as never published", half) } } } func prepared(t *testing.T, s release.Store, prefix string) *Prepared { t.Helper() p, err := NewPrepared(s, prefix) if err != nil { t.Fatal(err) } return p } // The prepared manifest (claim rev 6.30 R18-1, moves §7.6): one create-only // manifest at sequence 0 over create-only content objects, every object // keyed by its digest at sequence 0 under the prepared prefix. A second, // different build of the same prefix is refused at its commit (its objects // are referenced by no manifest, spec §7.6); the manifest the first build // wrote is still there, byte for byte. func TestPreparedIsCreateOnlyAtSequenceZero(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := prepared(t, s, pfx) if err := p.Stage(ctx, "a.txt", []byte("A"), 0o644); err != nil { t.Fatal(err) } if err := p.StagePlatform(ctx, EnvItem, []byte("K=1\n")); err != nil { t.Fatal(err) } m, body, version, err := p.Commit(ctx) if err != nil { t.Fatal(err) } if version == "" { t.Fatal("Commit returned no version") } if m.Seq != 0 || m.V != ManifestVersion { t.Fatalf("manifest seq %d v %d", m.Seq, m.V) } for name, e := range map[string]Entry{"a.txt": m.Entries["a.txt"], EnvItem: m.Platform[EnvItem]} { if e.Seq != 0 || !strings.HasPrefix(e.Object, pfx+"objects/0000000000000000-") || e.SHA256 != sum([]byte(map[string]string{"a.txt": "A", EnvItem: "K=1\n"}[name])) { t.Fatalf("%s: %+v", name, e) } if got, _, err := s.Get(ctx, e.Object); err != nil || sum(got) != e.SHA256 { t.Fatalf("%s object: %v %q", name, err, got) } } if err := ValidateManifest(m, []string{pfx}); err != nil { t.Fatal(err) } stored, _, err := s.Get(ctx, ManifestKey(pfx)) if err != nil || string(stored) != string(body) { t.Fatalf("stored manifest: %v %s", err, stored) } // A different build of the same prefix is refused and writes nothing. q := prepared(t, s, pfx) if err := q.Stage(ctx, "a.txt", []byte("B"), 0o644); err != nil { t.Fatal(err) } if _, _, _, err := q.Commit(ctx); !errors.Is(err, ErrManifestExists) { t.Fatalf("second build: %v", err) } if again, _, err := s.Get(ctx, ManifestKey(pfx)); err != nil || string(again) != string(body) { t.Fatalf("the manifest was replaced: %s", again) } // Once committed, the builder is spent. if err := p.Stage(ctx, "b.txt", []byte("B"), 0o644); err == nil { t.Fatal("staged after commit") } } // A replay of the same build finds every object by its digest and the // manifest by its key (create-only, so the late second write fails and is // read back): success, the same bytes, and no new object. func TestPreparedReplayReusesObjects(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) build := func() ([]byte, error) { p := prepared(t, s, pfx) if err := p.Stage(ctx, "a.txt", []byte("A"), 0o644); err != nil { return nil, err } if err := p.Stage(ctx, "big", make([]byte, MaxFileBytes+1), 0o644); err != nil { return nil, err } if err := p.StagePlatform(ctx, WakeItem, []byte(`{"v":1}`)); err != nil { return nil, err } _, body, version, err := p.Commit(ctx) if err != nil { return nil, err } if _, etag, err := s.Get(ctx, ManifestKey(pfx)); err != nil || version != etag { return nil, fmt.Errorf("Commit returned version %q, the manifest is at %q (%v)", version, etag, err) } return body, nil } first, err := build() if err != nil { t.Fatal(err) } keys, err := s.List(ctx, pfx) if err != nil { t.Fatal(err) } second, err := build() if err != nil { t.Fatalf("replay: %v", err) } if string(first) != string(second) { t.Fatalf("replay bytes differ:\n%s\n%s", first, second) } again, err := s.List(ctx, pfx) if err != nil { t.Fatal(err) } if len(again) != len(keys) || len(keys) != 3 { // two objects and the manifest t.Fatalf("objects after replay: %v, before %v", again, keys) } var m Manifest if err := json.Unmarshal(first, &m); err != nil { t.Fatal(err) } if e := m.Entries["big"]; e.Skipped == "" || e.Object != "" { t.Fatalf("the oversize file is not recorded as skipped: %+v", e) } } // StageSkipped records a path the builder did not read (a file over the // limit, by its size): an entry with the reason, no object, at sequence 0. func TestPreparedStageSkippedNamesNoObject(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := prepared(t, s, pfx) if err := p.StageSkipped("big.bin", "too large"); err != nil { t.Fatal(err) } if err := p.StageSkipped("../out", "x"); err == nil { t.Fatal("staged a path outside the root") } m, _, _, err := p.Commit(ctx) if err != nil { t.Fatal(err) } if e := m.Entries["big.bin"]; e.Skipped != "too large" || e.Object != "" || e.Seq != 0 { t.Fatalf("skipped entry: %+v", e) } keys, err := s.List(ctx, pfx) if err != nil || len(keys) != 1 { t.Fatalf("objects for a skipped entry: %v %v", keys, err) } } // OpenAt publishes over the bytes its caller checked and the token read // with them: a replacement at the key after that read fails the commit // (ErrSuperseded) and the replacement stands; the same bytes still at the // key commit as usual. func TestOpenAtPublishesOnlyOverTheBytesRead(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) src := open(t, s) stage(t, src, "a.txt", "A") commit(t, src) body, etag, err := s.Get(ctx, ManifestKey(pfx)) if err != nil { t.Fatal(err) } p, err := OpenAt(s, pfx, body, etag) if err != nil { t.Fatal(err) } if _, err := OpenAt(s, pfx, body, ""); err == nil { t.Fatal("opened without a token") } if _, err := OpenAt(s, pfx, []byte(`{"v":1}`), etag); err == nil { t.Fatal("opened a manifest of another version") } // Another writer replaces the key after the read. other := open(t, s) stage(t, other, "b.txt", "B") commit(t, other) replaced, _, _ := s.Get(ctx, ManifestKey(pfx)) stage(t, p, "c.txt", "C") if _, err := p.Commit(ctx); !errors.Is(err, ErrSuperseded) { t.Fatalf("commit over a replacement: %v", err) } if again, _, _ := s.Get(ctx, ManifestKey(pfx)); string(again) != string(replaced) { t.Fatal("the replacement was published over") } // The same bytes still at the key: the commit goes through. body, etag, _ = s.Get(ctx, ManifestKey(pfx)) q, err := OpenAt(s, pfx, body, etag) if err != nil { t.Fatal(err) } stage(t, q, "c.txt", "C") if seq := commit(t, q); seq != 3 { t.Fatalf("seq %d", seq) } } // The admitted origin set travels in the manifest (Task 6 fix round 2, // finding 3): a prepared manifest carries its own prefix; a copy carries // the set its caller validated (SetOrigins, sorted and deduplicated, its // own prefix included), and a commit keeps it; an old manifest without // the field reads with none, so a reader falls back to the admitted // chain alone. func TestManifestCarriesItsOriginSet(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := prepared(t, s, pfx) if err := p.Stage(ctx, "a.txt", []byte("A"), 0o644); err != nil { t.Fatal(err) } m, _, _, err := p.Commit(ctx) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(m.Origins, []string{pfx}) { t.Fatalf("prepared origins %v", m.Origins) } to := "instances/shop.test/abc/files/g2/" q, err := NewFromCopy(s, to, m, []string{pfx}) if err != nil { t.Fatal(err) } q.SetOrigins([]string{"instances/shop.test/abc/files/g1b/", pfx, to, pfx}) if _, err := q.Commit(ctx); err != nil { t.Fatal(err) } got, found, err := Load(ctx, s, to) if err != nil || !found { t.Fatal(err, found) } if !reflect.DeepEqual(got.Origins, []string{pfx, "instances/shop.test/abc/files/g1b/", to}) { // sorted, deduplicated t.Fatalf("copied origins %v", got.Origins) } // A later revision by the ordinary publisher keeps the set. r := open(t, s) stage(t, r, "b.txt", "B") commit(t, r) again, _, _ := Load(ctx, s, pfx) if !reflect.DeepEqual(again.Origins, []string{pfx}) { t.Fatalf("a commit dropped the set: %v", again.Origins) } // SetOrigins with the same set is not a change. same, _ := OpenAt(s, to, mustBytes(t, s, to), mustETag(t, s, to)) same.SetOrigins([]string{to, pfx, "instances/shop.test/abc/files/g1b/"}) if same.Dirty() { t.Fatal("the same set marked the publisher dirty") } // An old manifest without the field. var old Manifest if err := json.Unmarshal([]byte(`{"v":2,"seq":1,"entries":{},"platform":{}}`), &old); err != nil || old.Origins != nil { t.Fatalf("old manifest: %v %v", old.Origins, err) } } func mustBytes(t *testing.T, s release.Store, prefix string) []byte { t.Helper() b, _, err := s.Get(context.Background(), ManifestKey(prefix)) if err != nil { t.Fatal(err) } return b } func mustETag(t *testing.T, s release.Store, prefix string) string { t.Helper() _, etag, err := s.Get(context.Background(), ManifestKey(prefix)) if err != nil { t.Fatal(err) } return etag } // NewFromCopy seeds a publisher for a generation that has no manifest yet // with a predecessor's validated manifest: every entry keeps its // origin-qualified object, the sequence continues from the copy, and the // first Commit is create-only. A repeat with the same result reads its own // bytes back; another manifest at the target refuses. func TestNewFromCopyContinuesTheSequenceCreateOnly(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) src := open(t, s) stage(t, src, "a.txt", "A") if err := src.StagePlatform(ctx, EnvItem, []byte("E")); err != nil { t.Fatal(err) } if err := src.StagePlatform(ctx, "instance-key-adoption.json", []byte("{}")); err != nil { t.Fatal(err) } if seq := commit(t, src); seq != 3 { t.Fatalf("source seq %d", seq) } base, found, err := Load(ctx, s, pfx) if err != nil || !found { t.Fatal(err, found) } to := "instances/shop.test/abc/files/g2/" establish := func() (int64, error) { p, err := NewFromCopy(s, to, base, []string{pfx}) if err != nil { return 0, err } if err := p.StagePlatform(ctx, EnvItem, []byte("E2")); err != nil { return 0, err } if err := p.StagePlatformDelete("instance-key-adoption.json"); err != nil { return 0, err } return p.Commit(ctx) } seq, err := establish() if err != nil { t.Fatal(err) } if seq != 5 { t.Fatalf("seq %d, want 5 (3 copied, the env, the delete)", seq) } m, found, err := Load(ctx, s, to) if err != nil || !found { t.Fatal(err, found) } if m.Entries["a.txt"].Object != base.Entries["a.txt"].Object { t.Fatalf("the copy renamed an object: %+v", m.Entries["a.txt"]) } if e := m.Platform[EnvItem]; e.SHA256 != sum([]byte("E2")) || !strings.HasPrefix(e.Object, to+"objects/") { t.Fatalf("env: %+v", e) } if _, ok := m.Platform["instance-key-adoption.json"]; ok { t.Fatal("the adoption item was carried") } if again, err := establish(); err != nil || again != 5 { t.Fatalf("repeat: %d %v", again, err) } // Another manifest at the target: the copy refuses, nothing replaced. other := "instances/shop.test/abc/files/g3/" if _, err := s.Put(ctx, ManifestKey(other), []byte(`{"v":2,"seq":9,"entries":{},"platform":{}}`), ""); err != nil { t.Fatal(err) } p, err := NewFromCopy(s, other, base, []string{pfx}) if err != nil { t.Fatal(err) } if _, err := p.Commit(ctx); !errors.Is(err, ErrSuperseded) { t.Fatalf("over another manifest: %v", err) } // An empty base (the claim's own empty bootstrap, or a never-published // host) is allowed; a base that fails validation is not. if _, err := NewFromCopy(s, "instances/shop.test/abc/files/g4/", Manifest{V: ManifestVersion}, nil); err != nil { t.Fatal(err) } bad := base bad.Entries = map[string]Entry{"x": {Object: "elsewhere/objects/0000000000000001-" + sum(nil), SHA256: sum(nil), Seq: 1}} if _, err := NewFromCopy(s, "instances/shop.test/abc/files/g4/", bad, []string{pfx}); !errors.Is(err, ErrOrigin) { t.Fatalf("foreign origin: %v", err) } } // The prepared builder and OpenAt hold the same path cap as Publisher: // a prepared manifest cannot stage a path no reader accepts, and OpenAt // refuses to continue a manifest that holds one (as Open does). func TestPreparedAndOpenAtHoldThePathCap(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) long := strings.Repeat("d/", MaxPathBytes/2+1) + "f" p := prepared(t, s, pfx) if err := p.Stage(ctx, long, []byte("x"), 0o644); !errors.Is(err, ErrTooDeep) { t.Fatalf("Stage past the cap: %v; want ErrTooDeep", err) } if err := p.StageSkipped(long, "too large"); !errors.Is(err, ErrTooDeep) { t.Fatalf("StageSkipped past the cap: %v; want ErrTooDeep", err) } m := Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{long: {Seq: 1}}, Platform: map[string]Entry{}} body, err := json.Marshal(m) if err != nil { t.Fatal(err) } if _, err := OpenAt(s, pfx, body, "etag"); !errors.Is(err, ErrTooDeep) { t.Fatalf("OpenAt over a manifest past the cap: %v; want ErrTooDeep", err) } }