1# Copyright (c) 2023 Yonatan Schachter
2#
3# SPDX-License-Identifier: Apache-2.0
4
5import struct
6from textwrap import dedent
7
8from west.commands import WestCommand
9
10try:
11    from elftools.elf.elffile import ELFFile
12    from intelhex import IntelHex
13    MISSING_REQUIREMENTS = False
14except ImportError:
15    MISSING_REQUIREMENTS = True
16
17
18# Based on scripts/build/uf2conv.py
19def convert_from_uf2(cmd, buf):
20    UF2_MAGIC_START0 = 0x0A324655 # First magic number ('UF2\n')
21    UF2_MAGIC_START1 = 0x9E5D5157 # Second magic number
22    numblocks = len(buf) // 512
23    curraddr = None
24    outp = []
25    for blockno in range(numblocks):
26        ptr = blockno * 512
27        block = buf[ptr:ptr + 512]
28        hd = struct.unpack(b'<IIIIIIII', block[0:32])
29        if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
30            cmd.inf(f'Skipping block at {ptr}; bad magic')
31            continue
32        if hd[2] & 1:
33            # NO-flash flag set; skip block
34            continue
35        datalen = hd[4]
36        if datalen > 476:
37            cmd.die(f'Invalid UF2 data size at {ptr}')
38        newaddr = hd[3]
39        if curraddr is None:
40            curraddr = newaddr
41        padding = newaddr - curraddr
42        if padding < 0:
43            cmd.die(f'Block out of order at {ptr}')
44        if padding > 10*1024*1024:
45            cmd.die(f'More than 10M of padding needed at {ptr}')
46        if padding % 4 != 0:
47            cmd.die(f'Non-word padding size at {ptr}')
48        while padding > 0:
49            padding -= 4
50            outp += b'\x00\x00\x00\x00'
51        outp.append(block[32 : 32 + datalen])
52        curraddr = newaddr + datalen
53    return b''.join(outp)
54
55
56class Bindesc(WestCommand):
57    EXTENSIONS = ['bin', 'hex', 'elf', 'uf2']
58
59    # Corresponds to the definitions in include/zephyr/bindesc.h.
60    # Do not change without syncing the definitions in both files!
61    TYPE_UINT = 0
62    TYPE_STR = 1
63    TYPE_BYTES = 2
64    MAGIC = 0xb9863e5a7ea46046
65    DESCRIPTORS_END = 0xffff
66
67    def __init__(self):
68        self.TAG_TO_NAME = {
69            # Corresponds to the definitions in include/zephyr/bindesc.h.
70            # Do not change without syncing the definitions in both files!
71            self.bindesc_gen_tag(self.TYPE_STR, 0x800): 'APP_VERSION_STRING',
72            self.bindesc_gen_tag(self.TYPE_UINT, 0x801): 'APP_VERSION_MAJOR',
73            self.bindesc_gen_tag(self.TYPE_UINT, 0x802): 'APP_VERSION_MINOR',
74            self.bindesc_gen_tag(self.TYPE_UINT, 0x803): 'APP_VERSION_PATCHLEVEL',
75            self.bindesc_gen_tag(self.TYPE_UINT, 0x804): 'APP_VERSION_NUMBER',
76            self.bindesc_gen_tag(self.TYPE_STR, 0x805): 'APP_BUILD_VERSION',
77            self.bindesc_gen_tag(self.TYPE_STR, 0x900): 'KERNEL_VERSION_STRING',
78            self.bindesc_gen_tag(self.TYPE_UINT, 0x901): 'KERNEL_VERSION_MAJOR',
79            self.bindesc_gen_tag(self.TYPE_UINT, 0x902): 'KERNEL_VERSION_MINOR',
80            self.bindesc_gen_tag(self.TYPE_UINT, 0x903): 'KERNEL_VERSION_PATCHLEVEL',
81            self.bindesc_gen_tag(self.TYPE_UINT, 0x904): 'KERNEL_VERSION_NUMBER',
82            self.bindesc_gen_tag(self.TYPE_STR, 0x905): 'KERNEL_BUILD_VERSION',
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        extract_parser = subparsers.add_parser('extract',
124                                                help='Extract the binary descriptor blob to a file')
125        extract_parser.add_argument('file', type=str, help='Executable file')
126        extract_parser.add_argument('out_file', type=str, help='Bindesc binary dump file')
127        extract_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS,
128                                     help='Input file type')
129        extract_parser.add_argument('-b', '--big-endian', action='store_true',
130                                     help='Target CPU is big endian')
131        extract_parser.set_defaults(subcmd='extract', big_endian=False)
132
133        search_parser = subparsers.add_parser('search', help='Search for a specific descriptor')
134        search_parser.add_argument('descriptor', type=str, help='Descriptor name')
135        search_parser.add_argument('file', type=str, help='Executable file')
136        search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS,
137                                   help='File type')
138        search_parser.add_argument('-b', '--big-endian', action='store_true',
139                                   help='Target CPU is big endian')
140        search_parser.set_defaults(subcmd='search', big_endian=False)
141
142        custom_search_parser = subparsers.add_parser('custom_search',
143                                                     help='Search for a custom descriptor')
144        custom_search_parser.add_argument('type', type=str, choices=['UINT', 'STR', 'BYTES'],
145                                          help='Descriptor type')
146        custom_search_parser.add_argument('id', type=str, help='Descriptor ID in hex')
147        custom_search_parser.add_argument('file', type=str, help='Executable file')
148        custom_search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS,
149                                          help='File type')
150        custom_search_parser.add_argument('-b', '--big-endian', action='store_true',
151                                   help='Target CPU is big endian')
152        custom_search_parser.set_defaults(subcmd='custom_search', big_endian=False)
153
154        list_parser = subparsers.add_parser('list', help='List all known descriptors')
155        list_parser.set_defaults(subcmd='list', big_endian=False)
156
157        get_offset_parser = subparsers.add_parser('get_offset',
158                                                  help='Get the offset of the descriptors')
159        get_offset_parser.add_argument('file', type=str, help='Executable file')
160        get_offset_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS,
161                                          help='File type')
162        get_offset_parser.add_argument('-b', '--big-endian', action='store_true',
163                                       help='Target CPU is big endian')
164        get_offset_parser.set_defaults(subcmd='get_offset', big_endian=False)
165        return parser
166
167    def dump(self, args):
168        image = self.get_image_data(args.file)
169
170        descriptors = self.parse_descriptors(image)
171        for tag, value in descriptors.items():
172            if tag in self.TAG_TO_NAME:
173                tag = self.TAG_TO_NAME[tag]
174            self.inf(f'{tag}', self.bindesc_repr(value))
175
176    def list(self, args):
177        for tag in self.TAG_TO_NAME.values():
178            self.inf(f'{tag}')
179
180    def common_search(self, args, search_term):
181        image = self.get_image_data(args.file)
182
183        descriptors = self.parse_descriptors(image)
184
185        if search_term in descriptors:
186            value = descriptors[search_term]
187            self.inf(self.bindesc_repr(value))
188        else:
189            self.die('Descriptor not found')
190
191    def search(self, args):
192        try:
193            search_term = self.NAME_TO_TAG[args.descriptor]
194        except KeyError:
195            self.die(f'Descriptor {args.descriptor} is invalid')
196
197        self.common_search(args, search_term)
198
199    def custom_search(self, args):
200        custom_type = {
201            'STR': self.TYPE_STR,
202            'UINT': self.TYPE_UINT,
203            'BYTES': self.TYPE_BYTES
204        }[args.type]
205        custom_tag = self.bindesc_gen_tag(custom_type, int(args.id, 16))
206        self.common_search(args, custom_tag)
207
208    def get_offset(self, args):
209        image = self.get_image_data(args.file)
210
211        magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC)
212        index = image.find(magic)
213        if index == -1:
214            self.die('Could not find binary descriptor magic')
215        self.inf(f'{index} {hex(index)}')
216
217    def extract(self, args):
218        image = self.get_image_data(args.file)
219
220        magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC)
221        index = image.find(magic)
222        if index == -1:
223            self.die('Could not find binary descriptor magic')
224
225        index += len(magic) # index points to first descriptor
226        block_start = index
227        current_tag = self.bytes_to_short(image[index:index+2])
228        while current_tag != self.DESCRIPTORS_END:
229            index += 2 # index points to length
230            length = self.bytes_to_short(image[index:index+2])
231            # go to next tag
232            index = self.align(index + 2 + length, 4)
233            current_tag = self.bytes_to_short(image[index:index+2])
234        block_len = index - block_start
235
236        with open(args.out_file, 'wb') as out_file:
237            out_file.write(image[block_start:index])
238        self.inf(f'{block_start}+{block_len} {hex(block_start)}+{hex(block_len)}')
239
240    def do_run(self, args, _):
241        if MISSING_REQUIREMENTS:
242            raise RuntimeError('one or more Python dependencies were missing; '
243                               'see the getting started guide for details on '
244                               'how to fix')
245        self.is_big_endian = args.big_endian
246        self.file_type = self.guess_file_type(args)
247        subcmd = getattr(self, args.subcmd)
248        subcmd(args)
249
250    def get_image_data(self, file_name):
251        if self.file_type == 'bin':
252            with open(file_name, 'rb') as bin_file:
253                return bin_file.read()
254
255        if self.file_type == 'hex':
256            return IntelHex(file_name).tobinstr()
257
258        if self.file_type == 'uf2':
259            with open(file_name, 'rb') as uf2_file:
260                return convert_from_uf2(self, uf2_file.read())
261
262        if self.file_type == 'elf':
263            with open(file_name, 'rb') as f:
264                elffile = ELFFile(f)
265
266                section = elffile.get_section_by_name('rom_start')
267                if section:
268                    return section.data()
269
270                section = elffile.get_section_by_name('text')
271                if section:
272                    return section.data()
273
274            self.die('No "rom_start" or "text" section found')
275
276        self.die('Unknown file type')
277
278    def parse_descriptors(self, image):
279        magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC)
280        index = image.find(magic)
281        if index == -1:
282            self.die('Could not find binary descriptor magic')
283
284        descriptors = {}
285
286        index += len(magic) # index points to first descriptor
287        current_tag = self.bytes_to_short(image[index:index+2])
288        while current_tag != self.DESCRIPTORS_END:
289            index += 2 # index points to length
290            length = self.bytes_to_short(image[index:index+2])
291            index += 2 # index points to data
292            data = image[index:index+length]
293
294            tag_type = self.bindesc_get_type(current_tag)
295            if tag_type == self.TYPE_STR:
296                decoded_data = data[:-1].decode('ascii')
297            elif tag_type == self.TYPE_UINT:
298                decoded_data = self.bytes_to_uint(data)
299            elif tag_type == self.TYPE_BYTES:
300                decoded_data = data
301            else:
302                self.die(f'Unknown type for tag 0x{current_tag:04x}')
303
304            key = f'0x{current_tag:04x}'
305            descriptors[key] = decoded_data
306            index += length
307            index = self.align(index, 4)
308            current_tag = self.bytes_to_short(image[index:index+2])
309
310        return descriptors
311
312    def guess_file_type(self, args):
313        if "file" not in args:
314            return None
315
316        # If file type is explicitly given, use it
317        if args.file_type is not None:
318            return args.file_type
319
320        # If the file has a known extension, use it
321        for extension in self.EXTENSIONS:
322            if args.file.endswith(f'.{extension}'):
323                return extension
324
325        with open(args.file, 'rb') as f:
326            header = f.read(1024)
327
328        # Try the elf magic
329        if header.startswith(b'\x7fELF'):
330            return 'elf'
331
332        # Try the uf2 magic
333        if header.startswith(b'UF2\n'):
334            return 'uf2'
335
336        try:
337            # if the file is textual it's probably hex
338            header.decode('ascii')
339            return 'hex'
340        except UnicodeDecodeError:
341            # Default to bin
342            return 'bin'
343
344    def bytes_to_uint(self, b):
345        return struct.unpack('>I' if self.is_big_endian else 'I', b)[0]
346
347    def bytes_to_short(self, b):
348        return struct.unpack('>H' if self.is_big_endian else 'H', b)[0]
349
350    @staticmethod
351    def bindesc_gen_tag(_type, _id):
352        return f'0x{(_type << 12 | _id):04x}'
353
354    @staticmethod
355    def bindesc_get_type(tag):
356        return tag >> 12
357
358    @staticmethod
359    def align(x, alignment):
360        return (x + alignment - 1) & (~(alignment - 1))
361
362    @staticmethod
363    def bindesc_repr(value):
364        if isinstance(value, str):
365            return f'"{value}"'
366        if isinstance(value, int | bytes):
367            return f'{value}'
368