1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2022-2023 Texas Instruments Incorporated - https://www.ti.com/
3# Written by Neha Malcom Francis <n-francis@ti.com>
4#
5# Entry-type module for generating schema validated TI board
6# configuration binary
7#
8
9import os
10import struct
11import yaml
12import yamllint
13
14from collections import OrderedDict
15from jsonschema import validate
16from shutil import copyfileobj
17
18from binman.entry import Entry
19from binman.etype.section import Entry_section
20from dtoc import fdt_util
21from u_boot_pylib import tools
22from yamllint import config
23
24BOARDCFG = 0xB
25BOARDCFG_SEC = 0xD
26BOARDCFG_PM = 0xE
27BOARDCFG_RM = 0xC
28BOARDCFG_NUM_ELEMS = 4
29
30class Entry_ti_board_config(Entry_section):
31    """An entry containing a TI schema validated board config binary
32
33    This etype supports generation of two kinds of board configuration
34    binaries: singular board config binary as well as combined board config
35    binary.
36
37    Properties / Entry arguments:
38        - config-file: File containing board configuration data in YAML
39        - schema-file: File containing board configuration YAML schema against
40          which the config file is validated
41
42    Output files:
43        - board config binary: File containing board configuration binary
44
45    These above parameters are used only when the generated binary is
46    intended to be a single board configuration binary. Example::
47
48        my-ti-board-config {
49            ti-board-config {
50                config = "board-config.yaml";
51                schema = "schema.yaml";
52            };
53        };
54
55    To generate a combined board configuration binary, we pack the
56    needed individual binaries into a ti-board-config binary. In this case,
57    the available supported subnode names are board-cfg, pm-cfg, sec-cfg and
58    rm-cfg. The final binary is prepended with a header containing details about
59    the included board config binaries. Example::
60
61        my-combined-ti-board-config {
62            ti-board-config {
63                board-cfg {
64                    config = "board-cfg.yaml";
65                    schema = "schema.yaml";
66                };
67                sec-cfg {
68                    config = "sec-cfg.yaml";
69                    schema = "schema.yaml";
70                };
71            }
72        }
73    """
74    def __init__(self, section, etype, node):
75        super().__init__(section, etype, node)
76        self._config = None
77        self._schema = None
78        self._entries = OrderedDict()
79        self._num_elems = BOARDCFG_NUM_ELEMS
80        self._fmt = '<HHHBB'
81        self._index = 0
82        self._binary_offset = 0
83        self._sw_rev = 1
84        self._devgrp = 0
85
86    def ReadNode(self):
87        super().ReadNode()
88        self._config = fdt_util.GetString(self._node, 'config')
89        self._schema = fdt_util.GetString(self._node, 'schema')
90        # Depending on whether config file is present in node, we determine
91        # whether it is a combined board config binary or not
92        if self._config is None:
93            self.ReadEntries()
94        else:
95            self._config_file = tools.get_input_filename(self._config)
96            self._schema_file = tools.get_input_filename(self._schema)
97
98    def ReadEntries(self):
99        """Read the subnodes to find out what should go in this image
100        """
101        for node in self._node.subnodes:
102            if 'type' not in node.props:
103                entry = Entry.Create(self, node, 'ti-board-config')
104                entry.ReadNode()
105                cfg_data = entry.BuildSectionData(True)
106                entry._cfg_data = cfg_data
107                self._entries[entry.name] = entry
108        self._num_elems = len(self._node.subnodes)
109
110    def _convert_to_byte_chunk(self, val, data_type):
111        """Convert value into byte array
112
113        Args:
114            val: value to convert into byte array
115            data_type: data type used in schema, supported data types are u8,
116                u16 and u32
117
118        Returns:
119            array of bytes representing value
120        """
121        size = 0
122        br = bytearray()
123        if (data_type == '#/definitions/u8'):
124            size = 1
125        elif (data_type == '#/definitions/u16'):
126            size = 2
127        else:
128            size = 4
129        br = None
130        if type(val) == int:
131            br = val.to_bytes(size, byteorder='little')
132        return br
133
134    def _compile_yaml(self, schema_yaml, file_yaml):
135        """Convert YAML file into byte array based on YAML schema
136
137        Args:
138            schema_yaml: file containing YAML schema
139            file_yaml: file containing config to compile
140
141        Returns:
142            array of bytes repesenting YAML file against YAML schema
143        """
144        br = bytearray()
145        for key, node in file_yaml.items():
146            node_schema = schema_yaml['properties'][key]
147            node_type = node_schema.get('type')
148            if not 'type' in node_schema:
149                br += self._convert_to_byte_chunk(node,
150                                                node_schema.get('$ref'))
151            elif node_type == 'object':
152                br += self._compile_yaml(node_schema, node)
153            elif node_type == 'array':
154                for item in node:
155                    if not isinstance(item, dict):
156                        br += self._convert_to_byte_chunk(
157                            item, schema_yaml['properties'][key]['items']['$ref'])
158                    else:
159                        br += self._compile_yaml(node_schema.get('items'), item)
160        return br
161
162    def _generate_binaries(self):
163        """Generate config binary artifacts from the loaded YAML configuration file
164
165        Returns:
166            byte array containing config binary artifacts
167            or None if generation fails
168        """
169        cfg_binary = bytearray()
170        for key, node in self.file_yaml.items():
171            node_schema = self.schema_yaml['properties'][key]
172            br = self._compile_yaml(node_schema, node)
173            cfg_binary += br
174        return cfg_binary
175
176    def _add_boardcfg(self, bcfgtype, bcfgdata):
177        """Add board config to combined board config binary
178
179        Args:
180            bcfgtype (int): board config type
181            bcfgdata (byte array): board config data
182        """
183        size = len(bcfgdata)
184        desc = struct.pack(self._fmt, bcfgtype,
185                            self._binary_offset, size, self._devgrp, 0)
186        with open(self.descfile, 'ab+') as desc_fh:
187            desc_fh.write(desc)
188        with open(self.bcfgfile, 'ab+') as bcfg_fh:
189            bcfg_fh.write(bcfgdata)
190        self._binary_offset += size
191        self._index += 1
192
193    def _finalize(self):
194        """Generate final combined board config binary
195
196        Returns:
197            byte array containing combined board config data
198            or None if unable to generate
199        """
200        with open(self.descfile, 'rb') as desc_fh:
201            with open(self.bcfgfile, 'rb') as bcfg_fh:
202                with open(self.fh_file, 'ab+') as fh:
203                    copyfileobj(desc_fh, fh)
204                    copyfileobj(bcfg_fh, fh)
205        data = tools.read_file(self.fh_file)
206        return data
207
208    def BuildSectionData(self, required):
209        if self._config is None:
210            self._binary_offset = 0
211            uniq = self.GetUniqueName()
212            self.fh_file = tools.get_output_filename('fh.%s' % uniq)
213            self.descfile = tools.get_output_filename('desc.%s' % uniq)
214            self.bcfgfile = tools.get_output_filename('bcfg.%s' % uniq)
215
216            # when binman runs again make sure we start clean
217            if os.path.exists(self.fh_file):
218                os.remove(self.fh_file)
219            if os.path.exists(self.descfile):
220                os.remove(self.descfile)
221            if os.path.exists(self.bcfgfile):
222                os.remove(self.bcfgfile)
223
224            with open(self.fh_file, 'wb') as f:
225                t_bytes = f.write(struct.pack(
226                    '<BB', self._num_elems, self._sw_rev))
227            self._binary_offset += t_bytes
228            self._binary_offset += self._num_elems * struct.calcsize(self._fmt)
229
230            if 'board-cfg' in self._entries:
231                self._add_boardcfg(BOARDCFG, self._entries['board-cfg']._cfg_data)
232
233            if 'sec-cfg' in self._entries:
234                self._add_boardcfg(BOARDCFG_SEC, self._entries['sec-cfg']._cfg_data)
235
236            if 'pm-cfg' in self._entries:
237                self._add_boardcfg(BOARDCFG_PM, self._entries['pm-cfg']._cfg_data)
238
239            if 'rm-cfg' in self._entries:
240                self._add_boardcfg(BOARDCFG_RM, self._entries['rm-cfg']._cfg_data)
241
242            data = self._finalize()
243            return data
244
245        else:
246            with open(self._config_file, 'r') as f:
247                self.file_yaml = yaml.safe_load(f)
248            with open(self._schema_file, 'r') as sch:
249                self.schema_yaml = yaml.safe_load(sch)
250
251            yaml_config = config.YamlLintConfig("extends: default")
252            for p in yamllint.linter.run(open(self._config_file, "r"), yaml_config):
253                self.Raise(f"Yamllint error: Line {p.line} in {self._config_file}: {p.rule}")
254            try:
255                validate(self.file_yaml, self.schema_yaml)
256            except Exception as e:
257                self.Raise(f"Schema validation error: {e}")
258
259            data = self._generate_binaries()
260            return data
261
262    def SetImagePos(self, image_pos):
263        Entry.SetImagePos(self, image_pos)
264
265    def CheckEntries(self):
266        Entry.CheckEntries(self)
267