# SPDX-License-Identifier: GPL-2.0+ # Copyright 2019 Google LLC # Written by Simon Glass """Support for coreboot's CBFS format CBFS supports a header followed by a number of files, generally targeted at SPI flash. The format is somewhat defined by documentation in the coreboot tree although it is necessary to rely on the C structures and source code (mostly cbfstool) to fully understand it. Currently supported: raw and stage types with compression, padding empty areas with empty files, fixed-offset files """ from collections import OrderedDict import io import struct import sys from binman import bintool from binman import elf from u_boot_pylib import command from u_boot_pylib import tools # Set to True to enable printing output while working DEBUG = False # Set to True to enable output from running cbfstool for debugging VERBOSE = False # The master header, at the start of the CBFS HEADER_FORMAT = '>IIIIIIII' HEADER_LEN = 0x20 HEADER_MAGIC = 0x4f524243 HEADER_VERSION1 = 0x31313131 HEADER_VERSION2 = 0x31313132 # The file header, at the start of each file in the CBFS FILE_HEADER_FORMAT = b'>8sIIII' FILE_HEADER_LEN = 0x18 FILE_MAGIC = b'LARCHIVE' FILENAME_ALIGN = 16 # Filename lengths are aligned to this # A stage header containing information about 'stage' files # Yes this is correct: this header is in litte-endian format STAGE_FORMAT = ' offset: raise ValueError('No space for data before offset %#x (current offset %#x)' % (offset, fd.tell())) fd.write(tools.get_bytes(self._erase_byte, offset - fd.tell())) def _pad_to(self, fd, offset): """Write out pad bytes and/or an empty file until a given offset Args: fd: File objext to write to offset: Offset to write to """ self._align_to(fd, self._align) upto = fd.tell() if upto > offset: raise ValueError('No space for data before pad offset %#x (current offset %#x)' % (offset, upto)) todo = align_int_down(offset - upto, self._align) if todo: cbf = CbfsFile.empty(todo, self._erase_byte) fd.write(cbf.get_data_and_offset()[0]) self._skip_to(fd, offset) def _align_to(self, fd, align): """Write out pad bytes until a given alignment is reached This only aligns if the resulting output would not reach the end of the CBFS, since we want to leave the last 4 bytes for the master-header pointer. Args: fd: File objext to write to align: Alignment to require (e.g. 4 means pad to next 4-byte boundary) """ offset = align_int(fd.tell(), align) if offset < self._size: self._skip_to(fd, offset) def add_file_stage(self, name, data, cbfs_offset=None): """Add a new stage file to the CBFS Args: name: String file name to put in CBFS (does not need to correspond to the name that the file originally came from) data: Contents of file cbfs_offset: Offset of this file's data within the CBFS, in bytes, or None to place this file anywhere Returns: CbfsFile object created """ cfile = CbfsFile.stage(self._base_address, name, data, cbfs_offset) self._files[name] = cfile return cfile def add_file_raw(self, name, data, cbfs_offset=None, compress=COMPRESS_NONE): """Create a new raw file Args: name: String file name to put in CBFS (does not need to correspond to the name that the file originally came from) data: Contents of file cbfs_offset: Offset of this file's data within the CBFS, in bytes, or None to place this file anywhere compress: Compression algorithm to use (COMPRESS_...) Returns: CbfsFile object created """ cfile = CbfsFile.raw(name, data, cbfs_offset, compress) self._files[name] = cfile return cfile def _write_header(self, fd, add_fileheader): """Write out the master header to a CBFS Args: fd: File object add_fileheader: True to place the master header in a file header record """ if fd.tell() > self._header_offset: raise ValueError('No space for header at offset %#x (current offset %#x)' % (self._header_offset, fd.tell())) if not add_fileheader: self._pad_to(fd, self._header_offset) hdr = struct.pack(HEADER_FORMAT, HEADER_MAGIC, HEADER_VERSION2, self._size, self._bootblock_size, self._align, self._contents_offset, self._arch, 0xffffffff) if add_fileheader: name = _pack_string(self._master_name) fd.write(struct.pack(FILE_HEADER_FORMAT, FILE_MAGIC, len(hdr), TYPE_CBFSHEADER, 0, FILE_HEADER_LEN + len(name))) fd.write(name) self._header_offset = fd.tell() fd.write(hdr) self._align_to(fd, self._align) else: fd.write(hdr) def get_data(self): """Obtain the full contents of the CBFS Thhis builds the CBFS with headers and all required files. Returns: 'bytes' type containing the data """ fd = io.BytesIO() # THe header can go at the start in some cases if self._hdr_at_start: self._write_header(fd, add_fileheader=self._add_fileheader) self._skip_to(fd, self._contents_offset) # Write out each file for cbf in self._files.values(): # Place the file at its requested place, if any offset = cbf.calc_start_offset() if offset is not None: self._pad_to(fd, align_int_down(offset, self._align)) pos = fd.tell() data, data_offset = cbf.get_data_and_offset(pos, self._erase_byte) fd.write(data) self._align_to(fd, self._align) cbf.calced_cbfs_offset = pos + data_offset if not self._hdr_at_start: self._write_header(fd, add_fileheader=self._add_fileheader) # Pad to the end and write a pointer to the CBFS master header self._pad_to(fd, self._base_address or self._size - 4) rel_offset = self._header_offset - self._size fd.write(struct.pack('II", hdr) data = hdr + fd.read(alen - 8) if atag == FILE_ATTR_TAG_COMPRESSION: # We don't currently use this information atag, alen, compress, _decomp_size = struct.unpack( ATTR_COMPRESSION_FORMAT, data) elif atag == FILE_ATTR_TAG_UNUSED2: break else: print('Unknown attribute tag %x' % atag) attr_size -= len(data) return compress def _read_header(self, fd): """Read the master header Reads the header and stores the information obtained into the member variables. Args: fd: File to read from Returns: True if header was read OK, False if it is truncated or has the wrong magic or version """ pos = fd.tell() data = fd.read(HEADER_LEN) if len(data) < HEADER_LEN: print('Header at %x ran out of data' % pos) return False (self.magic, self.version, self.rom_size, self.boot_block_size, self.align, self.cbfs_offset, self.arch, _) = struct.unpack( HEADER_FORMAT, data) return self.magic == HEADER_MAGIC and ( self.version == HEADER_VERSION1 or self.version == HEADER_VERSION2) @classmethod def _read_string(cls, fd): """Read a string from a file This reads a string and aligns the data to the next alignment boundary Args: fd: File to read from Returns: string read ('str' type) encoded to UTF-8, or None if we ran out of data """ val = b'' while True: data = fd.read(FILENAME_ALIGN) if len(data) < FILENAME_ALIGN: return None pos = data.find(b'\0') if pos == -1: val += data else: val += data[:pos] break return val.decode('utf-8')