You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dateKeys-dart/tool/mutation_go_texts.go

310 lines
10 KiB

//go:build ignore
// Writes test/vectors/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, and the
// checks that it records, step by step with their details, so that the
// tests of datekeys-dart require the same text of the opening, byte for
// byte, and not only the same code and step. A port of
// scripts/mutation-go-texts.go of datekeys-ts, over the synced testdata of
// this repository, that of the branch v0.15 of datekeys-go at fe50885.
//
// Each case is replayed as internal/testkit replays the corpus
// (MutationInput.Open), which a program outside the reference cannot
// import: the Quicknet profile pinned, or no profile for the registry
// "empty"; a release source that answers every request with the release of
// the case, or with "testkit: no release" and ERR_RELEASE_UNAVAILABLE: for a
// case whose source is "network", as OpenOptions.Source, which verifies the
// release and discards it when it breaks a rule of step 10; for one whose
// source is "supplied", as OpenOptions.Release, which hands over its release
// object, with the Quicknet chain hash unless the case names another (spec
// v0.15, §63 step 9.c); 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 and the step of every case are checked against the corpus: a
// case where Go and the corpus disagree is written with go_error and
// go_step, and reported on the standard error, never resolved silently.
//
// Run it in the module of the reference implementation, which it imports,
// without changing anything there, from the datekeys-go next to this
// repository, on the branch v0.15 at fe50885 (spec v0.15):
//
// cd ../datekeys-go && go run ../datekeys-dart/tool/mutation_go_texts.go \
// ../datekeys-dart/testdata > ../datekeys-dart/test/vectors/mutation_texts.json
package main
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"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"`
}
// caseOut is one case: its name, the text of the error or "ok", the checks
// as [step, name, ok, detail, code], and, when Go disagrees with the corpus,
// what Go gives.
type caseOut struct {
Name string `json:"name"`
Text string `json:"text"`
Checks [][]any `json:"checks"`
GoError string `json:"go_error,omitempty"`
GoStep int `json:"go_step,omitempty"`
}
type output struct {
Spec string `json:"spec"`
Generator string `json:"generator"`
Description string `json:"description"`
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 testdata/README.md, "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{
Spec: c.Spec,
Generator: "tool/mutation_go_texts.go",
Description: "The text of the error of capsule.Open for every case of testdata/vectors/mutations.json, or ok for a capsule that opens, and its checks as [step, name, ok, detail, code]; go_error and go_step where Go and the corpus disagree. See the header of tool/mutation_go_texts.go.",
}
disagree := 0
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("unknown source " + 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)))
}
opened, err := capsule.Open(context.Background(), io.Discard, bytes.NewReader(dkc), o)
text, code, step := "ok", "ok", 0
if err != nil {
text, code = err.Error(), datekeys.Code(err)
}
co := caseOut{Name: m.Name, Text: text, Checks: [][]any{}}
for _, ch := range opened.Inspection.Checks {
co.Checks = append(co.Checks, []any{ch.Step, ch.Name, ch.OK, ch.Detail, ch.Error})
}
if err != nil {
step = opened.Inspection.Checks[len(opened.Inspection.Checks)-1].Step
}
if code != m.Error || step != m.Step {
disagree++
co.GoError, co.GoStep = code, step
fmt.Fprintf(os.Stderr, "%s: Go gives %s at step %d, the corpus says %s at step %d\n", m.Name, code, step, m.Error, m.Step)
}
out.Cases = append(out.Cases, co)
}
fmt.Fprintf(os.Stderr, "%d cases, %d where Go and the corpus disagree\n", len(out.Cases), disagree)
var b bytes.Buffer
e := json.NewEncoder(&b)
e.SetEscapeHTML(false)
if err := e.Encode(out.Cases); err != nil {
panic(err)
}
// One case per line.
var sb strings.Builder
head := func(v any) string {
var h bytes.Buffer
enc := json.NewEncoder(&h)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
panic(err)
}
return strings.TrimSuffix(h.String(), "\n")
}
sb.WriteString("{\n")
sb.WriteString(` "spec": ` + head(out.Spec) + ",\n")
sb.WriteString(` "generator": ` + head(out.Generator) + ",\n")
sb.WriteString(` "description": ` + head(out.Description) + ",\n")
sb.WriteString(` "cases": [`)
for i, co := range out.Cases {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString("\n " + head(co))
}
sb.WriteString("\n ]\n}\n")
os.Stdout.WriteString(sb.String())
}

Powered by TurnKey Linux.