py3/keksschema.py | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/py3/keksschema.py b/py3/keksschema.py new file mode 100755 index 0000000000000000000000000000000000000000..b96d33f9ed9d654bdffd051d0c34a5a326a3c6a0620cf523352f9d47d2bcd2bb --- /dev/null +++ b/py3/keksschema.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# Python KEKS implementation +# Copyright (C) 2024-2026 Sergey Matveev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +"""KEKS/Schema validation + +If any field is decoded as Raw, then currently it will fail the TYPE check. +""" + +from datetime import datetime + +from keks import Blob +from keks import Float +from keks import Magic + + +class ValidationError(Exception): + pass + + +knownTypes = {"B": bytes, "O": Blob, "b": bool, "F": Float, "I": int, + "L": list, "K": Magic, "M": dict, "N": type(None), "S": str, "T": datetime} + + +def validate(schemas, sname, data): + vs = [data] + needExaminedCheck, nonExamined = False, 0 + if isinstance(data, dict): + nonExamined = len(data) + for cmdIdx, cmd in enumerate(schemas[sname]): + cmd, args = cmd[0], cmd[1:] + arg = args[0] if (len(args) > 0) else None + if cmd == ".": # TAKE + if arg is None: + vs = [data] + continue + try: + vs = [data[arg]] + except (IndexError, KeyError): + vs = [] + elif cmd == "E": # EXISTS + if len(vs) == 0: + raise ValidationError("%s: %d: EXISTS" % (sname, cmdIdx)) + elif cmd == "!E": # !EXISTS + if len(vs) != 0: + raise ValidationError("%s: %d: !EXISTS" % (sname, cmdIdx)) + elif cmd == "T": # TYPE + types = set(knownTypes[t] for t in args) + for v in vs: + if type(v) not in types: + raise ValidationError("%s: %d: TYPE: %r %r" % (sname, cmdIdx, arg, v)) + elif cmd in (">", "<"): # GT/LT + for v in vs: + if isinstance(v, (str, bytes, list, dict)): + v = len(v) + if not ((v > arg) if (cmd == ">") else (v < arg)): + raise ValidationError("%s: %d: ><: %r %r" % (sname, cmdIdx, arg, v)) + elif cmd == "S": # SCHEMA + for v in vs: + validate(schemas, arg, v) + elif cmd == "*": # EACH + if len(vs) == 0: + continue + vs = list(vs[0]) if isinstance(vs[0], list) else vs[0].values() + elif cmd == "=": # EQ + for v in vs: + if isinstance(v, str): + v = v.encode("utf-8") + elif isinstance(v, Magic): + v = bytes(v) + if v != arg: + raise ValidationError("%s: %d: =: %r %r" % (sname, cmdIdx, arg, v)) + elif cmd == "U": # UTC + pass # assume that we either Raw or leapsecUTCAllow was True + elif cmd == "P": # PREC + for v in vs: + if ( + (arg == 0 and v.microsecond != 0) or + (arg == 3 and v.microsecond != ((v.microsecond // 1000) * 1000)) + # (arg == 6) # it is maximal Python's precision + # everything else will be Raw, failing TYPE check + ): + raise ValidationError("%s: %d: P: %r %r" % (sname, cmdIdx, arg, v)) + elif cmd == "x": # EXAMINED + needExaminedCheck = True + if len(vs) != 0: + nonExamined -= 1 + else: + raise ValueError("%s: %d: unknown cmd: %r" % (sname, cmdIdx, cmd)) + if needExaminedCheck and nonExamined != 0: + raise ValidationError("%s: x: %d" % (sname, nonExamined)) + + +if __name__ == "__main__": + import sys + if len(sys.argv) < 3: + print("Usage: keksschema.py SCHEMA.keks SCHEMA-NAME