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