JSON of RFC 8259 in UTF-8 whose value is an object; no object repeats a name, and names are compared exactly once their escapes are decoded, so "round" is round and ROUND another name; an escape of a lone surrogate is malformed; round is a number without sign, fraction or exponent from 1 to 2^53 - 1; signature and randomness are strings (spec v0.16, 47.1). Go's encoding/json kept the last of two repeated names and matched ROUND to round. ParseDrandJSON, exported, is the one reader of drand's JSON: provider/drand reads the answers of the relays with it too. The error texts do not change. release.json gains 25 cases of drand's JSON for v0.16. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
7e3b8104a6
commit
b57033813d
@ -0,0 +1,298 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// The strict reading of drand's JSON (spec v0.16, §47.1): JSON of RFC 8259,
|
||||
// in UTF-8, whose value is an object; no object of the JSON repeats a name,
|
||||
// and names are compared exactly, code point by code point, once their
|
||||
// escapes are decoded, so that "round" is round and Round is another
|
||||
// name; and an escape of a surrogate that does not pair with the next one
|
||||
// makes the JSON malformed. A common JSON reader keeps the last of two
|
||||
// repeated names, or does not tell upper from lower case in them, and two
|
||||
// readers would see two rounds in the same input.
|
||||
|
||||
// jsonMember is a member of the outer object: its name, decoded, the kind of
|
||||
// its value (a byte of `"`, `0`, `{`, `[`, `t`, `f` or `n`), the text of the
|
||||
// value as written, and for a string, the string decoded.
|
||||
type jsonMember struct {
|
||||
name string
|
||||
kind byte
|
||||
raw []byte
|
||||
str string
|
||||
}
|
||||
|
||||
// strictJSON reads b as a JSON object with the rules above, and returns the
|
||||
// members of the outer object in their order, or false when b breaks one.
|
||||
func strictJSON(b []byte) ([]jsonMember, bool) {
|
||||
if !utf8.Valid(b) {
|
||||
return nil, false
|
||||
}
|
||||
r := &jsonReader{b: b}
|
||||
r.space()
|
||||
if !r.at('{') {
|
||||
return nil, false
|
||||
}
|
||||
members, ok := r.object(true)
|
||||
r.space()
|
||||
return members, ok && r.i == len(b)
|
||||
}
|
||||
|
||||
// jsonReader reads JSON from b at i.
|
||||
type jsonReader struct {
|
||||
b []byte
|
||||
i int
|
||||
}
|
||||
|
||||
func (r *jsonReader) at(c byte) bool { return r.i < len(r.b) && r.b[r.i] == c }
|
||||
|
||||
// space skips the four spaces of JSON.
|
||||
func (r *jsonReader) space() {
|
||||
for r.i < len(r.b) && strings.IndexByte(" \t\n\r", r.b[r.i]) >= 0 {
|
||||
r.i++
|
||||
}
|
||||
}
|
||||
|
||||
// value reads one value of any kind.
|
||||
func (r *jsonReader) value() (jsonMember, bool) {
|
||||
if r.i >= len(r.b) {
|
||||
return jsonMember{}, false
|
||||
}
|
||||
start := r.i
|
||||
m := jsonMember{kind: r.b[r.i]}
|
||||
ok := false
|
||||
switch c := r.b[r.i]; {
|
||||
case c == '{':
|
||||
_, ok = r.object(false)
|
||||
case c == '[':
|
||||
ok = r.array()
|
||||
case c == '"':
|
||||
m.str, ok = r.string()
|
||||
case c == 't':
|
||||
ok = r.literal("true")
|
||||
case c == 'f':
|
||||
ok = r.literal("false")
|
||||
case c == 'n':
|
||||
ok = r.literal("null")
|
||||
case c == '-' || c >= '0' && c <= '9':
|
||||
m.kind, ok = '0', r.number()
|
||||
}
|
||||
m.raw = r.b[start:r.i]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
// object reads an object, with no name twice, and returns its members when
|
||||
// keep is set.
|
||||
func (r *jsonReader) object(keep bool) ([]jsonMember, bool) {
|
||||
r.i++ // {
|
||||
r.space()
|
||||
if r.at('}') {
|
||||
r.i++
|
||||
return nil, true
|
||||
}
|
||||
var out []jsonMember
|
||||
seen := map[string]bool{}
|
||||
for {
|
||||
r.space()
|
||||
if !r.at('"') {
|
||||
return nil, false
|
||||
}
|
||||
name, ok := r.string()
|
||||
if !ok || seen[name] {
|
||||
return nil, false
|
||||
}
|
||||
seen[name] = true
|
||||
r.space()
|
||||
if !r.at(':') {
|
||||
return nil, false
|
||||
}
|
||||
r.i++
|
||||
r.space()
|
||||
m, ok := r.value()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if keep {
|
||||
m.name = name
|
||||
out = append(out, m)
|
||||
}
|
||||
r.space()
|
||||
switch {
|
||||
case r.at(','):
|
||||
r.i++
|
||||
case r.at('}'):
|
||||
r.i++
|
||||
return out, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *jsonReader) array() bool {
|
||||
r.i++ // [
|
||||
r.space()
|
||||
if r.at(']') {
|
||||
r.i++
|
||||
return true
|
||||
}
|
||||
for {
|
||||
r.space()
|
||||
if _, ok := r.value(); !ok {
|
||||
return false
|
||||
}
|
||||
r.space()
|
||||
switch {
|
||||
case r.at(','):
|
||||
r.i++
|
||||
case r.at(']'):
|
||||
r.i++
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// string reads a string and decodes its escapes; a surrogate escaped alone,
|
||||
// without its pair, breaks it.
|
||||
func (r *jsonReader) string() (string, bool) {
|
||||
r.i++ // "
|
||||
var sb strings.Builder
|
||||
for r.i < len(r.b) {
|
||||
c := r.b[r.i]
|
||||
switch {
|
||||
case c == '"':
|
||||
r.i++
|
||||
return sb.String(), true
|
||||
case c < 0x20:
|
||||
return "", false
|
||||
case c != '\\':
|
||||
_, n := utf8.DecodeRune(r.b[r.i:])
|
||||
sb.Write(r.b[r.i : r.i+n])
|
||||
r.i += n
|
||||
continue
|
||||
}
|
||||
if r.i+1 >= len(r.b) {
|
||||
return "", false
|
||||
}
|
||||
e := r.b[r.i+1]
|
||||
r.i += 2
|
||||
if k := strings.IndexByte(`"\/bfnrt`, e); k >= 0 {
|
||||
sb.WriteByte("\"\\/\b\f\n\r\t"[k])
|
||||
continue
|
||||
}
|
||||
if e != 'u' {
|
||||
return "", false
|
||||
}
|
||||
u, ok := r.hex4()
|
||||
switch {
|
||||
case !ok || u >= 0xdc00 && u <= 0xdfff:
|
||||
return "", false
|
||||
case u >= 0xd800 && u <= 0xdbff:
|
||||
if r.i+1 >= len(r.b) || r.b[r.i] != '\\' || r.b[r.i+1] != 'u' {
|
||||
return "", false
|
||||
}
|
||||
r.i += 2
|
||||
low, ok := r.hex4()
|
||||
if !ok || low < 0xdc00 || low > 0xdfff {
|
||||
return "", false
|
||||
}
|
||||
u = 0x10000 + (u-0xd800)<<10 + (low - 0xdc00)
|
||||
}
|
||||
sb.WriteRune(rune(u))
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// hex4 reads the four hexadecimal digits of an escape \u.
|
||||
func (r *jsonReader) hex4() (int, bool) {
|
||||
if r.i+4 > len(r.b) {
|
||||
return 0, false
|
||||
}
|
||||
u := 0
|
||||
for _, c := range r.b[r.i : r.i+4] {
|
||||
var d int
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
d = int(c - '0')
|
||||
case c >= 'a' && c <= 'f':
|
||||
d = int(c-'a') + 10
|
||||
case c >= 'A' && c <= 'F':
|
||||
d = int(c-'A') + 10
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
u = u<<4 | d
|
||||
}
|
||||
r.i += 4
|
||||
return u, true
|
||||
}
|
||||
|
||||
func (r *jsonReader) literal(word string) bool {
|
||||
if !strings.HasPrefix(string(r.b[r.i:]), word) {
|
||||
return false
|
||||
}
|
||||
r.i += len(word)
|
||||
return true
|
||||
}
|
||||
|
||||
// number reads a number of the grammar of RFC 8259: a minus, an integer
|
||||
// part without leading zeros, and an optional fraction and exponent.
|
||||
func (r *jsonReader) number() bool {
|
||||
digits := func() int {
|
||||
n := 0
|
||||
for r.i < len(r.b) && r.b[r.i] >= '0' && r.b[r.i] <= '9' {
|
||||
r.i++
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
if r.at('-') {
|
||||
r.i++
|
||||
}
|
||||
switch {
|
||||
case r.at('0'):
|
||||
r.i++
|
||||
case digits() == 0:
|
||||
return false
|
||||
}
|
||||
if r.at('.') {
|
||||
r.i++
|
||||
if digits() == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if r.at('e') || r.at('E') {
|
||||
r.i++
|
||||
if r.at('+') || r.at('-') {
|
||||
r.i++
|
||||
}
|
||||
if digits() == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// maxJSONRound is the largest round of drand's JSON, 2^53 - 1, as in the
|
||||
// release object.
|
||||
const maxJSONRound = 1<<53 - 1
|
||||
|
||||
// jsonRound reads the round of drand's JSON: a number without sign, fraction
|
||||
// or exponent, from 1 to 2^53 - 1.
|
||||
func jsonRound(m jsonMember) (uint64, bool) {
|
||||
if m.kind != '0' || len(m.raw) == 0 || len(m.raw) > 16 || m.raw[0] == '0' {
|
||||
return 0, false
|
||||
}
|
||||
var n uint64
|
||||
for _, c := range m.raw {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, false
|
||||
}
|
||||
n = n*10 + uint64(c-'0')
|
||||
}
|
||||
return n, n <= maxJSONRound
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Spec v0.16, §47.1: the strict reading of drand's JSON, at the edges of the
|
||||
// grammar of RFC 8259 that release.json does not reach.
|
||||
func TestStrictJSON(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
in string
|
||||
ok bool
|
||||
}{
|
||||
{`{}`, true},
|
||||
{` {"a":1} `, true},
|
||||
{"\t{\"a\":1}\r\n", true},
|
||||
{`{"a":[]}`, true},
|
||||
{`{"a":[1,2,[3,{}]]}`, true},
|
||||
{`{"a":true,"b":false,"c":null}`, true},
|
||||
{`{"a":-0,"b":0.5,"c":1E+2,"d":1e-2,"e":-12.25e3}`, true},
|
||||
{`{"a":"\"\\\/\b\f\n\r\té"}`, true},
|
||||
{`{"é":1,"é":2}`, false}, // one name, escaped and not
|
||||
{`{"a":1,"a":2}`, false},
|
||||
{`{"a":{"b":1},"c":{"b":2}}`, true}, // the same name in two objects
|
||||
{`{"a":[{"b":1,"b":1}]}`, false},
|
||||
{`{"a":01}`, false},
|
||||
{`{"a":1.}`, false},
|
||||
{`{"a":.5}`, false},
|
||||
{`{"a":1e}`, false},
|
||||
{`{"a":+1}`, false},
|
||||
{`{"a":-}`, false},
|
||||
{`{"a":tru}`, false},
|
||||
{`{"a":nul}`, false},
|
||||
{`{"a":"\x"}`, false},
|
||||
{`{"a":"\u12"}`, false},
|
||||
{`{"a":"\u12g4"}`, false},
|
||||
{`{"a":"\ud800"}`, false},
|
||||
{`{"a":"\ud800A"}`, false},
|
||||
{`{"a":"\ud800x"}`, false},
|
||||
{`{"a":"\udfff\ud800"}`, false},
|
||||
{`{"a":"😀"}`, true},
|
||||
{`{"a":"` + "\x01" + `"}`, false},
|
||||
{`{"a":"` + "\xff" + `"}`, false},
|
||||
{"{\"a\":\"\xed\xa0\x80\"}", false}, // a surrogate in UTF-8
|
||||
{`{"a":1,}`, false},
|
||||
{`{,"a":1}`, false},
|
||||
{`{"a" 1}`, false},
|
||||
{`{"a":1 "b":2}`, false},
|
||||
{`{a:1}`, false},
|
||||
{`{"a":[1,]}`, false},
|
||||
{`{"a":[1 2]}`, false},
|
||||
{`{"a":"b"`, false},
|
||||
{`{"a":1}x`, false},
|
||||
{`[]`, false},
|
||||
{`"a"`, false},
|
||||
{``, false},
|
||||
{"{\"a\":1}\v", false}, // not a space of JSON
|
||||
} {
|
||||
if _, ok := strictJSON([]byte(tc.in)); ok != tc.ok {
|
||||
t.Errorf("%q: %v, want %v", tc.in, ok, tc.ok)
|
||||
}
|
||||
}
|
||||
m, ok := strictJSON([]byte(`{"round":1000,"note":"a😀","n":-1.5}`))
|
||||
if !ok || len(m) != 3 || m[0].name != "round" || string(m[0].raw) != "1000" || m[1].str != "a\U0001F600" || m[2].kind != '0' || string(m[2].raw) != "-1.5" {
|
||||
t.Errorf("members %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONRound(t *testing.T) {
|
||||
for raw, want := range map[string]bool{
|
||||
"1": true, "1000": true, "9007199254740991": true,
|
||||
"0": false, "9007199254740992": false, "99999999999999999": false,
|
||||
"-1": false, "1.0": false, "1e3": false, "01": false,
|
||||
} {
|
||||
if _, ok := jsonRound(jsonMember{kind: '0', raw: []byte(raw)}); ok != want {
|
||||
t.Errorf("%s: %v, want %v", raw, ok, want)
|
||||
}
|
||||
}
|
||||
if _, ok := jsonRound(jsonMember{kind: '"', raw: []byte(`"1"`)}); ok {
|
||||
t.Error("a string is not a round")
|
||||
}
|
||||
if _, err := ParseDrandJSON([]byte(`{"round":1000,"signature":"` + strings.Repeat("ab", 48) + `","note":"` + "\xff" + `"}`)); err == nil {
|
||||
t.Error("invalid UTF-8 is malformed")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue