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.
159 lines
4.9 KiB
159 lines
4.9 KiB
// Package extension implements the single generic extension mechanism shared
|
|
// by PUBLIC_HEADER, CONTROL_CBOR and .dkk (spec §31, §44, §54, §72).
|
|
//
|
|
// The base protocol does not interpret extension data. It enforces the
|
|
// structural rules only: valid UTF-8 identifiers, no identifier repeated
|
|
// within an object (V1 registers no schema that allows multiplicity), no
|
|
// identifier in both the critical and the noncritical array, canonical order
|
|
// by the UTF-8 bytes of extension_id and then by version, rejection of unknown
|
|
// critical extensions, and omission of empty arrays (spec §58.1).
|
|
package extension
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"fmt"
|
|
"slices"
|
|
"unicode/utf8"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
)
|
|
|
|
// MaxIDLen bounds extension_id. It is an implementation limit (spec §74).
|
|
const MaxIDLen = 256
|
|
|
|
// Extension is one entry of an extension array.
|
|
type Extension struct {
|
|
ID string // key 0, extension_id
|
|
Version uint64 // key 1, extension_version
|
|
// Data is the Deterministic CBOR encoding of the data item (key 2), or
|
|
// nil when the extension carries no data and the key is omitted.
|
|
Data []byte
|
|
}
|
|
|
|
// New builds an extension whose data is the deterministic encoding of value.
|
|
func New(id string, version uint64, value any) (Extension, error) {
|
|
b, err := codec.Marshal(value)
|
|
if err != nil {
|
|
return Extension{}, err
|
|
}
|
|
return Extension{ID: id, Version: version, Data: b}, nil
|
|
}
|
|
|
|
// Wire is the CBOR map of one extension (spec §54).
|
|
type Wire struct {
|
|
ID string `cbor:"0,keyasint"`
|
|
Version uint64 `cbor:"1,keyasint"`
|
|
Data cbor.RawMessage `cbor:"2,keyasint,omitempty"`
|
|
}
|
|
|
|
// Registry tells which critical extensions the application implements. A nil
|
|
// Registry knows none, which is the state of the base protocol V1.
|
|
type Registry interface {
|
|
Known(id string, version uint64) bool
|
|
}
|
|
|
|
// Set is a simple Registry.
|
|
type Set map[string][]uint64
|
|
|
|
// Known reports whether (id, version) is in the set.
|
|
func (s Set) Known(id string, version uint64) bool { return slices.Contains(s[id], version) }
|
|
|
|
func compare(a, b Extension) int {
|
|
if c := bytes.Compare([]byte(a.ID), []byte(b.ID)); c != 0 {
|
|
return c
|
|
}
|
|
return cmp.Compare(a.Version, b.Version)
|
|
}
|
|
|
|
func validate(e Extension) error {
|
|
if e.ID == "" || len(e.ID) > MaxIDLen || !utf8.ValidString(e.ID) {
|
|
return fmt.Errorf("extension: invalid extension_id %q: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if e.Data != nil {
|
|
if err := codec.Valid(e.Data); err != nil {
|
|
return fmt.Errorf("extension %s: data: %w", e.ID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Encode validates one extension array and returns its canonical wire form,
|
|
// sorted by extension_id bytes and then version. An empty input yields nil,
|
|
// so that the array key is omitted (spec §58.1).
|
|
func Encode(exts []Extension) ([]Wire, error) {
|
|
if len(exts) == 0 {
|
|
return nil, nil
|
|
}
|
|
sorted := slices.Clone(exts)
|
|
slices.SortFunc(sorted, compare)
|
|
out := make([]Wire, 0, len(sorted))
|
|
for i, e := range sorted {
|
|
if err := validate(e); err != nil {
|
|
return nil, err
|
|
}
|
|
if i > 0 && sorted[i-1].ID == e.ID {
|
|
return nil, fmt.Errorf("extension %s: appears more than once: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
out = append(out, Wire{ID: e.ID, Version: e.Version, Data: bytes.Clone(e.Data)})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Decode validates one decoded extension array: canonical order, no repeated
|
|
// identifier and canonical data.
|
|
func Decode(ws []Wire) ([]Extension, error) {
|
|
if len(ws) == 0 {
|
|
return nil, nil
|
|
}
|
|
out := make([]Extension, 0, len(ws))
|
|
for i, w := range ws {
|
|
e := Extension{ID: w.ID, Version: w.Version}
|
|
if w.Data != nil {
|
|
e.Data = bytes.Clone(w.Data)
|
|
}
|
|
if err := validate(e); err != nil {
|
|
return nil, err
|
|
}
|
|
if i > 0 {
|
|
prev := out[i-1]
|
|
if prev.ID == e.ID {
|
|
return nil, fmt.Errorf("extension %s: appears more than once: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if compare(prev, e) > 0 {
|
|
return nil, fmt.Errorf("extension %s: array is not in canonical order: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// CheckDisjoint applies the cross-array rule of one object: an extension_id
|
|
// must not appear in both critical_extensions and noncritical_extensions
|
|
// (spec §31, §54).
|
|
func CheckDisjoint(critical, noncritical []Extension) error {
|
|
for _, c := range critical {
|
|
for _, n := range noncritical {
|
|
if c.ID == n.ID {
|
|
return fmt.Errorf("extension %s: both critical and noncritical: %w", c.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CheckCritical rejects every critical extension unknown to reg (spec §54,
|
|
// §70). Unknown noncritical extensions may be ignored and are not checked.
|
|
func CheckCritical(critical []Extension, reg Registry) error {
|
|
for _, c := range critical {
|
|
if reg == nil || !reg.Known(c.ID, c.Version) {
|
|
return fmt.Errorf("extension %s v%d: %w", c.ID, c.Version, datekeys.ErrExtensionCriticalUnknown)
|
|
}
|
|
}
|
|
return nil
|
|
}
|