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/extension/extension_test.go

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

Powered by TurnKey Linux.