blob: 3b8f95f231870e1ab8ec5f32ee2d02b1b90dee52 [file]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2019, Data61, CSIRO (ABN 41 687 119 230)
#
# SPDX-License-Identifier: BSD-2-Clause
#
#
import argparse
import os
import six
import re
import logging
import pyfdt.pyfdt
from camkes.parser import Query
from .exception import DtbBindingError, DtbBindingQueryFormatError, \
DtbBindingNodeLookupError, DtbBindingSyntaxError, \
DtbBindingTypeError, DtbBindingNotImplementedError, ParseError
class FdtQueryEngine:
"""
This class is responsible for wrapping around an instance of pyfdt and
implementing a querying engine on top of it which can query for device nodes
by path, alias and by matching against device properties.
"""
ALIAS_STRING = "aliases"
PATH_STRING = "path"
PROPERTIES_STRING = "properties"
CHOSEN_STRING = "chosen"
def __init__(self, parsed_fdt):
"""
The constructor takes an initialized instance of the fdt parser library
for internal use.
"""
if not parsed_fdt or not isinstance(parsed_fdt, pyfdt.pyfdt.Fdt):
raise DtbBindingTypeError("fdt_parser expects an instance of class "
"pyfdt.Fdt")
self.parsed_fdt = parsed_fdt
def _match_node_by_alias_or_chosen(self, name, is_alias):
"""
Looks up an alias or chosen node by looking inside of /aliases or /chosen for
a subnode with the name passed in "name".
@param name Name of an alias you want to look for.
@param is_alias Boolean indicating if we should look in the 'alias' node
"""
# @param include_consume_nodes
if is_alias:
node_string = self.ALIAS_STRING
else:
node_string = self.CHOSEN_STRING
fdt_root = self.parsed_fdt.get_rootnode()
try:
node_idx = fdt_root.index(node_string)
except ValueError:
# Rethrow with a human readable error message
raise DtbBindingNodeLookupError("Request to lookup %s instance \"%s\" in "
"a DTB with no /%s node!"
% (node_string, name, node_string))
node = fdt_root.subdata[node_idx]
try:
ret_idx = node.index(name)
except ValueError:
raise DtbBindingNodeLookupError("DTB does not contain an %s instance "
"named %s!"
% (node_string, name))
prop = node.subdata[ret_idx]
if not isinstance(prop, pyfdt.pyfdt.FdtPropertyStrings):
if is_alias:
raise DtbBindingNodeLookupError("Alias node should only contain strings!")
else:
raise DtbBindingNotImplementedError(
"Only support paths or aliases for chosen instances.")
if len(prop.strings) != 1:
raise DtbBindingNodeLookupError(
"The %s instance should only contain one string." % node_string)
wanted_prop = prop.strings[0]
if not is_alias:
# Strip everything pass and including the ':' character if it exists,
# this is common in 'stdout-path'
wanted_prop = wanted_prop.split(':')[0]
if not wanted_prop.startswith('/'):
# The property contains an alias and we can recurse
return self._match_node_by_alias_or_chosen(wanted_prop, True)
# From here we have the path to the node the user wanted. Look it up.
ret = self._match_nodes_by_path(re.escape(wanted_prop))
if not len(ret):
raise DtbBindingNodeLookupError("%s instance %s maps to path %s, but "
"that path resolves to nothing."
% (node_string, name, wanted_prop))
if len(ret) > 1:
raise DtbBindingNodeLookupError("%s instance %s maps to path %s, but "
"that path resolves to multiple "
"results."
% (node_string, name, wanted_prop))
return ret
def _match_nodes_by_path(self, qstring):
"""
Parses a single path query string.
@param[in] qstring The query string
The query string can contain Regex
match operators.
@return An array of nodes whose paths match the query string.
"""
fdt_root = self.parsed_fdt.get_rootnode()
fdt_iter = fdt_root.walk()
matches = []
qstring = r"" + qstring
try:
regex = re.compile(qstring, re.IGNORECASE)
except re.error:
# Rethrow with a more user friendly message to help the dev debug
raise DtbBindingQueryFormatError("Input query string \"%s\" is not "
"a valid regex."
% qstring)
# Try the regex against each path from the DTB and build a list of those
# node paths which match.
for node in fdt_iter:
currpath = r"" + node[0]
currnode = node[1]
# We don't want property nodes -- only device nodes.
if not isinstance(currnode, pyfdt.pyfdt.FdtNode):
continue
if not regex.search(currpath):
continue
matches.append(currnode)
return matches
@staticmethod
def _compare_node_property_as_string(prop, p_key, val):
""" Negative index into the string array is how the the "*" operator is
implemented internally.
"""
if p_key["index"] < 0:
assert isinstance(val, list)
return (len(prop.strings) == len(val)) and (prop.strings == val)
else:
if not isinstance(val, six.string_types):
raise DtbBindingTypeError("Property %s is a string property, so "
"values matched against it must also be "
"strings"
% prop.get_name())
if not len(prop.strings) > p_key["index"]:
return False
return prop.strings[p_key["index"]] == val
@staticmethod
def _compare_node_property_as_integers(prop, p_key, val, size):
""" Negative index into the string array is how the the "*" operator is
implemented internally.
"""
if p_key["index"] < 0:
assert isinstance(val, list)
if size == "byte":
for i in val:
assert isinstance(i, six.integer_types)
if not -128 <= i <= 127:
raise DtbBindingTypeError("Value %d exceeds size of a "
"byte..."
% i)
return (len(prop.words) == len(val)) \
and (prop.words == val)
else:
if not isinstance(val, six.integer_types):
raise DtbBindingTypeError("Property %s is a string property, so "
"values matched against it must also be "
"strings"
% prop.get_name())
if not len(prop.words) > p_key["index"]:
return False
return prop.words[p_key["index"]] == val
@staticmethod
def _compare_node_property_as_null(prop, p_key, val):
if not val:
return True
if isinstance(val, six.string_types) and val == "":
return True
if isinstance(val, six.integer_types) and val == 0:
""" We're being very lenient here, but allow a 0-value to count as
a match against a property which is unset.
"""
return True
return False
def _compare_node_property_to_attr(self, prop, p_key, val):
if isinstance(prop, pyfdt.pyfdt.FdtPropertyStrings):
result = self._compare_node_property_as_string(prop, p_key, val)
elif isinstance(prop, pyfdt.pyfdt.FdtPropertyWords):
result = self._compare_node_property_as_integers(prop, p_key, val, "word")
elif isinstance(prop, pyfdt.pyfdt.FdtNop):
result = self._compare_node_property_as_null(prop, p_key, val)
elif isinstance(prop, pyfdt.pyfdt.FdtPropertyBytes):
result = self._compare_node_property_as_integers(prop, p_key, val, "byte")
else:
raise DtbBindingTypeError("Not sure how to handle FDT property %s "
"with unexpected type %s."
% (str(prop), type(prop)))
return result
@staticmethod
def _parse_key(key_str):
attr = key_str
index = 0
lbrace_idx = key_str.find('[')
if lbrace_idx != -1:
""" If we find an opening brace, assume that there is indexing
notation in the key. Extract the index.
"""
# The right brace must be encountered *after* the the left one.
rbrace_idx = key_str[lbrace_idx + 1:].find(']')
if rbrace_idx == -1:
raise DtbBindingSyntaxError("DTB Binding attribute has "
"incomplete brace notation!")
idx_str = key_str[lbrace_idx + 1:]
idx_str = idx_str[:rbrace_idx]
idx_str = idx_str.strip(' ')
if idx_str == "*":
# For the "*" index operator, we represent it internally as -1.
index = -1
else:
try:
if idx_str.startswith("0x"):
index = int(idx_str, 16)
else:
index = int(idx_str, 10)
except ValueError:
# Rethrow it with a human-readable error string.
raise DtbBindingTypeError("Invalid integer index %s in "
"key %s!"
% (idx_str, key_str))
attr = key_str[:lbrace_idx]
return {"key": attr, "index": index}
@staticmethod
def _get_matching_prop_from_node(node_props, parsed_key):
matching_props = [prop for prop in node_props
if prop.get_name() == parsed_key["key"]]
if len(matching_props) == 0:
return None
if len(matching_props) > 1:
raise DtbBindingSyntaxError("Input DTB file has a node which has "
"two properties having the same name!")
return matching_props[0]
def _match_node_by_attrs(self, node, attr_dict):
"""
Compare a pyfdt.FdtNode against a set of attrs given as a dict. If all
the the node matches all the attrs and values, returns true.
@param[in] node The pyfdt.FdtNode which is going to be scanned.
@param[in] attr_dict The attributes which "node" must be compared
against.
@return True if (ALL keys in "attr_dict" exist in "node") AND
(ALL values in "attr_dict" match their homologues in "node").
"""
node_props = [sub for sub in node.subdata
if isinstance(sub, pyfdt.pyfdt.FdtProperty)]
for key, val in attr_dict.items():
""" We allow things like indexing in the lvalue key
(e.g: 'regs[0]'), so we have to potentially extract the raw key
from an key with indexed notation attached to it.
"""
parsed_key = self._parse_key(key)
matching_prop = self._get_matching_prop_from_node(node_props,
parsed_key)
if not matching_prop:
return False
# The node must *both* have the attribute, AND have its value match.
if not self._compare_node_property_to_attr(matching_prop,
parsed_key, val):
return False
return True
def _match_nodes_by_attrs(self, attr_dict, search_data_set):
# If there are no attrs to compare against, exit early.
if not len(attr_dict.keys()):
return []
matches = []
if search_data_set and len(search_data_set):
""" If the caller has already narrowed down the set of nodes s/he
wants us to search, then use that data set:
"""
for node in search_data_set:
assert isinstance(node, pyfdt.pyfdt.FdtNode)
if self._match_node_by_attrs(node, attr_dict):
matches.append(node)
else:
# Else search all nodes in the entire FDT
fdt_root = self.parsed_fdt.get_rootnode()
fdt_iter = fdt_root.walk()
for node in fdt_iter:
# We don't want property nodes here either; just device nodes.
if not isinstance(node[1], pyfdt.pyfdt.FdtNode):
continue
if self._match_node_by_attrs(node[1], attr_dict):
matches.append(node[1])
return matches
def _match_attr_dict(self, attr_dict):
assert isinstance(attr_dict, dict)
""" If there is an alias binding query which we can use, then try that
first since those should only resolve to a single result.
"""
alias_value = attr_dict.pop(self.ALIAS_STRING, None)
if alias_value:
""" Since ALIAS_STRING is meant to be an unambiguous match
against a single node, do not try to resolve other attributes
if the user already supplied camkes_dts_alias.
Instead, assume that they meant to have a specific node
matched, and that it is a mistake on their part that other
attributes were also supplied.
"""
if len(attr_dict):
logging.warn("Silently ignoring other attributes supplied in "
"DTB binding since %s should be sufficient.\n"
"Ignored attributes were: %s."
% (self.ALIAS_STRING, str(attr_dict)))
""" An alias match, if found, is an unambiguous match. No need to
do any further searching.
"""
return self._match_node_by_alias_or_chosen(alias_value, True)
""" Otherwise, if there is a 'chosen' binding query, use that.
This should also resolve to a single result.
"""
chosen_value = attr_dict.pop(self.CHOSEN_STRING, None)
if chosen_value:
# Like above, warn them if they supply other attributes to match.
if len(attr_dict):
logging.warn("Silently ignoring other attributes supplied in "
"DTB binding since %s should be sufficient.\n"
"Ignored attributes were: %s."
% (self.ALIAS_STRING, str(attr_dict)))
# Again, it should be an unambiguous match. So we return.
return self._match_node_by_alias_or_chosen(chosen_value, False)
""" If there is a path query which we can use to cut the search set down,
get that out of the way first.
"""
path_matches = []
path_key = attr_dict.pop(self.PATH_STRING, None)
if path_key:
path_matches = self._match_nodes_by_path(path_key)
properties = attr_dict.pop(self.PROPERTIES_STRING, None)
if not properties:
return path_matches
""" Now, using the narrowed down results from the path query, attempt to
match based on the properties.
"""
return self._match_nodes_by_attrs(properties, path_matches)
def query(self, attr):
assert isinstance(attr, list)
return [self._match_attr_dict(attr_dict) for attr_dict in attr]
class DtbMatchQuery(Query):
"""Convert a dtb query into a dictionary of results from the device tree"""
def __init__(self):
self.engine = None
@staticmethod
def resolve_fdt_node(node):
resolved = {}
address_cells_key = 'this-address-cells'
size_cells_key = 'this-size-cells'
node_path_key = 'this-node-path'
# convert the properties we retrieved to a dictionary
# of property-name: values. If there is more than one
# value, use a list, otherwise the raw type.
for p in node.walk():
# properties are a tuple of (key, values)
# drop the leading slash on the key
key = p[0][1:]
values = p[1]
if type(values) is pyfdt.pyfdt.FdtProperty:
resolved[key] = []
else:
resolved[key] = list(values)
# retrieve the #address-cells and #size-cells property
# from the parent
for p in node.parent.walk():
key = p[0][1:]
values = p[1]
if key == '#address-cells':
resolved[address_cells_key] = list(values)
elif key == '#size-cells':
resolved[size_cells_key] = list(values)
# if the parent does have the #address-cells and
# #size-cells property, default to 2 and 1 respectively
# as according to the Devicetree spec
if address_cells_key not in resolved:
resolved[address_cells_key] = [2]
if size_cells_key not in resolved:
resolved[size_cells_key] = [1]
# Resolve the full path of the fdt node by walking backwards
# to the root of the device tree
curr_node = node
node_path = ""
while curr_node.parent:
if not node_path:
node_path = curr_node.get_name()
else:
node_path = curr_node.get_name() + "/" + node_path
last_node = curr_node
curr_node = curr_node.parent
node_path = '/' + node_path
resolved[node_path_key] = node_path
return resolved
def resolve(self, args):
result = self.engine.query(args)
resolved = {}
if not len(result):
raise ParseError("DTB query has no results.")
query_results = []
for entry in result:
if not len(entry):
query_results.append({})
else:
node = entry[0]
node_resolved = self.resolve_fdt_node(node)
query_results.append(node_resolved)
# place the results under the 'dtb' key
resolved['query'] = query_results
# inject the size of the dtb into into the dictionary
resolved['dtb-size'] = [self.dtb_file_size]
return resolved
@staticmethod
def get_parser():
parser = argparse.ArgumentParser('dtb')
parser.add_argument('--dtb',
type=str,
help='Flattened device tree blob (.dtb) to query for device tree properties.')
return parser
def check_options(self):
if self.options.dtb:
try:
self.dtb_file_size = os.path.getsize(self.options.dtb)
with open(self.options.dtb, 'rb') as dtb_file:
self.engine = FdtQueryEngine(pyfdt.pyfdt.FdtBlobParse(dtb_file).to_fdt())
except:
logging.fatal("Failed to parse dtb file {0}".format(self.options.dtb.name))
@staticmethod
def get_query_name():
return "dtb"
def get_deps(self):
return [self.options.dtb]