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.
301 lines
11 KiB
301 lines
11 KiB
package extension_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/extension"
|
|
)
|
|
|
|
func ext(t *testing.T, id string, v uint64, data []byte) extension.Extension {
|
|
t.Helper()
|
|
if data == nil {
|
|
return extension.Extension{ID: id, Version: v}
|
|
}
|
|
e, err := extension.New(id, v, data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return e
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
data := []byte("public label")
|
|
e, err := extension.New("org.example.label", 1, data)
|
|
if err != nil || e.ID != "org.example.label" || e.Version != 1 || !bytes.Equal(e.Data, data) {
|
|
t.Fatalf("%+v %v", e, err)
|
|
}
|
|
data[0] = 'P'
|
|
if e.Data[0] != 'p' {
|
|
t.Fatal("New does not copy data")
|
|
}
|
|
for name, tc := range map[string]struct {
|
|
id string
|
|
version uint64
|
|
data []byte
|
|
}{
|
|
// Spec §54, §58.1: an extension without data omits key 2; it is built
|
|
// as a literal, never through New.
|
|
"nil data": {"org.a", 1, nil},
|
|
"empty data": {"org.a", 1, []byte{}},
|
|
"empty id": {"", 1, []byte{1}},
|
|
"invalid UTF-8 id": {"org.\xff", 1, []byte{1}},
|
|
"id too long": {strings.Repeat("a", extension.MaxIDLen+1), 1, []byte{1}},
|
|
"version above 2^32": {"org.a", extension.MaxVersion + 1, []byte{1}},
|
|
} {
|
|
if _, err := extension.New(tc.id, tc.version, tc.data); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Errorf("%s: %v", name, err)
|
|
}
|
|
}
|
|
if _, err := extension.New("org.a", extension.MaxVersion, []byte{0}); err != nil {
|
|
t.Fatalf("version 2^32-1 rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEncodeSortsCanonically(t *testing.T) {
|
|
in := []extension.Extension{ext(t, "org.b", 1, nil), ext(t, "org.a", 2, []byte("x")), ext(t, "Z", 9, nil), ext(t, "org.aa", 1, []byte{7})}
|
|
w, err := extension.Encode(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var order []string
|
|
for _, e := range w {
|
|
order = append(order, e.ID)
|
|
}
|
|
// Bytewise UTF-8 order: uppercase before lowercase, prefixes first.
|
|
if got := []string{"Z", "org.a", "org.aa", "org.b"}; !slices.Equal(order, got) {
|
|
t.Fatalf("order %v, want %v", order, got)
|
|
}
|
|
if w, _ := extension.Encode(nil); w != nil {
|
|
t.Fatal("empty array must encode to nil so that the key is omitted")
|
|
}
|
|
back, err := extension.Decode(w)
|
|
if err != nil || len(back) != 4 || back[1].ID != "org.a" || string(back[1].Data) != "x" || back[0].Data != nil {
|
|
t.Fatalf("decode: %+v %v", back, err)
|
|
}
|
|
// The wire form: data is a byte string, and key 2 is omitted without data.
|
|
b, err := codec.Marshal(w[:2])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := hex.EncodeToString(b), "82"+"a200615a0109"+"a300656f72672e6101020241"+"78"; got != want {
|
|
t.Fatalf("wire %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func many(n int) []extension.Extension {
|
|
out := make([]extension.Extension, n)
|
|
for i := range out {
|
|
out[i] = extension.Extension{ID: fmt.Sprintf("org.example.%03d", i), Version: 1}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestEncodeRejects(t *testing.T) {
|
|
for name, in := range map[string][]extension.Extension{
|
|
"same id twice": {ext(t, "org.a", 1, nil), ext(t, "org.a", 2, nil)},
|
|
"empty id": {{ID: "", Version: 1}},
|
|
"invalid UTF-8 id": {{ID: "org.\xff", Version: 1}},
|
|
"present but empty": {{ID: "org.a", Version: 1, Data: []byte{}}},
|
|
"version above 2^32": {{ID: "org.a", Version: extension.MaxVersion + 1}},
|
|
"65 extensions (§64)": many(extension.MaxExtensions + 1),
|
|
"duplicate among many": append(many(3), extension.Extension{ID: "org.example.001", Version: 2}),
|
|
} {
|
|
if _, err := extension.Encode(in); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Errorf("%s: %v", name, err)
|
|
}
|
|
}
|
|
if w, err := extension.Encode(many(extension.MaxExtensions)); err != nil || len(w) != extension.MaxExtensions {
|
|
t.Fatalf("64 extensions rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func wires(exts []extension.Extension) []extension.Wire {
|
|
out := make([]extension.Wire, len(exts))
|
|
for i, e := range exts {
|
|
out[i] = extension.Wire{ID: e.ID, Version: e.Version, Data: e.Data}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestDecodeRejects(t *testing.T) {
|
|
for name, in := range map[string][]extension.Wire{
|
|
"out of order": {{ID: "org.b", Version: 1}, {ID: "org.a", Version: 1}},
|
|
"repeated id": {{ID: "org.a", Version: 1}, {ID: "org.a", Version: 2}},
|
|
"present but empty": {{ID: "org.a", Version: 1, Data: []byte{}}},
|
|
"version above 2^32": {{ID: "org.a", Version: extension.MaxVersion + 1}},
|
|
"empty id": {{ID: "", Version: 1}},
|
|
"65 extensions": wires(many(extension.MaxExtensions + 1)),
|
|
} {
|
|
if _, err := extension.Decode(in); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Errorf("%s: %v", name, err)
|
|
}
|
|
}
|
|
if got, err := extension.Decode(wires(many(extension.MaxExtensions))); err != nil || len(got) != extension.MaxExtensions {
|
|
t.Fatalf("64 extensions rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
// Spec §54: key 2 is absent or a non-empty byte string, whatever it contains.
|
|
// Every other form is rejected by the extension map itself.
|
|
func TestWireData(t *testing.T) {
|
|
const head = "a3006161" + "0101" + "02" // {0: "a", 1: 1, 2: ...}
|
|
valid := []struct{ name, item, data string }{
|
|
{"one byte", "4100", "00"},
|
|
{"bytes that are not CBOR", "44ff1c00f7", "ff1c00f7"},
|
|
{"CBOR that the base protocol never decodes", "49a2f97e0000f97e0001", "a2f97e0000f97e0001"},
|
|
{"14 nested arrays", "4f" + strings.Repeat("81", 14) + "00", strings.Repeat("81", 14) + "00"},
|
|
{"24 bytes, one-byte length", "5818" + strings.Repeat("ab", 24), strings.Repeat("ab", 24)},
|
|
}
|
|
for _, tc := range valid {
|
|
var w extension.Wire
|
|
b, _ := hex.DecodeString(head + tc.item)
|
|
if err := codec.Unmarshal(b, &w); err != nil || hex.EncodeToString(w.Data) != tc.data {
|
|
t.Errorf("%s: %x %v", tc.name, w.Data, err)
|
|
}
|
|
}
|
|
var none extension.Wire
|
|
if err := codec.Unmarshal([]byte{0xa2, 0x00, 0x61, 0x61, 0x01, 0x01}, &none); err != nil || none.Data != nil || none.ID != "a" {
|
|
t.Fatalf("extension without data: %+v %v", none, err)
|
|
}
|
|
for _, tc := range []struct{ name, item string }{
|
|
{"empty byte string h''", "40"},
|
|
{"text string", "6161"},
|
|
{"unsigned integer", "01"},
|
|
{"negative integer", "20"},
|
|
{"array", "820102"},
|
|
{"empty array", "80"},
|
|
{"map", "a10001"},
|
|
{"true", "f5"},
|
|
{"null", "f6"},
|
|
{"undefined", "f7"},
|
|
{"float", "f93c00"},
|
|
{"tag", "c24101"},
|
|
{"length not in shortest form", "5801" + "00"},
|
|
{"indefinite-length byte string", "5f4100ff"},
|
|
{"truncated byte string", "42" + "00"},
|
|
} {
|
|
var w extension.Wire
|
|
b, _ := hex.DecodeString(head + tc.item)
|
|
if err := codec.Unmarshal(b, &w); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Errorf("%s: %x %v", tc.name, w.Data, err)
|
|
}
|
|
}
|
|
// The same rule inside an array, as the containing objects decode it.
|
|
var arr []extension.Wire
|
|
b, _ := hex.DecodeString("81" + head + "40")
|
|
if err := codec.Unmarshal(b, &arr); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Fatalf("h'' in an array: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckDisjoint(t *testing.T) {
|
|
crit := []extension.Extension{ext(t, "org.a", 1, nil)}
|
|
non := []extension.Extension{ext(t, "org.a", 2, nil)}
|
|
if err := extension.CheckDisjoint(crit, non); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Fatalf("id in both arrays: %v", err)
|
|
}
|
|
a := []extension.Extension{{ID: "a"}, {ID: "c"}, {ID: "e"}}
|
|
b := []extension.Extension{{ID: "b"}, {ID: "d"}, {ID: "f"}}
|
|
if err := extension.CheckDisjoint(a, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Input that is not in canonical order is sorted before the merge.
|
|
unsorted := []extension.Extension{{ID: "z"}, {ID: "e"}}
|
|
if err := extension.CheckDisjoint(a, unsorted); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Fatalf("unsorted input: %v", err)
|
|
}
|
|
if !slices.IsSortedFunc(unsorted, func(x, y extension.Extension) int { return strings.Compare(y.ID, x.ID) }) {
|
|
t.Fatal("CheckDisjoint reordered its input")
|
|
}
|
|
if err := extension.CheckDisjoint(nil, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// The merge is linear: 200 000 + 200 000 identifiers take milliseconds, where
|
|
// the former pairwise comparison took hours (spec §76, case 6).
|
|
func TestCheckDisjointIsLinear(t *testing.T) {
|
|
const n = 200_000
|
|
crit, non := make([]extension.Extension, n), make([]extension.Extension, n)
|
|
for i := range n {
|
|
crit[i].ID = fmt.Sprintf("a.%07d", i)
|
|
non[i].ID = fmt.Sprintf("b.%07d", i)
|
|
}
|
|
start := time.Now()
|
|
if err := extension.CheckDisjoint(crit, non); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d := time.Since(start); d > 5*time.Second {
|
|
t.Fatalf("CheckDisjoint took %s", d)
|
|
}
|
|
}
|
|
|
|
// validator knows org.a v1 and org.b v1, and accepts only the data "ok".
|
|
type validator struct{}
|
|
|
|
func (validator) Known(id string, v uint64) bool { return (id == "org.a" || id == "org.b") && v == 1 }
|
|
|
|
func (validator) ValidateData(e extension.Extension) error {
|
|
if string(e.Data) != "ok" {
|
|
// A validator may report a normative code of its own; the result
|
|
// carries ErrExtensionDataInvalid only.
|
|
return fmt.Errorf("want \"ok\": %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestCheckCritical(t *testing.T) {
|
|
crit := []extension.Extension{ext(t, "org.a", 1, nil)}
|
|
if err := extension.CheckCritical(crit, nil); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
|
|
t.Fatalf("unknown critical accepted by the base protocol: %v", err)
|
|
}
|
|
if err := extension.CheckCritical(crit, extension.Set{"org.a": {2}}); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
|
|
t.Fatalf("other version accepted: %v", err)
|
|
}
|
|
if err := extension.CheckCritical(crit, extension.Set{"org.a": {1}}); err != nil {
|
|
t.Fatalf("known critical rejected: %v", err)
|
|
}
|
|
if err := extension.CheckCritical(nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Spec §54: a known critical extension with invalid data.
|
|
good, bad := ext(t, "org.a", 1, []byte("ok")), ext(t, "org.b", 1, []byte("ko"))
|
|
if err := extension.CheckCritical([]extension.Extension{good}, validator{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := extension.CheckCritical([]extension.Extension{good, bad}, validator{})
|
|
if datekeys.Code(err) != "ERR_EXTENSION_DATA_INVALID" || errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Fatalf("invalid data: %v", err)
|
|
}
|
|
// An unknown critical extension takes precedence over invalid data.
|
|
unknown := ext(t, "org.c", 1, nil)
|
|
if err := extension.CheckCritical([]extension.Extension{bad, unknown}, validator{}); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
|
|
t.Fatalf("unknown and invalid: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckNoncritical(t *testing.T) {
|
|
good, bad, unknown := ext(t, "org.a", 1, []byte("ok")), ext(t, "org.b", 1, nil), ext(t, "org.c", 1, []byte("ko"))
|
|
all := []extension.Extension{good, bad, unknown}
|
|
if u := extension.CheckNoncritical(all, nil); u != nil {
|
|
t.Fatalf("base protocol: %v", u)
|
|
}
|
|
if u := extension.CheckNoncritical(all, extension.Set{"org.b": {1}}); u != nil {
|
|
t.Fatalf("a Set does not validate data: %v", u)
|
|
}
|
|
u := extension.CheckNoncritical(all, validator{})
|
|
if len(u) != 1 || u[0].ID != "org.b" || u[0].Version != 1 || !errors.Is(u[0].Err, datekeys.ErrExtensionDataInvalid) {
|
|
t.Fatalf("unusable: %+v", u)
|
|
}
|
|
}
|