1# Copyright (c) 2023 Yonatan Schachter
2#
3# SPDX-License-Identifier: Apache-2.0
4
5from textwrap import dedent
6import struct
7
8from west.commands import WestCommand
9from west import log
10
11
12try:
13    from elftools.elf.elffile import ELFFile
14    from intelhex import IntelHex
15    MISSING_REQUIREMENTS = False
16except ImportError:
17    MISSING_REQUIREMENTS = True
18
19
20# Based on scripts/build/uf2conv.py
21def convert_from_uf2(buf):
22    UF2_MAGIC_START0 = 0x0A324655 # First magic number ('UF2\n')
23    UF2_MAGIC_START1 = 0x9E5D5157 # Second magic number
24    numblocks = len(buf) // 512
25    curraddr = None
26    outp = []
27    for blockno in range(numblocks):
28        ptr = blockno * 512
29        block = buf[ptr:ptr + 512]
30        hd = struct.unpack(b'<IIIIIIII', block[0:32])
31        if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
32            log.inf('Skipping block at ' + ptr + '; bad magic')
33            continue
34        if hd[2] & 1:
35            # NO-flash flag set; skip block
36            continue
37        datalen = hd[4]
38        if datalen > 476:
39            log.die(f'Invalid UF2 data size at {ptr}')
40        newaddr = hd[3]
41        if curraddr is None:
42            curraddr = newaddr
43        padding = newaddr - curraddr
44        if padding < 0:
45            log.die(f'Block out of order at {ptr}')
46        if padding > 10*1024*1024:
47            log.die(f'More than 10M of padding needed at {ptr}')
48        if padding % 4 != 0:
49            log.die(f'Non-word padding size at {ptr}')
50        while padding > 0:
51            padding -= 4
52            outp += b'\x00\x00\x00\x00'
53        outp.append(block[32 : 32 + datalen])
54        curraddr = newaddr + datalen
55    return b''.join(outp)
56
57
58class Bindesc(WestCommand):
59    EXTENSIONS = ['bin', 'hex', 'elf', 'uf2']
60
61    # Corresponds to the definitions in include/zephyr/bindesc.h.
62    # Do not change without syncing the definitions in both files!
63    TYPE_UINT = 0
64    TYPE_STR = 1
65    TYPE_BYTES = 2
66    MAGIC = 0xb9863e5a7ea46046
67    DESCRIPTORS_END = 0xffff
68
69    def __init__(self):
70        self.TAG_TO_NAME = {
71            # Corresponds to the definitions in include/zephyr/bindesc.h.
72            # Do not change without syncing the definitions in both files!
73            self.bindesc_gen_tag(self.TYPE_STR, 0x800): 'APP_VERSION_STRING',
74            self.bindesc_gen_tag(self.TYPE_UINT, 0x801): 'APP_VERSION_MAJOR',
75            self.bindesc_gen_tag(self.TYPE_UINT, 0x802): 'APP_VERSION_MINOR',
76            self.bindesc_gen_tag(self.TYPE_UINT, 0x803): 'APP_VERSION_PATCHLEVEL',
77            self.bindesc_gen_tag(self.TYPE_UINT, 0x804): 'APP_VERSION_NUMBER',
78            self.bindesc_gen_tag(self.TYPE_STR, 0x900): 'KERNEL_VERSION_STRING',
79            self.bindesc_gen_tag(self.TYPE_UINT, 0x901): 'KERNEL_VERSION_MAJOR',
80            self.bindesc_gen_tag(self.TYPE_UINT, 0x902): 'KERNEL_VERSION_MINOR',
81            self.bindesc_gen_tag(self.TYPE_UINT, 0x903): 'KERNEL_VERSION_PATCHLEVEL',
82            self.bindesc_gen_tag(self.TYPE_UINT, 0x904): 'KERNEL_VERSION_NUMBER',
83            self.bindesc_gen_tag(self.TYPE_UINT, 0xa00): 'BUILD_TIME_YEAR',
84            self.bindesc_gen_tag(self.TYPE_UINT, 0xa01): 'BUILD_TIME_MONTH',
85            self.bindesc_gen_tag(self.TYPE_UINT, 0xa02): 'BUILD_TIME_DAY',
86            self.bindesc_gen_tag(self.TYPE_UINT, 0xa03): 'BUILD_TIME_HOUR',
87            self.bindesc_gen_tag(self.TYPE_UINT, 0xa04): 'BUILD_TIME_MINUTE',
88            self.bindesc_gen_tag(self.TYPE_UINT, 0xa05): 'BUILD_TIME_SECOND',
89            self.bindesc_gen_tag(self.TYPE_UINT, 0xa06): 'BUILD_TIME_UNIX',
90            self.bindesc_gen_tag(self.TYPE_STR, 0xa07): 'BUILD_DATE_TIME_STRING',
91            self.bindesc_gen_tag(self.TYPE_STR, 0xa08): 'BUILD_DATE_STRING',
92            self.bindesc_gen_tag(self.TYPE_STR, 0xa09): 'BUILD_TIME_STRING',
93            self.bindesc_gen_tag(self.TYPE_STR, 0xb00): 'HOST_NAME',
94            self.bindesc_gen_tag(self.TYPE_STR, 0xb01): 'C_COMPILER_NAME',
95            self.bindesc_gen_tag(self.TYPE_STR, 0xb02): 'C_COMPILER_VERSION',
96            self.bindesc_gen_tag(self.TYPE_STR, 0xb03): 'CXX_COMPILER_NAME',
97            self.bindesc_gen_tag(self.TYPE_STR, 0xb04): 'CXX_COMPILER_VERSION',
98        }
99        self.NAME_TO_TAG = {v: k for k, v in self.TAG_TO_NAME.items()}
100
101        super().__init__(
102            'bindesc',
103            'work with Binary Descriptors',
104            dedent('''
105            Work with Binary Descriptors - constant data objects
106            describing a binary image
107            '''))
108
109    def do_add_parser(self, parser_adder):
110        parser = parser_adder.add_parser(self.name,
111                                         help=self.help,
112                                         description=self.description)
113
114        subparsers = parser.add_subparsers(help='sub-command to run', required=True)
115
116        dump_parser = subparsers.add_parser('dump', help='Dump all binary descriptors in the image')
117        dump_parser.add_argument('file', type=str, help='Executable file')
118        dump_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, help='File type')
119        dump_parser.add_argument('-b', '--big-endian', action='store_true',
120                                 help='Target CPU is big endian')
121        dump_parser.set_defaults(subcmd='dump', big_endian=False)
122
123        search_parser = subparsers.add_parser('search', help='Search for a specific descriptor')
124        search_parser.add_argument('descriptor', type=str, help='Descriptor name')
125        search_parser.add_argument('file', type=str, help='Executable file')
126        search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, help='File type')
127        search_parser.add_argument('-b', '--big-endian', action='store_true',
128                                   help='Target CPU is big endian')
129        search_parser.set_defaults(subcmd='search', big_endian=False)
130
131        custom_search_parser = subparsers.add_parser('custom_search',
132                                                     help='Search for a custom descriptor')
133        custom_search_parser.add_argument('type', type=str, choices=['UINT', 'STR', 'BYTES'],
134                                          help='Descriptor type')
135        custom_search_parser.add_argument('id', type=str, help='Descriptor ID in hex')
136        custom_search_parser.add_argument('file', type=str, help='Executable file')
137        custom_search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS,
138                                          help='File type')
139        custom_search_parser.add_argument('-b', '--big-endian', action='store_true',
140                                   help='Target CPU is big endian')
141        custom_search_parser.set_defaults(subcmd='custom_search', big_endian=False)
142
143        list_parser = subparsers.add_parser('list', help='List all known descriptors')
144        list_parser.set_defaults(subcmd='list', big_endian=False)
145
146        return parser
147
148    def dump(self, args):
149        image = self.get_image_data(args.file)
150
151        descriptors = self.parse_descriptors(image)
152        for tag, value in descriptors.items():
153            if tag in self.TAG_TO_NAME:
154                tag = self.TAG_TO_NAME[tag]
155            log.inf(f'{tag}', self.bindesc_repr(value))
156
157    def list(self, args):
158        for tag in self.TAG_TO_NAME.values():
159            log.inf(f'{tag}')
160
161    def common_search(self, args, search_term):
162        image = self.get_image_data(args.file)
163
164        descriptors = self.parse_descriptors(image)
165
166        if search_term in descriptors:
167            value = descriptors[search_term]
168            log.inf(self.bindesc_repr(value))
169        else:
170            log.die('Descriptor not found')
171
172    def search(self, args):
173        try:
174            search_term = self.NAME_TO_TAG[args.descriptor]
175        except KeyError:
176            log.die(f'Descriptor {args.descriptor} is invalid')
177
178        self.common_search(args, search_term)
179
180    def custom_search(self, args):
181        custom_type = {
182            'STR': self.TYPE_STR,
183            'UINT': self.TYPE_UINT,
184            'BYTES': self.TYPE_BYTES
185        }[args.type]
186        custom_tag = self.bindesc_gen_tag(custom_type, int(args.id, 16))
187        self.common_search(args, custom_tag)
188
189    def do_run(self, args, _):
190        if MISSING_REQUIREMENTS:
191            raise RuntimeError('one or more Python dependencies were missing; '
192                               'see the getting started guide for details on '
193                               'how to fix')
194        self.is_big_endian = args.big_endian
195        self.file_type = self.guess_file_type(args)
196        subcmd = getattr(self, args.subcmd)
197        subcmd(args)
198
199    def get_image_data(self, file_name):
200        if self.file_type == 'bin':
201            with open(file_name, 'rb') as bin_file:
202                return bin_file.read()
203
204        if self.file_type == 'hex':
205            return IntelHex(file_name).tobinstr()
206
207        if self.file_type == 'uf2':
208            with open(file_name, 'rb') as uf2_file:
209                return convert_from_uf2(uf2_file.read())
210
211        if self.file_type == 'elf':
212            with open(file_name, 'rb') as f:
213                elffile = ELFFile(f)
214
215                section = elffile.get_section_by_name('rom_start')
216                if section:
217                    return section.data()
218
219                section = elffile.get_section_by_name('text')
220                if section:
221                    return section.data()
222
223            log.die('No "rom_start" or "text" section found')
224
225        log.die('Unknown file type')
226
227    def parse_descriptors(self, image):
228        magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC)
229        index = image.find(magic)
230        if index == -1:
231            log.die('Could not find binary descriptor magic')
232
233        descriptors = {}
234
235        index += len(magic) # index points to first descriptor
236        current_tag = self.bytes_to_short(image[index:index+2])
237        while current_tag != self.DESCRIPTORS_END:
238            index += 2 # index points to length
239            length = self.bytes_to_short(image[index:index+2])
240            index += 2 # index points to data
241            data = image[index:index+length]
242
243            tag_type = self.bindesc_get_type(current_tag)
244            if tag_type == self.TYPE_STR:
245                decoded_data = data[:-1].decode('ascii')
246            elif tag_type == self.TYPE_UINT:
247                decoded_data = self.bytes_to_uint(data)
248            elif tag_type == self.TYPE_BYTES:
249                decoded_data = data
250            else:
251                log.die(f'Unknown type for tag 0x{current_tag:04x}')
252
253            key = f'0x{current_tag:04x}'
254            descriptors[key] = decoded_data
255            index += length
256            index = self.align(index, 4)
257            current_tag = self.bytes_to_short(image[index:index+2])
258
259        return descriptors
260
261    def guess_file_type(self, args):
262        if "file" not in args:
263            return None
264
265        # If file type is explicitly given, use it
266        if args.file_type is not None:
267            return args.file_type
268
269        # If the file has a known extension, use it
270        for extension in self.EXTENSIONS:
271            if args.file.endswith(f'.{extension}'):
272                return extension
273
274        with open(args.file, 'rb') as f:
275            header = f.read(1024)
276
277        # Try the elf magic
278        if header.startswith(b'\x7fELF'):
279            return 'elf'
280
281        # Try the uf2 magic
282        if header.startswith(b'UF2\n'):
283            return 'uf2'
284
285        try:
286            # if the file is textual it's probably hex
287            header.decode('ascii')
288            return 'hex'
289        except UnicodeDecodeError:
290            # Default to bin
291            return 'bin'
292
293    def bytes_to_uint(self, b):
294        return struct.unpack('>I' if self.is_big_endian else 'I', b)[0]
295
296    def bytes_to_short(self, b):
297        return struct.unpack('>H' if self.is_big_endian else 'H', b)[0]
298
299    @staticmethod
300    def bindesc_gen_tag(_type, _id):
301        return f'0x{(_type << 12 | _id):04x}'
302
303    @staticmethod
304    def bindesc_get_type(tag):
305        return tag >> 12
306
307    @staticmethod
308    def align(x, alignment):
309        return (x + alignment - 1) & (~(alignment - 1))
310
311    @staticmethod
312    def bindesc_repr(value):
313        if isinstance(value, str):
314            return f'"{value}"'
315        if isinstance(value, (int, bytes)):
316            return f'{value}'
317