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