From: Sergey Matveev Date: Thu, 9 Oct 2025 19:22:02 +0000 (+0300) Subject: Ability to check for excess fields X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=HEAD;p=keks.git Ability to check for excess fields --- diff --git a/c/lib/schema.c b/c/lib/schema.c index a1ec8dd..4c819a2 100644 --- a/c/lib/schema.c +++ b/c/lib/schema.c @@ -38,6 +38,7 @@ static const char CmdTake[] = "."; static const char CmdPrec[] = "P"; static const char CmdType[] = "T"; static const char CmdUTC[] = "U"; +static const char CmdExamined[] = "x"; static const char TypeBin[] = "B"; static const char TypeBlob[] = "O"; @@ -135,19 +136,25 @@ keksSchemaLens( return (struct KEKSSchemaErr){.code = KEKSSchemaErrNo}; } +struct keksSchemaValidationState { + size_t nonExamined; + bool eachInList; + bool eachInMap; + bool nonExaminedCheck; + char _pad[5]; +}; + static struct KEKSSchemaErr keksSchemaCmd( // NOLINT(misc-no-recursion) size_t *taken, - bool *eachInList, - bool *eachInMap, + struct keksSchemaValidationState *state, const struct KEKSItems *schema, struct KEKSItems *data, size_t idxSchema, size_t idxData) { assert(taken != NULL); - assert(eachInList != NULL); - assert(eachInMap != NULL); + assert(state != NULL); assert(schema != NULL); assert(data != NULL); size_t origIdxSchema = idxSchema; @@ -249,6 +256,19 @@ Eached: v = *taken; err.msg = "TAKE"; err.code = KEKSSchemaErrNo; + } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdExamined)) { + state->nonExaminedCheck = true; + if (v == SIZE_MAX) { + err.code = KEKSSchemaErrNo; + } else { + if (state->nonExamined == 0) { + err.code = KEKSSchemaErrInvalidSchema; + err.msg = "already zero toExamine"; + } else { + state->nonExamined--; + err.code = KEKSSchemaErrNo; + } + } } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdEq)) { err.msg = "EQ"; if (v == SIZE_MAX) { @@ -277,18 +297,18 @@ Eached: } } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdEach)) { err.msg = "EACH"; - (*eachInList) = false; - (*eachInMap) = false; + state->eachInList = false; + state->eachInMap = false; if (v == SIZE_MAX) { err.code = KEKSSchemaErrNo; return err; } switch (data->list[v].atom.typ) { case KEKSItemList: - (*eachInList) = true; + state->eachInList = true; break; case KEKSItemMap: - (*eachInMap) = true; + state->eachInMap = true; break; case KEKSItemInvalid: case KEKSItemEOC: @@ -314,7 +334,7 @@ Eached: (*taken) = SIZE_MAX; } else { (*taken) = data->list[v].atom.v.list.head; - if (*eachInMap) { + if (state->eachInMap) { (*taken) = data->list[(*taken)].next; } } @@ -599,13 +619,13 @@ Eached: if (err.code != KEKSSchemaErrNo) { return err; } - if (*eachInList) { + if (state->eachInList) { v = data->list[v].next; if (v != 0) { goto Eached; } } - if (*eachInMap) { + if (state->eachInMap) { v = data->list[v].next; // key if (v != 0) { v = data->list[v].next; // value @@ -614,8 +634,8 @@ Eached: } } } - (*eachInList) = false; - (*eachInMap) = false; + state->eachInList = false; + state->eachInMap = false; return err; } @@ -640,23 +660,35 @@ KEKSSchemaValidate( // NOLINT(misc-no-recursion) } idxSchema = schema->list[idxSchema].atom.v.list.head; err.offSchema = schema->offsets[idxSchema]; + struct keksSchemaValidationState state = { + .nonExamined = 0, + .eachInList = false, + .eachInMap = false, + .nonExaminedCheck = false, + }; + if (data->list[idxData].atom.typ == KEKSItemMap) { + state.nonExamined = data->list[idxData].atom.v.list.len; + } size_t taken = idxData; - bool eachInList = false; - bool eachInMap = false; while (idxSchema != 0) { if (schema->list[idxSchema].atom.typ != KEKSItemList) { err.code = KEKSSchemaErrInvalidSchema; err.msg = "non-list cmds"; return err; } - struct KEKSSchemaErr errCmd = keksSchemaCmd( - &taken, &eachInList, &eachInMap, schema, data, idxSchema, idxData); + struct KEKSSchemaErr errCmd = + keksSchemaCmd(&taken, &state, schema, data, idxSchema, idxData); if (errCmd.code != KEKSSchemaErrNo) { return errCmd; } idxSchema = schema->list[idxSchema].next; err.offSchema = schema->offsets[idxSchema]; } + if (state.nonExaminedCheck && state.nonExamined > 0) { + err.code = KEKSSchemaErrInvalidData; + err.msg = "non-examined fields left"; + return err; + } err.code = KEKSSchemaErrNo; return err; } diff --git a/go/schema/check.go b/go/schema/check.go index f03368b..8228cfc 100644 --- a/go/schema/check.go +++ b/go/schema/check.go @@ -42,6 +42,7 @@ const ( CmdPrec = "P" CmdType = "T" CmdUTC = "U" + CmdExamined = "x" Magic = "schema" ) @@ -85,6 +86,11 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { SchemaName: schemaName, Msg: "no such schema", }} } + var nonExamined uint64 + if m, ok := data.(map[string]any); ok { + nonExamined = uint64(len(m)) + } + var nonExaminedCheck bool var taken string vs := []any{data} for cmdIdx, cmd := range cmds { @@ -655,6 +661,21 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { }} } } + case CmdExamined: + nonExaminedCheck = true + if vs == nil { + continue + } + if nonExamined == 0 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Msg: "already zero nonExamined", + }} + } + nonExamined-- default: return &SchemaErr{BaseErr: BaseErr{ SchemaName: schemaName, @@ -664,5 +685,12 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { }} } } + if nonExaminedCheck && nonExamined > 0 { + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + Taken: taken, + Data: nonExamined, + }} + } return nil } diff --git a/spec/schema/cmds b/spec/schema/cmds index e84a31f..5042472 100644 --- a/spec/schema/cmds +++ b/spec/schema/cmds @@ -23,6 +23,14 @@ EXISTS | ["E"] !EXISTS | ["!E"] Assure that chosen element does not exist. +EXAMINED | ["x"] + Enable validation of excess fields in the MAP, decrement the number + of non-examined fields. Initially, if we are checking MAP, then + non-examined fields number equals to elements quantity. Each + examined command decreases that number. When we are finished + executing commands for the given schema, check if number of + non-examined fields is zero. + EACH | ["*"] Execute the next command against every element of the chosen (if it exists) list, or every value of the map. @@ -90,21 +98,25 @@ Corresponding schema can be: {"our": [ ["T", "M"], [".", "a"], + ["x"], ["E"], ["T", "S"], [">", 0], [".", "v"], + ["x"], ["E"], ["T", "B", "S"], [".", "fpr"], + ["x"], ["E"], ["T", "B"], [">", 31], ["<", 33], [".", "comment"], + ["x"], ["T", "S"], ]} diff --git a/spec/schema/tcl b/spec/schema/tcl index 2651db3..ef1275e 100644 --- a/spec/schema/tcl +++ b/spec/schema/tcl @@ -68,6 +68,10 @@ strings are not empty. "len=n" checks the exact length of bin/str/list/map, or integer's value. +len=~ can be set only in "." map field and it allows non-examined, +excess elements to be present in it. Otherwise only the specified ones +in the schema are allowed. + "=v" checks that given bin/str/hexlet/magic has specified binary value. "prec=p" issues PREC command, but instead of specifying the raw diff --git a/tcl/schema.t/generic.t b/tcl/schema.t/generic.t index 7a88013..341dc9b 100755 --- a/tcl/schema.t/generic.t +++ b/tcl/schema.t/generic.t @@ -176,11 +176,14 @@ test_expect_success "only schema" "$SCHEMA_VALIDATE schema.keks e schema.tcl <schema.keks $root/keks.tcl >data.keks.hex <data.keks test_expect_success "map exists" "$SCHEMA_VALIDATE schema.keks e data.keks test_expect_success "map !exists" "! $SCHEMA_VALIDATE schema.keks e schema.tcl <schema.keks $root/keks.tcl >data.keks.hex <data.keks test_expect_success "map optional bad type" "! $SCHEMA_VALIDATE schema.keks e schema.tcl <schema.keks $root/keks.tcl >data.keks.hex <data.keks test_expect_success "map !exists" "$SCHEMA_VALIDATE schema.keks e schema.tcl <schema.keks +$root/keks.tcl >data.keks.hex <data.keks +test_expect_success "map !len=~ without optional" " + ! $SCHEMA_VALIDATE schema.keks e data.keks.hex <data.keks +test_expect_success "map !len=~ with optional" " + ! $SCHEMA_VALIDATE schema.keks e data.keks.hex <data.keks +test_expect_success "map !len=~" "$SCHEMA_VALIDATE schema.keks e schema.tcl <data.keks test_expect_success "hexlet !=" "! $SCHEMA_VALIDATE schema.keks e schema.tcl <schema.keks $root/keks.tcl >data.keks.hex <our.schema.tcl <0}} fpr {{field . {bin} len=32}} our { + {field . {map}} {field a {with ai}} {field v {bin str}} {field fpr {with fpr}} diff --git a/tcl/schema.tcl b/tcl/schema.tcl index 664b66c..8fa2525 100755 --- a/tcl/schema.tcl +++ b/tcl/schema.tcl @@ -29,13 +29,14 @@ proc LT {n} {subst {LIST {{STR <} {INT $n}}}} proc SCHEMA {s} {subst {LIST {{STR S} {STR $s}}}} proc PREC {p} {subst {LIST {{STR P} {INT $p}}}} proc UTC {} {return {LIST {{STR U}}}} +proc EXAMINED {} {return {LIST {{STR x}}}} proc TAKE {k} { if {$k == "."} { set v [list NIL] } elseif {[string first ":" $k] == 0} { set v [list STR [string range $k 1 end]] - } elseif {[string is digit $k]} { + } elseif {[string is integer $k]} { set v [list INT $k] } else { set v [list STR $k] @@ -58,9 +59,16 @@ proc TYPE {types} { set precArgs [dict create s 0 ms 3 us 6 ns 9 ps 12 fs 15] proc field {k types args} { - upvar _cmds _cmds buf buf + upvar _cmds _cmds fieldsNum fieldsNum examinable examinable + if {($fieldsNum == 0 && $k != ".") || ($fieldsNum != 0 && $k == ".")} { + error {first field must be "."} + } + incr fieldsNum if {$k != "."} { lappend _cmds [TAKE $k] + if {$examinable} { + lappend _cmds [EXAMINED] + } if {[lsearch -exact $args !exists] != -1} { lappend _cmds [!EXISTS] } elseif {[lsearch -exact $args optional] == -1} { @@ -83,10 +91,16 @@ proc field {k types args} { lappend _cmds [TAKE $k] } set i [lsearch -glob $args "len=*"] - if {$i != -1} { + if {$i == -1} { + if {$k == "." && [lsearch -exact $types map] != -1} { + set examinable true + } + } else { set n [string range [lindex $args $i] 4 end] - lappend _cmds [GT [expr {$n - 1}]] - lappend _cmds [LT [expr {$n + 1}]] + if {$n != "~"} { + lappend _cmds [GT [expr {$n - 1}]] + lappend _cmds [LT [expr {$n + 1}]] + } } set i [lsearch -glob $args ">*"] if {$i != -1} { @@ -133,7 +147,11 @@ proc process {v} { process $inc continue } - foreach cmd $cmds {eval $cmd} + set fieldsNum 0 + set examinable false + foreach cmd $cmds { + eval $cmd + } lappend _pairs $name [list LIST $_cmds] } } diff --git a/tcl/schemas/encrypted.tcl b/tcl/schemas/encrypted.tcl index a92e189..3dbae4d 100644 --- a/tcl/schemas/encrypted.tcl +++ b/tcl/schemas/encrypted.tcl @@ -15,7 +15,7 @@ dem { } kem { - {field . {map}} + {field . {map} len=~} {field a {str} >0} {field cek {bin} >0} } diff --git a/tcl/schemas/prv.tcl b/tcl/schemas/prv.tcl index a7c484e..544aec3 100644 --- a/tcl/schemas/prv.tcl +++ b/tcl/schemas/prv.tcl @@ -1,5 +1,5 @@ prv { - {field . {map}} + {field . {map} len=~} {field a {str} >0} {# algorithm identifier} {field v {bin}} {field pub-id {with fpr} optional} diff --git a/tcl/schemas/pub-sig-tbs.tcl b/tcl/schemas/pub-sig-tbs.tcl index 834bb26..95b67e0 100644 --- a/tcl/schemas/pub-sig-tbs.tcl +++ b/tcl/schemas/pub-sig-tbs.tcl @@ -2,7 +2,7 @@ exp-tai {{field . {tai} prec=s utc}} expiration {{field . {list} {of exp-tai} len=2}} pub-sig-tbs { - {field . {map}} + {field . {map} len=~} {field sid {with fpr}} {field cid {hexlet}} {field exp {with expiration}} diff --git a/tcl/schemas/pub.tcl b/tcl/schemas/pub.tcl index f2ca6d9..76a3145 100644 --- a/tcl/schemas/pub.tcl +++ b/tcl/schemas/pub.tcl @@ -18,7 +18,7 @@ av { } sig { - {field . {map}} + {field . {map} len=~} {field tbs {with pub-sig-tbs}} {field sign {with av}} } diff --git a/tcl/schemas/signed.tcl b/tcl/schemas/signed.tcl index 27eedf4..da545bb 100644 --- a/tcl/schemas/signed.tcl +++ b/tcl/schemas/signed.tcl @@ -4,7 +4,7 @@ schema-include fpr.tcl signed { {field . {map}} {field tbs {with tbs}} - {# field data is optional, arbitrary type} + {field data {} optional} {field pubs {list} {of type map} >0 optional} {field sigs {list} {of sig} >0 optional} } @@ -22,7 +22,7 @@ sig { } sig-tbs { - {field . {map}} + {field . {map} len=~} {field sid {with fpr}} {field nonce {bin} >0 optional} {# random bytes} {field when {tai} utc prec=ms optional}