xref: /openbmc/qemu/scripts/qapi/expr.py (revision 19f4ed36)
1# -*- coding: utf-8 -*-
2#
3# Copyright IBM, Corp. 2011
4# Copyright (c) 2013-2021 Red Hat Inc.
5#
6# Authors:
7#  Anthony Liguori <aliguori@us.ibm.com>
8#  Markus Armbruster <armbru@redhat.com>
9#  Eric Blake <eblake@redhat.com>
10#  Marc-André Lureau <marcandre.lureau@redhat.com>
11#  John Snow <jsnow@redhat.com>
12#
13# This work is licensed under the terms of the GNU GPL, version 2.
14# See the COPYING file in the top-level directory.
15
16"""
17Normalize and validate (context-free) QAPI schema expression structures.
18
19`QAPISchemaParser` parses a QAPI schema into abstract syntax trees
20consisting of dict, list, str, bool, and int nodes.  This module ensures
21that these nested structures have the correct type(s) and key(s) where
22appropriate for the QAPI context-free grammar.
23
24The QAPI schema expression language allows for certain syntactic sugar;
25this module also handles the normalization process of these nested
26structures.
27
28See `check_exprs` for the main entry point.
29
30See `schema.QAPISchema` for processing into native Python data
31structures and contextual semantic validation.
32"""
33
34import re
35from typing import (
36    Collection,
37    Dict,
38    Iterable,
39    List,
40    Optional,
41    Union,
42    cast,
43)
44
45from .common import c_name
46from .error import QAPISemError
47from .parser import QAPIDoc
48from .source import QAPISourceInfo
49
50
51# Deserialized JSON objects as returned by the parser.
52# The values of this mapping are not necessary to exhaustively type
53# here (and also not practical as long as mypy lacks recursive
54# types), because the purpose of this module is to interrogate that
55# type.
56_JSONObject = Dict[str, object]
57
58
59# See check_name_str(), below.
60valid_name = re.compile(r'(__[a-z0-9.-]+_)?'
61                        r'(x-)?'
62                        r'([a-z][a-z0-9_-]*)$', re.IGNORECASE)
63
64
65def check_name_is_str(name: object,
66                      info: QAPISourceInfo,
67                      source: str) -> None:
68    """
69    Ensure that ``name`` is a ``str``.
70
71    :raise QAPISemError: When ``name`` fails validation.
72    """
73    if not isinstance(name, str):
74        raise QAPISemError(info, "%s requires a string name" % source)
75
76
77def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str:
78    """
79    Ensure that ``name`` is a valid QAPI name.
80
81    A valid name consists of ASCII letters, digits, ``-``, and ``_``,
82    starting with a letter.  It may be prefixed by a downstream prefix
83    of the form __RFQDN_, or the experimental prefix ``x-``.  If both
84    prefixes are present, the __RFDQN_ prefix goes first.
85
86    A valid name cannot start with ``q_``, which is reserved.
87
88    :param name: Name to check.
89    :param info: QAPI schema source file information.
90    :param source: Error string describing what ``name`` belongs to.
91
92    :raise QAPISemError: When ``name`` fails validation.
93    :return: The stem of the valid name, with no prefixes.
94    """
95    # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty'
96    # and 'q_obj_*' implicit type names.
97    match = valid_name.match(name)
98    if not match or c_name(name, False).startswith('q_'):
99        raise QAPISemError(info, "%s has an invalid name" % source)
100    return match.group(3)
101
102
103def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None:
104    """
105    Ensure that ``name`` is a valid event name.
106
107    This means it must be a valid QAPI name as checked by
108    `check_name_str()`, but where the stem prohibits lowercase
109    characters and ``-``.
110
111    :param name: Name to check.
112    :param info: QAPI schema source file information.
113    :param source: Error string describing what ``name`` belongs to.
114
115    :raise QAPISemError: When ``name`` fails validation.
116    """
117    stem = check_name_str(name, info, source)
118    if re.search(r'[a-z-]', stem):
119        raise QAPISemError(
120            info, "name of %s must not use lowercase or '-'" % source)
121
122
123def check_name_lower(name: str, info: QAPISourceInfo, source: str,
124                     permit_upper: bool = False,
125                     permit_underscore: bool = False) -> None:
126    """
127    Ensure that ``name`` is a valid command or member name.
128
129    This means it must be a valid QAPI name as checked by
130    `check_name_str()`, but where the stem prohibits uppercase
131    characters and ``_``.
132
133    :param name: Name to check.
134    :param info: QAPI schema source file information.
135    :param source: Error string describing what ``name`` belongs to.
136    :param permit_upper: Additionally permit uppercase.
137    :param permit_underscore: Additionally permit ``_``.
138
139    :raise QAPISemError: When ``name`` fails validation.
140    """
141    stem = check_name_str(name, info, source)
142    if ((not permit_upper and re.search(r'[A-Z]', stem))
143            or (not permit_underscore and '_' in stem)):
144        raise QAPISemError(
145            info, "name of %s must not use uppercase or '_'" % source)
146
147
148def check_name_camel(name: str, info: QAPISourceInfo, source: str) -> None:
149    """
150    Ensure that ``name`` is a valid user-defined type name.
151
152    This means it must be a valid QAPI name as checked by
153    `check_name_str()`, but where the stem must be in CamelCase.
154
155    :param name: Name to check.
156    :param info: QAPI schema source file information.
157    :param source: Error string describing what ``name`` belongs to.
158
159    :raise QAPISemError: When ``name`` fails validation.
160    """
161    stem = check_name_str(name, info, source)
162    if not re.match(r'[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$', stem):
163        raise QAPISemError(info, "name of %s must use CamelCase" % source)
164
165
166def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None:
167    """
168    Ensure that ``name`` is a valid definition name.
169
170    Based on the value of ``meta``, this means that:
171      - 'event' names adhere to `check_name_upper()`.
172      - 'command' names adhere to `check_name_lower()`.
173      - Else, meta is a type, and must pass `check_name_camel()`.
174        These names must not end with ``Kind`` nor ``List``.
175
176    :param name: Name to check.
177    :param info: QAPI schema source file information.
178    :param meta: Meta-type name of the QAPI expression.
179
180    :raise QAPISemError: When ``name`` fails validation.
181    """
182    if meta == 'event':
183        check_name_upper(name, info, meta)
184    elif meta == 'command':
185        check_name_lower(
186            name, info, meta,
187            permit_underscore=name in info.pragma.command_name_exceptions)
188    else:
189        check_name_camel(name, info, meta)
190        if name.endswith('Kind') or name.endswith('List'):
191            raise QAPISemError(
192                info, "%s name should not end in '%s'" % (meta, name[-4:]))
193
194
195def check_keys(value: _JSONObject,
196               info: QAPISourceInfo,
197               source: str,
198               required: Collection[str],
199               optional: Collection[str]) -> None:
200    """
201    Ensure that a dict has a specific set of keys.
202
203    :param value: The dict to check.
204    :param info: QAPI schema source file information.
205    :param source: Error string describing this ``value``.
206    :param required: Keys that *must* be present.
207    :param optional: Keys that *may* be present.
208
209    :raise QAPISemError: When unknown keys are present.
210    """
211
212    def pprint(elems: Iterable[str]) -> str:
213        return ', '.join("'" + e + "'" for e in sorted(elems))
214
215    missing = set(required) - set(value)
216    if missing:
217        raise QAPISemError(
218            info,
219            "%s misses key%s %s"
220            % (source, 's' if len(missing) > 1 else '',
221               pprint(missing)))
222    allowed = set(required) | set(optional)
223    unknown = set(value) - allowed
224    if unknown:
225        raise QAPISemError(
226            info,
227            "%s has unknown key%s %s\nValid keys are %s."
228            % (source, 's' if len(unknown) > 1 else '',
229               pprint(unknown), pprint(allowed)))
230
231
232def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
233    """
234    Ensure flag members (if present) have valid values.
235
236    :param expr: The expression to validate.
237    :param info: QAPI schema source file information.
238
239    :raise QAPISemError:
240        When certain flags have an invalid value, or when
241        incompatible flags are present.
242    """
243    for key in ('gen', 'success-response'):
244        if key in expr and expr[key] is not False:
245            raise QAPISemError(
246                info, "flag '%s' may only use false value" % key)
247    for key in ('boxed', 'allow-oob', 'allow-preconfig', 'coroutine'):
248        if key in expr and expr[key] is not True:
249            raise QAPISemError(
250                info, "flag '%s' may only use true value" % key)
251    if 'allow-oob' in expr and 'coroutine' in expr:
252        # This is not necessarily a fundamental incompatibility, but
253        # we don't have a use case and the desired semantics isn't
254        # obvious.  The simplest solution is to forbid it until we get
255        # a use case for it.
256        raise QAPISemError(info, "flags 'allow-oob' and 'coroutine' "
257                                 "are incompatible")
258
259
260def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
261    """
262    Normalize and validate the ``if`` member of an object.
263
264    The ``if`` member may be either a ``str`` or a ``List[str]``.
265    A ``str`` value will be normalized to ``List[str]``.
266
267    :forms:
268      :sugared: ``Union[str, List[str]]``
269      :canonical: ``List[str]``
270
271    :param expr: The expression containing the ``if`` member to validate.
272    :param info: QAPI schema source file information.
273    :param source: Error string describing ``expr``.
274
275    :raise QAPISemError:
276        When the "if" member fails validation, or when there are no
277        non-empty conditions.
278    :return: None, ``expr`` is normalized in-place as needed.
279    """
280    ifcond = expr.get('if')
281    if ifcond is None:
282        return
283
284    if isinstance(ifcond, list):
285        if not ifcond:
286            raise QAPISemError(
287                info, "'if' condition [] of %s is useless" % source)
288    else:
289        # Normalize to a list
290        ifcond = expr['if'] = [ifcond]
291
292    for elt in ifcond:
293        if not isinstance(elt, str):
294            raise QAPISemError(
295                info,
296                "'if' condition of %s must be a string or a list of strings"
297                % source)
298        if not elt.strip():
299            raise QAPISemError(
300                info,
301                "'if' condition '%s' of %s makes no sense"
302                % (elt, source))
303
304
305def normalize_members(members: object) -> None:
306    """
307    Normalize a "members" value.
308
309    If ``members`` is a dict, for every value in that dict, if that
310    value is not itself already a dict, normalize it to
311    ``{'type': value}``.
312
313    :forms:
314      :sugared: ``Dict[str, Union[str, TypeRef]]``
315      :canonical: ``Dict[str, TypeRef]``
316
317    :param members: The members value to normalize.
318
319    :return: None, ``members`` is normalized in-place as needed.
320    """
321    if isinstance(members, dict):
322        for key, arg in members.items():
323            if isinstance(arg, dict):
324                continue
325            members[key] = {'type': arg}
326
327
328def check_type(value: Optional[object],
329               info: QAPISourceInfo,
330               source: str,
331               allow_array: bool = False,
332               allow_dict: Union[bool, str] = False) -> None:
333    """
334    Normalize and validate the QAPI type of ``value``.
335
336    Python types of ``str`` or ``None`` are always allowed.
337
338    :param value: The value to check.
339    :param info: QAPI schema source file information.
340    :param source: Error string describing this ``value``.
341    :param allow_array:
342        Allow a ``List[str]`` of length 1, which indicates an array of
343        the type named by the list element.
344    :param allow_dict:
345        Allow a dict.  Its members can be struct type members or union
346        branches.  When the value of ``allow_dict`` is in pragma
347        ``member-name-exceptions``, the dict's keys may violate the
348        member naming rules.  The dict members are normalized in place.
349
350    :raise QAPISemError: When ``value`` fails validation.
351    :return: None, ``value`` is normalized in-place as needed.
352    """
353    if value is None:
354        return
355
356    # Type name
357    if isinstance(value, str):
358        return
359
360    # Array type
361    if isinstance(value, list):
362        if not allow_array:
363            raise QAPISemError(info, "%s cannot be an array" % source)
364        if len(value) != 1 or not isinstance(value[0], str):
365            raise QAPISemError(info,
366                               "%s: array type must contain single type name" %
367                               source)
368        return
369
370    # Anonymous type
371
372    if not allow_dict:
373        raise QAPISemError(info, "%s should be a type name" % source)
374
375    if not isinstance(value, dict):
376        raise QAPISemError(info,
377                           "%s should be an object or type name" % source)
378
379    permissive = False
380    if isinstance(allow_dict, str):
381        permissive = allow_dict in info.pragma.member_name_exceptions
382
383    # value is a dictionary, check that each member is okay
384    for (key, arg) in value.items():
385        key_source = "%s member '%s'" % (source, key)
386        if key.startswith('*'):
387            key = key[1:]
388        check_name_lower(key, info, key_source,
389                         permit_upper=permissive,
390                         permit_underscore=permissive)
391        if c_name(key, False) == 'u' or c_name(key, False).startswith('has_'):
392            raise QAPISemError(info, "%s uses reserved name" % key_source)
393        check_keys(arg, info, key_source, ['type'], ['if', 'features'])
394        check_if(arg, info, key_source)
395        check_features(arg.get('features'), info)
396        check_type(arg['type'], info, key_source, allow_array=True)
397
398
399def check_features(features: Optional[object],
400                   info: QAPISourceInfo) -> None:
401    """
402    Normalize and validate the ``features`` member.
403
404    ``features`` may be a ``list`` of either ``str`` or ``dict``.
405    Any ``str`` element will be normalized to ``{'name': element}``.
406
407    :forms:
408      :sugared: ``List[Union[str, Feature]]``
409      :canonical: ``List[Feature]``
410
411    :param features: The features member value to validate.
412    :param info: QAPI schema source file information.
413
414    :raise QAPISemError: When ``features`` fails validation.
415    :return: None, ``features`` is normalized in-place as needed.
416    """
417    if features is None:
418        return
419    if not isinstance(features, list):
420        raise QAPISemError(info, "'features' must be an array")
421    features[:] = [f if isinstance(f, dict) else {'name': f}
422                   for f in features]
423    for feat in features:
424        source = "'features' member"
425        assert isinstance(feat, dict)
426        check_keys(feat, info, source, ['name'], ['if'])
427        check_name_is_str(feat['name'], info, source)
428        source = "%s '%s'" % (source, feat['name'])
429        check_name_str(feat['name'], info, source)
430        check_if(feat, info, source)
431
432
433def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
434    """
435    Normalize and validate this expression as an ``enum`` definition.
436
437    :param expr: The expression to validate.
438    :param info: QAPI schema source file information.
439
440    :raise QAPISemError: When ``expr`` is not a valid ``enum``.
441    :return: None, ``expr`` is normalized in-place as needed.
442    """
443    name = expr['enum']
444    members = expr['data']
445    prefix = expr.get('prefix')
446
447    if not isinstance(members, list):
448        raise QAPISemError(info, "'data' must be an array")
449    if prefix is not None and not isinstance(prefix, str):
450        raise QAPISemError(info, "'prefix' must be a string")
451
452    permissive = name in info.pragma.member_name_exceptions
453
454    members[:] = [m if isinstance(m, dict) else {'name': m}
455                  for m in members]
456    for member in members:
457        source = "'data' member"
458        member_name = member['name']
459        check_keys(member, info, source, ['name'], ['if'])
460        check_name_is_str(member_name, info, source)
461        source = "%s '%s'" % (source, member_name)
462        # Enum members may start with a digit
463        if member_name[0].isdigit():
464            member_name = 'd' + member_name  # Hack: hide the digit
465        check_name_lower(member_name, info, source,
466                         permit_upper=permissive,
467                         permit_underscore=permissive)
468        check_if(member, info, source)
469
470
471def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
472    """
473    Normalize and validate this expression as a ``struct`` definition.
474
475    :param expr: The expression to validate.
476    :param info: QAPI schema source file information.
477
478    :raise QAPISemError: When ``expr`` is not a valid ``struct``.
479    :return: None, ``expr`` is normalized in-place as needed.
480    """
481    name = cast(str, expr['struct'])  # Checked in check_exprs
482    members = expr['data']
483
484    check_type(members, info, "'data'", allow_dict=name)
485    check_type(expr.get('base'), info, "'base'")
486
487
488def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
489    """
490    Normalize and validate this expression as a ``union`` definition.
491
492    :param expr: The expression to validate.
493    :param info: QAPI schema source file information.
494
495    :raise QAPISemError: when ``expr`` is not a valid ``union``.
496    :return: None, ``expr`` is normalized in-place as needed.
497    """
498    name = cast(str, expr['union'])  # Checked in check_exprs
499    base = expr.get('base')
500    discriminator = expr.get('discriminator')
501    members = expr['data']
502
503    if discriminator is None:   # simple union
504        if base is not None:
505            raise QAPISemError(info, "'base' requires 'discriminator'")
506    else:                       # flat union
507        check_type(base, info, "'base'", allow_dict=name)
508        if not base:
509            raise QAPISemError(info, "'discriminator' requires 'base'")
510        check_name_is_str(discriminator, info, "'discriminator'")
511
512    if not isinstance(members, dict):
513        raise QAPISemError(info, "'data' must be an object")
514
515    for (key, value) in members.items():
516        source = "'data' member '%s'" % key
517        if discriminator is None:
518            check_name_lower(key, info, source)
519        # else: name is in discriminator enum, which gets checked
520        check_keys(value, info, source, ['type'], ['if'])
521        check_if(value, info, source)
522        check_type(value['type'], info, source, allow_array=not base)
523
524
525def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
526    """
527    Normalize and validate this expression as an ``alternate`` definition.
528
529    :param expr: The expression to validate.
530    :param info: QAPI schema source file information.
531
532    :raise QAPISemError: When ``expr`` is not a valid ``alternate``.
533    :return: None, ``expr`` is normalized in-place as needed.
534    """
535    members = expr['data']
536
537    if not members:
538        raise QAPISemError(info, "'data' must not be empty")
539
540    if not isinstance(members, dict):
541        raise QAPISemError(info, "'data' must be an object")
542
543    for (key, value) in members.items():
544        source = "'data' member '%s'" % key
545        check_name_lower(key, info, source)
546        check_keys(value, info, source, ['type'], ['if'])
547        check_if(value, info, source)
548        check_type(value['type'], info, source)
549
550
551def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
552    """
553    Normalize and validate this expression as a ``command`` definition.
554
555    :param expr: The expression to validate.
556    :param info: QAPI schema source file information.
557
558    :raise QAPISemError: When ``expr`` is not a valid ``command``.
559    :return: None, ``expr`` is normalized in-place as needed.
560    """
561    args = expr.get('data')
562    rets = expr.get('returns')
563    boxed = expr.get('boxed', False)
564
565    if boxed and args is None:
566        raise QAPISemError(info, "'boxed': true requires 'data'")
567    check_type(args, info, "'data'", allow_dict=not boxed)
568    check_type(rets, info, "'returns'", allow_array=True)
569
570
571def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
572    """
573    Normalize and validate this expression as an ``event`` definition.
574
575    :param expr: The expression to validate.
576    :param info: QAPI schema source file information.
577
578    :raise QAPISemError: When ``expr`` is not a valid ``event``.
579    :return: None, ``expr`` is normalized in-place as needed.
580    """
581    args = expr.get('data')
582    boxed = expr.get('boxed', False)
583
584    if boxed and args is None:
585        raise QAPISemError(info, "'boxed': true requires 'data'")
586    check_type(args, info, "'data'", allow_dict=not boxed)
587
588
589def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]:
590    """
591    Validate and normalize a list of parsed QAPI schema expressions.
592
593    This function accepts a list of expressions and metadata as returned
594    by the parser.  It destructively normalizes the expressions in-place.
595
596    :param exprs: The list of expressions to normalize and validate.
597
598    :raise QAPISemError: When any expression fails validation.
599    :return: The same list of expressions (now modified).
600    """
601    for expr_elem in exprs:
602        # Expression
603        assert isinstance(expr_elem['expr'], dict)
604        for key in expr_elem['expr'].keys():
605            assert isinstance(key, str)
606        expr: _JSONObject = expr_elem['expr']
607
608        # QAPISourceInfo
609        assert isinstance(expr_elem['info'], QAPISourceInfo)
610        info: QAPISourceInfo = expr_elem['info']
611
612        # Optional[QAPIDoc]
613        tmp = expr_elem.get('doc')
614        assert tmp is None or isinstance(tmp, QAPIDoc)
615        doc: Optional[QAPIDoc] = tmp
616
617        if 'include' in expr:
618            continue
619
620        if 'enum' in expr:
621            meta = 'enum'
622        elif 'union' in expr:
623            meta = 'union'
624        elif 'alternate' in expr:
625            meta = 'alternate'
626        elif 'struct' in expr:
627            meta = 'struct'
628        elif 'command' in expr:
629            meta = 'command'
630        elif 'event' in expr:
631            meta = 'event'
632        else:
633            raise QAPISemError(info, "expression is missing metatype")
634
635        check_name_is_str(expr[meta], info, "'%s'" % meta)
636        name = cast(str, expr[meta])
637        info.set_defn(meta, name)
638        check_defn_name_str(name, info, meta)
639
640        if doc:
641            if doc.symbol != name:
642                raise QAPISemError(
643                    info, "documentation comment is for '%s'" % doc.symbol)
644            doc.check_expr(expr)
645        elif info.pragma.doc_required:
646            raise QAPISemError(info,
647                               "documentation comment required")
648
649        if meta == 'enum':
650            check_keys(expr, info, meta,
651                       ['enum', 'data'], ['if', 'features', 'prefix'])
652            check_enum(expr, info)
653        elif meta == 'union':
654            check_keys(expr, info, meta,
655                       ['union', 'data'],
656                       ['base', 'discriminator', 'if', 'features'])
657            normalize_members(expr.get('base'))
658            normalize_members(expr['data'])
659            check_union(expr, info)
660        elif meta == 'alternate':
661            check_keys(expr, info, meta,
662                       ['alternate', 'data'], ['if', 'features'])
663            normalize_members(expr['data'])
664            check_alternate(expr, info)
665        elif meta == 'struct':
666            check_keys(expr, info, meta,
667                       ['struct', 'data'], ['base', 'if', 'features'])
668            normalize_members(expr['data'])
669            check_struct(expr, info)
670        elif meta == 'command':
671            check_keys(expr, info, meta,
672                       ['command'],
673                       ['data', 'returns', 'boxed', 'if', 'features',
674                        'gen', 'success-response', 'allow-oob',
675                        'allow-preconfig', 'coroutine'])
676            normalize_members(expr.get('data'))
677            check_command(expr, info)
678        elif meta == 'event':
679            check_keys(expr, info, meta,
680                       ['event'], ['data', 'boxed', 'if', 'features'])
681            normalize_members(expr.get('data'))
682            check_event(expr, info)
683        else:
684            assert False, 'unexpected meta type'
685
686        check_if(expr, info, meta)
687        check_features(expr.get('features'), info)
688        check_flags(expr, info)
689
690    return exprs
691