//go:build ignore // Prints src/lib/dkc/testing/mutation-texts.json: the text of the error that // capsule.Open of the Go reference gives for every case of // testdata/vectors/mutations.json, or "ok" for a capsule that opens, so that // vectors.test.ts requires the same text of open, byte for byte, and not // only the same code and step (plan of format 3 in datekeys-ts, step 3). // // Each case is replayed as internal/testkit replays the corpus // (MutationInput.Open), which a module outside the reference cannot import: // the Quicknet profile pinned, or no profile for the registry "empty"; a // source that answers every request with the release of the case, or with // "testkit: no release" and ERR_RELEASE_UNAVAILABLE, given as // OpenOptions.Release when the source of the case is "supplied", a release // object with the Quicknet chain unless the case names another, and as // OpenOptions.Source when it is "network", which verifies the release and // discards it (spec v0.15, ยง63 step 9); the .dkk // decoded before, as OpenOptions.AccessKey; the identities of the case; the // extensions of the case, known at (id, version) and whose data is valid // only when it is valid_data, with the texts of testkit.KnownExtensions; a // sink that discards the files; and the clock of the case. The code of every // error is checked against the corpus: a mismatch panics. // // Run it from a scratch module that requires the reference implementation // (replace g.activething.com/go/DateKeys => ../datekeys-go, GOFLAGS=-mod=mod // and the go directive of the reference, so that its toolchain is used), // passing the testdata directory of this repository: // // go run mutation-go-texts.go ../datekeys-ts/testdata > mutation-texts.json package main import ( "bytes" "context" "encoding/hex" "encoding/json" "fmt" "io" "os" "path/filepath" "time" "filippo.io/age" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/extension" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/provider" ) type corpus struct { Spec string `json:"spec"` Cases []struct { Name string `json:"name"` DKC struct { Base string `json:"base"` Edits [][]json.RawMessage `json:"edits"` } `json:"dkc"` DKK string `json:"dkk"` Identities []string `json:"identities"` Release *struct { Round uint64 `json:"round"` Signature string `json:"signature"` ChainHash string `json:"chain_hash"` } `json:"release"` Source string `json:"source"` Now string `json:"now"` Registry string `json:"registry"` Extensions []struct { ID string `json:"id"` Version uint64 `json:"version"` ValidData string `json:"valid_data"` } `json:"extensions"` Error string `json:"error"` Step int `json:"step"` } `json:"cases"` } type caseOut struct { Name string `json:"name"` Text string `json:"text"` } type output struct { Description string `json:"description"` Generator string `json:"generator"` Spec string `json:"spec"` Cases []caseOut `json:"cases"` } // known is testkit.KnownExtensions: an extension.Registry and // extension.DataValidator with the same texts. type known []struct { id string version uint64 valid []byte } func (k known) find(id string, version uint64) int { for i := range k { if k[i].id == id && k[i].version == version { return i } } return -1 } func (k known) Known(id string, version uint64) bool { return k.find(id, version) >= 0 } func (k known) ValidateData(e extension.Extension) error { i := k.find(e.ID, e.Version) if i < 0 { return fmt.Errorf("extension %s version %d is not known", e.ID, e.Version) } if !bytes.Equal(k[i].valid, e.Data) { return fmt.Errorf("data %x is not %x", e.Data, k[i].valid) } return nil } // source is testkit's singleSource: as a network source, Fetch verifies the // release and discards it when it breaks a rule of step 10; as a release in // hand, Supply hands over its release object, with the chain of the pinned // profile unless the release names another. type source struct{ release *provider.Release } func (s source) Fetch(_ context.Context, p *profile.Profile, c provider.Condition) (provider.Release, error) { if s.release == nil { return provider.Release{}, fmt.Errorf("testkit: no release: %w", datekeys.ErrReleaseUnavailable) } if err := provider.Verify(p, c, *s.release); err != nil { return provider.Release{}, fmt.Errorf("testkit: the release is discarded: %v: %w", err, datekeys.ErrReleaseUnavailable) } return *s.release, nil } func (s source) Supply(p *profile.Profile, _ provider.Condition) ([]byte, error) { if s.release == nil { return nil, fmt.Errorf("testkit: no release: %w", datekeys.ErrReleaseUnavailable) } r := *s.release if r.ChainHash == nil { r.ChainHash = p.ChainHash[:] } return provider.EncodeRelease(r) } // discard is testkit's DiscardSink. type discard struct{} func (discard) Begin(*capsule.Head) error { return nil } func (discard) Create(int) (io.WriteCloser, error) { return nopCloser{io.Discard}, nil } func (discard) Commit() error { return nil } func (discard) Abort() {} type nopCloser struct{ io.Writer } func (nopCloser) Close() error { return nil } func must[T any](v T, err error) T { if err != nil { panic(err) } return v } // applyEdits is the reading of README "Edited files": [at, delete, insert] // on the offsets of the unmodified base, sorted and not overlapping. func applyEdits(base []byte, edits [][]json.RawMessage) []byte { var out []byte pos := 0 for _, e := range edits { var at, del int var insert string if len(e) != 3 || json.Unmarshal(e[0], &at) != nil || json.Unmarshal(e[1], &del) != nil || json.Unmarshal(e[2], &insert) != nil { panic("an edit is [at, delete, insert]") } if at < pos || at+del > len(base) { panic("edits out of order or beyond the base") } out = append(append(out, base[pos:at]...), must(hex.DecodeString(insert))...) pos = at + del } return append(out, base[pos:]...) } func main() { dir := os.Args[1] var c corpus if err := json.Unmarshal(must(os.ReadFile(filepath.Join(dir, "vectors", "mutations.json"))), &c); err != nil { panic(err) } out := output{ Description: "The text of the error of capsule.Open for every case of testdata/vectors/mutations.json, or ok for a capsule that opens; see the header of scripts/mutation-go-texts.go.", Generator: "scripts/mutation-go-texts.go", Spec: c.Spec, } for _, m := range c.Cases { var base []byte if m.DKC.Base != "" { base = must(os.ReadFile(filepath.Join(dir, "fixtures", m.DKC.Base))) } dkc := applyEdits(base, m.DKC.Edits) reg := must(profile.Default()) if m.Registry == "empty" { reg = must(profile.NewRegistry()) } var src source if m.Release != nil { src.release = &provider.Release{Round: m.Release.Round, Signature: must(hex.DecodeString(m.Release.Signature))} if m.Release.ChainHash != "" { src.release.ChainHash = must(hex.DecodeString(m.Release.ChainHash)) } } now := must(time.Parse(time.RFC3339Nano, m.Now)) o := capsule.OpenOptions{Registry: reg, Now: func() time.Time { return now }, Sink: discard{}} switch m.Source { case "supplied": o.Release = src case "network": o.Source = src default: panic(fmt.Sprintf("%s: unknown source %q", m.Name, m.Source)) } if m.Extensions != nil { var k known for _, x := range m.Extensions { k = append(k, struct { id string version uint64 valid []byte }{x.ID, x.Version, must(hex.DecodeString(x.ValidData))}) } o.Extensions = k } if m.DKK != "" { o.AccessKey = must(accesskey.Decode(bytes.NewReader(must(hex.DecodeString(m.DKK))))) } for _, s := range m.Identities { o.Identities = append(o.Identities, must(age.ParseX25519Identity(s))) } _, err := capsule.Open(context.Background(), io.Discard, bytes.NewReader(dkc), o) text, code := "ok", "ok" if err != nil { text, code = err.Error(), datekeys.Code(err) } if code != m.Error { panic(fmt.Sprintf("%s: %s, the corpus says %s", m.Name, text, m.Error)) } out.Cases = append(out.Cases, caseOut{m.Name, text}) } e := json.NewEncoder(os.Stdout) e.SetEscapeHTML(false) e.SetIndent("", " ") if err := e.Encode(out); err != nil { panic(err) } }