The vector files of spec 67 for format 3, generated with the result each case is written for, so that the generator fails when the implementation or the tables change: - paths.json: 83 paths with the result of the rules of one entry, the violation worded as every implementation must word it, and 16 trees, the paths of a head and the result of decoding it: U+00A0, accepted, and U+3000, R6c, at both ends of a segment; best-fit, full-width forms, U+00B4 of cp1253; 8.3 aliases with ~1; Cn; U+206A to U+206F, tags and other ignorables; a dot and ZWJ, and ZWJ alone; 127 and 85 times U+0390; U+F03A; .datekeys-x at two levels; U+FF5E and U+1F600 in both orders; ab with and without ZWNJ; U+00BF, U+00A7 and U+2665, accepted; VS16 after U+2764 and after a; ZWJ at the start, at the end and twice; the rainbow flag and the flag of Scotland; b/.. and a. - path_fold.json: 22 segments with their NFD and their key of R7, among them the entries F of CaseFolding, the dotless i, the Kelvin and Angstrom signs, Cherokee, Hangul and the whitelist dropped before NFD. - head_schema.json: 63 heads through layers 2, 3 and 4, in key order, with the violation of each ERR_HEAD_INVALID; they add comments with tags and with loose variation selectors. - security.json: 21 areas with their verdicts, X, F0, F1, S0, S1 and S2, among them key 2 that is not a byte string, a key 4, a byte more, alg 0, an empty key with the seal intact, and a seal that breaks its schema with an unknown seal_type. - cbor.json gains the control of schema version 3. A test replays every committed vector through the implementation, and another checks that the files are current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.10
parent
e070f891ea
commit
db862d5524
@ -0,0 +1,577 @@
|
||||
package testkit
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
datekeys "g.activething.com/go/DateKeys"
|
||||
"g.activething.com/go/DateKeys/capsule"
|
||||
"g.activething.com/go/DateKeys/internal/cbortest"
|
||||
"g.activething.com/go/DateKeys/internal/pathrule"
|
||||
)
|
||||
|
||||
// The vectors of format 3 (spec §67): testdata/vectors/paths.json,
|
||||
// path_fold.json, head_schema.json and security.json.
|
||||
|
||||
// PathVectorFile is testdata/vectors/paths.json: the rules of spec §29.5 for
|
||||
// one path, and for the paths of a head.
|
||||
type PathVectorFile struct {
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
UnicodeVersion string `json:"unicode_version"`
|
||||
TablesDigest string `json:"tables_digest"`
|
||||
Paths []PathVector `json:"paths"`
|
||||
Trees []TreeVector `json:"trees"`
|
||||
}
|
||||
|
||||
// PathVector is a path and the result of the rules of one entry, R2 to R6c
|
||||
// and R10 (spec §29.5): ResultOK, or the violation as every implementation
|
||||
// words it, such as "R4: segment 1: control U+0009".
|
||||
type PathVector struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
// TreeVector is the paths of the files of a head, of 0 bytes each, and the
|
||||
// result of decoding that head (spec §29.4, §29.5): ResultOK or its code, R1
|
||||
// and R8 being of layer 3 and the other rules of layer 4. Detail is the
|
||||
// violation of an ERR_HEAD_INVALID, as for HeadVector.
|
||||
type TreeVector struct {
|
||||
Name string `json:"name"`
|
||||
Paths []string `json:"paths"`
|
||||
Result string `json:"result"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// PathFoldFile is testdata/vectors/path_fold.json: the key of R7 of a
|
||||
// segment, NFD(fold(NFD(s'))) with s' the segment without the whitelist of
|
||||
// R4 (spec §29.5, §29.5.1).
|
||||
type PathFoldFile struct {
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
UnicodeVersion string `json:"unicode_version"`
|
||||
TablesDigest string `json:"tables_digest"`
|
||||
Keys []FoldVector `json:"keys"`
|
||||
}
|
||||
|
||||
// FoldVector is a segment, its NFD and its key of R7.
|
||||
type FoldVector struct {
|
||||
Name string `json:"name"`
|
||||
Segment string `json:"segment"`
|
||||
NFD string `json:"nfd"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// HeadSchemaFile is testdata/vectors/head_schema.json: heads and the result
|
||||
// of decoding them with no extension known (spec §29.4 to §29.6, §69.1).
|
||||
type HeadSchemaFile struct {
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
Heads []HeadVector `json:"heads"`
|
||||
}
|
||||
|
||||
// HeadVector is HEAD_CBOR and its result: ResultOK or the normative code.
|
||||
// Detail is the violation of an ERR_HEAD_INVALID, as every implementation
|
||||
// words it: "comment: text: bidirectional control U+202E", "file 1: R3:
|
||||
// segment 1: the segment is two dots" or "R7: path 2 collides with path 1 in
|
||||
// segment 1".
|
||||
type HeadVector struct {
|
||||
Name string `json:"name"`
|
||||
Hex string `json:"hex"`
|
||||
Result string `json:"result"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// SecurityVectorFile is testdata/vectors/security.json: SECURITY_CBOR and
|
||||
// its verdicts (spec §29.3, §29.7).
|
||||
type SecurityVectorFile struct {
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
Vectors []SecurityVector `json:"vectors"`
|
||||
}
|
||||
|
||||
// SecurityVector is SECURITY_CBOR, exactly its SECURITY_LEN bytes, and the
|
||||
// verdicts of the signature and of the seal.
|
||||
type SecurityVector struct {
|
||||
Name string `json:"name"`
|
||||
Hex string `json:"hex"`
|
||||
Signature string `json:"signature"`
|
||||
Seal string `json:"seal"`
|
||||
}
|
||||
|
||||
// HeadDetail is the violation of an ERR_HEAD_INVALID of capsule.DecodeHead,
|
||||
// without its prefix and its code, and "" for any other error.
|
||||
func HeadDetail(err error) string {
|
||||
if !errors.Is(err, datekeys.ErrHeadInvalid) {
|
||||
return ""
|
||||
}
|
||||
s := strings.TrimPrefix(err.Error(), "capsule: head: ")
|
||||
return strings.TrimSuffix(s, ": "+datekeys.ErrHeadInvalid.Code())
|
||||
}
|
||||
|
||||
// PathResult is the result of the rules of one entry for path.
|
||||
func PathResult(path string) string {
|
||||
if err := pathrule.CheckPath(path); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ResultOK
|
||||
}
|
||||
|
||||
// emptySHA256 is the SHA-256 of a file of 0 bytes.
|
||||
var emptySHA256 = sha256.Sum256(nil)
|
||||
|
||||
// head3 returns a head map with a zero salt and the given keys; a nil value
|
||||
// removes the key.
|
||||
func head3(kv ...any) map[uint64]any {
|
||||
h := map[uint64]any{0: capsule.HeadTypeTag, 1: uint64(capsule.HeadVersion), 2: make([]byte, capsule.SaltSize)}
|
||||
for i := 0; i < len(kv); i += 2 {
|
||||
k := uint64(kv[i].(int))
|
||||
if kv[i+1] == nil {
|
||||
delete(h, k)
|
||||
} else {
|
||||
h[k] = kv[i+1]
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// entry3 is a file entry of a head.
|
||||
func entry3(path any, size, start, end uint64) map[uint64]any {
|
||||
return map[uint64]any{0: path, 1: size, 2: start, 3: end, 4: emptySHA256[:]}
|
||||
}
|
||||
|
||||
// TreeHead is HEAD_CBOR with a file of 0 bytes for each path, in the order
|
||||
// given.
|
||||
func TreeHead(paths []string) ([]byte, error) {
|
||||
var files []any
|
||||
for _, p := range paths {
|
||||
files = append(files, entry3(p, 0, 0, 0))
|
||||
}
|
||||
return cbortest.Marshal(head3(5, files))
|
||||
}
|
||||
|
||||
// HeadResult decodes HEAD_CBOR with no extension known and returns its
|
||||
// result and, for ERR_HEAD_INVALID, its violation.
|
||||
func HeadResult(b []byte) (string, string) {
|
||||
_, err := capsule.DecodeHead(b, nil)
|
||||
return Result(err), HeadDetail(err)
|
||||
}
|
||||
|
||||
// SecurityResult returns the verdicts of SECURITY_CBOR.
|
||||
func SecurityResult(b []byte) (string, string) {
|
||||
v := capsule.EvaluateSecurity(b)
|
||||
return string(v.Signature), string(v.Seal)
|
||||
}
|
||||
|
||||
// check compares got with want: "*" accepts anything, and otherwise want
|
||||
// is got or its start up to a ": ".
|
||||
func check(what, name, got, want string) error {
|
||||
if want == "*" || got == want || strings.HasPrefix(got, want+": ") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s %q: got %q, want %q", what, name, got, want)
|
||||
}
|
||||
|
||||
// PathVectors computes testdata/vectors/paths.json, and fails if a vector
|
||||
// does not get the result it is written for.
|
||||
func PathVectors() (PathVectorFile, error) {
|
||||
f := PathVectorFile{
|
||||
Spec: SpecVersion,
|
||||
Description: "Paths of a format 3 head (spec §29.5) with the Unicode 18.0.0 and best-fit tables of §29.5.1, generated by the reference implementation. " +
|
||||
"paths: one path and the rules of one entry, R2 to R6c and R10; trees: the paths of a head, of 0 bytes each, and the result of decoding it. See testdata/README.md.",
|
||||
UnicodeVersion: pathrule.UnicodeVersion,
|
||||
TablesDigest: pathrule.TablesDigest,
|
||||
}
|
||||
var errs []error
|
||||
for _, v := range pathCases() {
|
||||
got := PathResult(v.path)
|
||||
if err := check("path", v.name, got, v.want); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
f.Paths = append(f.Paths, PathVector{Name: v.name, Path: v.path, Result: got})
|
||||
}
|
||||
for _, v := range treeCases() {
|
||||
b, err := TreeHead(v.paths)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
got, detail := HeadResult(b)
|
||||
full := got
|
||||
if detail != "" {
|
||||
full += ": " + detail
|
||||
}
|
||||
if err := check("tree", v.name, full, v.want); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
f.Trees = append(f.Trees, TreeVector{Name: v.name, Paths: v.paths, Result: got, Detail: detail})
|
||||
}
|
||||
return f, errors.Join(errs...)
|
||||
}
|
||||
|
||||
type pathCase struct{ name, path, want string }
|
||||
|
||||
func pathCases() []pathCase {
|
||||
scotland := "\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f"
|
||||
return []pathCase{
|
||||
{"a file", "nota.txt", ResultOK},
|
||||
{"a file in two folders", "fotos/2025/playa.jpg", ResultOK},
|
||||
{"non-ASCII letters", "música/canción.txt", ResultOK},
|
||||
{"U+00BF, which bestfit1250 maps to '?'", "¿Qué es esto.jpg", ResultOK},
|
||||
{"U+00A7, which bestfit874 maps to a C0 control", "§ 3 contrato.pdf", ResultOK},
|
||||
{"U+2665, which bestfit874 maps to a C0 control", "Para ti ♥.jpg", ResultOK},
|
||||
{"U+2192, which bestfit1253 maps to '>'", "Madrid → Lisboa", ResultOK},
|
||||
{"U+00A0 at the start of a segment: no table maps it to ASCII", "\u00a0a", ResultOK},
|
||||
{"U+00A0 at the end of a segment", "a\u00a0", ResultOK},
|
||||
{"U+3000 at the start: bestfit1250 and others map it to U+0020", "\u3000a", "R6c"},
|
||||
{"U+3000 at the end", "a\u3000", "R6c"},
|
||||
{"CON.txt in full-width forms", "\uff23\uff2f\uff2e.txt", "R6c"},
|
||||
{"U+2216 SET MINUS", "a\u2216b", "R6c"},
|
||||
{"U+2236 RATIO", "a\u2236b", "R6c"},
|
||||
{"U+00A5, which cp932 maps to '\\'", "a\u00a5b", "R6c"},
|
||||
{"U+20A9, which cp949 maps to '\\'", "a\u20a9b", "R6c"},
|
||||
{"U+00B4, which cp1253 maps to '/'", "a\u00b4b", "R6c"},
|
||||
{"full-width solidus", "a\uff0fb", "R6c"},
|
||||
{"full-width reverse solidus", "a\uff3cb", "R6c"},
|
||||
{"full-width colon", "a\uff1ab", "R6c"},
|
||||
{"two full-width full stops", "\uff0e\uff0e", "R6c"},
|
||||
{"8.3 alias", "ABCDEF~1", "R6b"},
|
||||
{"8.3 alias with an extension", "ABCDEF~1.TXT", "R6b"},
|
||||
{"~1 alone", "~1", "R6b"},
|
||||
{"8.3 alias with a non-ASCII letter", "\u00c4~1.txt", "R6b"},
|
||||
{"nine characters before ~1", "ABCDEFGHI~1", ResultOK},
|
||||
{"~1 inside a name", "a~1b", ResultOK},
|
||||
{"~2023 in a long name", "report~2023.txt", ResultOK},
|
||||
{"unassigned U+0378", "a\u0378b", "R4"},
|
||||
{"noncharacter U+FFFE", "a\ufffeb", "R4"},
|
||||
{"U+206A", "a\u206ab", "R4"},
|
||||
{"U+206F", "a\u206fb", "R4"},
|
||||
{"soft hyphen U+00AD", "a\u00adb", "R4"},
|
||||
{"U+034F COMBINING GRAPHEME JOINER", "a\u034fb", "R4"},
|
||||
{"U+200B ZERO WIDTH SPACE", "a\u200bb", "R4"},
|
||||
{"U+2060 WORD JOINER", "a\u2060b", "R4"},
|
||||
{"U+3164 HANGUL FILLER", "a\u3164b", "R4"},
|
||||
{"U+FEFF", "a\ufeffb", "R4"},
|
||||
{"U+202E RIGHT-TO-LEFT OVERRIDE", "a\u202eb", "R4"},
|
||||
{"tag U+E0041", "a\U000e0041", "R4"},
|
||||
{"variation selector VS17", "\U0001f600\U000e0100", "R4"},
|
||||
{"the flag of Scotland, with tags", scotland, "R4"},
|
||||
{"U+F03A of the private use area", "informe\uf03aanexo", "R4"},
|
||||
{"TAB", "a\u0009b", "R4"},
|
||||
{"U+2028 LINE SEPARATOR", "a\u2028b", "R4"},
|
||||
{"':'", "a:b", "R4"},
|
||||
{"'\\'", "a\\b", "R4"},
|
||||
{"'?'", "a?b", "R4"},
|
||||
{"'\"'", "a\"b", "R4"},
|
||||
{"'*', '<', '>' and '|'", "a*b<c>d|e", "R4"},
|
||||
{"a dot and ZWJ", ".\u200d", "R3"},
|
||||
{"a segment of ZWJ alone", "\u200d", "R3"},
|
||||
{"two dots", "..", "R3"},
|
||||
{"one dot inside", "a/./b", "R3"},
|
||||
{"a segment of 256 bytes", strings.Repeat("a", 256), "R3"},
|
||||
{"a segment of 255 bytes", strings.Repeat("a", 255), ResultOK},
|
||||
{"127 times U+0390: 381 UTF-16 units after NFD", strings.Repeat("\u0390", 127), "R3"},
|
||||
{"85 times U+0390: 255 UTF-16 units after NFD", strings.Repeat("\u0390", 85), ResultOK},
|
||||
{"VS16 after U+2764, which admits it", "\u2764\ufe0f.txt", ResultOK},
|
||||
{"VS16 after a", "a\ufe0f", "R4b"},
|
||||
{"ZWJ at the start", "\u200da", "R4b"},
|
||||
{"ZWJ at the end", "a\u200d", "R4b"},
|
||||
{"two ZWJ in a row", "a\u200d\u200db", "R4b"},
|
||||
{"ZWNJ and ZWJ in a row", "a\u200c\u200db", "R4b"},
|
||||
{"ZWNJ inside a name", "ab\u200cc", ResultOK},
|
||||
{"the rainbow flag", "\U0001f3f3\ufe0f\u200d\U0001f308", ResultOK},
|
||||
{"a family, joined with ZWJ", "\U0001f468\u200d\U0001f469\u200d\U0001f467", ResultOK},
|
||||
{"a leading slash", "/a", "R2"},
|
||||
{"two slashes", "a//b", "R2"},
|
||||
{"a trailing slash", "a/", "R2"},
|
||||
{"33 segments", strings.Repeat("a/", 32) + "a", "R2"},
|
||||
{"a leading space", " a", "R5"},
|
||||
{"a trailing space", "a ", "R5"},
|
||||
{"a trailing dot", "a.", "R5"},
|
||||
{"CON.txt", "CON.txt", "R6"},
|
||||
{"con", "con", "R6"},
|
||||
{"Aux with a space before the dot", "Aux .log", "R6"},
|
||||
{"COM with a superscript one", "COM\u00b9", "R6"},
|
||||
{"lpt9.doc", "lpt9.doc", "R6"},
|
||||
{"CONIN$", "CONIN$", "R6"},
|
||||
{".datekeys-x at the first level", ".datekeys-x", "R10"},
|
||||
{".DateKeys-X at the first level, compared by its key", ".DateKeys-X/b", "R10"},
|
||||
{".datekeys-x at another level", "a/.datekeys-x", ResultOK},
|
||||
}
|
||||
}
|
||||
|
||||
type treeCase struct {
|
||||
name string
|
||||
paths []string
|
||||
want string
|
||||
}
|
||||
|
||||
func treeCases() []treeCase {
|
||||
nc, hi := datekeys.ErrNonCanonicalCBOR.Code(), datekeys.ErrHeadInvalid.Code()
|
||||
return []treeCase{
|
||||
{"three files in two folders", []string{"a/b", "a/c", "d"}, ResultOK},
|
||||
{"U+FF5E before U+1F600: UTF-8 byte order", []string{"\uff5e", "\U0001f600"}, ResultOK},
|
||||
{"U+1F600 before U+FF5E: UTF-16 order, not UTF-8 byte order", []string{"\U0001f600", "\uff5e"}, nc},
|
||||
{"b and a, in that order", []string{"b", "a"}, nc},
|
||||
{"b/.. and a: R8 comes first, in layer 3", []string{"b/..", "a"}, nc},
|
||||
{"the same path twice", []string{"a", "a"}, nc},
|
||||
{"a path of 1025 bytes", []string{strings.Repeat("a/", 512) + "a"}, nc},
|
||||
{".. and a: R3, in layer 4", []string{"..", "a"}, hi + ": file 1: R3"},
|
||||
{"A.txt and a.txt", []string{"A.txt", "a.txt"}, hi + ": R7"},
|
||||
{"A and a/b", []string{"A", "a/b"}, hi + ": R7"},
|
||||
{"a/b and a/b/c", []string{"a/b", "a/b/c"}, hi + ": R7"},
|
||||
{"Fotos/b and fotos/a", []string{"Fotos/b", "fotos/a"}, hi + ": R7"},
|
||||
{"STRASSE and Straße", []string{"STRASSE", "Straße"}, hi + ": R7"},
|
||||
{"k and the Kelvin sign", []string{"k", "\u212a"}, hi + ": R7"},
|
||||
{"ab with and without ZWNJ", []string{"ab", "a\u200cb"}, hi + ": R7"},
|
||||
{"the NFD and the NFC of a name", []string{"e\u0301", "\u00e9"}, hi + ": R7"},
|
||||
}
|
||||
}
|
||||
|
||||
// PathFoldVectors computes testdata/vectors/path_fold.json.
|
||||
func PathFoldVectors() (PathFoldFile, error) {
|
||||
f := PathFoldFile{
|
||||
Spec: SpecVersion,
|
||||
Description: "The key of R7 (spec §29.5) of segments, with the Unicode 18.0.0 tables of §29.5.1, generated by the reference implementation: " +
|
||||
"nfd is NFD(segment) and key is NFD(fold(NFD(s'))), s' the segment without ZWNJ, ZWJ, VS15 and VS16. See testdata/README.md.",
|
||||
UnicodeVersion: pathrule.UnicodeVersion,
|
||||
TablesDigest: pathrule.TablesDigest,
|
||||
}
|
||||
var errs []error
|
||||
for _, v := range foldCases() {
|
||||
key := pathrule.Key(v.segment)
|
||||
if v.key != "" && key != v.key {
|
||||
errs = append(errs, fmt.Errorf("fold %q: key %+q, want %+q", v.name, key, v.key))
|
||||
}
|
||||
f.Keys = append(f.Keys, FoldVector{Name: v.name, Segment: v.segment, NFD: pathrule.NFD(v.segment), Key: key})
|
||||
}
|
||||
return f, errors.Join(errs...)
|
||||
}
|
||||
|
||||
type foldCase struct{ name, segment, key string }
|
||||
|
||||
func foldCases() []foldCase {
|
||||
return []foldCase{
|
||||
{"ASCII capitals", "A.txt", "a.txt"},
|
||||
{"sharp s, entry F", "Straße", "strasse"},
|
||||
{"capital sharp s", "\u1e9e", "ss"},
|
||||
{"the Kelvin sign, a singleton decomposition", "\u212a", "k"},
|
||||
{"the Angstrom sign", "\u212b", "a\u030a"},
|
||||
{"dotless i, the extra folding of NTFS", "\u0131", "i"},
|
||||
{"capital I with a dot, entry F", "\u0130", "i\u0307"},
|
||||
{"precomposed e acute", "\u00e9", "e\u0301"},
|
||||
{"decomposed e acute", "e\u0301", "e\u0301"},
|
||||
{"final sigma", "\u03c2", "\u03c3"},
|
||||
{"U+0390, recursive decomposition", "\u0390", "\u03b9\u0308\u0301"},
|
||||
{"a Cherokee small letter folds to the capital", "\uab70", "\u13a0"},
|
||||
{"a Cherokee capital stays", "\u13a0", "\u13a0"},
|
||||
{"ligature ff, entry F", "\ufb00", "ff"},
|
||||
{"DZ with caron, titlecase", "\u01c5", "\u01c6"},
|
||||
{"Hangul syllable with a final jamo", "\ud55c", "\u1112\u1161\u11ab"},
|
||||
{"canonical order of two marks", "a\u0301\u0323", "a\u0323\u0301"},
|
||||
{"ZWJ between marks is dropped before NFD", "a\u0301\u200d\u0323", "a\u0323\u0301"},
|
||||
{"ZWNJ is dropped", "a\u200cb", "ab"},
|
||||
{"VS16 and ZWJ of the rainbow flag are dropped", "\U0001f3f3\ufe0f\u200d\U0001f308", "\U0001f3f3\U0001f308"},
|
||||
{"a reserved prefix, compared by its key", ".DateKeys-X", ".datekeys-x"},
|
||||
{"full-width letters fold, not to ASCII", "\uff23\uff2f\uff2e", "\uff43\uff4f\uff4e"},
|
||||
}
|
||||
}
|
||||
|
||||
// HeadSchemaVectors computes testdata/vectors/head_schema.json, and fails if
|
||||
// a vector does not get the result it is written for.
|
||||
func HeadSchemaVectors() (HeadSchemaFile, error) {
|
||||
f := HeadSchemaFile{
|
||||
Spec: SpecVersion,
|
||||
Description: "HEAD_CBOR of format 3 (spec §29.4 to §29.6) and the result of decoding it with no extension known, generated by the reference implementation: " +
|
||||
"layer 2 (type tag and version), layer 3 (the CDDL with R1 and R8), then layer 4 in key order (spec §69.1). See testdata/README.md.",
|
||||
}
|
||||
var errs []error
|
||||
for _, v := range headCases() {
|
||||
b, err := cbortest.Marshal(v.value)
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("head %q: %w", v.name, err)
|
||||
}
|
||||
got, detail := HeadResult(b)
|
||||
full := got
|
||||
if detail != "" {
|
||||
full += ": " + detail
|
||||
}
|
||||
if err := check("head", v.name, full, v.want); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
f.Heads = append(f.Heads, HeadVector{Name: v.name, Hex: hex.EncodeToString(b), Result: got, Detail: detail})
|
||||
}
|
||||
return f, errors.Join(errs...)
|
||||
}
|
||||
|
||||
type headCase struct {
|
||||
name string
|
||||
value any
|
||||
want string
|
||||
}
|
||||
|
||||
func headCases() []headCase {
|
||||
nc, un, hi := datekeys.ErrNonCanonicalCBOR.Code(), datekeys.ErrUnsupportedVersion.Code(), datekeys.ErrHeadInvalid.Code()
|
||||
one := []any{entry3("a", 0, 0, 0)}
|
||||
withFile := func(kv ...any) []any {
|
||||
e := entry3("a", 0, 0, 0)
|
||||
for i := 0; i < len(kv); i += 2 {
|
||||
k := uint64(kv[i].(int))
|
||||
if kv[i+1] == nil {
|
||||
delete(e, k)
|
||||
} else {
|
||||
e[k] = kv[i+1]
|
||||
}
|
||||
}
|
||||
return []any{e}
|
||||
}
|
||||
ext := func(id string) map[uint64]any { return map[uint64]any{0: id, 1: uint64(1)} }
|
||||
return []headCase{
|
||||
// Accepted.
|
||||
{"no files and no comment", head3(), ResultOK},
|
||||
{"a comment of one byte", head3(3, "a"), ResultOK},
|
||||
{"a comment with TAB and LF", head3(3, "Hola\u0009mundo\u000aadiós"), ResultOK},
|
||||
{"a comment with an emoji and VS16", head3(3, "\u2764\ufe0f para ti"), ResultOK},
|
||||
{"a declared author", head3(4, "Ana López"), ResultOK},
|
||||
{"one file of 0 bytes", head3(5, one), ResultOK},
|
||||
{"a file with an mtime of 0", head3(5, withFile(5, uint64(0))), ResultOK},
|
||||
{"a file with the last mtime, 9999-12-31T23:59:59Z", head3(5, withFile(5, uint64(capsule.MaxMTime))), ResultOK},
|
||||
{"a comment of 16384 bytes", head3(3, strings.Repeat("a", 16384)), ResultOK},
|
||||
{"a declared author of 256 bytes", head3(4, strings.Repeat("a", 256)), ResultOK},
|
||||
{"a path of 1024 bytes, five segments of 204", head3(5, withFile(0, strings.Join([]string{strings.Repeat("a", 204), strings.Repeat("b", 204), strings.Repeat("c", 204), strings.Repeat("d", 204), strings.Repeat("e", 204)}, "/"))), ResultOK},
|
||||
{"a noncritical extension", head3(7, []any{ext("org.example.a")}), ResultOK},
|
||||
|
||||
// Layer 2.
|
||||
{"the type tag of CONTROL_CBOR", head3(0, capsule.ControlTypeTag), nc},
|
||||
{"version 2", head3(1, uint64(2)), un},
|
||||
{"version 0", head3(1, uint64(0)), un},
|
||||
{"a byte string as the type tag", head3(0, []byte(capsule.HeadTypeTag)), nc},
|
||||
|
||||
// Layer 3.
|
||||
{"no salt", head3(2, nil), nc},
|
||||
{"a salt of 31 bytes", head3(2, make([]byte, 31)), nc},
|
||||
{"a salt as a text string", head3(2, strings.Repeat("a", 32)), nc},
|
||||
{"an empty comment", head3(3, ""), nc},
|
||||
{"a comment of 16385 bytes", head3(3, strings.Repeat("a", 16385)), nc},
|
||||
{"a comment as a byte string", head3(3, []byte("a")), nc},
|
||||
{"a comment that is not UTF-8", head3(3, cbortest.Raw{0x62, 0x61, 0xff}), nc},
|
||||
{"an empty declared author", head3(4, ""), nc},
|
||||
{"a declared author of 257 bytes", head3(4, strings.Repeat("a", 257)), nc},
|
||||
{"an empty array of files", head3(5, []any{}), nc},
|
||||
{"a file without its SHA-256", head3(5, withFile(4, nil)), nc},
|
||||
{"a file with a SHA-256 of 31 bytes", head3(5, withFile(4, make([]byte, 31))), nc},
|
||||
{"a file with an unknown key 6", head3(5, withFile(6, uint64(0))), nc},
|
||||
{"an empty path", head3(5, withFile(0, "")), nc},
|
||||
{"a path of 1025 bytes", head3(5, withFile(0, strings.Repeat("a/", 512)+"a")), nc},
|
||||
{"a path that is not UTF-8", head3(5, withFile(0, cbortest.Raw{0x62, 0x61, 0xff})), nc},
|
||||
{"a size above L_MAX", head3(5, withFile(1, uint64(capsule.MaxPayloadLength+1), 3, uint64(capsule.MaxPayloadLength+1))), nc},
|
||||
{"an mtime of 253402300800", head3(5, withFile(5, uint64(capsule.MaxMTime+1))), nc},
|
||||
{"an unknown key 8", head3(8, uint64(0)), nc},
|
||||
{"keys out of order", cbortest.Pairs{uint64(0), capsule.HeadTypeTag, uint64(1), uint64(1), uint64(3), "a", uint64(2), make([]byte, 32)}, nc},
|
||||
{"a byte more after the map", cbortest.Raw(append(mustMarshal(head3()), 0x00)), nc},
|
||||
{"paths b and a, in that order", head3(5, []any{entry3("b", 0, 0, 0), entry3("a", 0, 0, 0)}), nc},
|
||||
{"paths b/.. and a: R8 before R3", head3(5, []any{entry3("b/..", 0, 0, 0), entry3("a", 0, 0, 0)}), nc},
|
||||
{"an empty array of critical extensions", head3(6, []any{}), nc},
|
||||
|
||||
// Layer 4, in key order: the comment, the declared author, the files.
|
||||
{"a comment with U+202E", head3(3, "a\u202eb"), hi + ": comment: text: bidirectional control U+202E"},
|
||||
{"a comment with the tag U+E0041", head3(3, "Hola\U000e0041"), hi + ": comment: text: invisible U+E0041"},
|
||||
{"a comment with tags that spell a text", head3(3, "Hola\U000e0049\U000e0047\U000e004e\U000e004f\U000e0052\U000e0041"), hi + ": comment: text: invisible U+E0049"},
|
||||
{"a comment with VS16 after a letter", head3(3, "a\ufe0f"), hi + ": comment: text: line 1"},
|
||||
{"a comment with a run of variation selectors", head3(3, "\u2764\ufe0f\ufe0f"), hi + ": comment: text: line 1"},
|
||||
{"a comment with VS17", head3(3, "\U0001f600\U000e0100"), hi + ": comment: text: invisible U+E0100"},
|
||||
{"a comment with CR LF", head3(3, "a\u000d\u000ab"), hi + ": comment: text: control U+000D"},
|
||||
{"a comment with ZWJ at the end of a line", head3(3, "a\u200d\u000ab"), hi + ": comment: text: line 1"},
|
||||
{"a declared author with LF", head3(4, "Ana\u000aLópez"), hi + ": declared author: text: control U+000A in the declared author"},
|
||||
{"a declared author with TAB", head3(4, "Ana\u0009López"), hi + ": declared author: text: control U+0009 in the declared author"},
|
||||
{"a declared author with a leading space", head3(4, " Ana"), hi + ": declared author: text: the declared author starts or ends with U+0020"},
|
||||
{"a declared author with U+200E", head3(4, "a\u200eb"), hi + ": declared author: text: bidirectional control U+200E"},
|
||||
{"path ..", head3(5, withFile(0, "..")), hi + ": file 1: R3: segment 1: the segment is two dots"},
|
||||
{"path /a", head3(5, withFile(0, "/a")), hi + ": file 1: R2"},
|
||||
{"path CON.txt", head3(5, withFile(0, "CON.txt")), hi + ": file 1: R6"},
|
||||
{"path .datekeys-x", head3(5, withFile(0, ".datekeys-x")), hi + ": file 1: R10"},
|
||||
{"a first start that is not 0", head3(5, []any{entry3("a", 1, 1, 2)}), hi + ": file 1: start 1 is not 0, the end of the file before"},
|
||||
{"end minus start that is not size", head3(5, []any{entry3("a", 2, 0, 1)}), hi + ": file 1: from start 0 to end 1 is not the size 2"},
|
||||
{"a gap between two files", head3(5, []any{entry3("a", 1, 0, 1), entry3("b", 1, 2, 3)}), hi + ": file 2: start 2 is not 1, the end of the file before"},
|
||||
{"paths A.txt and a.txt", head3(5, []any{entry3("A.txt", 0, 0, 0), entry3("a.txt", 0, 0, 0)}), hi + ": R7: path 2 collides with path 1 in segment 1"},
|
||||
{"a comment and a path that break: the comment first", head3(3, "a\u202e", 5, withFile(0, "..")), hi + ": comment"},
|
||||
{"a path that breaks and an unknown critical extension: the path first", head3(5, withFile(0, ".."), 6, []any{ext("org.example.a")}), hi + ": file 1: R3"},
|
||||
{"an unknown critical extension", head3(6, []any{ext("org.example.a")}), datekeys.ErrExtensionCriticalUnknown.Code()},
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshal(v any) []byte {
|
||||
b, err := cbortest.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// SecurityVectors computes testdata/vectors/security.json, and fails if a
|
||||
// vector does not get the verdicts it is written for.
|
||||
func SecurityVectors() (SecurityVectorFile, error) {
|
||||
f := SecurityVectorFile{
|
||||
Spec: SpecVersion,
|
||||
Description: "SECURITY_CBOR of format 3, exactly its SECURITY_LEN bytes, and the verdicts of the signature and of the seal (spec §29.3, §29.7), " +
|
||||
"generated by the reference implementation, which implements no alg and no seal_type. See testdata/README.md.",
|
||||
}
|
||||
var errs []error
|
||||
for _, v := range securityCases() {
|
||||
b, err := cbortest.Marshal(v.value)
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("security %q: %w", v.name, err)
|
||||
}
|
||||
sig, seal := SecurityResult(b)
|
||||
if sig != v.sig || seal != v.seal {
|
||||
errs = append(errs, fmt.Errorf("security %q: %s and %s, want %s and %s", v.name, sig, seal, v.sig, v.seal))
|
||||
}
|
||||
f.Vectors = append(f.Vectors, SecurityVector{Name: v.name, Hex: hex.EncodeToString(b), Signature: sig, Seal: seal})
|
||||
}
|
||||
return f, errors.Join(errs...)
|
||||
}
|
||||
|
||||
type securityCase struct {
|
||||
name string
|
||||
value any
|
||||
sig, seal string
|
||||
}
|
||||
|
||||
func securityCases() []securityCase {
|
||||
outer := func(kv ...any) map[uint64]any {
|
||||
m := map[uint64]any{0: capsule.SecurityTypeTag, 1: uint64(capsule.SecurityVersion)}
|
||||
for i := 0; i < len(kv); i += 2 {
|
||||
m[uint64(kv[i].(int))] = kv[i+1]
|
||||
}
|
||||
return m
|
||||
}
|
||||
signature := mustMarshal(map[uint64]any{0: uint64(1), 1: fill(0x11, 32), 2: append(fill(0x22, 32), fill(0x22, 32)...)})
|
||||
seal := mustMarshal(map[uint64]any{0: uint64(1), 1: fill(0x33, 32)})
|
||||
x, f0, f1, s0, s1, s2 := "X", "F0", "F1", "S0", "S1", "S2"
|
||||
return []securityCase{
|
||||
{"empty, as writers of this version write it", outer(), f0, s0},
|
||||
{"a signature of alg 1", outer(2, signature), f1, s0},
|
||||
{"a seal of seal_type 1", outer(3, seal), f0, s1},
|
||||
{"a seal of seal_type 2, RFC 3161", outer(3, mustMarshal(map[uint64]any{0: uint64(2), 1: fill(0x33, 32)})), f0, s1},
|
||||
{"a seal of seal_type 3, OpenTimestamps", outer(3, mustMarshal(map[uint64]any{0: uint64(3), 1: fill(0x33, 32)})), f0, s1},
|
||||
{"a signature and a seal", outer(2, signature, 3, seal), f1, s1},
|
||||
{"a signature of alg 0, and the seal intact", outer(2, mustMarshal(map[uint64]any{0: uint64(0), 1: fill(0x11, 32), 2: fill(0x22, 32)}), 3, seal), f1, s1},
|
||||
{"a signature with an empty key, and the seal intact", outer(2, mustMarshal(map[uint64]any{0: uint64(1), 1: []byte{}, 2: fill(0x22, 32)}), 3, seal), f1, s1},
|
||||
{"a signature that is not CBOR", outer(2, []byte{0xff}), f1, s0},
|
||||
{"a signature with a byte more", outer(2, append(append([]byte{}, signature...), 0x00)), f1, s0},
|
||||
{"a seal of seal_type 0", outer(3, mustMarshal(map[uint64]any{0: uint64(0), 1: fill(0x33, 32)})), f0, s2},
|
||||
{"a seal that breaks its schema, with an unknown seal_type", outer(3, mustMarshal(map[uint64]any{0: uint64(99), 1: fill(0x33, 32), 2: uint64(0)})), f0, s2},
|
||||
{"a seal that is not CBOR", outer(3, []byte{0xff}), f0, s2},
|
||||
{"key 2 that is not a byte string", outer(2, map[uint64]any{0: uint64(1)}), x, x},
|
||||
{"key 2 that is an empty byte string", outer(2, []byte{}), x, x},
|
||||
{"an unknown key 4", outer(4, []byte{1}), x, x},
|
||||
{"a byte more after the map", cbortest.Raw(append(mustMarshal(outer()), 0x00)), x, x},
|
||||
{"version 2", outer(1, uint64(2)), x, x},
|
||||
{"the type tag of the head", outer(0, capsule.HeadTypeTag), x, x},
|
||||
{"keys 2 and 3 out of order", cbortest.Pairs{uint64(0), capsule.SecurityTypeTag, uint64(1), uint64(1), uint64(3), seal, uint64(2), signature}, x, x},
|
||||
{"an array", []any{capsule.SecurityTypeTag, uint64(1)}, x, x},
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,132 @@
|
||||
{
|
||||
"spec": "0.10",
|
||||
"description": "SECURITY_CBOR of format 3, exactly its SECURITY_LEN bytes, and the verdicts of the signature and of the seal (spec §29.3, §29.7), generated by the reference implementation, which implements no alg and no seal_type. See testdata/README.md.",
|
||||
"vectors": [
|
||||
{
|
||||
"name": "empty, as writers of this version write it",
|
||||
"hex": "a20071646174656b6579732d73656375726974790101",
|
||||
"signature": "F0",
|
||||
"seal": "S0"
|
||||
},
|
||||
{
|
||||
"name": "a signature of alg 1",
|
||||
"hex": "a30071646174656b6579732d73656375726974790101025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222",
|
||||
"signature": "F1",
|
||||
"seal": "S0"
|
||||
},
|
||||
{
|
||||
"name": "a seal of seal_type 1",
|
||||
"hex": "a30071646174656b6579732d73656375726974790101035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
|
||||
"signature": "F0",
|
||||
"seal": "S1"
|
||||
},
|
||||
{
|
||||
"name": "a seal of seal_type 2, RFC 3161",
|
||||
"hex": "a30071646174656b6579732d73656375726974790101035826a200020158203333333333333333333333333333333333333333333333333333333333333333",
|
||||
"signature": "F0",
|
||||
"seal": "S1"
|
||||
},
|
||||
{
|
||||
"name": "a seal of seal_type 3, OpenTimestamps",
|
||||
"hex": "a30071646174656b6579732d73656375726974790101035826a200030158203333333333333333333333333333333333333333333333333333333333333333",
|
||||
"signature": "F0",
|
||||
"seal": "S1"
|
||||
},
|
||||
{
|
||||
"name": "a signature and a seal",
|
||||
"hex": "a40071646174656b6579732d73656375726974790101025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
|
||||
"signature": "F1",
|
||||
"seal": "S1"
|
||||
},
|
||||
{
|
||||
"name": "a signature of alg 0, and the seal intact",
|
||||
"hex": "a40071646174656b6579732d73656375726974790101025849a3000001582011111111111111111111111111111111111111111111111111111111111111110258202222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
|
||||
"signature": "F1",
|
||||
"seal": "S1"
|
||||
},
|
||||
{
|
||||
"name": "a signature with an empty key, and the seal intact",
|
||||
"hex": "a40071646174656b6579732d73656375726974790101025828a3000101400258202222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
|
||||
"signature": "F1",
|
||||
"seal": "S1"
|
||||
},
|
||||
{
|
||||
"name": "a signature that is not CBOR",
|
||||
"hex": "a30071646174656b6579732d736563757269747901010241ff",
|
||||
"signature": "F1",
|
||||
"seal": "S0"
|
||||
},
|
||||
{
|
||||
"name": "a signature with a byte more",
|
||||
"hex": "a30071646174656b6579732d7365637572697479010102586aa3000101582011111111111111111111111111111111111111111111111111111111111111110258402222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222200",
|
||||
"signature": "F1",
|
||||
"seal": "S0"
|
||||
},
|
||||
{
|
||||
"name": "a seal of seal_type 0",
|
||||
"hex": "a30071646174656b6579732d73656375726974790101035826a200000158203333333333333333333333333333333333333333333333333333333333333333",
|
||||
"signature": "F0",
|
||||
"seal": "S2"
|
||||
},
|
||||
{
|
||||
"name": "a seal that breaks its schema, with an unknown seal_type",
|
||||
"hex": "a30071646174656b6579732d73656375726974790101035829a300186301582033333333333333333333333333333333333333333333333333333333333333330200",
|
||||
"signature": "F0",
|
||||
"seal": "S2"
|
||||
},
|
||||
{
|
||||
"name": "a seal that is not CBOR",
|
||||
"hex": "a30071646174656b6579732d736563757269747901010341ff",
|
||||
"signature": "F0",
|
||||
"seal": "S2"
|
||||
},
|
||||
{
|
||||
"name": "key 2 that is not a byte string",
|
||||
"hex": "a30071646174656b6579732d7365637572697479010102a10001",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
},
|
||||
{
|
||||
"name": "key 2 that is an empty byte string",
|
||||
"hex": "a30071646174656b6579732d736563757269747901010240",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
},
|
||||
{
|
||||
"name": "an unknown key 4",
|
||||
"hex": "a30071646174656b6579732d73656375726974790101044101",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
},
|
||||
{
|
||||
"name": "a byte more after the map",
|
||||
"hex": "a20071646174656b6579732d7365637572697479010100",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
},
|
||||
{
|
||||
"name": "version 2",
|
||||
"hex": "a20071646174656b6579732d73656375726974790102",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
},
|
||||
{
|
||||
"name": "the type tag of the head",
|
||||
"hex": "a2006d646174656b6579732d686561640101",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
},
|
||||
{
|
||||
"name": "keys 2 and 3 out of order",
|
||||
"hex": "a40071646174656b6579732d73656375726974790101035826a200010158203333333333333333333333333333333333333333333333333333333333333333025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
},
|
||||
{
|
||||
"name": "an array",
|
||||
"hex": "8271646174656b6579732d736563757269747901",
|
||||
"signature": "X",
|
||||
"seal": "X"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in new issue