Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | # |
| 3 | # Check (context-free) QAPI schema expression structure |
| 4 | # |
| 5 | # Copyright IBM, Corp. 2011 |
| 6 | # Copyright (c) 2013-2019 Red Hat Inc. |
| 7 | # |
| 8 | # Authors: |
| 9 | # Anthony Liguori <aliguori@us.ibm.com> |
| 10 | # Markus Armbruster <armbru@redhat.com> |
| 11 | # Eric Blake <eblake@redhat.com> |
| 12 | # Marc-André Lureau <marcandre.lureau@redhat.com> |
| 13 | # |
| 14 | # This work is licensed under the terms of the GNU GPL, version 2. |
| 15 | # See the COPYING file in the top-level directory. |
| 16 | |
| 17 | import re |
| 18 | from collections import OrderedDict |
| 19 | from qapi.common import c_name |
| 20 | from qapi.error import QAPISemError |
| 21 | |
| 22 | |
| 23 | # Names must be letters, numbers, -, and _. They must start with letter, |
| 24 | # except for downstream extensions which must start with __RFQDN_. |
| 25 | # Dots are only valid in the downstream extension prefix. |
| 26 | valid_name = re.compile(r'^(__[a-zA-Z0-9.-]+_)?' |
| 27 | '[a-zA-Z][a-zA-Z0-9_-]*$') |
| 28 | |
| 29 | |
| 30 | def check_name_is_str(name, info, source): |
| 31 | if not isinstance(name, str): |
| 32 | raise QAPISemError(info, "%s requires a string name" % source) |
| 33 | |
| 34 | |
| 35 | def check_name_str(name, info, source, |
| 36 | allow_optional=False, enum_member=False, |
| 37 | permit_upper=False): |
| 38 | global valid_name |
| 39 | membername = name |
| 40 | |
| 41 | if allow_optional and name.startswith('*'): |
| 42 | membername = name[1:] |
| 43 | # Enum members can start with a digit, because the generated C |
| 44 | # code always prefixes it with the enum name |
| 45 | if enum_member and membername[0].isdigit(): |
| 46 | membername = 'D' + membername |
| 47 | # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty' |
| 48 | # and 'q_obj_*' implicit type names. |
| 49 | if not valid_name.match(membername) or \ |
| 50 | c_name(membername, False).startswith('q_'): |
| 51 | raise QAPISemError(info, "%s has an invalid name" % source) |
| 52 | if not permit_upper and name.lower() != name: |
| 53 | raise QAPISemError( |
| 54 | info, "%s uses uppercase in name" % source) |
| 55 | assert not membername.startswith('*') |
| 56 | |
| 57 | |
| 58 | def check_defn_name_str(name, info, meta): |
| 59 | check_name_str(name, info, meta, permit_upper=True) |
| 60 | if name.endswith('Kind') or name.endswith('List'): |
| 61 | raise QAPISemError( |
| 62 | info, "%s name should not end in '%s'" % (meta, name[-4:])) |
| 63 | |
| 64 | |
| 65 | def check_keys(value, info, source, required, optional): |
| 66 | |
| 67 | def pprint(elems): |
| 68 | return ', '.join("'" + e + "'" for e in sorted(elems)) |
| 69 | |
| 70 | missing = set(required) - set(value) |
| 71 | if missing: |
| 72 | raise QAPISemError( |
| 73 | info, |
| 74 | "%s misses key%s %s" |
| 75 | % (source, 's' if len(missing) > 1 else '', |
| 76 | pprint(missing))) |
| 77 | allowed = set(required + optional) |
| 78 | unknown = set(value) - allowed |
| 79 | if unknown: |
| 80 | raise QAPISemError( |
| 81 | info, |
| 82 | "%s has unknown key%s %s\nValid keys are %s." |
| 83 | % (source, 's' if len(unknown) > 1 else '', |
| 84 | pprint(unknown), pprint(allowed))) |
| 85 | |
| 86 | |
| 87 | def check_flags(expr, info): |
| 88 | for key in ['gen', 'success-response']: |
| 89 | if key in expr and expr[key] is not False: |
| 90 | raise QAPISemError( |
| 91 | info, "flag '%s' may only use false value" % key) |
| 92 | for key in ['boxed', 'allow-oob', 'allow-preconfig']: |
| 93 | if key in expr and expr[key] is not True: |
| 94 | raise QAPISemError( |
| 95 | info, "flag '%s' may only use true value" % key) |
| 96 | |
| 97 | |
| 98 | def normalize_if(expr): |
| 99 | ifcond = expr.get('if') |
| 100 | if isinstance(ifcond, str): |
| 101 | expr['if'] = [ifcond] |
| 102 | |
| 103 | |
| 104 | def check_if(expr, info, source): |
| 105 | |
| 106 | def check_if_str(ifcond, info): |
| 107 | if not isinstance(ifcond, str): |
| 108 | raise QAPISemError( |
| 109 | info, |
| 110 | "'if' condition of %s must be a string or a list of strings" |
| 111 | % source) |
| 112 | if ifcond.strip() == '': |
| 113 | raise QAPISemError( |
| 114 | info, |
| 115 | "'if' condition '%s' of %s makes no sense" |
| 116 | % (ifcond, source)) |
| 117 | |
| 118 | ifcond = expr.get('if') |
| 119 | if ifcond is None: |
| 120 | return |
| 121 | if isinstance(ifcond, list): |
| 122 | if ifcond == []: |
| 123 | raise QAPISemError( |
| 124 | info, "'if' condition [] of %s is useless" % source) |
| 125 | for elt in ifcond: |
| 126 | check_if_str(elt, info) |
| 127 | else: |
| 128 | check_if_str(ifcond, info) |
| 129 | |
| 130 | |
| 131 | def normalize_members(members): |
| 132 | if isinstance(members, OrderedDict): |
| 133 | for key, arg in members.items(): |
| 134 | if isinstance(arg, dict): |
| 135 | continue |
| 136 | members[key] = {'type': arg} |
| 137 | |
| 138 | |
| 139 | def check_type(value, info, source, |
| 140 | allow_array=False, allow_dict=False): |
| 141 | if value is None: |
| 142 | return |
| 143 | |
| 144 | # Array type |
| 145 | if isinstance(value, list): |
| 146 | if not allow_array: |
| 147 | raise QAPISemError(info, "%s cannot be an array" % source) |
| 148 | if len(value) != 1 or not isinstance(value[0], str): |
| 149 | raise QAPISemError(info, |
| 150 | "%s: array type must contain single type name" % |
| 151 | source) |
| 152 | return |
| 153 | |
| 154 | # Type name |
| 155 | if isinstance(value, str): |
| 156 | return |
| 157 | |
| 158 | # Anonymous type |
| 159 | |
| 160 | if not allow_dict: |
| 161 | raise QAPISemError(info, "%s should be a type name" % source) |
| 162 | |
| 163 | if not isinstance(value, OrderedDict): |
| 164 | raise QAPISemError(info, |
| 165 | "%s should be an object or type name" % source) |
| 166 | |
| 167 | permit_upper = allow_dict in info.pragma.name_case_whitelist |
| 168 | |
| 169 | # value is a dictionary, check that each member is okay |
| 170 | for (key, arg) in value.items(): |
| 171 | key_source = "%s member '%s'" % (source, key) |
| 172 | check_name_str(key, info, key_source, |
| 173 | allow_optional=True, permit_upper=permit_upper) |
| 174 | if c_name(key, False) == 'u' or c_name(key, False).startswith('has_'): |
| 175 | raise QAPISemError(info, "%s uses reserved name" % key_source) |
| 176 | check_keys(arg, info, key_source, ['type'], ['if']) |
| 177 | check_if(arg, info, key_source) |
| 178 | normalize_if(arg) |
| 179 | check_type(arg['type'], info, key_source, allow_array=True) |
| 180 | |
| 181 | |
| 182 | def normalize_features(features): |
| 183 | if isinstance(features, list): |
| 184 | features[:] = [f if isinstance(f, dict) else {'name': f} |
| 185 | for f in features] |
| 186 | |
| 187 | |
Peter Krempa | 23394b4 | 2019-10-18 10:14:51 +0200 | [diff] [blame^] | 188 | def check_features(features, info): |
| 189 | if features is None: |
| 190 | return |
| 191 | if not isinstance(features, list): |
| 192 | raise QAPISemError(info, "'features' must be an array") |
| 193 | for f in features: |
| 194 | source = "'features' member" |
| 195 | assert isinstance(f, dict) |
| 196 | check_keys(f, info, source, ['name'], ['if']) |
| 197 | check_name_is_str(f['name'], info, source) |
| 198 | source = "%s '%s'" % (source, f['name']) |
| 199 | check_name_str(f['name'], info, source) |
| 200 | check_if(f, info, source) |
| 201 | normalize_if(f) |
| 202 | |
| 203 | |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 204 | def normalize_enum(expr): |
| 205 | if isinstance(expr['data'], list): |
| 206 | expr['data'] = [m if isinstance(m, dict) else {'name': m} |
| 207 | for m in expr['data']] |
| 208 | |
| 209 | |
| 210 | def check_enum(expr, info): |
| 211 | name = expr['enum'] |
| 212 | members = expr['data'] |
| 213 | prefix = expr.get('prefix') |
| 214 | |
| 215 | if not isinstance(members, list): |
| 216 | raise QAPISemError(info, "'data' must be an array") |
| 217 | if prefix is not None and not isinstance(prefix, str): |
| 218 | raise QAPISemError(info, "'prefix' must be a string") |
| 219 | |
| 220 | permit_upper = name in info.pragma.name_case_whitelist |
| 221 | |
| 222 | for member in members: |
| 223 | source = "'data' member" |
| 224 | check_keys(member, info, source, ['name'], ['if']) |
| 225 | check_name_is_str(member['name'], info, source) |
| 226 | source = "%s '%s'" % (source, member['name']) |
| 227 | check_name_str(member['name'], info, source, |
| 228 | enum_member=True, permit_upper=permit_upper) |
| 229 | check_if(member, info, source) |
| 230 | normalize_if(member) |
| 231 | |
| 232 | |
| 233 | def check_struct(expr, info): |
| 234 | name = expr['struct'] |
| 235 | members = expr['data'] |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 236 | |
| 237 | check_type(members, info, "'data'", allow_dict=name) |
| 238 | check_type(expr.get('base'), info, "'base'") |
Peter Krempa | 23394b4 | 2019-10-18 10:14:51 +0200 | [diff] [blame^] | 239 | check_features(expr.get('features'), info) |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 240 | |
| 241 | |
| 242 | def check_union(expr, info): |
| 243 | name = expr['union'] |
| 244 | base = expr.get('base') |
| 245 | discriminator = expr.get('discriminator') |
| 246 | members = expr['data'] |
| 247 | |
| 248 | if discriminator is None: # simple union |
| 249 | if base is not None: |
| 250 | raise QAPISemError(info, "'base' requires 'discriminator'") |
| 251 | else: # flat union |
| 252 | check_type(base, info, "'base'", allow_dict=name) |
| 253 | if not base: |
| 254 | raise QAPISemError(info, "'discriminator' requires 'base'") |
| 255 | check_name_is_str(discriminator, info, "'discriminator'") |
| 256 | |
| 257 | for (key, value) in members.items(): |
| 258 | source = "'data' member '%s'" % key |
| 259 | check_name_str(key, info, source) |
| 260 | check_keys(value, info, source, ['type'], ['if']) |
| 261 | check_if(value, info, source) |
| 262 | normalize_if(value) |
| 263 | check_type(value['type'], info, source, allow_array=not base) |
| 264 | |
| 265 | |
| 266 | def check_alternate(expr, info): |
| 267 | members = expr['data'] |
| 268 | |
| 269 | if len(members) == 0: |
| 270 | raise QAPISemError(info, "'data' must not be empty") |
| 271 | for (key, value) in members.items(): |
| 272 | source = "'data' member '%s'" % key |
| 273 | check_name_str(key, info, source) |
| 274 | check_keys(value, info, source, ['type'], ['if']) |
| 275 | check_if(value, info, source) |
| 276 | normalize_if(value) |
| 277 | check_type(value['type'], info, source) |
| 278 | |
| 279 | |
| 280 | def check_command(expr, info): |
| 281 | args = expr.get('data') |
| 282 | rets = expr.get('returns') |
| 283 | boxed = expr.get('boxed', False) |
| 284 | |
| 285 | if boxed and args is None: |
| 286 | raise QAPISemError(info, "'boxed': true requires 'data'") |
| 287 | check_type(args, info, "'data'", allow_dict=not boxed) |
| 288 | check_type(rets, info, "'returns'", allow_array=True) |
Peter Krempa | 23394b4 | 2019-10-18 10:14:51 +0200 | [diff] [blame^] | 289 | check_features(expr.get('features'), info) |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 290 | |
| 291 | |
| 292 | def check_event(expr, info): |
| 293 | args = expr.get('data') |
| 294 | boxed = expr.get('boxed', False) |
| 295 | |
| 296 | if boxed and args is None: |
| 297 | raise QAPISemError(info, "'boxed': true requires 'data'") |
| 298 | check_type(args, info, "'data'", allow_dict=not boxed) |
| 299 | |
| 300 | |
| 301 | def check_exprs(exprs): |
| 302 | for expr_elem in exprs: |
| 303 | expr = expr_elem['expr'] |
| 304 | info = expr_elem['info'] |
| 305 | doc = expr_elem.get('doc') |
| 306 | |
| 307 | if 'include' in expr: |
| 308 | continue |
| 309 | |
| 310 | if 'enum' in expr: |
| 311 | meta = 'enum' |
| 312 | elif 'union' in expr: |
| 313 | meta = 'union' |
| 314 | elif 'alternate' in expr: |
| 315 | meta = 'alternate' |
| 316 | elif 'struct' in expr: |
| 317 | meta = 'struct' |
| 318 | elif 'command' in expr: |
| 319 | meta = 'command' |
| 320 | elif 'event' in expr: |
| 321 | meta = 'event' |
| 322 | else: |
| 323 | raise QAPISemError(info, "expression is missing metatype") |
| 324 | |
| 325 | name = expr[meta] |
| 326 | check_name_is_str(name, info, "'%s'" % meta) |
| 327 | info.set_defn(meta, name) |
| 328 | check_defn_name_str(name, info, meta) |
| 329 | |
| 330 | if doc: |
| 331 | if doc.symbol != name: |
| 332 | raise QAPISemError( |
| 333 | info, "documentation comment is for '%s'" % doc.symbol) |
| 334 | doc.check_expr(expr) |
| 335 | elif info.pragma.doc_required: |
| 336 | raise QAPISemError(info, |
| 337 | "documentation comment required") |
| 338 | |
| 339 | if meta == 'enum': |
| 340 | check_keys(expr, info, meta, |
| 341 | ['enum', 'data'], ['if', 'prefix']) |
| 342 | normalize_enum(expr) |
| 343 | check_enum(expr, info) |
| 344 | elif meta == 'union': |
| 345 | check_keys(expr, info, meta, |
| 346 | ['union', 'data'], |
| 347 | ['base', 'discriminator', 'if']) |
| 348 | normalize_members(expr.get('base')) |
| 349 | normalize_members(expr['data']) |
| 350 | check_union(expr, info) |
| 351 | elif meta == 'alternate': |
| 352 | check_keys(expr, info, meta, |
| 353 | ['alternate', 'data'], ['if']) |
| 354 | normalize_members(expr['data']) |
| 355 | check_alternate(expr, info) |
| 356 | elif meta == 'struct': |
| 357 | check_keys(expr, info, meta, |
| 358 | ['struct', 'data'], ['base', 'if', 'features']) |
| 359 | normalize_members(expr['data']) |
| 360 | normalize_features(expr.get('features')) |
| 361 | check_struct(expr, info) |
| 362 | elif meta == 'command': |
| 363 | check_keys(expr, info, meta, |
| 364 | ['command'], |
Peter Krempa | 23394b4 | 2019-10-18 10:14:51 +0200 | [diff] [blame^] | 365 | ['data', 'returns', 'boxed', 'if', 'features', |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 366 | 'gen', 'success-response', 'allow-oob', |
| 367 | 'allow-preconfig']) |
| 368 | normalize_members(expr.get('data')) |
Peter Krempa | 23394b4 | 2019-10-18 10:14:51 +0200 | [diff] [blame^] | 369 | normalize_features(expr.get('features')) |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 370 | check_command(expr, info) |
| 371 | elif meta == 'event': |
| 372 | check_keys(expr, info, meta, |
| 373 | ['event'], ['data', 'boxed', 'if']) |
| 374 | normalize_members(expr.get('data')) |
| 375 | check_event(expr, info) |
| 376 | else: |
| 377 | assert False, 'unexpected meta type' |
| 378 | |
| 379 | normalize_if(expr) |
| 380 | check_if(expr, info, meta) |
| 381 | check_flags(expr, info) |
| 382 | |
| 383 | return exprs |