baseline: P1 契约、agent 核心与中继投递(既有工作首次入库)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// marshal is encoding/json with HTML escaping off. The default escapes <, > and &, which would make
|
||||
// a body containing those characters hash differently here than it does in Node or Swift.
|
||||
func marshal(value any) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.TrimRight(buf.Bytes(), "\n"), nil
|
||||
}
|
||||
|
||||
func unmarshal(raw []byte, value any) error {
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(value); err != nil {
|
||||
return err
|
||||
}
|
||||
// Trailing content would mean the caller is looking at a different document than the one that
|
||||
// was decoded, which for a signed field is a correctness bug rather than a style choice.
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
||||
return errors.New("wire: trailing data after JSON value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanonicalJSON renders a JSON value in the one form this protocol hashes.
|
||||
//
|
||||
// The rules are deliberately small, and every implementation (Go agent, Node relay, Swift app)
|
||||
// must follow exactly these:
|
||||
//
|
||||
// 1. Object keys are sorted by UTF-8 byte order and emitted without whitespace.
|
||||
// 2. Arrays keep their order.
|
||||
// 3. Strings use the same escapes as Go's encoder with HTML escaping disabled: \" \\ \n \r \t,
|
||||
// \u00XX for other control characters, and literal UTF-8 for everything else.
|
||||
// 4. Numbers must be integers in canonical form: an optional minus, no leading zero, no fraction,
|
||||
// no exponent, no plus. This is stricter than JSON allows and on purpose. Any wider grammar has
|
||||
// values that are the same number but different bytes (1 and 1.0, 1e2 and 100), and "which
|
||||
// spelling did the phone hash" is not a question this protocol is willing to leave open. The
|
||||
// APIs behind it take integers, strings, booleans, arrays and objects, so nothing is lost.
|
||||
// 5. null, true and false are literal.
|
||||
//
|
||||
// An empty or absent body canonicalises to the empty byte string, not to the four characters
|
||||
// "null", so "no body" and "empty body" cannot be confused.
|
||||
func CanonicalJSON(raw []byte) ([]byte, error) {
|
||||
trimmed := bytes.TrimSpace(raw)
|
||||
if len(trimmed) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(trimmed))
|
||||
decoder.UseNumber()
|
||||
var value any
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return nil, fmt.Errorf("wire: body is not valid JSON: %w", err)
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
||||
return nil, errors.New("wire: body has trailing data after the JSON value")
|
||||
}
|
||||
buf := bytes.NewBuffer(make([]byte, 0, len(trimmed)))
|
||||
if err := writeCanonical(buf, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// canonicalNumber is the only number spelling this protocol will hash. See rule 4 above.
|
||||
//
|
||||
// Negative zero is excluded even though it is an integer: no implementation can be relied on to
|
||||
// round-trip "-0" as text rather than as the number 0, and nothing in these payloads needs it.
|
||||
var canonicalNumber = regexp.MustCompile(`^-?[1-9][0-9]*$|^-?[1-9][0-9]*$|^0$`)
|
||||
|
||||
func writeCanonical(buf *bytes.Buffer, value any) error {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
buf.WriteString("null")
|
||||
case bool:
|
||||
if typed {
|
||||
buf.WriteString("true")
|
||||
} else {
|
||||
buf.WriteString("false")
|
||||
}
|
||||
case json.Number:
|
||||
literal := typed.String()
|
||||
if !canonicalNumber.MatchString(literal) || literal == "-0" {
|
||||
return fmt.Errorf("wire: number %q is not a canonical integer", literal)
|
||||
}
|
||||
buf.WriteString(literal)
|
||||
case string:
|
||||
encoded, err := marshal(typed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.Write(encoded)
|
||||
case []any:
|
||||
buf.WriteByte('[')
|
||||
for index, item := range typed {
|
||||
if index > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
if err := writeCanonical(buf, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
buf.WriteByte(']')
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(typed))
|
||||
for key := range typed {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
buf.WriteByte('{')
|
||||
for index, key := range keys {
|
||||
if index > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
encodedKey, err := marshal(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.Write(encodedKey)
|
||||
buf.WriteByte(':')
|
||||
if err := writeCanonical(buf, typed[key]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
default:
|
||||
return fmt.Errorf("wire: cannot canonicalise %T", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user