]> Cypherpunks repositories - keks.git/commitdiff
Ability to check for excess fields master
authorSergey Matveev <stargrave@stargrave.org>
Thu, 9 Oct 2025 19:22:02 +0000 (22:22 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 9 Oct 2025 19:54:13 +0000 (22:54 +0300)
12 files changed:
c/lib/schema.c
go/schema/check.go
spec/schema/cmds
spec/schema/tcl
tcl/schema.t/generic.t
tcl/schema.t/specs-our.t
tcl/schema.tcl
tcl/schemas/encrypted.tcl
tcl/schemas/prv.tcl
tcl/schemas/pub-sig-tbs.tcl
tcl/schemas/pub.tcl
tcl/schemas/signed.tcl

index a1ec8ddcd71d45e5eef7e79006de357d60a58937d18d124cf56d8415f268c25f..4c819a27ae8523dda09f202d00ca765efe9a0ebe23cd10ce528441d21b308d65 100644 (file)
@@ -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;
 }
index f03368b1d03f987220823fcf1011ad1f4aa83fabf4fd709738e76bfdd14dd3c8..8228cfc8f68d171eea3556ddcbe0f3dd36368a228e13e12e1dde242c9c26c642 100644 (file)
@@ -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
 }
index e84a31fcc258dec3347c9285962f56264691ed586f015091f5ecb02e52fd9303..5042472bd9fb49c2b1e15655c93b734afc04c8fe2ba0a3c28a5aa891469e357d 100644 (file)
@@ -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"],
     ]}
 
index 2651db380b9d76f23de74a18b15734e7074b452349ea8f8e4b06d58ea518fb20..ef1275e957e6b94de6eca77b6ed8d9ccaae4c59b8cd95f6a0533c67dab29d247 100644 (file)
@@ -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
index 7a88013bd8b7aa1bfec65aeaf203059bffb8fb70f8add999e09dc1d0e2bb39c4..341dc9b2556e38867dc076f24f657a6a793b76b9bf665e4da63fe748a169a652 100755 (executable)
@@ -176,11 +176,14 @@ test_expect_success "only schema" "$SCHEMA_VALIDATE schema.keks e <data.keks"
 ########################################################################
 
 cat >schema.tcl <<EOF
-e {{field bar {int}}}
+e {
+    {field . {map} len=~}
+    {field bar {int}}
+}
 EOF
 $root/schema.tcl schema.tcl | xxd -r -p >schema.keks
 $root/keks.tcl >data.keks.hex <<EOF
-MAP {foo NIL bar {INT 0}}
+MAP {bar {INT 0}}
 EOF
 xxd -r -p <data.keks.hex >data.keks
 test_expect_success "map exists" "$SCHEMA_VALIDATE schema.keks e <data.keks"
@@ -192,7 +195,10 @@ xxd -r -p <data.keks.hex >data.keks
 test_expect_success "map !exists" "! $SCHEMA_VALIDATE schema.keks e <data.keks"
 
 cat >schema.tcl <<EOF
-e {{field bar {int} optional}}
+e {
+    {field . {map} len=~}
+    {field bar {int} optional}
+}
 EOF
 $root/schema.tcl schema.tcl | xxd -r -p >schema.keks
 $root/keks.tcl >data.keks.hex <<EOF
@@ -214,7 +220,10 @@ xxd -r -p <data.keks.hex >data.keks
 test_expect_success "map optional bad type" "! $SCHEMA_VALIDATE schema.keks e <data.keks"
 
 cat >schema.tcl <<EOF
-e {{field bar {} !exists}}
+e {
+    {field . {map} len=~}
+    {field bar {} !exists}
+}
 EOF
 $root/schema.tcl schema.tcl | xxd -r -p >schema.keks
 $root/keks.tcl >data.keks.hex <<EOF
@@ -229,6 +238,34 @@ EOF
 xxd -r -p <data.keks.hex >data.keks
 test_expect_success "map !exists" "$SCHEMA_VALIDATE schema.keks e <data.keks"
 
+cat >schema.tcl <<EOF
+e {
+    {field . {map}}
+    {field bar {int}}
+    {field baz {int} optional}
+}
+EOF
+$root/schema.tcl schema.tcl | xxd -r -p >schema.keks
+$root/keks.tcl >data.keks.hex <<EOF
+MAP {foo NIL bar {INT 0}}
+EOF
+xxd -r -p <data.keks.hex >data.keks
+test_expect_success "map !len=~ without optional" "
+    ! $SCHEMA_VALIDATE schema.keks e <data.keks"
+
+$root/keks.tcl >data.keks.hex <<EOF
+MAP {foo NIL bar {INT 0} baz {INT 0}}
+EOF
+xxd -r -p <data.keks.hex >data.keks
+test_expect_success "map !len=~ with optional" "
+    ! $SCHEMA_VALIDATE schema.keks e <data.keks"
+
+$root/keks.tcl >data.keks.hex <<EOF
+MAP {bar {INT 0} baz {INT 0}}
+EOF
+xxd -r -p <data.keks.hex >data.keks
+test_expect_success "map !len=~" "$SCHEMA_VALIDATE schema.keks e <data.keks"
+
 ########################################################################
 
 cat >schema.tcl <<EOF
@@ -446,7 +483,10 @@ xxd -r -p <data.keks.hex >data.keks
 test_expect_success "hexlet !=" "! $SCHEMA_VALIDATE schema.keks e <data.keks"
 
 cat >schema.tcl <<EOF
-e {{field foo {magic} =world}}
+e {
+    {field . {map}}
+    {field foo {magic} =world}
+}
 EOF
 $root/schema.tcl schema.tcl | xxd -r -p >schema.keks
 $root/keks.tcl >data.keks.hex <<EOF
index 70aaf70e9ccea03bb93474f85fee9f76a87a5cc4e80effb1efdc6ae667246844..f7e829042b5d4a79bb8f7ab11345e3055ac8a635c3dd47787aacd474286882c6 100755 (executable)
@@ -10,6 +10,7 @@ cat >our.schema.tcl <<EOF
 ai {{field . {str} >0}}
 fpr {{field . {bin} len=32}}
 our {
+    {field . {map}}
     {field a {with ai}}
     {field v {bin str}}
     {field fpr {with fpr}}
index 664b66c41bd676b08844e98687f7554fa75c56e1b78b475ee29133aae51563c6..8fa25252f92af1ed835748784a78d3985c8f7d5b785f67b40484cd1a20db5b65 100755 (executable)
@@ -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]
     }
 }
index a92e1893e7f1f66c7987c5d8928d23ac149d1a63b0c3aee3acd98f0fd3c1b13b..3dbae4d5612b2058960f2654b15e4b72e4ec4854a49b4066cea9a14c215d4c1d 100644 (file)
@@ -15,7 +15,7 @@ dem {
 }
 
 kem {
-    {field . {map}}
+    {field . {map} len=~}
     {field a {str} >0}
     {field cek {bin} >0}
 }
index a7c484e487e567ee2de0c766359ca674049a9951c7f3e01f40817b2e9e60c0a8..544aec34c553bbf4a9fbd6bc3e8dce3353902273571db5a655b9fcf64870c2aa 100644 (file)
@@ -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}
index 834bb2602597367c423d26354aab292030e4dafff10f899770529ba1c1bc7641..95b67e064dc190154b6bb268688de06b30b5f3ee6e4307e2e1dd4794863c49b6 100644 (file)
@@ -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}}
index f2ca6d9d92cb39ef0c2aa8ba14c454bbc528b1d54184573e9208d64f6dec3f2a..76a314534abbf2ed38a5834b8c489d532cdb260ec955840f5371f69cf3e61245 100644 (file)
@@ -18,7 +18,7 @@ av {
 }
 
 sig {
-    {field . {map}}
+    {field . {map} len=~}
     {field tbs {with pub-sig-tbs}}
     {field sign {with av}}
 }
index 27eedf47cb7325a71964a255f2a949dff83f6703782e7833bd5370ec0348e3fb..da545bbfc6a991acfae9769a696b466774196a3272182814a2223e26722893de 100644 (file)
@@ -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}