diff --git a/tools/cot_dt2c/Makefile b/tools/cot_dt2c/Makefile new file mode 100644 index 000000000..ad8d9f5e9 --- /dev/null +++ b/tools/cot_dt2c/Makefile @@ -0,0 +1,68 @@ +# +# Copyright (c) 2024, Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +##* Variables +SHELL := /usr/bin/env bash +PYTHON := python +PYTHONPATH := `pwd` + +.PHONY: dist +dist: clean + poetry build + +#* Installation +.PHONY: dev-install +dev-install: + pip3 install mypy + pip3 install pytest + pip install -r requirements.txt + poetry lock -n && poetry export --without-hashes > requirements.txt + poetry install -n + -poetry run mypy --install-types --non-interactive ./ + +.PHONY: install +install: dist + pip install mypy + pip install pytest + pip install -r requirements.txt + pip install dist/*.whl + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + find . | grep -E ".pytest_cache" | xargs rm -rf + find . | grep -E ".mypy_cache" | xargs rm -rf + + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-tmp: + rm -rf ./tmp + +#* Cleaning +.PHONY: clean clean-build clean-pyc clean-test +clean: uninstall clean-build clean-pyc clean-test clean-tmp ## remove all build, test, coverage and Python artifacts + +uninstall: + pip uninstall -y cot-dt2c + +.PHONY: reinstall +reinstall: clean install + +.PHONY: test +test: + PYTHONPATH=$(PYTHONPATH) poetry run pytest -c pyproject.toml tests/ diff --git a/tools/cot_dt2c/cot_dt2c/LICENSE b/tools/cot_dt2c/cot_dt2c/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tools/cot_dt2c/cot_dt2c/__init__.py b/tools/cot_dt2c/cot_dt2c/__init__.py new file mode 100644 index 000000000..621c55a1e --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# type: ignore[attr-defined] + +# +# Copyright (c) 2024, Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +import sys + +if sys.version_info >= (3, 8): + from importlib import metadata as importlib_metadata +else: + import importlib_metadata + + +def get_version() -> str: + try: + return importlib_metadata.version(__name__) + except importlib_metadata.PackageNotFoundError: # pragma: no cover + return "unknown" + + +version: str = get_version() diff --git a/tools/cot_dt2c/cot_dt2c/__main__.py b/tools/cot_dt2c/cot_dt2c/__main__.py new file mode 100644 index 000000000..5aa4a92f2 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/__main__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# type: ignore[attr-defined] +# +# Copyright (c) 2024, Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# +from cot_dt2c.cli import cli +if __name__ == "__main__": + cli() diff --git a/tools/cot_dt2c/cot_dt2c/cli.py b/tools/cot_dt2c/cot_dt2c/cli.py new file mode 100644 index 000000000..d338430c7 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/cli.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2024, Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +from pathlib import Path +from cot_dt2c.cot_dt2c import generateMain +from cot_dt2c.cot_dt2c import validateMain +from cot_dt2c.cot_dt2c import visualizeMain +from cot_dt2c.dt_validator import dtValidatorMain + +import click + +@click.group() +@click.version_option() +def cli(): + pass + +@cli.command() +@click.argument("inputfile", type=click.Path(dir_okay=True)) +@click.argument("outputfile", type=click.Path(dir_okay=True)) +def convert_to_c(inputfile, outputfile): + generateMain(inputfile, outputfile) + +@cli.command() +@click.argument("inputfile", type=click.Path(dir_okay=True)) +def validate_cot(inputfile): + validateMain(inputfile) + +@cli.command() +@click.argument("inputfile", type=click.Path(dir_okay=True)) +def visualize_cot(inputfile): + visualizeMain(inputfile) + +@cli.command() +@click.argument("inputfiledir", type=click.Path(dir_okay=True)) +def validate_dt(inputfiledir): + dtValidatorMain(inputfiledir) diff --git a/tools/cot_dt2c/cot_dt2c/cot_dt2c.py b/tools/cot_dt2c/cot_dt2c/cot_dt2c.py new file mode 100644 index 000000000..4056aac91 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/cot_dt2c.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2024, Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +import sys +from cot_dt2c.cot_parser import COT + +def generateMain(input, output=None): + cot = COT(input, output) + cot.generate_c_file() + +def validateMain(input): + cot = COT(input) + if not cot.validate_nodes(): + print("not a valid CoT DT file") + +def visualizeMain(input): + cot = COT(input) + cot.tree_visualization() + +if __name__=="__main__": + if (len(sys.argv) < 2): + print("usage: python3 " + sys.argv[0] + " [dtsi file path] [optional output c file path]") + exit() + if len(sys.argv) == 3: + generateMain(sys.argv[1], sys.argv[2]) + if len(sys.argv) == 2: + validateMain(sys.argv[1]) diff --git a/tools/cot_dt2c/cot_dt2c/cot_parser.py b/tools/cot_dt2c/cot_dt2c/cot_parser.py new file mode 100644 index 000000000..c1d53e211 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/cot_parser.py @@ -0,0 +1,804 @@ +# +# Copyright (c) 2024, Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +import sys +import re +from cot_dt2c.pydevicetree.source.parser import ifdef_stack +from cot_dt2c.pydevicetree.ast import CellArray, LabelReference +from cot_dt2c.pydevicetree import * +from pathlib import Path + +def extractNumber(s): + for i in s: + if i.isdigit(): + return (int)(i) + + return -1 + +def removeNumber(s): + result = ''.join([i for i in s if not i.isdigit()]) + return result + +class COT: + def __init__(self, inputfile: str, outputfile=None): + with open(inputfile, 'r') as f: + contents = f.read() + pos = contents.find("cot") + if pos == -1: + print("not a valid CoT DT file") + exit(1) + + contents = contents[pos:] + + try: + self.tree = Devicetree.parseStr(contents) + except: + print("not a valid CoT DT file") + exit(1) + + self.output = outputfile + self.input = inputfile + self.has_root = False + + # edge cases + certs = self.get_all_certificates() + for c in certs: + if self.if_root(c): + if not c.get_fields("signing-key"): + c.properties.append(Property("signing-key", CellArray([LabelReference("subject_pk")]))) + + def print_cert_info(self, node:Node): + img_id = node.get_field("image-id").values[0].replace('"', "") + sign_key = self.get_sign_key(node) + nv = self.get_nv_ctr(node) + + info = "name: {}
image-id: {}
{}{}{}"\ + .format(node.name, img_id, "root-certificate
" if self.if_root(node) else "", \ + "signing-key: " + self.extract_label(sign_key) + "
" if sign_key else "", \ + "nv counter: " + self.extract_label(nv) + "
" if nv else "") + return info + + def print_data_info(self, node:Node): + oid = node.get_field("oid") + info = "name: {}
oid: {}
" \ + .format(node.name, oid) + + return info + + def print_img_info(self, node:Node): + hash = self.extract_label(node.get_fields("hash")) + img_id = node.get_field("image-id").values[0].replace('"', "") + info = "name: {}
image-id: {}
hash: {}"\ + .format(node.name, img_id, hash) + + return info + + def tree_width(self, parent_set, root): + ans = 1 + stack = [root] + + while stack: + tmp_stack = [] + while stack: + cur_node = stack.pop() + child = parent_set[cur_node] + for c in child: + tmp_stack.append(c) + + stack = tmp_stack.copy() + ans = max(ans, len(tmp_stack)) + + return ans + + def resolve_lay(self, parent_set, lay, name_idx, root, bounds, break_name): + child = parent_set[root] + + if len(child) == 0: + return + + width = [] + total_width = 0 + for c in child: + w = self.tree_width(parent_set, c) + width.append(w) + total_width += w + + allow_width = bounds[1] - bounds[0] + interval = allow_width / total_width + start = bounds[0] + for i, c in enumerate(child): + end = start + interval * width[i] + new_bounds = [start, end] + lay[name_idx[c]][0] = start + (end - start) / 2 + if end - start < 0.28: + break_name.add(c) + start = end + self.resolve_lay(parent_set, lay, name_idx, c, new_bounds, break_name) + + def tree_visualization(self): + import igraph + from igraph import Graph, EdgeSeq + import collections + + cert = self.get_certificates() + pk = self.get_rot_keys() + nv = self.get_nv_counters() + image = self.get_images() + + certs = cert.children + if pk: + pks = pk.children + else: + pks = [] + nvs = nv.children + images = image.children + + root_name = "CoT" + + G = Graph() + detail = [] + lay = [] + name_idx = {} + parent_set = collections.defaultdict(list) + + G.add_vertex(root_name) + detail.append("CoT Root") + name_idx[root_name] = len(lay) + lay.append([0,0]) + + G.add_vertex(cert.name) + G.add_edge(root_name, cert.name) + detail.append("All Certificates") + name_idx[cert.name] = len(lay) + lay.append([0, 1]) + parent_set[root_name].append(cert.name) + + if pk: + G.add_vertex(pk.name) + detail.append("All Public Trusted Key") + G.add_edge(root_name, pk.name) + name_idx[pk.name] = len(lay) + lay.append([-2.0, 1]) + parent_set[root_name].append(pk.name) + + G.add_vertex(nv.name) + detail.append("All NV Counters") + G.add_edge(root_name, nv.name) + name_idx[nv.name] = len(lay) + lay.append([2.0, 1]) + parent_set[root_name].append(nv.name) + + if pks: + for i, p in enumerate(pks): + G.add_vertex(p.name) + detail.append(self.print_data_info(p)) + G.add_edge(pk.name, p.name) + name_idx[p.name] = len(lay) + parent_set[pk.name].append(p.name) + lay.append([0, lay[name_idx[pk.name]][1] + 1]) + + for c in certs: + G.add_vertex(c.name) + detail.append(self.print_cert_info(c)) + name_idx[c.name] = len(lay) + if self.if_root(c): + G.add_edge(cert.name, c.name) + parent_set[cert.name].append(c.name) + lay.append([0, 2]) + else: + parent = self.extract_label(c.get_fields("parent")) + G.add_edge(parent, c.name) + parent_set[parent].append(c.name) + lay.append([0, lay[name_idx[parent]][1] + 1]) + + for idx, i in enumerate(images): + G.add_vertex(i.name) + detail.append(self.print_img_info(i)) + parent = self.extract_label(i.get_fields("parent")) + G.add_edge(parent, i.name) + parent_set[parent].append(i.name) + name_idx[i.name] = len(lay) + lay.append([0, lay[name_idx[parent]][1] + 1]) + + for i, n in enumerate(nvs): + G.add_vertex(n.name) + detail.append(self.print_data_info(n)) + G.add_edge(nv.name, n.name) + name_idx[n.name] = len(lay) + parent_set[nv.name].append(n.name) + lay.append([0, lay[name_idx[nv.name]][1] + 1]) + + break_name = set() + self.resolve_lay(parent_set, lay, name_idx, root_name, [-3, 3], break_name) + #lay = G.layout('rt') + + numVertex = len(G.get_vertex_dataframe()) + vertices = G.get_vertex_dataframe() + v_label = [] + + for i in vertices['name']: + if i in break_name and len(i) > 10: + middle = len(i) // 2 + v_label.append(i[:middle] + "
" + i[middle:]) + else: + v_label.append(i) + + position = {k: lay[k] for k in range(numVertex)} + Y = [lay[k][1] for k in range(numVertex)] + M = max(Y) + + es = EdgeSeq(G) # sequence of edges + E = [e.tuple for e in G.es] # list of edges + + L = len(position) + Xn = [position[k][0] for k in range(L)] + Yn = [2*M-position[k][1] for k in range(L)] + Xe = [] + Ye = [] + for edge in E: + Xe += [position[edge[0]][0], position[edge[1]][0], None] + Ye += [2*M-position[edge[0]][1], 2*M-position[edge[1]][1], None] + + labels = v_label + + import plotly.graph_objects as go + fig = go.Figure() + fig.add_trace(go.Scatter(x = Xe, + y = Ye, + mode = 'lines', + line = dict(color='rgb(210,210,210)', width=2), + hoverinfo = 'none' + )) + fig.add_trace(go.Scatter(x = Xn, + y = Yn, + mode = 'markers', + name = 'detail', + marker = dict(symbol = 'circle-dot', + size = 50, + color = 'rgba(135, 206, 250, 0.8)', #'#DB4551', + line = dict(color='MediumPurple', width=3) + ), + text=detail, + hoverinfo='text', + hovertemplate = + 'Detail
' + '%{text}', + opacity=0.8 + )) + + def make_annotations(pos, text, font_size=10, font_color='rgb(0,0,0)'): + L = len(pos) + if len(text) != L: + raise ValueError('The lists pos and text must have the same len') + annotations = [] + for k in range(L): + annotations.append( + dict( + text = labels[k], + x = pos[k][0], y = 2*M-position[k][1], + xref = 'x1', yref = 'y1', + font = dict(color = font_color, size = font_size), + showarrow = False) + ) + return annotations + + axis = dict(showline=False, # hide axis line, grid, ticklabels and title + zeroline=False, + showgrid=False, + showticklabels=False, + ) + + fig.update_layout(title= 'CoT Device Tree', + annotations=make_annotations(position, v_label), + font_size=12, + showlegend=False, + xaxis=axis, + yaxis=axis, + margin=dict(l=40, r=40, b=85, t=100), + hovermode='closest', + plot_bgcolor='rgb(248,248,248)' + ) + + fig.show() + + return + + def if_root(self, node:Node) -> bool: + for p in node.properties: + if p.name == "root-certificate": + return True + return False + + def get_sign_key(self, node:Node): + for p in node.properties: + if p.name == "signing-key": + return p.values + + return None + + def get_nv_ctr(self, node:Node): + for nv in node.properties: + if nv.name == "antirollback-counter": + return nv.values + + return None + + def extract_label(self, label) -> str: + if not label: + return label + return label[0].label.name + + def get_auth_data(self, node:Node): + return node.children + + def format_auth_data_val(self, node:Node, cert:Node): + type_desc = node.name + if "sp_pkg" in type_desc: + ptr = removeNumber(type_desc) + "_buf" + else: + ptr = type_desc + "_buf" + len = "(unsigned int)HASH_DER_LEN" + if "pk" in type_desc: + len = "(unsigned int)PK_DER_LEN" + + # edge case + if not self.if_root(cert) and "key_cert" in cert.name: + if "content_pk" in ptr: + ptr = "content_pk_buf" + + return type_desc, ptr, len + + def get_node(self, nodes: list[Node], name: str) -> Node: + for i in nodes: + if i.name == name: + return i + + def get_certificates(self) -> Node: + children = self.tree.children + for i in children: + if i.name == "cot": + return self.get_node(i.children, "manifests") + + def get_images(self)-> Node: + children = self.tree.children + for i in children: + if i.name == "cot": + return self.get_node(i.children, "images") + + def get_nv_counters(self) -> Node: + children = self.tree.children + return self.get_node(children, "non_volatile_counters") + + def get_rot_keys(self) -> Node: + children = self.tree.children + return self.get_node(children, "rot_keys") + + def get_all_certificates(self) -> Node: + cert = self.get_certificates() + return cert.children + + def get_all_images(self) -> Node: + image = self.get_images() + return image.children + + def get_all_nv_counters(self) -> Node: + nv = self.get_nv_counters() + return nv.children + + def get_all_pks(self) -> Node: + pk = self.get_rot_keys() + if not pk: + return [] + return pk.children + + def validate_cert(self, node:Node) -> bool: + valid = True + if not node.has_field("image-id"): + print("{} missing mandatory attribute image-id".format(node.name)) + valid = False + + if not node.has_field("root-certificate"): + if not node.has_field("parent"): + print("{} missing mandatory attribute parent".format(node.name)) + valid = False + else: + # check if refer to non existing parent + certs = self.get_all_certificates() + found = False + for c in certs: + if c.name == self.extract_label(node.get_fields("parent")): + found = True + + if not found: + print("{} refer to non existing parent".format(node.name)) + valid = False + + else: + self.has_root = True + + child = node.children + if child: + for c in child: + if not c.has_field("oid"): + print("{} missing mandatory attribute oid".format(c.name)) + valid = False + + return valid + + def validate_img(self, node:Node) -> bool: + valid = True + if not node.has_field("image-id"): + print("{} missing mandatory attribute image-id".format(node.name)) + valid = False + + if not node.has_field("parent"): + print("{} missing mandatory attribute parent".format(node.name)) + valid = False + + if not node.has_field("hash"): + print("{} missing mandatory attribute hash".format(node.name)) + valid = False + + # check if refer to non existing parent + certs = self.get_all_certificates() + found = False + for c in certs: + if c.name == self.extract_label(node.get_fields("parent")): + found = True + + if not found: + print("{} refer to non existing parent".format(node.name)) + valid = False + + return valid + + def validate_nodes(self) -> bool: + valid = True + + if ifdef_stack: + print("invalid ifdef macro") + valid = False + + certs = self.get_all_certificates() + images = self.get_all_images() + + for n in certs: + node_valid = self.validate_cert(n) + valid = valid and node_valid + + for i in images: + node_valid = self.validate_img(i) + valid = valid and node_valid + + if not self.has_root: + print("missing root certificate") + + return valid + + def extract_licence(self, f): + licence = [] + + licencereg = re.compile(r'/\*') + licenceendReg = re.compile(r'\*/') + + licencePre = False + + for line in f: + match = licencereg.search(line) + if match != None: + licence.append(line) + licencePre = True + continue + + match = licenceendReg.search(line) + if match != None: + licence.append(line) + licencePre = False + return licence + + if licencePre: + licence.append(line) + else: + return licence + + return licence + + def licence_to_c(self, licence, f): + if len(licence) != 0: + for i in licence: + f.write(i) + + f.write("\n") + return + + def extract_include(self, f): + include = [] + + for line in f: + if "cot" in line: + return include + + if line != "" and "common" not in line and line != "\n": + include.append(line) + + return include + + def include_to_c(self, include, f): + f.write("#include \n") + f.write("#include \n") + f.write("#include \n") + f.write("#include \n") + f.write("\n") + for i in include: + f.write(i) + f.write("\n") + f.write("#include \n\n") + return + + def generate_header(self, input, output): + licence = self.extract_licence(input) + include = self.extract_include(input) + self.licence_to_c(licence, output) + self.include_to_c(include, output) + + def all_cert_to_c(self, f): + certs = self.get_all_certificates() + for c in certs: + self.cert_to_c(c, f) + + f.write("\n") + + def cert_to_c(self, node: Node, f): + ifdef = node.get_fields("ifdef") + if ifdef: + for i in ifdef: + f.write("{}\n".format(i)) + + f.write("static const auth_img_desc_t {} = {{\n".format(node.name)) + f.write("\t.img_id = {},\n".format(node.get_field("image-id").values[0].replace('"', ""))) + f.write("\t.img_type = IMG_CERT,\n") + + if not self.if_root(node): + f.write("\t.parent = &{},\n".format(node.get_field("parent").label.name)) + else: + f.write("\t.parent = NULL,\n") + + sign = self.get_sign_key(node) + nv_ctr = self.get_nv_ctr(node) + + if sign or nv_ctr: + f.write("\t.img_auth_methods = (const auth_method_desc_t[AUTH_METHOD_NUM]) {\n") + + if sign: + f.write("\t\t[0] = {\n") + f.write("\t\t\t.type = AUTH_METHOD_SIG,\n") + f.write("\t\t\t.param.sig = {\n") + + f.write("\t\t\t\t.pk = &{},\n".format(self.extract_label(sign))) + f.write("\t\t\t\t.sig = &sig,\n") + f.write("\t\t\t\t.alg = &sig_alg,\n") + f.write("\t\t\t\t.data = &raw_data\n") + f.write("\t\t\t}\n") + f.write("\t\t}}{}\n".format("," if nv_ctr else "")) + + if nv_ctr: + f.write("\t\t[1] = {\n") + f.write("\t\t\t.type = AUTH_METHOD_NV_CTR,\n") + f.write("\t\t\t.param.nv_ctr = {\n") + + f.write("\t\t\t\t.cert_nv_ctr = &{},\n".format(self.extract_label(nv_ctr))) + f.write("\t\t\t\t.plat_nv_ctr = &{}\n".format(self.extract_label(nv_ctr))) + + f.write("\t\t\t}\n") + f.write("\t\t}\n") + + f.write("\t},\n") + + auth_data = self.get_auth_data(node) + if auth_data: + f.write("\t.authenticated_data = (const auth_param_desc_t[COT_MAX_VERIFIED_PARAMS]) {\n") + + for i, d in enumerate(auth_data): + type_desc, ptr, data_len = self.format_auth_data_val(d, node) + + f.write("\t\t[{}] = {{\n".format(i)) + f.write("\t\t\t.type_desc = &{},\n".format(type_desc)) + f.write("\t\t\t.data = {\n") + + n = extractNumber(type_desc) + if "pkg" not in type_desc or n == -1: + f.write("\t\t\t\t.ptr = (void *){},\n".format(ptr)) + else: + f.write("\t\t\t\t.ptr = (void *){}[{}],\n".format(ptr, n-1)) + + f.write("\t\t\t\t.len = {}\n".format(data_len)) + f.write("\t\t\t}\n") + + f.write("\t\t}}{}\n".format("," if i != len(auth_data) - 1 else "")) + + f.write("\t}\n") + + f.write("};\n\n") + + if ifdef: + for i in ifdef: + f.write("#endif\n") + f.write("\n") + + return + + + def img_to_c(self, node:Node, f): + ifdef = node.get_fields("ifdef") + if ifdef: + for i in ifdef: + f.write("{}\n".format(i)) + + f.write("static const auth_img_desc_t {} = {{\n".format(node.name)) + f.write("\t.img_id = {},\n".format(node.get_field("image-id").values[0].replace('"', ""))) + f.write("\t.img_type = IMG_RAW,\n") + f.write("\t.parent = &{},\n".format(node.get_field("parent").label.name)) + f.write("\t.img_auth_methods = (const auth_method_desc_t[AUTH_METHOD_NUM]) {\n") + + f.write("\t\t[0] = {\n") + f.write("\t\t\t.type = AUTH_METHOD_HASH,\n") + f.write("\t\t\t.param.hash = {\n") + f.write("\t\t\t\t.data = &raw_data,\n") + f.write("\t\t\t\t.hash = &{}\n".format(node.get_field("hash").label.name)) + f.write("\t\t\t}\n") + + f.write("\t\t}\n") + f.write("\t}\n") + f.write("};\n\n") + + if ifdef: + for i in ifdef: + f.write("#endif\n") + f.write("\n") + + return + + def all_img_to_c(self, f): + images = self.get_all_images() + for i in images: + self.img_to_c(i, f) + + f.write("\n") + + def nv_to_c(self, f): + nv_ctr = self.get_all_nv_counters() + + for nv in nv_ctr: + f.write("static auth_param_type_desc_t {} = AUTH_PARAM_TYPE_DESC(AUTH_PARAM_NV_CTR, {});\n".format(nv.name, nv.get_field("oid"))) + + f.write("\n") + + return + + def pk_to_c(self, f): + pks = self.get_all_pks() + + for p in pks: + f.write("static auth_param_type_desc_t {} = AUTH_PARAM_TYPE_DESC(AUTH_PARAM_PUB_KEY, {});\n".format(p.name, p.get_field("oid"))) + + f.write("\n") + return + + def buf_to_c(self, f): + certs = self.get_all_certificates() + + buffers = {} + + for c in certs: + auth_data = self.get_auth_data(c) + for a in auth_data: + type_desc, ptr, data_len = self.format_auth_data_val(a, c) + if ptr not in buffers: + buffers[ptr] = c.get_fields("ifdef") + + for key, values in buffers.items(): + if values: + for i in values: + f.write("{}\n".format(i)) + + if "sp_pkg_hash_buf" in key: + f.write("static unsigned char {}[MAX_SP_IDS][HASH_DER_LEN];\n".format(key)) + elif "pk" in key: + f.write("static unsigned char {}[PK_DER_LEN];\n".format(key)) + else: + f.write("static unsigned char {}[HASH_DER_LEN];\n".format(key)) + + if values: + for i in values: + f.write("#endif\n") + + f.write("\n") + + def param_to_c(self, f): + f.write("static auth_param_type_desc_t subject_pk = AUTH_PARAM_TYPE_DESC(AUTH_PARAM_PUB_KEY, 0);\n") + f.write("static auth_param_type_desc_t sig = AUTH_PARAM_TYPE_DESC(AUTH_PARAM_SIG, 0);\n") + f.write("static auth_param_type_desc_t sig_alg = AUTH_PARAM_TYPE_DESC(AUTH_PARAM_SIG_ALG, 0);\n") + f.write("static auth_param_type_desc_t raw_data = AUTH_PARAM_TYPE_DESC(AUTH_PARAM_RAW_DATA, 0);\n") + f.write("\n") + + certs = self.get_all_certificates() + for c in certs: + ifdef = c.get_fields("ifdef") + if ifdef: + for i in ifdef: + f.write("{}\n".format(i)) + + hash = c.children + for h in hash: + name = h.name + oid = h.get_field("oid") + + if "pk" in name and "pkg" not in name: + f.write("static auth_param_type_desc_t {} = "\ + "AUTH_PARAM_TYPE_DESC(AUTH_PARAM_PUB_KEY, {});\n".format(name, oid)) + elif "hash" in name: + f.write("static auth_param_type_desc_t {} = "\ + "AUTH_PARAM_TYPE_DESC(AUTH_PARAM_HASH, {});\n".format(name, oid)) + elif "ctr" in name: + f.write("static auth_param_type_desc_t {} = "\ + "AUTH_PARAM_TYPE_DESC(AUTH_PARAM_NV_CTR, {});\n".format(name, oid)) + + if ifdef: + for i in ifdef: + f.write("#endif\n") + + f.write("\n") + + def cot_to_c(self, f): + certs = self.get_all_certificates() + images = self.get_all_images() + + f.write("static const auth_img_desc_t * const cot_desc[] = {\n") + + for i, c in enumerate(certs): + ifdef = c.get_fields("ifdef") + if ifdef: + for i in ifdef: + f.write("{}\n".format(i)) + + f.write("\t[{}] = &{}{}\n".format(c.get_field("image-id").values[0], c.name, ",")) + + if ifdef: + for i in ifdef: + f.write("#endif\n") + + for i, c in enumerate(images): + ifdef = c.get_fields("ifdef") + if ifdef: + for i in ifdef: + f.write("{}\n".format(i)) + + f.write("\t[{}] = &{}{}\n".format(c.get_field("image-id").values[0], c.name, "," if i != len(images) - 1 else "")) + + if ifdef: + for i in ifdef: + f.write("#endif\n") + + f.write("};\n\n") + f.write("REGISTER_COT(cot_desc);\n") + return + + def generate_c_file(self): + filename = Path(self.output) + filename.parent.mkdir(exist_ok=True, parents=True) + output = open(self.output, 'w+') + input = open(self.input, "r") + + self.generate_header(input, output) + self.buf_to_c(output) + self.param_to_c(output) + self.nv_to_c(output) + self.pk_to_c(output) + self.all_cert_to_c(output) + self.all_img_to_c(output) + self.cot_to_c(output) + + return diff --git a/tools/cot_dt2c/cot_dt2c/dt_validator.py b/tools/cot_dt2c/cot_dt2c/dt_validator.py new file mode 100644 index 000000000..65e8ca231 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/dt_validator.py @@ -0,0 +1,130 @@ +# +# Copyright (c) 2024, Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +import sys +from os import path, walk, mkdir +import subprocess +from cot_dt2c.pydevicetree import * + +class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +class DTTree: + def __init__(self, input): + self.input = input + self.test_dir = "./tmp" + self.logging_file = self.test_dir + "/result.log" + + def dtValidate(self): + subprocess.run(["rm", "-rf", self.test_dir]) + + if not path.exists(self.test_dir): + mkdir(self.test_dir) + + if path.isfile(self.input): + self.dtValidateFile(self.input, printInfo=True) + return + + if path.isdir(self.input): + self.dtValidateFiles() + return + + def dtValidateFile(self, input, printInfo=False): + valid, tree = self.dtParseFile(input, printInfo) + + if not valid: + return False + + if input.rfind("/") != -1: + filename = self.test_dir + input[input.rfind("/"):] + else: + filename = self.test_dir + "/" + input + + f = open(filename, "w+") + if "/dts-v1/;" not in str(tree): + f.write("/dts-v1/;\n\n") + f.write(str(tree)) + f.close() + + if str(tree) == "": + return valid + + return valid + + def dtParseFile(self, input, printInfo=False): + with open(input, 'r') as f: + contents = f.read() + + pos = contents.find("/ {") + if pos != -1: + contents = contents[pos:] + + try: + tree = Devicetree.parseStr(contents) + if printInfo: + print(bcolors.OKGREEN + "{} parse tree successfully".format(input) + bcolors.ENDC) + except Exception as e: + if printInfo: + print(bcolors.FAIL + "{} parse tree failed:\t{}".format(input, str(e)) + bcolors.ENDC) + else: + f = open(self.logging_file, "a") + f.write("=====================================================================================\n") + f.write("{} result:\n".format(input)) + f.write("{} INVALID:\t{}\n".format(input, str(e))) + f.close() + return False, None + + return True, tree + + def dtValidateFiles(self): + f = [] + for (dirpath, dirnames, filenames) in walk(self.input): + f.extend(filenames) + + allFile = len(f) + dtsiFile = 0 + validFile = 0 + invalidFile = 0 + + for i in f: + if (".dtsi" in i or ".dts" in i) and "cot" not in i and "fw-config" not in i: + dtsiFile += 1 + valid = True + + if self.input[-1] == "/": + valid = self.dtValidateFile(self.input + i) + else: + valid = self.dtValidateFile(self.input + "/" + i) + + if valid: + validFile += 1 + else: + invalidFile += 1 + + print("=====================================================") + print("Total File: " + str(allFile)) + print("Total DT File: " + str(dtsiFile)) + print("Total Valid File: " + str(validFile)) + print("Total Invalid File: " + str(invalidFile)) + +def dtValidatorMain(input): + dt = DTTree(input) + dt.dtValidate() + +if __name__=="__main__": + if (len(sys.argv) < 2): + print("usage: python3 " + sys.argv[0] + " [dtsi file path] or [dtsi folder path]") + exit() + if len(sys.argv) == 2: + dtValidatorMain(sys.argv[1]) diff --git a/tools/cot_dt2c/cot_dt2c/pydevicetree/__init__.py b/tools/cot_dt2c/cot_dt2c/pydevicetree/__init__.py new file mode 100644 index 000000000..49595a71c --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/pydevicetree/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 SiFive Inc. +# SPDX-License-Identifier: Apache-2.0 + +from cot_dt2c.pydevicetree.ast import Devicetree, Node, Property, Directive, CellArray, LabelReference diff --git a/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/__init__.py b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/__init__.py new file mode 100644 index 000000000..f30d89791 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 SiFive Inc. +# SPDX-License-Identifier: Apache-2.0 + +from cot_dt2c.pydevicetree.ast.directive import Directive +from cot_dt2c.pydevicetree.ast.node import Node, NodeReference, Devicetree +from cot_dt2c.pydevicetree.ast.property import PropertyValues, Bytestring, CellArray, StringList, Property, \ + RegArray, OneString +from cot_dt2c.pydevicetree.ast.reference import Label, Path, Reference, LabelReference, PathReference diff --git a/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/directive.py b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/directive.py new file mode 100644 index 000000000..fdd6f0e15 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/directive.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 SiFive Inc. +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any + +from cot_dt2c.pydevicetree.ast.helpers import formatLevel, wrapStrings + +class Directive: + """Represents a Devicetree directive + + Directives in Devicetree source are statements of the form + + /directive-name/ [option1 [option2 [...]]]; + + Common directive examples include: + + /dts-v1/; + /include/ "overlay.dtsi"; + /delete-node/ &uart0; + /delete-property/ status; + + Their semantic meaning depends on the directive name, their location in the Devicetree, + and their options. + """ + def __init__(self, directive: str, option: Any = None): + """Create a directive object""" + self.directive = directive + self.option = option + + def __repr__(self) -> str: + return "" % self.directive + + def __str__(self) -> str: + return self.to_dts() + + def to_dts(self, level: int = 0) -> str: + """Format the Directive in Devicetree Source format""" + if isinstance(self.option, list): + return formatLevel(level, "%s %s;\n" % (self.directive, + wrapStrings(self.option))) + if isinstance(self.option, str): + if self.directive == "/include/": + return formatLevel(level, "%s \"%s\"\n" % (self.directive, self.option)) + return formatLevel(level, "%s \"%s\";\n" % (self.directive, self.option)) + return formatLevel(level, "%s;\n" % self.directive) diff --git a/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/helpers.py b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/helpers.py new file mode 100644 index 000000000..30c54dc20 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/helpers.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 SiFive Inc. +# SPDX-License-Identifier: Apache-2.0 + +from typing import List, Any + +from cot_dt2c.pydevicetree.ast.reference import Reference + +def formatLevel(level: int, s: str) -> str: + """Helper to indent a string with a number of tabs""" + return "\t" * level + s + +def wrapStrings(values: List[Any], formatHex: bool = False) -> List[Any]: + """Helper to wrap strings in quotes where appropriate""" + wrapped = [] + for v in values: + if isinstance(v, Reference): + wrapped.append(v.to_dts()) + elif isinstance(v, str): + wrapped.append("\"%s\"" % v) + elif isinstance(v, int): + if formatHex: + wrapped.append("0x%x" % v) + else: + wrapped.append(str(v)) + else: + wrapped.append(str(v)) + return wrapped diff --git a/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/node.py b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/node.py new file mode 100644 index 000000000..d203af870 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/node.py @@ -0,0 +1,514 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 SiFive Inc. +# SPDX-License-Identifier: Apache-2.0 + +import re +import os +from typing import List, Union, Optional, Iterable, Callable, Any, cast, Pattern + +from cot_dt2c.pydevicetree.ast.helpers import formatLevel +from cot_dt2c.pydevicetree.ast.property import Property, PropertyValues, RegArray, RangeArray +from cot_dt2c.pydevicetree.ast.directive import Directive +from cot_dt2c.pydevicetree.ast.reference import Label, Path, Reference, LabelReference, PathReference + +# Type signature for elements passed to Devicetree constructor +ElementList = Iterable[Union['Node', Property, Directive]] + +# Callback type signatures for Devicetree.match() and Devicetree.chosen() +MatchFunc = Callable[['Node'], bool] +MatchCallback = Optional[Callable[['Node'], None]] +ChosenCallback = Optional[Callable[[PropertyValues], None]] + +class Node: + """Represents a Devicetree Node + + A Devicetree Node generally takes the form + + [label:] node-name@unit-address { + [directives] + [properties] + [child nodes] + }; + + The structure formed by creating trees of Nodes is the bulk of any Devicetree. As the naming + system implies, then, each node roughly corresponds to some conceptual device, subsystem of + devices, bus, etc. + + Devices can be referenced by label or by path, and are generally uniquely identified by a + collection of string identifiers assigned to the "compatible" property. + + For instance, a UART device might look like + + uart0: uart@10013000 { + compatible = "sifive,uart0"; + reg = <0x10013000 0x1000>; + reg-names = "control"; + interrupt-parent = <&plic>; + interrupts = <3>; + clocks = <&busclk>; + status = "okay"; + }; + + This node can be identified in the following ways: + + - By label: uart0 + - By path: /path/to/uart@10013000 + - By name: uart@10013000 (for example when referenced in a /delete-node/ directive) + """ + # pylint: disable=too-many-arguments + def __init__(self, name: str, label: Optional[str], address: Optional[int], + properties: List[Property], directives: List[Directive], + children: List['Node']): + """Initializes a Devicetree Node + + Also evaluates the /delete-node/ and /delete-property/ directives found in the node + and deletes the respective nodes and properties. + """ + self.name = name + self.parent = None # type: Optional['Node'] + + self.label = label + self.address = address + self.properties = properties + self.directives = directives + self.children = children + self.ifdef = [] + + for d in self.directives: + if d.directive == "/delete-node/": + if isinstance(d.option, LabelReference): + node = self.get_by_reference(d.option) + elif isinstance(d.option, str): + node = self.__get_child_by_handle(d.option) + if node: + self.remove_child(node) + elif d.directive == "/delete-property/": + # pylint: disable=cell-var-from-loop + properties = list(filter(lambda p: p.name == d.option, self.properties)) + if properties: + del self.properties[self.properties.index(properties[0])] + + def __repr__(self) -> str: + if self.address: + return "" % (self.name, self.address) + return "" % self.name + + def __str__(self) -> str: + return self.to_dts() + + def __eq__(self, other) -> bool: + return self.name == other.name and self.address == other.address + + def __hash__(self): + return hash((self.name, self.address)) + + @staticmethod + def from_dts(source: str) -> 'Node': + """Create a node from Devicetree Source""" + # pylint: disable=import-outside-toplevel,cyclic-import + from pydevicetree.source import parseNode + return parseNode(source) + + def add_child(self, node: 'Node', merge: bool = True): + """Add a child node and merge it into the tree""" + node.parent = self + self.children.append(node) + if merge: + self.merge_tree() + + def to_dts(self, level: int = 0) -> str: + """Format the subtree starting at the node as Devicetree Source""" + out = "" + if isinstance(self.address, int) and self.label: + out += formatLevel(level, + "%s: %s@%x {\n" % (self.label, self.name, self.address)) + elif isinstance(self.address, int): + out += formatLevel(level, "%s@%x {\n" % (self.name, self.address)) + elif self.label: + out += formatLevel(level, "%s: %s {\n" % (self.label, self.name)) + elif self.name != "": + out += formatLevel(level, "%s {\n" % self.name) + + for d in self.directives: + out += d.to_dts(level + 1) + for p in self.properties: + out += p.to_dts(level + 1) + for c in self.children: + out += c.to_dts(level + 1) + + if self.name != "": + out += formatLevel(level, "};\n") + + return out + + def merge_tree(self): + """Recursively merge child nodes into a single tree + + Parsed Devicetrees can describe the same tree multiple times, adding nodes and properties + each time. After parsing, this method is called to recursively merge the tree. + """ + partitioned_children = [] + for n in self.children: + partitioned_children.append([e for e in self.children if e == n]) + + new_children = [] + for part in partitioned_children: + first = part[0] + rest = part[1:] + if first not in new_children: + for n in rest: + first.merge(n) + new_children.append(first) + + self.children = new_children + + for n in self.children: + n.parent = self + n.merge_tree() + + def merge(self, other: 'Node'): + """Merge the contents of a node into this node. + + Used by Node.merge_trees() + """ + if not self.label and other.label: + self.label = other.label + self.properties += other.properties + self.directives += other.directives + self.children += other.children + self.ifdef += other.ifdef + + def get_path(self, includeAddress: bool = True) -> str: + """Get the path of a node (ex. /cpus/cpu@0)""" + if self.name == "/": + return "" + if self.parent is None: + return "/" + self.name + if isinstance(self.address, int) and includeAddress: + return self.parent.get_path() + "/" + self.name + "@" + ("%x" % self.address) + return self.parent.get_path() + "/" + self.name + + def get_by_reference(self, reference: Reference) -> Optional['Node']: + """Get a node from the subtree by reference (ex. &label, &{/path/to/node})""" + if isinstance(reference, LabelReference): + return self.get_by_label(reference.label) + if isinstance(reference, PathReference): + return self.get_by_path(reference.path) + + return None + + def get_by_label(self, label: Union[Label, str]) -> Optional['Node']: + """Get a node from the subtree by label""" + matching_nodes = list(filter(lambda n: n.label == label, self.child_nodes())) + if len(matching_nodes) != 0: + return matching_nodes[0] + return None + + def __get_child_by_handle(self, handle: str) -> Optional['Node']: + """Get a child node by name or name and unit address""" + if '@' in handle: + name, addr_s = handle.split('@') + address = int(addr_s, base=16) + nodes = list(filter(lambda n: n.name == name and n.address == address, self.children)) + else: + name = handle + nodes = list(filter(lambda n: n.name == name, self.children)) + + if not nodes: + return None + if len(nodes) > 1: + raise Exception("Handle %s is ambiguous!" % handle) + return nodes[0] + + def get_by_path(self, path: Union[Path, str]) -> Optional['Node']: + """Get a node in the subtree by path""" + matching_nodes = list(filter(lambda n: path == n.get_path(includeAddress=True), \ + self.child_nodes())) + if len(matching_nodes) != 0: + return matching_nodes[0] + + matching_nodes = list(filter(lambda n: path == n.get_path(includeAddress=False), \ + self.child_nodes())) + if len(matching_nodes) != 0: + return matching_nodes[0] + return None + + def filter(self, matchFunc: MatchFunc, cbFunc: MatchCallback = None) -> List['Node']: + """Filter all child nodes by matchFunc + + If cbFunc is provided, this method will iterate over the Nodes selected by matchFunc + and call cbFunc on each Node + + Returns a list of all matching Nodes + """ + nodes = list(filter(matchFunc, self.child_nodes())) + + if cbFunc is not None: + for n in nodes: + cbFunc(n) + + return nodes + + def match(self, compatible: Pattern, func: MatchCallback = None) -> List['Node']: + """Get a node from the subtree by compatible string + + Accepts a regular expression to match one of the strings in the compatible property. + """ + regex = re.compile(compatible) + + def match_compat(node: Node) -> bool: + compatibles = node.get_fields("compatible") + if compatibles is not None: + return any(regex.match(c) for c in compatibles) + return False + + return self.filter(match_compat, func) + + def child_nodes(self) -> Iterable['Node']: + """Get an iterable over all the nodes in the subtree""" + for n in self.children: + yield n + for m in n.child_nodes(): + yield m + + def remove_child(self, node): + """Remove a child node""" + del self.children[self.children.index(node)] + + def get_fields(self, field_name: str) -> Optional[PropertyValues]: + """Get all the values of a property""" + for p in self.properties: + if p.name == field_name: + return p.values + return None + + def has_field(self, field_name: str) -> bool: + for p in self.properties: + if p.name == field_name: + return True + return False + + def get_field(self, field_name: str) -> Any: + """Get the first value of a property""" + fields = self.get_fields(field_name) + if fields is not None: + if len(cast(PropertyValues, fields)) != 0: + return fields[0] + return None + + def get_reg(self) -> Optional[RegArray]: + """If the node defines a `reg` property, return a RegArray for easier querying""" + reg = self.get_fields("reg") + reg_names = self.get_fields("reg-names") + if reg is not None: + if reg_names is not None: + return RegArray(reg.values, self.address_cells(), self.size_cells(), + reg_names.values) + return RegArray(reg.values, self.address_cells(), self.size_cells()) + return None + + def get_ranges(self) -> Optional[RangeArray]: + """If the node defines a `ranges` property, return a RangeArray for easier querying""" + ranges = self.get_fields("ranges") + child_address_cells = self.get_field("#address-cells") + parent_address_cells = self.address_cells() + size_cells = self.get_field("#size-cells") + if ranges is not None: + return RangeArray(ranges.values, child_address_cells, parent_address_cells, size_cells) + return None + + def address_cells(self): + """Get the number of address cells + + The #address-cells property is defined by the parent of a node and describes how addresses + are encoded in cell arrays. If no property is defined, the default value is 2. + """ + if self.parent is not None: + cells = self.parent.get_field("#address-cells") + if cells is not None: + return cells + return 2 + return 2 + + def size_cells(self): + """Get the number of size cells + + The #size-cells property is defined by the parent of a node and describes how addresses + are encoded in cell arrays. If no property is defined, the default value is 1. + """ + if self.parent is not None: + cells = self.parent.get_field("#size-cells") + if cells is not None: + return cells + return 1 + return 1 + +class NodeReference(Node): + """A NodeReference is used to extend the definition of a previously-defined Node + + NodeReferences are commonly used by Devicetree "overlays" to extend the properties of a node + or add child devices, such as to a bus like I2C. + """ + def __init__(self, reference: Reference, properties: List[Property], + directives: List[Directive], children: List[Node]): + """Instantiate a Node identified by reference to another node""" + self.reference = reference + Node.__init__(self, label=None, name="", address=None, properties=properties, + directives=directives, children=children) + + def __repr__(self) -> str: + return "" % self.reference.to_dts() + + def resolve_reference(self, tree: 'Devicetree') -> Node: + """Given the full tree, get the node being referenced""" + node = tree.get_by_reference(self.reference) + if node is None: + raise Exception("Node reference %s cannot be resolved" % self.reference.to_dts()) + return cast(Node, node) + + def to_dts(self, level: int = 0) -> str: + out = formatLevel(level, self.reference.to_dts() + " {\n") + + for d in self.directives: + out += d.to_dts(level + 1) + for p in self.properties: + out += p.to_dts(level + 1) + for c in self.children: + out += c.to_dts(level + 1) + + out += formatLevel(level, "};\n") + + return out + + +class Devicetree(Node): + """A Devicetree object describes the full Devicetree tree + + This class encapsulates both the tree itself (starting at the root node /) and any Directives + or nodes which exist at the top level of the Devicetree Source files. + + Devicetree Source files can be parsed by calling Devicetree.parseFile(). + """ + def __init__(self, elements: ElementList): + """Instantiate a Devicetree with the list of parsed elements + + Resolves all reference nodes and merges the tree to combine all identical nodes. + """ + properties = [] # type: List[Property] + directives = [] # type: List[Directive] + children = [] # type: List[Node] + + for e in elements: + if isinstance(e, Node): + children.append(cast(Node, e)) + elif isinstance(e, Property): + properties.append(cast(Property, e)) + elif isinstance(e, Directive): + directives.append(cast(Directive, e)) + + Node.__init__(self, label=None, name="", address=None, + properties=properties, directives=directives, children=children) + + for node in self.children: + node.parent = self + + reference_nodes = self.filter(lambda n: isinstance(n, NodeReference)) + for refnode in reference_nodes: + refnode = cast(NodeReference, refnode) + + node = refnode.resolve_reference(self) + + if refnode.parent: + cast(Node, refnode.parent).remove_child(refnode) + + node.properties += refnode.properties + node.directives += refnode.directives + node.children += refnode.children + + self.merge_tree() + + def __repr__(self) -> str: + name = self.root().get_field("compatible") + return "" % name + + def to_dts(self, level: int = 0) -> str: + """Convert the tree back to Devicetree Source""" + out = "" + + for d in self.directives: + out += d.to_dts() + for p in self.properties: + out += p.to_dts() + for c in self.children: + out += c.to_dts() + + return out + + def get_by_path(self, path: Union[Path, str]) -> Optional[Node]: + """Get a node in the tree by path (ex. /cpus/cpu@0)""" + + # Find and replace all aliases in the path + aliases = self.aliases() + if aliases: + for prop in aliases.properties: + if prop.name in path and len(prop.values) > 0: + path = path.replace(prop.name, prop.values[0]) + + return self.root().get_by_path(path) + + @staticmethod + # pylint: disable=arguments-differ + def from_dts(dts: str) -> 'Devicetree': + """Parse a string and return a Devicetree object""" + # pylint: disable=import-outside-toplevel,cyclic-import + from pydevicetree.source import parseTree + return parseTree(dts) + + @staticmethod + def parseFile(filename: str, followIncludes: bool = False) -> 'Devicetree': + """Parse a file and return a Devicetree object""" + # pylint: disable=import-outside-toplevel,cyclic-import + from cot_dt2c.pydevicetree.source.parser import parseTree + with open(filename, 'r') as f: + contents = f.read() + dirname = os.path.dirname(filename) + if dirname != "": + dirname += "/" + return parseTree(contents, dirname, followIncludes) + + @staticmethod + def parseStr(input: str, followIncludes: bool = False) -> 'Devicetree': + from cot_dt2c.pydevicetree.source.parser import parseTree + return parseTree(input, "", followIncludes) + + def all_nodes(self) -> Iterable[Node]: + """Get an iterable over all nodes in the tree""" + return self.child_nodes() + + def root(self) -> Node: + """Get the root node of the tree""" + for n in self.all_nodes(): + if n.name == "/": + return n + raise Exception("Devicetree has no root node!") + + def aliases(self) -> Optional[Node]: + """Get the aliases node of the tree if it exists""" + for n in self.all_nodes(): + if n.name == "aliases": + return n + return None + + def chosen(self, property_name: str, func: ChosenCallback = None) -> Optional[PropertyValues]: + """Get the values associated with one of the properties in the chosen node""" + def match_chosen(node: Node) -> bool: + return node.name == "chosen" + + for n in filter(match_chosen, self.all_nodes()): + for p in n.properties: + if p.name == property_name: + if func is not None: + func(p.values) + return p.values + + return None diff --git a/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/property.py b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/property.py new file mode 100644 index 000000000..d5fb687a5 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/property.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 SiFive Inc. +# SPDX-License-Identifier: Apache-2.0 + +from typing import List, Any, cast, Tuple, Optional, Iterable +from itertools import zip_longest + +from cot_dt2c.pydevicetree.ast.helpers import wrapStrings, formatLevel + +class PropertyValues: + """PropertyValues is the parent class of all values which can be assigned to a Property + + Child classes include + + Bytestring + CellArray + StringList + """ + def __init__(self, values: List[Any]): + """Create a PropertyValue""" + self.values = values + + def __repr__(self) -> str: + return "" + + def __str__(self) -> str: + return self.to_dts() + + def __iter__(self): + return iter(self.values) + + def __len__(self) -> int: + return len(self.values) + + def to_dts(self, formatHex: bool = False) -> str: + """Format the values in Devicetree Source format""" + return ", ".join(wrapStrings(self.values, formatHex)) + + def __getitem__(self, key) -> Any: + return self.values[key] + + def __eq__(self, other) -> bool: + if isinstance(other, PropertyValues): + return self.values == other.values + return self.values == other + +class Bytestring(PropertyValues): + """A Bytestring is a sequence of bytes + + In Devicetree, Bytestrings are represented as a sequence of two-digit hexadecimal integers, + optionally space-separated, enclosed by square brackets: + + [de ad be eef] + """ + def __init__(self, bytelist: List[int]): + """Create a Bytestring object""" + PropertyValues.__init__(self, cast(List[Any], bytearray(bytelist))) + + def __repr__(self) -> str: + return "" + + def to_dts(self, formatHex: bool = False) -> str: + """Format the bytestring in Devicetree Source format""" + return "[" + " ".join("%02x" % v for v in self.values) + "]" + +class CellArray(PropertyValues): + """A CellArray is an array of integer values + + CellArrays are commonly used as the value of Devicetree properties like `reg` and `interrupts`. + The interpretation of each element of a CellArray is device-dependent. For example, the `reg` + property encodes a CellArray as a list of tuples (base address, size), while the `interrupts` + property encodes a CellArray as simply a list of interrupt line numbers. + """ + def __init__(self, cells: List[Any]): + """Create a CellArray object""" + PropertyValues.__init__(self, cells) + + def __repr__(self) -> str: + return "" + + def to_dts(self, formatHex: bool = False) -> str: + """Format the cell array in Devicetree Source format""" + dtsValues = [] + for i in self.values: + if not isinstance(i, OneString) and not isinstance(i, str): + dtsValues.append(i) + return "<" + " ".join(wrapStrings(dtsValues, formatHex)) + ">" + +class RegArray(CellArray): + """A RegArray is the CellArray assigned to the reg property""" + def __init__(self, cells: List[int], + address_cells: int, size_cells: int, + names: Optional[List[str]] = None): + """Create a RegArray from a list of ints""" + # pylint: disable=too-many-locals + CellArray.__init__(self, cells) + self.address_cells = address_cells + self.size_cells = size_cells + + self.tuples = [] # type: List[Tuple[int, int, Optional[str]]] + + group_size = self.address_cells + self.size_cells + + if len(cells) % group_size != 0: + raise Exception("CellArray does not contain enough cells") + + grouped_cells = [cells[i:i+group_size] for i in range(0, len(cells), group_size)] + + if not names: + names = [] + + for group, name in zip_longest(grouped_cells, cast(Iterable[Any], names)): + address = 0 + a_cells = list(reversed(group[:self.address_cells])) + for a, i in zip(a_cells, range(len(a_cells))): + address += (1 << (32 * i)) * a + + size = 0 + s_cells = list(reversed(group[self.address_cells:])) + for s, i in zip(s_cells, range(len(s_cells))): + size += (1 << (32 * i)) * s + + self.tuples.append(cast(Tuple[int, int, Optional[str]], tuple([address, size, name]))) + + def get_by_name(self, name: str) -> Optional[Tuple[int, int]]: + """Returns the (address, size) tuple with a given name""" + for t in self.tuples: + if t[2] == name: + return cast(Tuple[int, int], tuple(t[:2])) + return None + + def __repr__(self) -> str: + return "" + + def __iter__(self) -> Iterable[Tuple[int, int]]: + return cast(Iterable[Tuple[int, int]], map(lambda t: tuple(t[:2]), self.tuples)) + + def __len__(self) -> int: + return len(self.tuples) + + def __getitem__(self, key) -> Optional[Tuple[int, int]]: + return list(self.__iter__())[key] + +class RangeArray(CellArray): + """A RangeArray is the CellArray assigned to the range property""" + def __init__(self, cells: List[int], child_address_cells: int, + parent_address_cells: int, size_cells: int): + """Create a RangeArray from a list of ints""" + # pylint: disable=too-many-locals + CellArray.__init__(self, cells) + self.child_address_cells = child_address_cells + self.parent_address_cells = parent_address_cells + self.size_cells = size_cells + + self.tuples = [] # type: List[Tuple[int, int, int]] + + group_size = self.child_address_cells + self.parent_address_cells + self.size_cells + + if len(cells) % group_size != 0: + raise Exception("CellArray does not contain enough cells") + + grouped_cells = [cells[i:i+group_size] for i in range(0, len(cells), group_size)] + + def sum_cells(cells: List[int]): + value = 0 + for cell, index in zip(list(reversed(cells)), range(len(cells))): + value += (1 << (32 * index)) * cell + return value + + for group in grouped_cells: + child_address = sum_cells(group[:self.child_address_cells]) + parent_address = sum_cells(group[self.child_address_cells: \ + self.child_address_cells + self.parent_address_cells]) + size = sum_cells(group[self.child_address_cells + self.parent_address_cells:]) + + self.tuples.append(cast(Tuple[int, int, int], + tuple([child_address, parent_address, size]))) + + def __repr__(self) -> str: + return "" + + def __iter__(self): + return iter(self.tuples) + + def __len__(self) -> int: + return len(self.tuples) + + def __getitem__(self, key) -> Any: + return self.tuples[key] + +class StringList(PropertyValues): + """A StringList is a list of null-terminated strings + + The most common use of a StringList in Devicetree is to describe the `compatible` property. + """ + def __init__(self, strings: List[str]): + """Create a StringList object""" + PropertyValues.__init__(self, strings) + + def __repr__(self) -> str: + return "" + + def to_dts(self, formatHex: bool = False) -> str: + """Format the list of strings in Devicetree Source format""" + return ", ".join(wrapStrings(self.values)) + +class OneString(PropertyValues): + def __init__(self, string: str): + PropertyValues.__init__(self, string) + + def __repr__(self) -> str: + return self.values.__repr__() + + def to_dts(self, formatHex: bool = False) -> str: + return super().to_dts(formatHex) + +class Property: + """A Property is a key-value pair for a Devicetree Node + + Properties are used to describe Nodes in the tree. There are many common properties, like + + - compatible + - reg + - reg-names + - ranges + - interrupt-controller + - interrupts + - interrupt-parent + - clocks + - status + + Which might commonly describe many or all nodes in a tree, and there are device, vendor, + operating system, runtime-specific properties. + + Properties can possess no value, conveing meaning solely by their presence: + + interrupt-controller; + + Properties can also possess values such as an array of cells, a list of strings, etc. + + reg = <0x10013000 0x1000>; + compatible = "sifive,rocket0", "riscv"; + + And properties can posses arbitrarily complex values, such as the following from the + Devicetree specification: + + example = <0xf00f0000 19>, "a strange property format"; + """ + def __init__(self, name: str, values: PropertyValues): + """Create a Property object""" + self.name = name + self.values = values + + def __repr__(self) -> str: + return "" % self.name + + def __str__(self) -> str: + return self.to_dts() + + @staticmethod + def from_dts(dts: str) -> 'Property': + """Parse a file and return a Devicetree object""" + # pylint: disable=import-outside-toplevel,cyclic-import + from pydevicetree.source import parseProperty + return parseProperty(dts) + + def to_dts(self, level: int = 0) -> str: + """Format the Property assignment in Devicetree Source format""" + if self.name in ["reg", "ranges"]: + value = self.values.to_dts(formatHex=True) + else: + value = self.values.to_dts(formatHex=False) + + if value != "": + return formatLevel(level, "%s = %s;\n" % (self.name, value)) + if self.name == "ifdef": + return "" + return formatLevel(level, "%s;\n" % self.name) diff --git a/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/reference.py b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/reference.py new file mode 100644 index 000000000..54b2d28c8 --- /dev/null +++ b/tools/cot_dt2c/cot_dt2c/pydevicetree/ast/reference.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 SiFive Inc. +# SPDX-License-Identifier: Apache-2.0 + +from typing import Union, Iterator + +class Label: + """A Label is a unique identifier for a Node + + For example, the following node has the label "uart0": + + uart0: uart@10013000 { + ... + }; + """ + def __init__(self, name: str): + """Create a Label""" + self.name = name + + def __repr__(self) -> str: + return "