1#!/usr/bin/env python3 2 3# 4# Copyright (C) 2012 The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18 19""" 20Usage: 21 metadata_validate.py <filename.xml> 22 - validates that the metadata properties defined in filename.xml are 23 semantically correct. 24 - does not do any XSD validation, use xmllint for that (in metadata-validate) 25 26Module: 27 A set of helpful functions for dealing with BeautifulSoup element trees. 28 Especially the find_* and fully_qualified_name functions. 29 30Dependencies: 31 BeautifulSoup - an HTML/XML parser available to download from 32 http://www.crummy.com/software/BeautifulSoup/ 33""" 34 35from bs4 import BeautifulSoup 36from bs4 import Tag 37import sys 38 39 40##################### 41##################### 42 43def fully_qualified_name(entry): 44 """ 45 Calculates the fully qualified name for an entry by walking the path 46 to the root node. 47 48 Args: 49 entry: a BeautifulSoup Tag corresponding to an <entry ...> XML node, 50 or a <clone ...> XML node. 51 52 Raises: 53 ValueError: if entry does not correspond to one of the above XML nodes 54 55 Returns: 56 A string with the full name, e.g. "android.lens.info.availableApertureSizes" 57 """ 58 59 filter_tags = ['namespace', 'section'] 60 parents = [i['name'] for i in entry.parents if i.name in filter_tags] 61 62 if entry.name == 'entry': 63 name = entry['name'] 64 elif entry.name == 'clone': 65 name = entry['entry'].split(".")[-1] # "a.b.c" => "c" 66 else: 67 raise ValueError("Unsupported tag type '%s' for element '%s'" \ 68 %(entry.name, entry)) 69 70 parents.reverse() 71 parents.append(name) 72 73 fqn = ".".join(parents) 74 75 return fqn 76 77def find_parent_by_name(element, names): 78 """ 79 Find the ancestor for an element whose name matches one of those 80 in names. 81 82 Args: 83 element: A BeautifulSoup Tag corresponding to an XML node 84 85 Returns: 86 A BeautifulSoup element corresponding to the matched parent, or None. 87 88 For example, assuming the following XML structure: 89 <static> 90 <anything> 91 <entry name="Hello" /> # this is in variable 'Hello' 92 </anything> 93 </static> 94 95 el = find_parent_by_name(Hello, ['static']) 96 # el is now a value pointing to the '<static>' element 97 """ 98 matching_parents = [i.name for i in element.parents if i.name in names] 99 100 if matching_parents: 101 return matching_parents[0] 102 else: 103 return None 104 105def find_all_child_tags(element, tag): 106 """ 107 Finds all the children that are a Tag (as opposed to a NavigableString), 108 with a name of tag. This is useful to filter out the NavigableString out 109 of the children. 110 111 Args: 112 element: A BeautifulSoup Tag corresponding to an XML node 113 tag: A string representing the name of the tag 114 115 Returns: 116 A list of Tag instances 117 118 For example, given the following XML structure: 119 <enum> # This is the variable el 120 Hello world # NavigableString 121 <value>Apple</value> # this is the variale apple (Tag) 122 <value>Orange</value> # this is the variable orange (Tag) 123 Hello world again # NavigableString 124 </enum> 125 126 lst = find_all_child_tags(el, 'value') 127 # lst is [apple, orange] 128 129 """ 130 matching_tags = [i for i in element.children if isinstance(i, Tag) and i.name == tag] 131 return matching_tags 132 133def find_child_tag(element, tag): 134 """ 135 Finds the first child that is a Tag with the matching name. 136 137 Args: 138 element: a BeautifulSoup Tag 139 tag: A String representing the name of the tag 140 141 Returns: 142 An instance of a Tag, or None if there was no matches. 143 144 For example, given the following XML structure: 145 <enum> # This is the variable el 146 Hello world # NavigableString 147 <value>Apple</value> # this is the variale apple (Tag) 148 <value>Orange</value> # this is the variable orange (Tag) 149 Hello world again # NavigableString 150 </enum> 151 152 res = find_child_tag(el, 'value') 153 # res is apple 154 """ 155 matching_tags = find_all_child_tags(element, tag) 156 if matching_tags: 157 return matching_tags[0] 158 else: 159 return None 160 161def find_kind(element): 162 """ 163 Finds the kind Tag ancestor for an element. 164 165 Args: 166 element: a BeautifulSoup Tag 167 168 Returns: 169 a BeautifulSoup tag, or None if there was no matches 170 171 Remarks: 172 This function only makes sense to be called for an Entry, Clone, or 173 InnerNamespace XML types. It will always return 'None' for other nodes. 174 """ 175 kinds = ['dynamic', 'static', 'controls'] 176 parent_kind = find_parent_by_name(element, kinds) 177 return parent_kind 178 179def validate_error(msg): 180 """ 181 Print a validation error to stderr. 182 183 Args: 184 msg: a string you want to be printed 185 """ 186 print("ERROR: %s" % (msg), file=sys.stderr) 187 188 189def validate_clones(soup): 190 """ 191 Validate that all <clone> elements point to an existing <entry> element. 192 193 Args: 194 soup - an instance of BeautifulSoup 195 196 Returns: 197 True if the validation succeeds, False otherwise 198 """ 199 success = True 200 201 for clone in soup.find_all("clone"): 202 clone_entry = clone['entry'] 203 clone_kind = clone['kind'] 204 205 parent_kind = find_kind(clone) 206 207 find_entry = lambda x: x.name == 'entry' \ 208 and find_kind(x) == clone_kind \ 209 and fully_qualified_name(x) == clone_entry 210 matching_entry = soup.find(find_entry) 211 212 if matching_entry is None: 213 error_msg = ("Did not find corresponding clone entry '%s' " + \ 214 "with kind '%s'") %(clone_entry, clone_kind) 215 validate_error(error_msg) 216 success = False 217 218 clone_name = fully_qualified_name(clone) 219 if clone_name != clone_entry: 220 error_msg = ("Clone entry target '%s' did not match fully qualified " + \ 221 "name '%s'.") %(clone_entry, clone_name) 222 validate_error(error_msg) 223 success = False 224 225 if matching_entry is not None: 226 entry_hal_major_version = 3 227 entry_hal_minor_version = 2 228 entry_hal_version = matching_entry.get('hal_version') 229 if entry_hal_version is not None: 230 entry_hal_major_version = int(entry_hal_version.partition('.')[0]) 231 entry_hal_minor_version = int(entry_hal_version.partition('.')[2]) 232 233 clone_hal_major_version = entry_hal_major_version 234 clone_hal_minor_version = entry_hal_minor_version 235 clone_hal_version = clone.get('hal_version') 236 if clone_hal_version is not None: 237 clone_hal_major_version = int(clone_hal_version.partition('.')[0]) 238 clone_hal_minor_version = int(clone_hal_version.partition('.')[2]) 239 240 if clone_hal_major_version < entry_hal_major_version or \ 241 (clone_hal_major_version == entry_hal_major_version and \ 242 clone_hal_minor_version < entry_hal_minor_version): 243 error_msg = ("Clone '%s' HAL version '%d.%d' is older than entry target HAL version '%d.%d'" \ 244 % (clone_name, clone_hal_major_version, clone_hal_minor_version, entry_hal_major_version, entry_hal_minor_version)) 245 validate_error(error_msg) 246 success = False 247 248 return success 249 250# All <entry> elements with container=$foo have a <$foo> child 251# If type="enum", <enum> tag is present 252# In <enum> for all <value id="$x">, $x is numeric 253def validate_entries(soup): 254 """ 255 Validate all <entry> elements with the following rules: 256 * If there is a container="$foo" attribute, there is a <$foo> child 257 * If there is a type="enum" attribute, there is an <enum> child 258 * In the <enum> child, all <value id="$x"> have a numeric $x 259 260 Args: 261 soup - an instance of BeautifulSoup 262 263 Returns: 264 True if the validation succeeds, False otherwise 265 """ 266 success = True 267 for entry in soup.find_all("entry"): 268 entry_container = entry.attrs.get('container') 269 270 if entry_container is not None: 271 container_tag = entry.find(entry_container) 272 273 if container_tag is None: 274 success = False 275 validate_error(("Entry '%s' in kind '%s' has type '%s' but " + \ 276 "missing child element <%s>") \ 277 %(fully_qualified_name(entry), find_kind(entry), \ 278 entry_container, entry_container)) 279 280 enum = entry.attrs.get('enum') 281 if enum and enum == 'true': 282 if entry.enum is None: 283 validate_error(("Entry '%s' in kind '%s' is missing enum") \ 284 % (fully_qualified_name(entry), find_kind(entry), 285 )) 286 success = False 287 288 else: 289 for value in entry.enum.find_all('value'): 290 value_id = value.attrs.get('id') 291 292 if value_id is not None: 293 try: 294 id_int = int(value_id, 0) #autoguess base 295 except ValueError: 296 validate_error(("Entry '%s' has id '%s', which is not" + \ 297 " numeric.") \ 298 %(fully_qualified_name(entry), value_id)) 299 success = False 300 else: 301 if entry.enum: 302 validate_error(("Entry '%s' kind '%s' has enum el, but no enum attr") \ 303 % (fully_qualified_name(entry), find_kind(entry), 304 )) 305 success = False 306 307 deprecated = entry.attrs.get('deprecated') 308 if deprecated and deprecated == 'true': 309 if entry.deprecation_description is None: 310 validate_error(("Entry '%s' in kind '%s' is deprecated, but missing deprecation description") \ 311 % (fully_qualified_name(entry), find_kind(entry), 312 )) 313 success = False 314 else: 315 if entry.deprecation_description is not None: 316 validate_error(("Entry '%s' in kind '%s' has deprecation description, but is not deprecated") \ 317 % (fully_qualified_name(entry), find_kind(entry), 318 )) 319 success = False 320 321 return success 322 323def validate_xml(xml): 324 """ 325 Validate all XML nodes according to the rules in validate_clones and 326 validate_entries. 327 328 Args: 329 xml - A string containing a block of XML to validate 330 331 Returns: 332 a BeautifulSoup instance if validation succeeds, None otherwise 333 """ 334 335 soup = BeautifulSoup(xml, features='xml') 336 337 succ = validate_clones(soup) 338 succ = validate_entries(soup) and succ 339 340 if succ: 341 return soup 342 else: 343 return None 344 345##################### 346##################### 347 348if __name__ == "__main__": 349 if len(sys.argv) <= 1: 350 print("Usage: %s <filename.xml>" % (sys.argv[0]), file=sys.stderr) 351 sys.exit(0) 352 353 file_name = sys.argv[1] 354 succ = validate_xml(open(file_name).read()) is not None 355 356 if succ: 357 print("%s: SUCCESS! Document validated" % (file_name)) 358 sys.exit(0) 359 else: 360 print("%s: ERRORS: Document failed to validate" % (file_name), file=sys.stderr) 361 sys.exit(1) 362