aboutsummaryrefslogtreecommitdiff
path: root/tools/binman/fip_util.py
blob: 868d0b6b16daa457090cda82f8a25cb9b72da9cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0+
# Copyright 2021 Google LLC
# Written by Simon Glass <sjg@chromium.org>

"""Support for ARM's Firmware Image Package (FIP) format

FIP is a format similar to FMAP[1] but with fewer features and an obscure UUID
instead of the region name.

It consists of a header and a table of entries, each pointing to a place in the
firmware image where something can be found.

[1] https://chromium.googlesource.com/chromiumos/third_party/flashmap/+/refs/heads/master/lib/fmap.h

If ATF updates, run this program to update the FIT_TYPE_LIST.

ARM Trusted Firmware is available at:

https://github.com/ARM-software/arm-trusted-firmware.git
"""

from argparse import ArgumentParser
import collections
import io
import os
import re
import struct
import sys
from uuid import UUID

OUR_FILE = os.path.realpath(__file__)
OUR_PATH = os.path.dirname(OUR_FILE)

# Bring in the patman and dtoc libraries (but don't override the first path
# in PYTHONPATH)
sys.path.insert(2, os.path.join(OUR_PATH, '..'))

# pylint: disable=C0413
from patman import command
from patman import tools

# The TOC header, at the start of the FIP
HEADER_FORMAT = '<IIQ'
HEADER_LEN = 0x10
HEADER_MAGIC = 0xaA640001
HEADER_SERIAL = 0x12345678

# The entry header (a table of these comes after the TOC header)
UUID_LEN = 16
ENTRY_FORMAT = f'<{UUID_LEN}sQQQ'
ENTRY_SIZE = 0x28

HEADER_NAMES = (
    'name',
    'serial',
    'flags',
)

ENTRY_NAMES = (
    'uuid',
    'offset',
    'size',
    'flags',
)

# Set to True to enable output from running fiptool for debugging
VERBOSE = False

# Use a class so we can convert the bytes, making the table more readable
# pylint: disable=R0903
class FipType:
    """A FIP entry type that we understand"""
    def __init__(self, name, desc, uuid_bytes):
        """Create up a new type

        Args:
            name (str): Short name for the type
            desc (str): Longer description for the type
            uuid_bytes (bytes): List of 16 bytes for the UUID
        """
        self.name = name
        self.desc = desc
        self.uuid = bytes(uuid_bytes)

# This is taken from tbbr_config.c in ARM Trusted Firmware
FIP_TYPE_LIST = [
    # ToC Entry UUIDs
    FipType('scp-fwu-cfg', 'SCP Firmware Updater Configuration FWU SCP_BL2U',
            [0x65, 0x92, 0x27, 0x03, 0x2f, 0x74, 0xe6, 0x44,
             0x8d, 0xff, 0x57, 0x9a, 0xc1, 0xff, 0x06, 0x10]),
    FipType('ap-fwu-cfg', 'AP Firmware Updater Configuration BL2U',
            [0x60, 0xb3, 0xeb, 0x37, 0xc1, 0xe5, 0xea, 0x41,
             0x9d, 0xf3, 0x19, 0xed, 0xa1, 0x1f, 0x68, 0x01]),
    FipType('fwu', 'Firmware Updater NS_BL2U',
            [0x4f, 0x51, 0x1d, 0x11, 0x2b, 0xe5, 0x4e, 0x49,
             0xb4, 0xc5, 0x83, 0xc2, 0xf7, 0x15, 0x84, 0x0a]),
    FipType('fwu-cert', 'Non-Trusted Firmware Updater certificate',
            [0x71, 0x40, 0x8a, 0xb2, 0x18, 0xd6, 0x87, 0x4c,
             0x8b, 0x2e, 0xc6, 0xdc, 0xcd, 0x50, 0xf0, 0x96]),
    FipType('tb-fw', 'Trusted Boot Firmware BL2',
            [0x5f, 0xf9, 0xec, 0x0b, 0x4d, 0x22, 0x3e, 0x4d,
             0xa5, 0x44, 0xc3, 0x9d, 0x81, 0xc7, 0x3f, 0x0a]),
    FipType('scp-fw', 'SCP Firmware SCP_BL2',
            [0x97, 0x66, 0xfd, 0x3d, 0x89, 0xbe, 0xe8, 0x49,
             0xae, 0x5d, 0x78, 0xa1, 0x40, 0x60, 0x82, 0x13]),
    FipType('soc-fw', 'EL3 Runtime Firmware BL31',
            [0x47, 0xd4, 0x08, 0x6d, 0x4c, 0xfe, 0x98, 0x46,
             0x9b, 0x95, 0x29, 0x50, 0xcb, 0xbd, 0x5a, 0x00]),
    FipType('tos-fw', 'Secure Payload BL32 (Trusted OS)',
            [0x05, 0xd0, 0xe1, 0x89, 0x53, 0xdc, 0x13, 0x47,
             0x8d, 0x2b, 0x50, 0x0a, 0x4b, 0x7a, 0x3e, 0x38]),
    FipType('tos-fw-extra1', 'Secure Payload BL32 Extra1 (Trusted OS Extra1)',
            [0x0b, 0x70, 0xc2, 0x9b, 0x2a, 0x5a, 0x78, 0x40,
             0x9f, 0x65, 0x0a, 0x56, 0x82, 0x73, 0x82, 0x88]),
    FipType('tos-fw-extra2', 'Secure Payload BL32 Extra2 (Trusted OS Extra2)',
            [0x8e, 0xa8, 0x7b, 0xb1, 0xcf, 0xa2, 0x3f, 0x4d,
             0x85, 0xfd, 0xe7, 0xbb, 0xa5, 0x02, 0x20, 0xd9]),
    FipType('nt-fw', 'Non-Trusted Firmware BL33',
            [0xd6, 0xd0, 0xee, 0xa7, 0xfc, 0xea, 0xd5, 0x4b,
             0x97, 0x82, 0x99, 0x34, 0xf2, 0x34, 0xb6, 0xe4]),
    FipType('rmm-fw', 'Realm Monitor Management Firmware',
            [0x6c, 0x07, 0x62, 0xa6, 0x12, 0xf2, 0x4b, 0x56,
             0x92, 0xcb, 0xba, 0x8f, 0x63, 0x36, 0x06, 0xd9]),
    # Key certificates
    FipType('rot-cert', 'Root Of Trust key certificate',
            [0x86, 0x2d, 0x1d, 0x72, 0xf8, 0x60, 0xe4, 0x11,
             0x92, 0x0b, 0x8b, 0xe7, 0x62, 0x16, 0x0f, 0x24]),
    FipType('trusted-key-cert', 'Trusted key certificate',
            [0x82, 0x7e, 0xe8, 0x90, 0xf8, 0x60, 0xe4, 0x11,
             0xa1, 0xb4, 0x77, 0x7a, 0x21, 0xb4, 0xf9, 0x4c]),
    FipType('scp-fw-key-cert', 'SCP Firmware key certificate',
            [0x02, 0x42, 0x21, 0xa1, 0xf8, 0x60, 0xe4, 0x11,
             0x8d, 0x9b, 0xf3, 0x3c, 0x0e, 0x15, 0xa0, 0x14]),
    FipType('soc-fw-key-cert', 'SoC Firmware key certificate',
            [0x8a, 0xb8, 0xbe, 0xcc, 0xf9, 0x60, 0xe4, 0x11,
             0x9a, 0xd0, 0xeb, 0x48, 0x22, 0xd8, 0xdc, 0xf8]),
    FipType('tos-fw-key-cert', 'Trusted OS Firmware key certificate',
            [0x94, 0x77, 0xd6, 0x03, 0xfb, 0x60, 0xe4, 0x11,
             0x85, 0xdd, 0xb7, 0x10, 0x5b, 0x8c, 0xee, 0x04]),
    FipType('nt-fw-key-cert', 'Non-Trusted Firmware key certificate',
            [0x8a, 0xd5, 0x83, 0x2a, 0xfb, 0x60, 0xe4, 0x11,
             0x8a, 0xaf, 0xdf, 0x30, 0xbb, 0xc4, 0x98, 0x59]),
    # Content certificates
    FipType('tb-fw-cert', 'Trusted Boot Firmware BL2 certificate',
            [0xd6, 0xe2, 0x69, 0xea, 0x5d, 0x63, 0xe4, 0x11,
             0x8d, 0x8c, 0x9f, 0xba, 0xbe, 0x99, 0x56, 0xa5]),
    FipType('scp-fw-cert', 'SCP Firmware content certificate',
            [0x44, 0xbe, 0x6f, 0x04, 0x5e, 0x63, 0xe4, 0x11,
             0xb2, 0x8b, 0x73, 0xd8, 0xea, 0xae, 0x96, 0x56]),
    FipType('soc-fw-cert', 'SoC Firmware content certificate',
            [0xe2, 0xb2, 0x0c, 0x20, 0x5e, 0x63, 0xe4, 0x11,
             0x9c, 0xe8, 0xab, 0xcc, 0xf9, 0x2b, 0xb6, 0x66]),
    FipType('tos-fw-cert', 'Trusted OS Firmware content certificate',
            [0xa4, 0x9f, 0x44, 0x11, 0x5e, 0x63, 0xe4, 0x11,
             0x87, 0x28, 0x3f, 0x05, 0x72, 0x2a, 0xf3, 0x3d]),
    FipType('nt-fw-cert', 'Non-Trusted Firmware content certificate',
            [0x8e, 0xc4, 0xc1, 0xf3, 0x5d, 0x63, 0xe4, 0x11,
             0xa7, 0xa9, 0x87, 0xee, 0x40, 0xb2, 0x3f, 0xa7]),
    FipType('sip-sp-cert', 'SiP owned Secure Partition content certificate',
            [0x77, 0x6d, 0xfd, 0x44, 0x86, 0x97, 0x4c, 0x3b,
             0x91, 0xeb, 0xc1, 0x3e, 0x02, 0x5a, 0x2a, 0x6f]),
    FipType('plat-sp-cert', 'Platform owned Secure Partition content certificate',
            [0xdd, 0xcb, 0xbf, 0x4a, 0xca, 0xd6, 0x11, 0xea,
             0x87, 0xd0, 0x02, 0x42, 0xac, 0x13, 0x00, 0x03]),
    # Dynamic configs
    FipType('hw-config', 'HW_CONFIG',
            [0x08, 0xb8, 0xf1, 0xd9, 0xc9, 0xcf, 0x93, 0x49,
             0xa9, 0x62, 0x6f, 0xbc, 0x6b, 0x72, 0x65, 0xcc]),
    FipType('tb-fw-config', 'TB_FW_CONFIG',
            [0x6c, 0x04, 0x58, 0xff, 0xaf, 0x6b, 0x7d, 0x4f,
             0x82, 0xed, 0xaa, 0x27, 0xbc, 0x69, 0xbf, 0xd2]),
    FipType('soc-fw-config', 'SOC_FW_CONFIG',
            [0x99, 0x79, 0x81, 0x4b, 0x03, 0x76, 0xfb, 0x46,
             0x8c, 0x8e, 0x8d, 0x26, 0x7f, 0x78, 0x59, 0xe0]),
    FipType('tos-fw-config', 'TOS_FW_CONFIG',
            [0x26, 0x25, 0x7c, 0x1a, 0xdb, 0xc6, 0x7f, 0x47,
             0x8d, 0x96, 0xc4, 0xc4, 0xb0, 0x24, 0x80, 0x21]),
    FipType('nt-fw-config', 'NT_FW_CONFIG',
            [0x28, 0xda, 0x98, 0x15, 0x93, 0xe8, 0x7e, 0x44,
             0xac, 0x66, 0x1a, 0xaf, 0x80, 0x15, 0x50, 0xf9]),
    FipType('fw-config', 'FW_CONFIG',
            [0x58, 0x07, 0xe1, 0x6a, 0x84, 0x59, 0x47, 0xbe,
             0x8e, 0xd5, 0x64, 0x8e, 0x8d, 0xdd, 0xab, 0x0e]),
    ] # end

FIP_TYPES = {ftype.name: ftype for ftype in FIP_TYPE_LIST}


def get_type_uuid(fip_type_or_uuid):
    """get_type_uuid() - Convert a type or uuid into both

    This always returns a UUID, but may not return a type since it does not do
    the reverse lookup.

    Args:
        fip_type_or_uuid (str or bytes): Either a string containing the name of
            an entry (e.g. 'soc-fw') or a bytes(16) containing the UUID

    Returns:
        tuple:
            str: fip type (None if not known)
            bytes(16): uuid

    Raises:
        ValueError: An unknown type was requested
    """
    if isinstance(fip_type_or_uuid, str):
        fip_type = fip_type_or_uuid
        lookup = FIP_TYPES.get(fip_type)
        if not lookup:
            raise ValueError(f"Unknown FIP entry type '{fip_type}'")
        uuid = lookup.uuid
    else:
        fip_type = None
        uuid = fip_type_or_uuid
    return fip_type, uuid


# pylint: disable=R0903
class FipHeader:
    """Class to represent a FIP header"""
    def __init__(self, name, serial, flags):
        """Set up a new header object

        Args:
            name (str): Name, i.e. HEADER_MAGIC
            serial (str): Serial value, i.e. HEADER_SERIAL
            flags (int64): Flags value
        """
        self.name = name
        self.serial = serial
        self.flags = flags


# pylint: disable=R0903
class FipEntry:
    """Class to represent a single FIP entry

    This is used to hold the information about an entry, including its contents.
    Use the get_data() method to obtain the raw output for writing to the FIP
    file.
    """
    def __init__(self, uuid, offset, size, flags):
        self.uuid = uuid
        self.offset = offset
        self.size = size
        self.flags = flags
        self.fip_type = None
        self.data = None
        self.valid = uuid != tools.GetBytes(0, UUID_LEN)
        if self.valid:
            # Look up the friendly name
            matches = {val for (key, val) in FIP_TYPES.items()
                       if val.uuid == uuid}
            if len(matches) == 1:
                self.fip_type = matches.pop().name

    @classmethod
    def from_type(cls, fip_type_or_uuid, data, flags):
        """Create a FipEntry from a type name

        Args:
            cls (class): This class
            fip_type_or_uuid (str or bytes): Name of the type to create, or
                bytes(16) uuid
            data (bytes): Contents of entry
            flags (int64): Flags value

        Returns:
            FipEntry: Created 241
        """
        fip_type, uuid = get_type_uuid(fip_type_or_uuid)
        fent = FipEntry(uuid, None, len(data), flags)
        fent.fip_type = fip_type
        fent.data = data
        return fent


def decode_fip(data):
    """Decode a FIP into a header and list of FIP entries

    Args:
        data (bytes): Data block containing the FMAP

    Returns:
        Tuple:
            header: FipHeader object
            List of FipArea objects
    """
    fields = list(struct.unpack(HEADER_FORMAT, data[:HEADER_LEN]))
    header = FipHeader(*fields)
    fents = []
    pos = HEADER_LEN
    while True:
        fields = list(struct.unpack(ENTRY_FORMAT, data[pos:pos + ENTRY_SIZE]))
        fent = FipEntry(*fields)
        if not fent.valid:
            break
        fent.data = data[fent.offset:fent.offset + fent.size]
        fents.append(fent)
        pos += ENTRY_SIZE
    return header, fents


class FipWriter:
    """Class to handle writing a ARM Trusted Firmware's Firmware Image Package

    Usage is something like:

        fip = FipWriter(size)
        fip.add_entry('scp-fwu-cfg', tools.ReadFile('something.bin'))
        ...
        data = cbw.get_data()

    Attributes:
    """
    def __init__(self, flags, align):
        self._fip_entries = []
        self._flags = flags
        self._align = align

    def add_entry(self, fip_type, data, flags):
        """Add a new entry to the FIP

        Args:
            fip_type (str): Type to add, e.g. 'tos-fw-config'
            data (bytes): Contents of entry
            flags (int64): Entry flags

        Returns:
            FipEntry: entry that was added
        """
        fent = FipEntry.from_type(fip_type, data, flags)
        self._fip_entries.append(fent)
        return fent

    def get_data(self):
        """Obtain the full contents of the FIP

        Thhis builds the FIP with headers and all required FIP entries.

        Returns:
            bytes: data resulting from building the FIP
        """
        buf = io.BytesIO()
        hdr = struct.pack(HEADER_FORMAT, HEADER_MAGIC, HEADER_SERIAL,
                          self._flags)
        buf.write(hdr)

        # Calculate the position fo the first entry
        offset = len(hdr)
        offset += len(self._fip_entries) * ENTRY_SIZE
        offset += ENTRY_SIZE   # terminating entry

        for fent in self._fip_entries:
            offset = tools.Align(offset, self._align)
            fent.offset = offset
            offset += fent.size

        # Write out the TOC
        for fent in self._fip_entries:
            hdr = struct.pack(ENTRY_FORMAT, fent.uuid, fent.offset, fent.size,
                              fent.flags)
            buf.write(hdr)

        # Write out the entries
        for fent in self._fip_entries:
            buf.seek(fent.offset)
            buf.write(fent.data)

        return buf.getvalue()


class FipReader():
    """Class to handle reading a Firmware Image Package (FIP)

    Usage is something like:
        fip = fip_util.FipReader(data)
        fent = fip.get_entry('fwu')
        self.WriteFile('ufwu.bin', fent.data)
        blob = fip.get_entry(
            bytes([0xe3, 0xb7, 0x8d, 0x9e, 0x4a, 0x64, 0x11, 0xec,
                   0xb4, 0x5c, 0xfb, 0xa2, 0xb9, 0xb4, 0x97, 0x88]))
        self.WriteFile('blob.bin', blob.data)
    """
    def __init__(self, data, read=True):
        """Set up a new FitReader

        Args:
            data (bytes): data to read
            read (bool): True to read the data now
        """
        self.fents = collections.OrderedDict()
        self.data = data
        if read:
            self.read()

    def read(self):
        """Read all the files in the FIP and add them to self.files"""
        self.header, self.fents = decode_fip(self.data)

    def get_entry(self, fip_type_or_uuid):
        """get_entry() - Find an entry by type or UUID

        Args:
            fip_type_or_uuid (str or bytes): Name of the type to create, or
                    bytes(16) uuid

        Returns:
            FipEntry: if found

        Raises:
            ValueError: entry type not found
        """
        fip_type, uuid = get_type_uuid(fip_type_or_uuid)
        for fent in self.fents:
            if fent.uuid == uuid:
                return fent
        label = fip_type
        if not label:
            label = UUID(bytes=uuid)
        raise ValueError(f"Cannot find FIP entry '{label}'")


def parse_macros(srcdir):
    """parse_macros: Parse the firmware_image_package.h file

    Args:
        srcdir (str): 'arm-trusted-firmware' source directory

    Returns:
        dict:
            key: UUID macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
            value: list:
                file comment, e.g. 'ToC Entry UUIDs'
                macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
                uuid as bytes(16)

    Raises:
        ValueError: a line cannot be parsed
    """
    re_uuid = re.compile('0x[0-9a-fA-F]{2}')
    re_comment = re.compile(r'^/\* (.*) \*/$')
    fname = os.path.join(srcdir, 'include/tools_share/firmware_image_package.h')
    data = tools.ReadFile(fname, binary=False)
    macros = collections.OrderedDict()
    comment = None
    for linenum, line in enumerate(data.splitlines()):
        if line.startswith('/*'):
            mat = re_comment.match(line)
            if mat:
                comment = mat.group(1)
        else:
            # Example: #define UUID_TOS_FW_CONFIG \
            if 'UUID' in line:
                macro = line.split()[1]
            elif '{{' in line:
                mat = re_uuid.findall(line)
                if not mat or len(mat) != 16:
                    raise ValueError(
                        f'{fname}: Cannot parse UUID line {linenum + 1}: Got matches: {mat}')

                uuid = bytes([int(val, 16) for val in mat])
                macros[macro] = comment, macro, uuid
    if not macros:
        raise ValueError(f'{fname}: Cannot parse file')
    return macros


def parse_names(srcdir):
    """parse_names: Parse the tbbr_config.c file

    Args:
        srcdir (str): 'arm-trusted-firmware' source directory

    Returns:
        tuple: dict of entries:
            key: UUID macro, e.g. 'UUID_NON_TRUSTED_FIRMWARE_BL33'
            tuple: entry information
                Description of entry, e.g. 'Non-Trusted Firmware BL33'
                UUID macro, e.g. 'UUID_NON_TRUSTED_FIRMWARE_BL33'
                Name of entry, e.g. 'nt-fw'

    Raises:
        ValueError: the file cannot be parsed
    """
    # Extract the .name, .uuid and .cmdline_name values
    re_data = re.compile(r'\.name = "([^"]*)",\s*\.uuid = (UUID_\w*),\s*\.cmdline_name = "([^"]+)"',
                         re.S)
    fname = os.path.join(srcdir, 'tools/fiptool/tbbr_config.c')
    data = tools.ReadFile(fname, binary=False)

    # Example entry:
    #   {
    #       .name = "Secure Payload BL32 Extra2 (Trusted OS Extra2)",
    #       .uuid = UUID_SECURE_PAYLOAD_BL32_EXTRA2,
    #       .cmdline_name = "tos-fw-extra2"
    #   },
    mat = re_data.findall(data)
    if not mat:
        raise ValueError(f'{fname}: Cannot parse file')
    names = {uuid: (desc, uuid, name) for desc, uuid, name in mat}
    return names


def create_code_output(macros, names):
    """create_code_output() - Create the new version of this Python file

    Args:
        macros (dict):
            key (str): UUID macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
            value: list:
                file comment, e.g. 'ToC Entry UUIDs'
                macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
                uuid as bytes(16)

        names (dict): list of entries, each
            tuple: entry information
                Description of entry, e.g. 'Non-Trusted Firmware BL33'
                UUID macro, e.g. 'UUID_NON_TRUSTED_FIRMWARE_BL33'
                Name of entry, e.g. 'nt-fw'

    Returns:
        str: Table of FipType() entries
    """
    def _to_hex_list(data):
        """Convert bytes into C code

        Args:
            bytes to convert

        Returns:
            str: in the format '0x12, 0x34, 0x56...'
        """
        # Use 0x instead of %# since the latter ignores the 0 modifier in
        # Python 3.8.10
        return ', '.join(['0x%02x' % byte for byte in data])

    out = ''
    last_comment = None
    for comment, macro, uuid in macros.values():
        name_entry = names.get(macro)
        if not name_entry:
            print(f"Warning: UUID '{macro}' is not mentioned in tbbr_config.c file")
            continue
        desc, _, name = name_entry
        if last_comment != comment:
            out += f'    # {comment}\n'
            last_comment = comment
        out += """    FipType('%s', '%s',
            [%s,
             %s]),
""" % (name, desc, _to_hex_list(uuid[:8]), _to_hex_list(uuid[8:]))
    return out


def parse_atf_source(srcdir, dstfile, oldfile):
    """parse_atf_source(): Parse the ATF source tree and update this file

    Args:
        srcdir (str): Path to 'arm-trusted-firmware' directory. Get this from:
            https://github.com/ARM-software/arm-trusted-firmware.git
        dstfile (str): File to write new code to, if an update is needed
        oldfile (str): Python source file to compare against

    Raises:
        ValueError: srcdir readme.rst is missing or the first line does not
            match what is expected
    """
    # We expect a readme file
    readme_fname = os.path.join(srcdir, 'readme.rst')
    if not os.path.exists(readme_fname):
        raise ValueError(
            f"Expected file '{readme_fname}' - try using -s to specify the "
            'arm-trusted-firmware directory')
    readme = tools.ReadFile(readme_fname, binary=False)
    first_line = 'Trusted Firmware-A'
    if readme.splitlines()[0] != first_line:
        raise ValueError(f"'{readme_fname}' does not start with '{first_line}'")
    macros = parse_macros(srcdir)
    names = parse_names(srcdir)
    output = create_code_output(macros, names)
    orig = tools.ReadFile(oldfile, binary=False)
    re_fip_list = re.compile(r'(.*FIP_TYPE_LIST = \[).*?(    ] # end.*)', re.S)
    mat = re_fip_list.match(orig)
    new_code = mat.group(1) + '\n' + output + mat.group(2) if mat else output
    if new_code == orig:
        print(f"Existing code in '{oldfile}' is up-to-date")
    else:
        tools.WriteFile(dstfile, new_code, binary=False)
        print(f'Needs update, try:\n\tmeld {dstfile} {oldfile}')


def main(argv, oldfile):
    """Main program for this tool

    Args:
        argv (list): List of str command-line arguments
        oldfile (str): Python source file to compare against

    Returns:
        int: 0 (exit code)
    """
    parser = ArgumentParser(epilog='''Creates an updated version of this code,
with a table of FIP-entry types parsed from the arm-trusted-firmware source
directory''')
    parser.add_argument(
        '-D', '--debug', action='store_true',
        help='Enabling debugging (provides a full traceback on error)')
    parser.add_argument(
        '-o', '--outfile', type=str, default='fip_util.py.out',
        help='Output file to write new fip_util.py file to')
    parser.add_argument(
        '-s', '--src', type=str, default='.',
        help='Directory containing the arm-trusted-firmware source')
    args = parser.parse_args(argv)

    if not args.debug:
        sys.tracebacklimit = 0

    parse_atf_source(args.src, args.outfile, oldfile)
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:], OUR_FILE))  # pragma: no cover