#!/usr/bin/env python # SPDX-License-Identifier: GPL-2.0+ # Copyright 2019 Google LLC # Written by Simon Glass """Tests for cbfs_util These create and read various CBFSs and compare the results with expected values and with cbfstool """ import io import os import shutil import struct import tempfile import unittest from binman import bintool from binman import cbfs_util from binman.cbfs_util import CbfsWriter from binman import elf from u_boot_pylib import test_util from u_boot_pylib import tools U_BOOT_DATA = b'1234' U_BOOT_DTB_DATA = b'udtb' COMPRESS_DATA = b'compress xxxxxxxxxxxxxxxxxxxxxx data' class TestCbfs(unittest.TestCase): """Test of cbfs_util classes""" #pylint: disable=W0212 @classmethod def setUpClass(cls): # Create a temporary directory for test files cls._indir = tempfile.mkdtemp(prefix='cbfs_util.') tools.set_input_dirs([cls._indir]) # Set up some useful data files TestCbfs._make_input_file('u-boot.bin', U_BOOT_DATA) TestCbfs._make_input_file('u-boot.dtb', U_BOOT_DTB_DATA) TestCbfs._make_input_file('compress', COMPRESS_DATA) # Set up a temporary output directory, used by the tools library when # compressing files tools.prepare_output_dir(None) cls.cbfstool = bintool.Bintool.create('cbfstool') cls.have_cbfstool = cls.cbfstool.is_present() lz4 = bintool.Bintool.create('lz4') cls.have_lz4 = lz4.is_present() @classmethod def tearDownClass(cls): """Remove the temporary input directory and its contents""" if cls._indir: shutil.rmtree(cls._indir) cls._indir = None tools.finalise_output_dir() @classmethod def _make_input_file(cls, fname, contents): """Create a new test input file, creating directories as needed Args: fname: Filename to create contents: File contents to write in to the file Returns: Full pathname of file created """ pathname = os.path.join(cls._indir, fname) tools.write_file(pathname, contents) return pathname def _check_hdr(self, data, size, offset=0, arch=cbfs_util.ARCHITECTURE_X86): """Check that the CBFS has the expected header Args: data: Data to check size: Expected ROM size offset: Expected offset to first CBFS file arch: Expected architecture Returns: CbfsReader object containing the CBFS """ cbfs = cbfs_util.CbfsReader(data) self.assertEqual(cbfs_util.HEADER_MAGIC, cbfs.magic) self.assertEqual(cbfs_util.HEADER_VERSION2, cbfs.version) self.assertEqual(size, cbfs.rom_size) self.assertEqual(0, cbfs.boot_block_size) self.assertEqual(cbfs_util.ENTRY_ALIGN, cbfs.align) self.assertEqual(offset, cbfs.cbfs_offset) self.assertEqual(arch, cbfs.arch) return cbfs def _check_uboot(self, cbfs, ftype=cbfs_util.TYPE_RAW, offset=0x38, data=U_BOOT_DATA, cbfs_offset=None): """Check that the U-Boot file is as expected Args: cbfs: CbfsReader object to check ftype: Expected file type offset: Expected offset of file data: Expected data in file cbfs_offset: Expected CBFS offset for file's data Returns: CbfsFile object containing the file """ self.assertIn('u-boot', cbfs.files) cfile = cbfs.files['u-boot'] self.assertEqual('u-boot', cfile.name) self.assertEqual(offset, cfile.offset) if cbfs_offset is not None: self.assertEqual(cbfs_offset, cfile.cbfs_offset) self.assertEqual(data, cfile.data) self.assertEqual(ftype, cfile.ftype) self.assertEqual(cbfs_util.COMPRESS_NONE, cfile.compress) self.assertEqual(len(data), cfile.memlen) return cfile def _check_dtb(self, cbfs, offset=0x38, data=U_BOOT_DTB_DATA, cbfs_offset=None): """Check that the U-Boot dtb file is as expected Args: cbfs: CbfsReader object to check offset: Expected offset of file data: Expected data in file cbfs_offset: Expected CBFS offset for file's data """ self.assertIn('u-boot-dtb', cbfs.files) cfile = cbfs.files['u-boot-dtb'] self.assertEqual('u-boot-dtb', cfile.name) self.assertEqual(offset, cfile.offset) if cbfs_offset is not None: self.assertEqual(cbfs_offset, cfile.cbfs_offset) self.assertEqual(U_BOOT_DTB_DATA, cfile.data) self.assertEqual(cbfs_util.TYPE_RAW, cfile.ftype) self.assertEqual(cbfs_util.COMPRESS_NONE, cfile.compress) self.assertEqual(len(U_BOOT_DTB_DATA), cfile.memlen) def _check_raw(self, data, size, offset=0, arch=cbfs_util.ARCHITECTURE_X86): """Check that two raw files are added as expected Args: data: Data to check size: Expected ROM size offset: Expected offset to first CBFS file arch: Expected architecture """ cbfs = self._check_hdr(data, size, offset=offset, arch=arch) self._check_uboot(cbfs) self._check_dtb(cbfs) def _get_expected_cbfs(self, size, arch='x86', compress=None, base=None): """Get the file created by cbfstool for a particular scenario Args: size: Size of the CBFS in bytes arch: Architecture of the CBFS, as a string compress: Compression to use, e.g. cbfs_util.COMPRESS_LZMA base: Base address of file, or None to put it anywhere Returns: Resulting CBFS file, or None if cbfstool is not available """ if not self.have_cbfstool or not self.have_lz4: return None cbfs_fname = os.path.join(self._indir, 'test.cbfs') self.cbfstool.create_new(cbfs_fname, size, arch) if base: base = [(1 << 32) - size + b for b in base] self.cbfstool.add_raw( cbfs_fname, 'u-boot', tools.get_input_filename(compress and 'compress' or 'u-boot.bin'), compress[0] if compress else None, base[0] if base else None) self.cbfstool.add_raw( cbfs_fname, 'u-boot-dtb', tools.get_input_filename(compress and 'compress' or 'u-boot.dtb'), compress[1] if compress else None, base[1] if base else None) return cbfs_fname def _compare_expected_cbfs(self, data, cbfstool_fname): """Compare against what cbfstool creates This compares what binman creates with what cbfstool creates for what is proportedly the same thing. Args: data: CBFS created by binman cbfstool_fname: CBFS created by cbfstool """ if not self.have_cbfstool or not self.have_lz4: return expect = tools.read_file(cbfstool_fname) if expect != data: tools.write_file('/tmp/expect', expect) tools.write_file('/tmp/actual', data) print('diff -y <(xxd -g1 /tmp/expect) <(xxd -g1 /tmp/actual) | colordiff') self.fail('cbfstool produced a different result') def test_cbfs_functions(self): """Test global functions of cbfs_util""" self.assertEqual(cbfs_util.ARCHITECTURE_X86, cbfs_util.find_arch('x86')) self.assertIsNone(cbfs_util.find_arch('bad-arch')) self.assertEqual(cbfs_util.COMPRESS_LZMA, cbfs_util.find_compress('lzma')) self.assertIsNone(cbfs_util.find_compress('bad-comp')) def test_cbfstool_failure(self): """Test failure to run cbfstool""" if not self.have_cbfstool: self.skipTest('No cbfstool available') with self.assertRaises(ValueError) as exc: out = self.cbfstool.fail() self.assertIn('cbfstool missing-file bad-command', str(exc.exception)) def test_cbfs_raw(self): """Test base handling of a Coreboot Filesystem (CBFS)""" size = 0xb0 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA) cbw.add_file_raw('u-boot-dtb', U_BOOT_DTB_DATA) data = cbw.get_data() self._check_raw(data, size) cbfs_fname = self._get_expected_cbfs(size=size) self._compare_expected_cbfs(data, cbfs_fname) def test_cbfs_invalid_file_type(self): """Check handling of an invalid file type when outputiing a CBFS""" size = 0xb0 cbw = CbfsWriter(size) cfile = cbw.add_file_raw('u-boot', U_BOOT_DATA) # Change the type manually before generating the CBFS, and make sure # that the generator complains cfile.ftype = 0xff with self.assertRaises(ValueError) as e: cbw.get_data() self.assertIn('Unknown type 0xff when writing', str(e.exception)) def test_cbfs_invalid_file_type_on_read(self): """Check handling of an invalid file type when reading the CBFS""" size = 0xb0 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA) data = cbw.get_data() # Read in the first file header cbr = cbfs_util.CbfsReader(data, read=False) with io.BytesIO(data) as fd: self.assertTrue(cbr._find_and_read_header(fd, len(data))) pos = fd.tell() hdr_data = fd.read(cbfs_util.FILE_HEADER_LEN) magic, size, ftype, attr, offset = struct.unpack( cbfs_util.FILE_HEADER_FORMAT, hdr_data) # Create a new CBFS with a change to the file type ftype = 0xff newdata = data[:pos] newdata += struct.pack(cbfs_util.FILE_HEADER_FORMAT, magic, size, ftype, attr, offset) newdata += data[pos + cbfs_util.FILE_HEADER_LEN:] # Read in this CBFS and make sure that the reader complains with self.assertRaises(ValueError) as e: cbfs_util.CbfsReader(newdata) self.assertIn('Unknown type 0xff when reading', str(e.exception)) def test_cbfs_no_space(self): """Check handling of running out of space in the CBFS""" size = 0x60 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA) with self.assertRaises(ValueError) as e: cbw.get_data() self.assertIn('No space for header', str(e.exception)) def test_cbfs_no_space_skip(self): """Check handling of running out of space in CBFS with file header""" size = 0x5c cbw = CbfsWriter(size, arch=cbfs_util.ARCHITECTURE_PPC64) cbw._add_fileheader = True cbw.add_file_raw('u-boot', U_BOOT_DATA) with self.assertRaises(ValueError) as e: cbw.get_data() self.assertIn('No space for data before offset', str(e.exception)) def test_cbfs_no_space_pad(self): """Check handling of running out of space in CBFS with file header""" size = 0x70 cbw = CbfsWriter(size) cbw._add_fileheader = True cbw.add_file_raw('u-boot', U_BOOT_DATA) with self.assertRaises(ValueError) as e: cbw.get_data() self.assertIn('No space for data before pad offset', str(e.exception)) def test_cbfs_bad_header_ptr(self): """Check handling of a bad master-header pointer""" size = 0x70 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA) data = cbw.get_data() # Add one to the pointer to make it invalid newdata = data[:-4] + struct.pack('I', fd.read(4)) if tag == cbfs_util.FILE_ATTR_TAG_COMPRESSION: break # Create a new CBFS with the tag changed to something invalid newdata = data[:pos] + struct.pack('>I', 0x123) + data[pos + 4:] with test_util.capture_sys_output() as (stdout, _stderr): cbfs_util.CbfsReader(newdata) self.assertEqual('Unknown attribute tag 123\n', stdout.getvalue()) def test_cbfs_missing_attribute(self): """Check handling of an incomplete attribute tag""" if not self.have_lz4: self.skipTest('lz4 --no-frame-crc not available') size = 0x140 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', COMPRESS_DATA, None, compress=cbfs_util.COMPRESS_LZ4) data = cbw.get_data() # Read in the CBFS master header (only), then stop cbr = cbfs_util.CbfsReader(data, read=False) with io.BytesIO(data) as fd: self.assertTrue(cbr._find_and_read_header(fd, len(data))) pos = fd.tell() # Create a new CBFS with only the first 4 bytes of the compression tag, # then try to read the file tag_pos = pos + cbfs_util.FILE_HEADER_LEN + cbfs_util.FILENAME_ALIGN newdata = data[:tag_pos + 4] with test_util.capture_sys_output() as (stdout, _stderr): with io.BytesIO(newdata) as fd: fd.seek(pos) self.assertEqual(False, cbr._read_next_file(fd)) self.assertIn('Attribute tag at %x ran out of data' % tag_pos, stdout.getvalue()) def test_cbfs_file_master_header(self): """Check handling of a file containing a master header""" size = 0x100 cbw = CbfsWriter(size) cbw._add_fileheader = True cbw.add_file_raw('u-boot', U_BOOT_DATA) data = cbw.get_data() cbr = cbfs_util.CbfsReader(data) self.assertIn('u-boot', cbr.files) self.assertEqual(size, cbr.rom_size) def test_cbfs_arch(self): """Test on non-x86 architecture""" size = 0x100 cbw = CbfsWriter(size, arch=cbfs_util.ARCHITECTURE_PPC64) cbw.add_file_raw('u-boot', U_BOOT_DATA) cbw.add_file_raw('u-boot-dtb', U_BOOT_DTB_DATA) data = cbw.get_data() self._check_raw(data, size, offset=0x40, arch=cbfs_util.ARCHITECTURE_PPC64) # Compare against what cbfstool creates cbfs_fname = self._get_expected_cbfs(size=size, arch='ppc64') self._compare_expected_cbfs(data, cbfs_fname) def test_cbfs_stage(self): """Tests handling of a Coreboot Filesystem (CBFS)""" if not elf.ELF_TOOLS: self.skipTest('Python elftools not available') elf_fname = os.path.join(self._indir, 'cbfs-stage.elf') elf.MakeElf(elf_fname, U_BOOT_DATA, U_BOOT_DTB_DATA) size = 0xb0 cbw = CbfsWriter(size) cbw.add_file_stage('u-boot', tools.read_file(elf_fname)) data = cbw.get_data() cbfs = self._check_hdr(data, size) load = 0xfef20000 entry = load + 2 cfile = self._check_uboot(cbfs, cbfs_util.TYPE_STAGE, offset=0x28, data=U_BOOT_DATA + U_BOOT_DTB_DATA) self.assertEqual(entry, cfile.entry) self.assertEqual(load, cfile.load) self.assertEqual(len(U_BOOT_DATA) + len(U_BOOT_DTB_DATA), cfile.data_len) # Compare against what cbfstool creates if self.have_cbfstool: cbfs_fname = os.path.join(self._indir, 'test.cbfs') self.cbfstool.create_new(cbfs_fname, size) self.cbfstool.add_stage(cbfs_fname, 'u-boot', elf_fname) self._compare_expected_cbfs(data, cbfs_fname) def test_cbfs_raw_compress(self): """Test base handling of compressing raw files""" if not self.have_lz4: self.skipTest('lz4 --no-frame-crc not available') size = 0x140 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', COMPRESS_DATA, None, compress=cbfs_util.COMPRESS_LZ4) cbw.add_file_raw('u-boot-dtb', COMPRESS_DATA, None, compress=cbfs_util.COMPRESS_LZMA) data = cbw.get_data() cbfs = self._check_hdr(data, size) self.assertIn('u-boot', cbfs.files) cfile = cbfs.files['u-boot'] self.assertEqual(cfile.name, 'u-boot') self.assertEqual(cfile.offset, 56) self.assertEqual(cfile.data, COMPRESS_DATA) self.assertEqual(cfile.ftype, cbfs_util.TYPE_RAW) self.assertEqual(cfile.compress, cbfs_util.COMPRESS_LZ4) self.assertEqual(cfile.memlen, len(COMPRESS_DATA)) self.assertIn('u-boot-dtb', cbfs.files) cfile = cbfs.files['u-boot-dtb'] self.assertEqual(cfile.name, 'u-boot-dtb') self.assertEqual(cfile.offset, 56) self.assertEqual(cfile.data, COMPRESS_DATA) self.assertEqual(cfile.ftype, cbfs_util.TYPE_RAW) self.assertEqual(cfile.compress, cbfs_util.COMPRESS_LZMA) self.assertEqual(cfile.memlen, len(COMPRESS_DATA)) cbfs_fname = self._get_expected_cbfs(size=size, compress=['lz4', 'lzma']) self._compare_expected_cbfs(data, cbfs_fname) def test_cbfs_raw_space(self): """Test files with unused space in the CBFS""" size = 0xf0 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA) cbw.add_file_raw('u-boot-dtb', U_BOOT_DTB_DATA) data = cbw.get_data() self._check_raw(data, size) cbfs_fname = self._get_expected_cbfs(size=size) self._compare_expected_cbfs(data, cbfs_fname) def test_cbfs_offset(self): """Test a CBFS with files at particular offsets""" size = 0x200 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA, 0x40) cbw.add_file_raw('u-boot-dtb', U_BOOT_DTB_DATA, 0x140) data = cbw.get_data() cbfs = self._check_hdr(data, size) self._check_uboot(cbfs, ftype=cbfs_util.TYPE_RAW, offset=0x40, cbfs_offset=0x40) self._check_dtb(cbfs, offset=0x40, cbfs_offset=0x140) cbfs_fname = self._get_expected_cbfs(size=size, base=(0x40, 0x140)) self._compare_expected_cbfs(data, cbfs_fname) def test_cbfs_invalid_file_type_header(self): """Check handling of an invalid file type when outputting a header""" size = 0xb0 cbw = CbfsWriter(size) cfile = cbw.add_file_raw('u-boot', U_BOOT_DATA, 0) # Change the type manually before generating the CBFS, and make sure # that the generator complains cfile.ftype = 0xff with self.assertRaises(ValueError) as e: cbw.get_data() self.assertIn('Unknown file type 0xff', str(e.exception)) def test_cbfs_offset_conflict(self): """Test a CBFS with files that want to overlap""" size = 0x200 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA, 0x40) cbw.add_file_raw('u-boot-dtb', U_BOOT_DTB_DATA, 0x80) with self.assertRaises(ValueError) as e: cbw.get_data() self.assertIn('No space for data before pad offset', str(e.exception)) def test_cbfs_check_offset(self): """Test that we can discover the offset of a file after writing it""" size = 0xb0 cbw = CbfsWriter(size) cbw.add_file_raw('u-boot', U_BOOT_DATA) cbw.add_file_raw('u-boot-dtb', U_BOOT_DTB_DATA) data = cbw.get_data() cbfs = cbfs_util.CbfsReader(data) self.assertEqual(0x38, cbfs.files['u-boot'].cbfs_offset) self.assertEqual(0x78, cbfs.files['u-boot-dtb'].cbfs_offset) if __name__ == '__main__': unittest.main()