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