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