uboot/tools/binman/cbfs_util.py
<<
>>
Prefs
   1# SPDX-License-Identifier: GPL-2.0+
   2# Copyright 2019 Google LLC
   3# Written by Simon Glass <sjg@chromium.org>
   4
   5"""Support for coreboot's CBFS format
   6
   7CBFS supports a header followed by a number of files, generally targeted at SPI
   8flash.
   9
  10The format is somewhat defined by documentation in the coreboot tree although
  11it is necessary to rely on the C structures and source code (mostly cbfstool)
  12to fully understand it.
  13
  14Currently supported: raw and stage types with compression, padding empty areas
  15    with empty files, fixed-offset files
  16"""
  17
  18from collections import OrderedDict
  19import io
  20import struct
  21import sys
  22
  23from binman import elf
  24from patman import command
  25from patman import tools
  26
  27# Set to True to enable printing output while working
  28DEBUG = False
  29
  30# Set to True to enable output from running cbfstool for debugging
  31VERBOSE = False
  32
  33# The master header, at the start of the CBFS
  34HEADER_FORMAT      = '>IIIIIIII'
  35HEADER_LEN         = 0x20
  36HEADER_MAGIC       = 0x4f524243
  37HEADER_VERSION1    = 0x31313131
  38HEADER_VERSION2    = 0x31313132
  39
  40# The file header, at the start of each file in the CBFS
  41FILE_HEADER_FORMAT = b'>8sIIII'
  42FILE_HEADER_LEN    = 0x18
  43FILE_MAGIC         = b'LARCHIVE'
  44FILENAME_ALIGN     = 16  # Filename lengths are aligned to this
  45
  46# A stage header containing information about 'stage' files
  47# Yes this is correct: this header is in litte-endian format
  48STAGE_FORMAT       = '<IQQII'
  49STAGE_LEN          = 0x1c
  50
  51# An attribute describring the compression used in a file
  52ATTR_COMPRESSION_FORMAT = '>IIII'
  53ATTR_COMPRESSION_LEN = 0x10
  54
  55# Attribute tags
  56# Depending on how the header was initialised, it may be backed with 0x00 or
  57# 0xff. Support both.
  58FILE_ATTR_TAG_UNUSED        = 0
  59FILE_ATTR_TAG_UNUSED2       = 0xffffffff
  60FILE_ATTR_TAG_COMPRESSION   = 0x42435a4c
  61FILE_ATTR_TAG_HASH          = 0x68736148
  62FILE_ATTR_TAG_POSITION      = 0x42435350  # PSCB
  63FILE_ATTR_TAG_ALIGNMENT     = 0x42434c41  # ALCB
  64FILE_ATTR_TAG_PADDING       = 0x47444150  # PDNG
  65
  66# This is 'the size of bootblock reserved in firmware image (cbfs.txt)'
  67# Not much more info is available, but we set it to 4, due to this comment in
  68# cbfstool.c:
  69# This causes 4 bytes to be left out at the end of the image, for two reasons:
  70# 1. The cbfs master header pointer resides there
  71# 2. Ssme cbfs implementations assume that an image that resides below 4GB has
  72#    a bootblock and get confused when the end of the image is at 4GB == 0.
  73MIN_BOOTBLOCK_SIZE     = 4
  74
  75# Files start aligned to this boundary in the CBFS
  76ENTRY_ALIGN    = 0x40
  77
  78# CBFSs must declare an architecture since much of the logic is designed with
  79# x86 in mind. The effect of setting this value is not well documented, but in
  80# general x86 is used and this makes use of a boot block and an image that ends
  81# at the end of 32-bit address space.
  82ARCHITECTURE_UNKNOWN  = 0xffffffff
  83ARCHITECTURE_X86      = 0x00000001
  84ARCHITECTURE_ARM      = 0x00000010
  85ARCHITECTURE_AARCH64  = 0x0000aa64
  86ARCHITECTURE_MIPS     = 0x00000100
  87ARCHITECTURE_RISCV    = 0xc001d0de
  88ARCHITECTURE_PPC64    = 0x407570ff
  89
  90ARCH_NAMES = {
  91    ARCHITECTURE_UNKNOWN  : 'unknown',
  92    ARCHITECTURE_X86      : 'x86',
  93    ARCHITECTURE_ARM      : 'arm',
  94    ARCHITECTURE_AARCH64  : 'arm64',
  95    ARCHITECTURE_MIPS     : 'mips',
  96    ARCHITECTURE_RISCV    : 'riscv',
  97    ARCHITECTURE_PPC64    : 'ppc64',
  98    }
  99
 100# File types. Only supported ones are included here
 101TYPE_CBFSHEADER     = 0x02   # Master header, HEADER_FORMAT
 102TYPE_STAGE          = 0x10   # Stage, holding an executable, see STAGE_FORMAT
 103TYPE_RAW            = 0x50   # Raw file, possibly compressed
 104TYPE_EMPTY          = 0xffffffff     # Empty data
 105
 106# Compression types
 107COMPRESS_NONE, COMPRESS_LZMA, COMPRESS_LZ4 = range(3)
 108
 109COMPRESS_NAMES = {
 110    COMPRESS_NONE : 'none',
 111    COMPRESS_LZMA : 'lzma',
 112    COMPRESS_LZ4  : 'lz4',
 113    }
 114
 115def find_arch(find_name):
 116    """Look up an architecture name
 117
 118    Args:
 119        find_name: Architecture name to find
 120
 121    Returns:
 122        ARCHITECTURE_... value or None if not found
 123    """
 124    for arch, name in ARCH_NAMES.items():
 125        if name == find_name:
 126            return arch
 127    return None
 128
 129def find_compress(find_name):
 130    """Look up a compression algorithm name
 131
 132    Args:
 133        find_name: Compression algorithm name to find
 134
 135    Returns:
 136        COMPRESS_... value or None if not found
 137    """
 138    for compress, name in COMPRESS_NAMES.items():
 139        if name == find_name:
 140            return compress
 141    return None
 142
 143def compress_name(compress):
 144    """Look up the name of a compression algorithm
 145
 146    Args:
 147        compress: Compression algorithm number to find (COMPRESS_...)
 148
 149    Returns:
 150        Compression algorithm name (string)
 151
 152    Raises:
 153        KeyError if the algorithm number is invalid
 154    """
 155    return COMPRESS_NAMES[compress]
 156
 157def align_int(val, align):
 158    """Align a value up to the given alignment
 159
 160    Args:
 161        val: Integer value to align
 162        align: Integer alignment value (e.g. 4 to align to 4-byte boundary)
 163
 164    Returns:
 165        integer value aligned to the required boundary, rounding up if necessary
 166    """
 167    return int((val + align - 1) / align) * align
 168
 169def align_int_down(val, align):
 170    """Align a value down to the given alignment
 171
 172    Args:
 173        val: Integer value to align
 174        align: Integer alignment value (e.g. 4 to align to 4-byte boundary)
 175
 176    Returns:
 177        integer value aligned to the required boundary, rounding down if
 178            necessary
 179    """
 180    return int(val / align) * align
 181
 182def _pack_string(instr):
 183    """Pack a string to the required aligned size by adding padding
 184
 185    Args:
 186        instr: String to process
 187
 188    Returns:
 189        String with required padding (at least one 0x00 byte) at the end
 190    """
 191    val = tools.ToBytes(instr)
 192    pad_len = align_int(len(val) + 1, FILENAME_ALIGN)
 193    return val + tools.GetBytes(0, pad_len - len(val))
 194
 195
 196class CbfsFile(object):
 197    """Class to represent a single CBFS file
 198
 199    This is used to hold the information about a file, including its contents.
 200    Use the get_data_and_offset() method to obtain the raw output for writing to
 201    CBFS.
 202
 203    Properties:
 204        name: Name of file
 205        offset: Offset of file data from start of file header
 206        cbfs_offset: Offset of file data in bytes from start of CBFS, or None to
 207            place this file anyway
 208        data: Contents of file, uncompressed
 209        orig_data: Original data added to the file, possibly compressed
 210        data_len: Length of (possibly compressed) data in bytes
 211        ftype: File type (TYPE_...)
 212        compression: Compression type (COMPRESS_...)
 213        memlen: Length of data in memory, i.e. the uncompressed length, None if
 214            no compression algortihm is selected
 215        load: Load address in memory if known, else None
 216        entry: Entry address in memory if known, else None. This is where
 217            execution starts after the file is loaded
 218        base_address: Base address to use for 'stage' files
 219        erase_byte: Erase byte to use for padding between the file header and
 220            contents (used for empty files)
 221        size: Size of the file in bytes (used for empty files)
 222    """
 223    def __init__(self, name, ftype, data, cbfs_offset, compress=COMPRESS_NONE):
 224        self.name = name
 225        self.offset = None
 226        self.cbfs_offset = cbfs_offset
 227        self.data = data
 228        self.orig_data = data
 229        self.ftype = ftype
 230        self.compress = compress
 231        self.memlen = None
 232        self.load = None
 233        self.entry = None
 234        self.base_address = None
 235        self.data_len = len(data)
 236        self.erase_byte = None
 237        self.size = None
 238
 239    def decompress(self):
 240        """Handle decompressing data if necessary"""
 241        indata = self.data
 242        if self.compress == COMPRESS_LZ4:
 243            data = tools.Decompress(indata, 'lz4', with_header=False)
 244        elif self.compress == COMPRESS_LZMA:
 245            data = tools.Decompress(indata, 'lzma', with_header=False)
 246        else:
 247            data = indata
 248        self.memlen = len(data)
 249        self.data = data
 250        self.data_len = len(indata)
 251
 252    @classmethod
 253    def stage(cls, base_address, name, data, cbfs_offset):
 254        """Create a new stage file
 255
 256        Args:
 257            base_address: Int base address for memory-mapping of ELF file
 258            name: String file name to put in CBFS (does not need to correspond
 259                to the name that the file originally came from)
 260            data: Contents of file
 261            cbfs_offset: Offset of file data in bytes from start of CBFS, or
 262                None to place this file anyway
 263
 264        Returns:
 265            CbfsFile object containing the file information
 266        """
 267        cfile = CbfsFile(name, TYPE_STAGE, data, cbfs_offset)
 268        cfile.base_address = base_address
 269        return cfile
 270
 271    @classmethod
 272    def raw(cls, name, data, cbfs_offset, compress):
 273        """Create a new raw file
 274
 275        Args:
 276            name: String file name to put in CBFS (does not need to correspond
 277                to the name that the file originally came from)
 278            data: Contents of file
 279            cbfs_offset: Offset of file data in bytes from start of CBFS, or
 280                None to place this file anyway
 281            compress: Compression algorithm to use (COMPRESS_...)
 282
 283        Returns:
 284            CbfsFile object containing the file information
 285        """
 286        return CbfsFile(name, TYPE_RAW, data, cbfs_offset, compress)
 287
 288    @classmethod
 289    def empty(cls, space_to_use, erase_byte):
 290        """Create a new empty file of a given size
 291
 292        Args:
 293            space_to_use:: Size of available space, which must be at least as
 294                large as the alignment size for this CBFS
 295            erase_byte: Byte to use for contents of file (repeated through the
 296                whole file)
 297
 298        Returns:
 299            CbfsFile object containing the file information
 300        """
 301        cfile = CbfsFile('', TYPE_EMPTY, b'', None)
 302        cfile.size = space_to_use - FILE_HEADER_LEN - FILENAME_ALIGN
 303        cfile.erase_byte = erase_byte
 304        return cfile
 305
 306    def calc_start_offset(self):
 307        """Check if this file needs to start at a particular offset in CBFS
 308
 309        Returns:
 310            None if the file can be placed anywhere, or
 311            the largest offset where the file could start (integer)
 312        """
 313        if self.cbfs_offset is None:
 314            return None
 315        return self.cbfs_offset - self.get_header_len()
 316
 317    def get_header_len(self):
 318        """Get the length of headers required for a file
 319
 320        This is the minimum length required before the actual data for this file
 321        could start. It might start later if there is padding.
 322
 323        Returns:
 324            Total length of all non-data fields, in bytes
 325        """
 326        name = _pack_string(self.name)
 327        hdr_len = len(name) + FILE_HEADER_LEN
 328        if self.ftype == TYPE_STAGE:
 329            pass
 330        elif self.ftype == TYPE_RAW:
 331            hdr_len += ATTR_COMPRESSION_LEN
 332        elif self.ftype == TYPE_EMPTY:
 333            pass
 334        else:
 335            raise ValueError('Unknown file type %#x\n' % self.ftype)
 336        return hdr_len
 337
 338    def get_data_and_offset(self, offset=None, pad_byte=None):
 339        """Obtain the contents of the file, in CBFS format and the offset of
 340        the data within the file
 341
 342        Returns:
 343            tuple:
 344                bytes representing the contents of this file, packed and aligned
 345                    for directly inserting into the final CBFS output
 346                offset to the file data from the start of the returned data.
 347        """
 348        name = _pack_string(self.name)
 349        hdr_len = len(name) + FILE_HEADER_LEN
 350        attr_pos = 0
 351        content = b''
 352        attr = b''
 353        pad = b''
 354        data = self.data
 355        if self.ftype == TYPE_STAGE:
 356            elf_data = elf.DecodeElf(data, self.base_address)
 357            content = struct.pack(STAGE_FORMAT, self.compress,
 358                                  elf_data.entry, elf_data.load,
 359                                  len(elf_data.data), elf_data.memsize)
 360            data = elf_data.data
 361        elif self.ftype == TYPE_RAW:
 362            orig_data = data
 363            if self.compress == COMPRESS_LZ4:
 364                data = tools.Compress(orig_data, 'lz4', with_header=False)
 365            elif self.compress == COMPRESS_LZMA:
 366                data = tools.Compress(orig_data, 'lzma', with_header=False)
 367            self.memlen = len(orig_data)
 368            self.data_len = len(data)
 369            attr = struct.pack(ATTR_COMPRESSION_FORMAT,
 370                               FILE_ATTR_TAG_COMPRESSION, ATTR_COMPRESSION_LEN,
 371                               self.compress, self.memlen)
 372        elif self.ftype == TYPE_EMPTY:
 373            data = tools.GetBytes(self.erase_byte, self.size)
 374        else:
 375            raise ValueError('Unknown type %#x when writing\n' % self.ftype)
 376        if attr:
 377            attr_pos = hdr_len
 378            hdr_len += len(attr)
 379        if self.cbfs_offset is not None:
 380            pad_len = self.cbfs_offset - offset - hdr_len
 381            if pad_len < 0:  # pragma: no cover
 382                # Test coverage of this is not available since this should never
 383                # happen. It indicates that get_header_len() provided an
 384                # incorrect value (too small) so that we decided that we could
 385                # put this file at the requested place, but in fact a previous
 386                # file extends far enough into the CBFS that this is not
 387                # possible.
 388                raise ValueError("Internal error: CBFS file '%s': Requested offset %#x but current output position is %#x" %
 389                                 (self.name, self.cbfs_offset, offset))
 390            pad = tools.GetBytes(pad_byte, pad_len)
 391            hdr_len += pad_len
 392
 393        # This is the offset of the start of the file's data,
 394        size = len(content) + len(data)
 395        hdr = struct.pack(FILE_HEADER_FORMAT, FILE_MAGIC, size,
 396                          self.ftype, attr_pos, hdr_len)
 397
 398        # Do a sanity check of the get_header_len() function, to ensure that it
 399        # stays in lockstep with this function
 400        expected_len = self.get_header_len()
 401        actual_len = len(hdr + name + attr)
 402        if expected_len != actual_len:  # pragma: no cover
 403            # Test coverage of this is not available since this should never
 404            # happen. It probably indicates that get_header_len() is broken.
 405            raise ValueError("Internal error: CBFS file '%s': Expected headers of %#x bytes, got %#d" %
 406                             (self.name, expected_len, actual_len))
 407        return hdr + name + attr + pad + content + data, hdr_len
 408
 409
 410class CbfsWriter(object):
 411    """Class to handle writing a Coreboot File System (CBFS)
 412
 413    Usage is something like:
 414
 415        cbw = CbfsWriter(size)
 416        cbw.add_file_raw('u-boot', tools.ReadFile('u-boot.bin'))
 417        ...
 418        data, cbfs_offset = cbw.get_data_and_offset()
 419
 420    Attributes:
 421        _master_name: Name of the file containing the master header
 422        _size: Size of the filesystem, in bytes
 423        _files: Ordered list of files in the CBFS, each a CbfsFile
 424        _arch: Architecture of the CBFS (ARCHITECTURE_...)
 425        _bootblock_size: Size of the bootblock, typically at the end of the CBFS
 426        _erase_byte: Byte to use for empty space in the CBFS
 427        _align: Alignment to use for files, typically ENTRY_ALIGN
 428        _base_address: Boot block offset in bytes from the start of CBFS.
 429            Typically this is located at top of the CBFS. It is 0 when there is
 430            no boot block
 431        _header_offset: Offset of master header in bytes from start of CBFS
 432        _contents_offset: Offset of first file header
 433        _hdr_at_start: True if the master header is at the start of the CBFS,
 434            instead of the end as normal for x86
 435        _add_fileheader: True to add a fileheader around the master header
 436    """
 437    def __init__(self, size, arch=ARCHITECTURE_X86):
 438        """Set up a new CBFS
 439
 440        This sets up all properties to default values. Files can be added using
 441        add_file_raw(), etc.
 442
 443        Args:
 444            size: Size of CBFS in bytes
 445            arch: Architecture to declare for CBFS
 446        """
 447        self._master_name = 'cbfs master header'
 448        self._size = size
 449        self._files = OrderedDict()
 450        self._arch = arch
 451        self._bootblock_size = 0
 452        self._erase_byte = 0xff
 453        self._align = ENTRY_ALIGN
 454        self._add_fileheader = False
 455        if self._arch == ARCHITECTURE_X86:
 456            # Allow 4 bytes for the header pointer. That holds the
 457            # twos-compliment negative offset of the master header in bytes
 458            # measured from one byte past the end of the CBFS
 459            self._base_address = self._size - max(self._bootblock_size,
 460                                                  MIN_BOOTBLOCK_SIZE)
 461            self._header_offset = self._base_address - HEADER_LEN
 462            self._contents_offset = 0
 463            self._hdr_at_start = False
 464        else:
 465            # For non-x86, different rules apply
 466            self._base_address = 0
 467            self._header_offset = align_int(self._base_address +
 468                                            self._bootblock_size, 4)
 469            self._contents_offset = align_int(self._header_offset +
 470                                              FILE_HEADER_LEN +
 471                                              self._bootblock_size, self._align)
 472            self._hdr_at_start = True
 473
 474    def _skip_to(self, fd, offset):
 475        """Write out pad bytes until a given offset
 476
 477        Args:
 478            fd: File objext to write to
 479            offset: Offset to write to
 480        """
 481        if fd.tell() > offset:
 482            raise ValueError('No space for data before offset %#x (current offset %#x)' %
 483                             (offset, fd.tell()))
 484        fd.write(tools.GetBytes(self._erase_byte, offset - fd.tell()))
 485
 486    def _pad_to(self, fd, offset):
 487        """Write out pad bytes and/or an empty file until a given offset
 488
 489        Args:
 490            fd: File objext to write to
 491            offset: Offset to write to
 492        """
 493        self._align_to(fd, self._align)
 494        upto = fd.tell()
 495        if upto > offset:
 496            raise ValueError('No space for data before pad offset %#x (current offset %#x)' %
 497                             (offset, upto))
 498        todo = align_int_down(offset - upto, self._align)
 499        if todo:
 500            cbf = CbfsFile.empty(todo, self._erase_byte)
 501            fd.write(cbf.get_data_and_offset()[0])
 502        self._skip_to(fd, offset)
 503
 504    def _align_to(self, fd, align):
 505        """Write out pad bytes until a given alignment is reached
 506
 507        This only aligns if the resulting output would not reach the end of the
 508        CBFS, since we want to leave the last 4 bytes for the master-header
 509        pointer.
 510
 511        Args:
 512            fd: File objext to write to
 513            align: Alignment to require (e.g. 4 means pad to next 4-byte
 514                boundary)
 515        """
 516        offset = align_int(fd.tell(), align)
 517        if offset < self._size:
 518            self._skip_to(fd, offset)
 519
 520    def add_file_stage(self, name, data, cbfs_offset=None):
 521        """Add a new stage file to the CBFS
 522
 523        Args:
 524            name: String file name to put in CBFS (does not need to correspond
 525                to the name that the file originally came from)
 526            data: Contents of file
 527            cbfs_offset: Offset of this file's data within the CBFS, in bytes,
 528                or None to place this file anywhere
 529
 530        Returns:
 531            CbfsFile object created
 532        """
 533        cfile = CbfsFile.stage(self._base_address, name, data, cbfs_offset)
 534        self._files[name] = cfile
 535        return cfile
 536
 537    def add_file_raw(self, name, data, cbfs_offset=None,
 538                     compress=COMPRESS_NONE):
 539        """Create a new raw file
 540
 541        Args:
 542            name: String file name to put in CBFS (does not need to correspond
 543                to the name that the file originally came from)
 544            data: Contents of file
 545            cbfs_offset: Offset of this file's data within the CBFS, in bytes,
 546                or None to place this file anywhere
 547            compress: Compression algorithm to use (COMPRESS_...)
 548
 549        Returns:
 550            CbfsFile object created
 551        """
 552        cfile = CbfsFile.raw(name, data, cbfs_offset, compress)
 553        self._files[name] = cfile
 554        return cfile
 555
 556    def _write_header(self, fd, add_fileheader):
 557        """Write out the master header to a CBFS
 558
 559        Args:
 560            fd: File object
 561            add_fileheader: True to place the master header in a file header
 562                record
 563        """
 564        if fd.tell() > self._header_offset:
 565            raise ValueError('No space for header at offset %#x (current offset %#x)' %
 566                             (self._header_offset, fd.tell()))
 567        if not add_fileheader:
 568            self._pad_to(fd, self._header_offset)
 569        hdr = struct.pack(HEADER_FORMAT, HEADER_MAGIC, HEADER_VERSION2,
 570                          self._size, self._bootblock_size, self._align,
 571                          self._contents_offset, self._arch, 0xffffffff)
 572        if add_fileheader:
 573            name = _pack_string(self._master_name)
 574            fd.write(struct.pack(FILE_HEADER_FORMAT, FILE_MAGIC, len(hdr),
 575                                 TYPE_CBFSHEADER, 0,
 576                                 FILE_HEADER_LEN + len(name)))
 577            fd.write(name)
 578            self._header_offset = fd.tell()
 579            fd.write(hdr)
 580            self._align_to(fd, self._align)
 581        else:
 582            fd.write(hdr)
 583
 584    def get_data(self):
 585        """Obtain the full contents of the CBFS
 586
 587        Thhis builds the CBFS with headers and all required files.
 588
 589        Returns:
 590            'bytes' type containing the data
 591        """
 592        fd = io.BytesIO()
 593
 594        # THe header can go at the start in some cases
 595        if self._hdr_at_start:
 596            self._write_header(fd, add_fileheader=self._add_fileheader)
 597        self._skip_to(fd, self._contents_offset)
 598
 599        # Write out each file
 600        for cbf in self._files.values():
 601            # Place the file at its requested place, if any
 602            offset = cbf.calc_start_offset()
 603            if offset is not None:
 604                self._pad_to(fd, align_int_down(offset, self._align))
 605            pos = fd.tell()
 606            data, data_offset = cbf.get_data_and_offset(pos, self._erase_byte)
 607            fd.write(data)
 608            self._align_to(fd, self._align)
 609            cbf.calced_cbfs_offset = pos + data_offset
 610        if not self._hdr_at_start:
 611            self._write_header(fd, add_fileheader=self._add_fileheader)
 612
 613        # Pad to the end and write a pointer to the CBFS master header
 614        self._pad_to(fd, self._base_address or self._size - 4)
 615        rel_offset = self._header_offset - self._size
 616        fd.write(struct.pack('<I', rel_offset & 0xffffffff))
 617
 618        return fd.getvalue()
 619
 620
 621class CbfsReader(object):
 622    """Class to handle reading a Coreboot File System (CBFS)
 623
 624    Usage is something like:
 625        cbfs = cbfs_util.CbfsReader(data)
 626        cfile = cbfs.files['u-boot']
 627        self.WriteFile('u-boot.bin', cfile.data)
 628
 629    Attributes:
 630        files: Ordered list of CbfsFile objects
 631        align: Alignment to use for files, typically ENTRT_ALIGN
 632        stage_base_address: Base address to use when mapping ELF files into the
 633            CBFS for TYPE_STAGE files. If this is larger than the code address
 634            of the ELF file, then data at the start of the ELF file will not
 635            appear in the CBFS. Currently there are no tests for behaviour as
 636            documentation is sparse
 637        magic: Integer magic number from master header (HEADER_MAGIC)
 638        version: Version number of CBFS (HEADER_VERSION2)
 639        rom_size: Size of CBFS
 640        boot_block_size: Size of boot block
 641        cbfs_offset: Offset of the first file in bytes from start of CBFS
 642        arch: Architecture of CBFS file (ARCHITECTURE_...)
 643    """
 644    def __init__(self, data, read=True):
 645        self.align = ENTRY_ALIGN
 646        self.arch = None
 647        self.boot_block_size = None
 648        self.cbfs_offset = None
 649        self.files = OrderedDict()
 650        self.magic = None
 651        self.rom_size = None
 652        self.stage_base_address = 0
 653        self.version = None
 654        self.data = data
 655        if read:
 656            self.read()
 657
 658    def read(self):
 659        """Read all the files in the CBFS and add them to self.files"""
 660        with io.BytesIO(self.data) as fd:
 661            # First, get the master header
 662            if not self._find_and_read_header(fd, len(self.data)):
 663                raise ValueError('Cannot find master header')
 664            fd.seek(self.cbfs_offset)
 665
 666            # Now read in the files one at a time
 667            while True:
 668                cfile = self._read_next_file(fd)
 669                if cfile:
 670                    self.files[cfile.name] = cfile
 671                elif cfile is False:
 672                    break
 673
 674    def _find_and_read_header(self, fd, size):
 675        """Find and read the master header in the CBFS
 676
 677        This looks at the pointer word at the very end of the CBFS. This is an
 678        offset to the header relative to the size of the CBFS, which is assumed
 679        to be known. Note that the offset is in *little endian* format.
 680
 681        Args:
 682            fd: File to read from
 683            size: Size of file
 684
 685        Returns:
 686            True if header was found, False if not
 687        """
 688        orig_pos = fd.tell()
 689        fd.seek(size - 4)
 690        rel_offset, = struct.unpack('<I', fd.read(4))
 691        pos = (size + rel_offset) & 0xffffffff
 692        fd.seek(pos)
 693        found = self._read_header(fd)
 694        if not found:
 695            print('Relative offset seems wrong, scanning whole image')
 696            for pos in range(0, size - HEADER_LEN, 4):
 697                fd.seek(pos)
 698                found = self._read_header(fd)
 699                if found:
 700                    break
 701        fd.seek(orig_pos)
 702        return found
 703
 704    def _read_next_file(self, fd):
 705        """Read the next file from a CBFS
 706
 707        Args:
 708            fd: File to read from
 709
 710        Returns:
 711            CbfsFile object, if found
 712            None if no object found, but data was parsed (e.g. TYPE_CBFSHEADER)
 713            False if at end of CBFS and reading should stop
 714        """
 715        file_pos = fd.tell()
 716        data = fd.read(FILE_HEADER_LEN)
 717        if len(data) < FILE_HEADER_LEN:
 718            print('File header at %#x ran out of data' % file_pos)
 719            return False
 720        magic, size, ftype, attr, offset = struct.unpack(FILE_HEADER_FORMAT,
 721                                                         data)
 722        if magic != FILE_MAGIC:
 723            return False
 724        pos = fd.tell()
 725        name = self._read_string(fd)
 726        if name is None:
 727            print('String at %#x ran out of data' % pos)
 728            return False
 729
 730        if DEBUG:
 731            print('name', name)
 732
 733        # If there are attribute headers present, read those
 734        compress = self._read_attr(fd, file_pos, attr, offset)
 735        if compress is None:
 736            return False
 737
 738        # Create the correct CbfsFile object depending on the type
 739        cfile = None
 740        cbfs_offset = file_pos + offset
 741        fd.seek(cbfs_offset, io.SEEK_SET)
 742        if ftype == TYPE_CBFSHEADER:
 743            self._read_header(fd)
 744        elif ftype == TYPE_STAGE:
 745            data = fd.read(STAGE_LEN)
 746            cfile = CbfsFile.stage(self.stage_base_address, name, b'',
 747                                   cbfs_offset)
 748            (cfile.compress, cfile.entry, cfile.load, cfile.data_len,
 749             cfile.memlen) = struct.unpack(STAGE_FORMAT, data)
 750            cfile.data = fd.read(cfile.data_len)
 751        elif ftype == TYPE_RAW:
 752            data = fd.read(size)
 753            cfile = CbfsFile.raw(name, data, cbfs_offset, compress)
 754            cfile.decompress()
 755            if DEBUG:
 756                print('data', data)
 757        elif ftype == TYPE_EMPTY:
 758            # Just read the data and discard it, since it is only padding
 759            fd.read(size)
 760            cfile = CbfsFile('', TYPE_EMPTY, b'', cbfs_offset)
 761        else:
 762            raise ValueError('Unknown type %#x when reading\n' % ftype)
 763        if cfile:
 764            cfile.offset = offset
 765
 766        # Move past the padding to the start of a possible next file. If we are
 767        # already at an alignment boundary, then there is no padding.
 768        pad = (self.align - fd.tell() % self.align) % self.align
 769        fd.seek(pad, io.SEEK_CUR)
 770        return cfile
 771
 772    @classmethod
 773    def _read_attr(cls, fd, file_pos, attr, offset):
 774        """Read attributes from the file
 775
 776        CBFS files can have attributes which are things that cannot fit into the
 777        header. The only attributes currently supported are compression and the
 778        unused tag.
 779
 780        Args:
 781            fd: File to read from
 782            file_pos: Position of file in fd
 783            attr: Offset of attributes, 0 if none
 784            offset: Offset of file data (used to indicate the end of the
 785                                         attributes)
 786
 787        Returns:
 788            Compression to use for the file (COMPRESS_...)
 789        """
 790        compress = COMPRESS_NONE
 791        if not attr:
 792            return compress
 793        attr_size = offset - attr
 794        fd.seek(file_pos + attr, io.SEEK_SET)
 795        while attr_size:
 796            pos = fd.tell()
 797            hdr = fd.read(8)
 798            if len(hdr) < 8:
 799                print('Attribute tag at %x ran out of data' % pos)
 800                return None
 801            atag, alen = struct.unpack(">II", hdr)
 802            data = hdr + fd.read(alen - 8)
 803            if atag == FILE_ATTR_TAG_COMPRESSION:
 804                # We don't currently use this information
 805                atag, alen, compress, _decomp_size = struct.unpack(
 806                    ATTR_COMPRESSION_FORMAT, data)
 807            elif atag == FILE_ATTR_TAG_UNUSED2:
 808                break
 809            else:
 810                print('Unknown attribute tag %x' % atag)
 811            attr_size -= len(data)
 812        return compress
 813
 814    def _read_header(self, fd):
 815        """Read the master header
 816
 817        Reads the header and stores the information obtained into the member
 818        variables.
 819
 820        Args:
 821            fd: File to read from
 822
 823        Returns:
 824            True if header was read OK, False if it is truncated or has the
 825                wrong magic or version
 826        """
 827        pos = fd.tell()
 828        data = fd.read(HEADER_LEN)
 829        if len(data) < HEADER_LEN:
 830            print('Header at %x ran out of data' % pos)
 831            return False
 832        (self.magic, self.version, self.rom_size, self.boot_block_size,
 833         self.align, self.cbfs_offset, self.arch, _) = struct.unpack(
 834             HEADER_FORMAT, data)
 835        return self.magic == HEADER_MAGIC and (
 836            self.version == HEADER_VERSION1 or
 837            self.version == HEADER_VERSION2)
 838
 839    @classmethod
 840    def _read_string(cls, fd):
 841        """Read a string from a file
 842
 843        This reads a string and aligns the data to the next alignment boundary
 844
 845        Args:
 846            fd: File to read from
 847
 848        Returns:
 849            string read ('str' type) encoded to UTF-8, or None if we ran out of
 850                data
 851        """
 852        val = b''
 853        while True:
 854            data = fd.read(FILENAME_ALIGN)
 855            if len(data) < FILENAME_ALIGN:
 856                return None
 857            pos = data.find(b'\0')
 858            if pos == -1:
 859                val += data
 860            else:
 861                val += data[:pos]
 862                break
 863        return val.decode('utf-8')
 864
 865
 866def cbfstool(fname, *cbfs_args, **kwargs):
 867    """Run cbfstool with provided arguments
 868
 869    If the tool fails then this function raises an exception and prints out the
 870    output and stderr.
 871
 872    Args:
 873        fname: Filename of CBFS
 874        *cbfs_args: List of arguments to pass to cbfstool
 875
 876    Returns:
 877        CommandResult object containing the results
 878    """
 879    args = ['cbfstool', fname] + list(cbfs_args)
 880    if kwargs.get('base') is not None:
 881        args += ['-b', '%#x' % kwargs['base']]
 882    result = command.RunPipe([args], capture=not VERBOSE,
 883                             capture_stderr=not VERBOSE, raise_on_error=False)
 884    if result.return_code:
 885        print(result.stderr, file=sys.stderr)
 886        raise Exception("Failed to run (error %d): '%s'" %
 887                        (result.return_code, ' '.join(args)))
 888