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-App/scripts/mutation-go-texts.go

260 lines
8.3 KiB

//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)
}
}

Powered by TurnKey Linux.