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.
310 lines
10 KiB
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())
|
|
}
|