CLI: Validate JSON keymap input (#16261)

* Fix schema validator

It should use the passed schema.

* Add required attributes to keymap schema

* Rework subcommands to validate the JSON keymaps

The 'compile', 'flash' and 'json2c' subcommands were reworked to add
JSON keymap validation so error is reported for non-JSON and
non-compliant-JSON inputs.

* Fix required fields in keymap schema

* Add tests

* Fix compiling keymaps directly from keymap directory

* Schema should not require version for now.
This commit is contained in:
Erovia 2022-02-28 20:02:39 +00:00 committed by GitHub
parent 779c7debcf
commit fbfd5312b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 47 additions and 18 deletions

View file

@ -53,5 +53,10 @@
"type": "string", "type": "string",
"description": "asdf" "description": "asdf"
} }
} },
"required": [
"keyboard",
"layout",
"layers"
]
} }

View file

@ -1,12 +1,11 @@
"""Generate a keymap.c from a configurator export. """Generate a keymap.c from a configurator export.
""" """
import json
from argcomplete.completers import FilesCompleter from argcomplete.completers import FilesCompleter
from milc import cli from milc import cli
import qmk.keymap import qmk.keymap
import qmk.path import qmk.path
from qmk.commands import parse_configurator_json
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@ -19,14 +18,8 @@ def json2c(cli):
This command uses the `qmk.keymap` module to generate a keymap.c from a configurator export. The generated keymap is written to stdout, or to a file if -o is provided. This command uses the `qmk.keymap` module to generate a keymap.c from a configurator export. The generated keymap is written to stdout, or to a file if -o is provided.
""" """
try:
# Parse the configurator from json file (or stdin) # Parse the configurator from json file (or stdin)
user_keymap = json.load(cli.args.filename) user_keymap = parse_configurator_json(cli.args.filename)
except json.decoder.JSONDecodeError as ex:
cli.log.error('The JSON input does not appear to be valid.')
cli.log.error(ex)
return False
# Environment processing # Environment processing
if cli.args.output and cli.args.output.name == '-': if cli.args.output and cli.args.output.name == '-':

View file

@ -1,6 +1,5 @@
"""Helper functions for commands. """Helper functions for commands.
""" """
import json
import os import os
import sys import sys
import shutil import shutil
@ -9,10 +8,11 @@ from subprocess import DEVNULL
from time import strftime from time import strftime
from milc import cli from milc import cli
import jsonschema
import qmk.keymap import qmk.keymap
from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX
from qmk.json_schema import json_load from qmk.json_schema import json_load, validate
time_fmt = '%Y-%m-%d-%H:%M:%S' time_fmt = '%Y-%m-%d-%H:%M:%S'
@ -185,6 +185,10 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
A command to run to compile and flash the C file. A command to run to compile and flash the C file.
""" """
# In case the user passes a keymap.json from a keymap directory directly to the CLI.
# e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
user_keymap["keymap"] = user_keymap.get("keymap", "default_json")
# Write the keymap.c file # Write the keymap.c file
keyboard_filesafe = user_keymap['keyboard'].replace('/', '_') keyboard_filesafe = user_keymap['keyboard'].replace('/', '_')
target = f'{keyboard_filesafe}_{user_keymap["keymap"]}' target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
@ -248,8 +252,15 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
def parse_configurator_json(configurator_file): def parse_configurator_json(configurator_file):
"""Open and parse a configurator json export """Open and parse a configurator json export
""" """
# FIXME(skullydazed/anyone): Add validation here user_keymap = json_load(configurator_file)
user_keymap = json.load(configurator_file) # Validate against the jsonschema
try:
validate(user_keymap, 'qmk.keymap.v1')
except jsonschema.ValidationError as e:
cli.log.error(f'Invalid JSON keymap: {configurator_file} : {e.message}')
exit(1)
orig_keyboard = user_keymap['keyboard'] orig_keyboard = user_keymap['keyboard']
aliases = json_load(Path('data/mappings/keyboard_aliases.json')) aliases = json_load(Path('data/mappings/keyboard_aliases.json'))

View file

@ -16,7 +16,11 @@ def json_load(json_file):
Note: file must be a Path object. Note: file must be a Path object.
""" """
try: try:
return hjson.load(json_file.open(encoding='utf-8')) # Get the IO Stream for Path objects
# Not necessary if the data is provided via stdin
if isinstance(json_file, Path):
json_file = json_file.open(encoding='utf-8')
return hjson.load(json_file)
except (json.decoder.JSONDecodeError, hjson.HjsonDecodeError) as e: except (json.decoder.JSONDecodeError, hjson.HjsonDecodeError) as e:
cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e) cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
@ -62,7 +66,7 @@ def create_validator(schema):
"""Creates a validator for the given schema id. """Creates a validator for the given schema id.
""" """
schema_store = compile_schema_store() schema_store = compile_schema_store()
resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store) resolver = jsonschema.RefResolver.from_schema(schema_store[schema], store=schema_store)
return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate

View file

@ -70,9 +70,13 @@ def normpath(path):
class FileType(argparse.FileType): class FileType(argparse.FileType):
def __init__(self, encoding='UTF-8'):
# Use UTF8 by default for stdin
return super().__init__(encoding=encoding)
def __call__(self, string): def __call__(self, string):
"""normalize and check exists """normalize and check exists
otherwise magic strings like '-' for stdin resolve to bad paths otherwise magic strings like '-' for stdin resolve to bad paths
""" """
norm = normpath(string) norm = normpath(string)
return super().__call__(norm if norm.exists() else string) return norm if norm.exists() else super().__call__(string)

View file

@ -156,6 +156,18 @@ def test_json2c_stdin():
assert result.stdout == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT_ortho_1x1(KC_A)};\n\n' assert result.stdout == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT_ortho_1x1(KC_A)};\n\n'
def test_json2c_wrong_json():
result = check_subcommand('json2c', 'keyboards/handwired/pytest/info.json')
check_returncode(result, [1])
assert 'Invalid JSON keymap' in result.stdout
def test_json2c_no_json():
result = check_subcommand('json2c', 'keyboards/handwired/pytest/pytest.h')
check_returncode(result, [1])
assert 'Invalid JSON encountered' in result.stdout
def test_info(): def test_info():
result = check_subcommand('info', '-kb', 'handwired/pytest/basic') result = check_subcommand('info', '-kb', 'handwired/pytest/basic')
check_returncode(result) check_returncode(result)