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