]>
Commit | Line | Data |
---|---|---|
4997a7ed SG |
1 | # SPDX-License-Identifier: GPL-2.0+ |
2 | # Copyright 2019 Google LLC | |
3 | # Written by Simon Glass <[email protected]> | |
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 | ||
4997a7ed SG |
18 | from collections import OrderedDict |
19 | import io | |
20 | import struct | |
21 | import sys | |
22 | ||
16287933 | 23 | from binman import elf |
bf776679 SG |
24 | from patman import command |
25 | from patman import tools | |
4997a7ed SG |
26 | |
27 | # Set to True to enable printing output while working | |
28 | DEBUG = False | |
29 | ||
30 | # Set to True to enable output from running cbfstool for debugging | |
31 | VERBOSE = False | |
32 | ||
33 | # The master header, at the start of the CBFS | |
34 | HEADER_FORMAT = '>IIIIIIII' | |
35 | HEADER_LEN = 0x20 | |
36 | HEADER_MAGIC = 0x4f524243 | |
37 | HEADER_VERSION1 = 0x31313131 | |
38 | HEADER_VERSION2 = 0x31313132 | |
39 | ||
40 | # The file header, at the start of each file in the CBFS | |
41 | FILE_HEADER_FORMAT = b'>8sIIII' | |
42 | FILE_HEADER_LEN = 0x18 | |
43 | FILE_MAGIC = b'LARCHIVE' | |
44 | FILENAME_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 | |
48 | STAGE_FORMAT = '<IQQII' | |
49 | STAGE_LEN = 0x1c | |
50 | ||
51 | # An attribute describring the compression used in a file | |
52 | ATTR_COMPRESSION_FORMAT = '>IIII' | |
53 | ATTR_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. | |
58 | FILE_ATTR_TAG_UNUSED = 0 | |
59 | FILE_ATTR_TAG_UNUSED2 = 0xffffffff | |
60 | FILE_ATTR_TAG_COMPRESSION = 0x42435a4c | |
61 | FILE_ATTR_TAG_HASH = 0x68736148 | |
62 | FILE_ATTR_TAG_POSITION = 0x42435350 # PSCB | |
63 | FILE_ATTR_TAG_ALIGNMENT = 0x42434c41 # ALCB | |
64 | FILE_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. | |
73 | MIN_BOOTBLOCK_SIZE = 4 | |
74 | ||
75 | # Files start aligned to this boundary in the CBFS | |
76 | ENTRY_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. | |
82 | ARCHITECTURE_UNKNOWN = 0xffffffff | |
83 | ARCHITECTURE_X86 = 0x00000001 | |
84 | ARCHITECTURE_ARM = 0x00000010 | |
85 | ARCHITECTURE_AARCH64 = 0x0000aa64 | |
86 | ARCHITECTURE_MIPS = 0x00000100 | |
87 | ARCHITECTURE_RISCV = 0xc001d0de | |
88 | ARCHITECTURE_PPC64 = 0x407570ff | |
89 | ||
90 | ARCH_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 | |
101 | TYPE_CBFSHEADER = 0x02 # Master header, HEADER_FORMAT | |
102 | TYPE_STAGE = 0x10 # Stage, holding an executable, see STAGE_FORMAT | |
103 | TYPE_RAW = 0x50 # Raw file, possibly compressed | |
7c173ced | 104 | TYPE_EMPTY = 0xffffffff # Empty data |
4997a7ed SG |
105 | |
106 | # Compression types | |
107 | COMPRESS_NONE, COMPRESS_LZMA, COMPRESS_LZ4 = range(3) | |
108 | ||
109 | COMPRESS_NAMES = { | |
110 | COMPRESS_NONE : 'none', | |
111 | COMPRESS_LZMA : 'lzma', | |
112 | COMPRESS_LZ4 : 'lz4', | |
113 | } | |
114 | ||
115 | def 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 | ||
129 | def 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 | ||
3a9c2525 SG |
143 | def 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 | ||
4997a7ed SG |
157 | def 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 | ||
7c173ced SG |
169 | def 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 | ||
4997a7ed SG |
182 | def _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 | ||
196 | class 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. | |
1223db03 SG |
200 | Use the get_data_and_offset() method to obtain the raw output for writing to |
201 | CBFS. | |
4997a7ed SG |
202 | |
203 | Properties: | |
204 | name: Name of file | |
205 | offset: Offset of file data from start of file header | |
e073d4e1 SG |
206 | cbfs_offset: Offset of file data in bytes from start of CBFS, or None to |
207 | place this file anyway | |
4997a7ed | 208 | data: Contents of file, uncompressed |
eb0f4a4c | 209 | orig_data: Original data added to the file, possibly compressed |
4997a7ed SG |
210 | data_len: Length of (possibly compressed) data in bytes |
211 | ftype: File type (TYPE_...) | |
212 | compression: Compression type (COMPRESS_...) | |
52107ee4 SG |
213 | memlen: Length of data in memory, i.e. the uncompressed length, None if |
214 | no compression algortihm is selected | |
4997a7ed SG |
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 | |
7c173ced SG |
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) | |
4997a7ed | 222 | """ |
e073d4e1 | 223 | def __init__(self, name, ftype, data, cbfs_offset, compress=COMPRESS_NONE): |
4997a7ed SG |
224 | self.name = name |
225 | self.offset = None | |
e073d4e1 | 226 | self.cbfs_offset = cbfs_offset |
4997a7ed | 227 | self.data = data |
eb0f4a4c | 228 | self.orig_data = data |
4997a7ed SG |
229 | self.ftype = ftype |
230 | self.compress = compress | |
52107ee4 | 231 | self.memlen = None |
4997a7ed SG |
232 | self.load = None |
233 | self.entry = None | |
234 | self.base_address = None | |
52107ee4 | 235 | self.data_len = len(data) |
7c173ced SG |
236 | self.erase_byte = None |
237 | self.size = None | |
4997a7ed SG |
238 | |
239 | def decompress(self): | |
240 | """Handle decompressing data if necessary""" | |
241 | indata = self.data | |
242 | if self.compress == COMPRESS_LZ4: | |
eb0f4a4c | 243 | data = tools.Decompress(indata, 'lz4', with_header=False) |
4997a7ed | 244 | elif self.compress == COMPRESS_LZMA: |
eb0f4a4c | 245 | data = tools.Decompress(indata, 'lzma', with_header=False) |
4997a7ed SG |
246 | else: |
247 | data = indata | |
248 | self.memlen = len(data) | |
249 | self.data = data | |
250 | self.data_len = len(indata) | |
251 | ||
252 | @classmethod | |
e073d4e1 | 253 | def stage(cls, base_address, name, data, cbfs_offset): |
4997a7ed SG |
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 | |
e073d4e1 SG |
261 | cbfs_offset: Offset of file data in bytes from start of CBFS, or |
262 | None to place this file anyway | |
4997a7ed SG |
263 | |
264 | Returns: | |
265 | CbfsFile object containing the file information | |
266 | """ | |
e073d4e1 | 267 | cfile = CbfsFile(name, TYPE_STAGE, data, cbfs_offset) |
4997a7ed SG |
268 | cfile.base_address = base_address |
269 | return cfile | |
270 | ||
271 | @classmethod | |
e073d4e1 | 272 | def raw(cls, name, data, cbfs_offset, compress): |
4997a7ed SG |
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 | |
e073d4e1 SG |
279 | cbfs_offset: Offset of file data in bytes from start of CBFS, or |
280 | None to place this file anyway | |
4997a7ed SG |
281 | compress: Compression algorithm to use (COMPRESS_...) |
282 | ||
283 | Returns: | |
284 | CbfsFile object containing the file information | |
285 | """ | |
e073d4e1 | 286 | return CbfsFile(name, TYPE_RAW, data, cbfs_offset, compress) |
4997a7ed | 287 | |
7c173ced SG |
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 | """ | |
e073d4e1 | 301 | cfile = CbfsFile('', TYPE_EMPTY, b'', None) |
7c173ced SG |
302 | cfile.size = space_to_use - FILE_HEADER_LEN - FILENAME_ALIGN |
303 | cfile.erase_byte = erase_byte | |
304 | return cfile | |
305 | ||
e073d4e1 SG |
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 | ||
1223db03 SG |
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 | |
4997a7ed SG |
341 | |
342 | Returns: | |
1223db03 SG |
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. | |
4997a7ed SG |
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'' | |
e073d4e1 | 353 | pad = b'' |
4997a7ed SG |
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: | |
eb0f4a4c | 364 | data = tools.Compress(orig_data, 'lz4', with_header=False) |
4997a7ed | 365 | elif self.compress == COMPRESS_LZMA: |
eb0f4a4c | 366 | data = tools.Compress(orig_data, 'lzma', with_header=False) |
52107ee4 SG |
367 | self.memlen = len(orig_data) |
368 | self.data_len = len(data) | |
4997a7ed SG |
369 | attr = struct.pack(ATTR_COMPRESSION_FORMAT, |
370 | FILE_ATTR_TAG_COMPRESSION, ATTR_COMPRESSION_LEN, | |
52107ee4 | 371 | self.compress, self.memlen) |
7c173ced SG |
372 | elif self.ftype == TYPE_EMPTY: |
373 | data = tools.GetBytes(self.erase_byte, self.size) | |
4997a7ed SG |
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) | |
e073d4e1 SG |
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 | |
1223db03 SG |
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, | |
4997a7ed | 396 | self.ftype, attr_pos, hdr_len) |
e073d4e1 SG |
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)) | |
1223db03 | 407 | return hdr + name + attr + pad + content + data, hdr_len |
4997a7ed SG |
408 | |
409 | ||
410 | class 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 | ... | |
1223db03 | 418 | data, cbfs_offset = cbw.get_data_and_offset() |
4997a7ed SG |
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 | ||
7c173ced SG |
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) | |
1223db03 | 501 | fd.write(cbf.get_data_and_offset()[0]) |
7c173ced SG |
502 | self._skip_to(fd, offset) |
503 | ||
4997a7ed SG |
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 | ||
e073d4e1 | 520 | def add_file_stage(self, name, data, cbfs_offset=None): |
4997a7ed SG |
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 | |
e073d4e1 SG |
527 | cbfs_offset: Offset of this file's data within the CBFS, in bytes, |
528 | or None to place this file anywhere | |
4997a7ed SG |
529 | |
530 | Returns: | |
531 | CbfsFile object created | |
532 | """ | |
e073d4e1 | 533 | cfile = CbfsFile.stage(self._base_address, name, data, cbfs_offset) |
4997a7ed SG |
534 | self._files[name] = cfile |
535 | return cfile | |
536 | ||
e073d4e1 SG |
537 | def add_file_raw(self, name, data, cbfs_offset=None, |
538 | compress=COMPRESS_NONE): | |
4997a7ed SG |
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 | |
e073d4e1 SG |
545 | cbfs_offset: Offset of this file's data within the CBFS, in bytes, |
546 | or None to place this file anywhere | |
4997a7ed SG |
547 | compress: Compression algorithm to use (COMPRESS_...) |
548 | ||
549 | Returns: | |
550 | CbfsFile object created | |
551 | """ | |
e073d4e1 | 552 | cfile = CbfsFile.raw(name, data, cbfs_offset, compress) |
4997a7ed SG |
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: | |
7c173ced | 568 | self._pad_to(fd, self._header_offset) |
4997a7ed SG |
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(): | |
e073d4e1 SG |
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)) | |
1223db03 SG |
605 | pos = fd.tell() |
606 | data, data_offset = cbf.get_data_and_offset(pos, self._erase_byte) | |
607 | fd.write(data) | |
4997a7ed | 608 | self._align_to(fd, self._align) |
1223db03 | 609 | cbf.calced_cbfs_offset = pos + data_offset |
4997a7ed SG |
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 | |
7c173ced | 614 | self._pad_to(fd, self._base_address or self._size - 4) |
4997a7ed SG |
615 | rel_offset = self._header_offset - self._size |
616 | fd.write(struct.pack('<I', rel_offset & 0xffffffff)) | |
617 | ||
618 | return fd.getvalue() | |
619 | ||
620 | ||
621 | class 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: | |
17a7421f | 718 | print('File header at %#x ran out of data' % file_pos) |
4997a7ed SG |
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: | |
17a7421f | 727 | print('String at %#x ran out of data' % pos) |
4997a7ed SG |
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 | |
e073d4e1 SG |
740 | cbfs_offset = file_pos + offset |
741 | fd.seek(cbfs_offset, io.SEEK_SET) | |
4997a7ed SG |
742 | if ftype == TYPE_CBFSHEADER: |
743 | self._read_header(fd) | |
744 | elif ftype == TYPE_STAGE: | |
745 | data = fd.read(STAGE_LEN) | |
e073d4e1 SG |
746 | cfile = CbfsFile.stage(self.stage_base_address, name, b'', |
747 | cbfs_offset) | |
4997a7ed SG |
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) | |
e073d4e1 | 753 | cfile = CbfsFile.raw(name, data, cbfs_offset, compress) |
4997a7ed SG |
754 | cfile.decompress() |
755 | if DEBUG: | |
756 | print('data', data) | |
7c173ced SG |
757 | elif ftype == TYPE_EMPTY: |
758 | # Just read the data and discard it, since it is only padding | |
759 | fd.read(size) | |
e073d4e1 | 760 | cfile = CbfsFile('', TYPE_EMPTY, b'', cbfs_offset) |
4997a7ed SG |
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 | |
e073d4e1 SG |
777 | header. The only attributes currently supported are compression and the |
778 | unused tag. | |
4997a7ed SG |
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) | |
e073d4e1 SG |
807 | elif atag == FILE_ATTR_TAG_UNUSED2: |
808 | break | |
4997a7ed SG |
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 | ||
e073d4e1 | 866 | def cbfstool(fname, *cbfs_args, **kwargs): |
4997a7ed SG |
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 | """ | |
e073d4e1 SG |
879 | args = ['cbfstool', fname] + list(cbfs_args) |
880 | if kwargs.get('base') is not None: | |
881 | args += ['-b', '%#x' % kwargs['base']] | |
4997a7ed SG |
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))) |