cbfs.py 5.02 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# -*- coding: utf-8 -*-
#
# diffoscope: in-depth comparison of files, archives, and directories
#
# Copyright © 2015 Jérémy Bobbio <lunar@debian.org>
#
# diffoscope is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# diffoscope is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
18
# along with diffoscope.  If not, see <https://www.gnu.org/licenses/>.
19 20 21 22 23

import io
import os
import re
import struct
24
import logging
Chris Lamb's avatar
Chris Lamb committed
25 26
import subprocess

27
from diffoscope.tools import tool_required
28
from diffoscope.difference import Difference
29

30
from .utils.file import File
31 32
from .utils.archive import Archive
from .utils.command import Command
33

34 35
logger = logging.getLogger(__name__)

36 37 38 39

class CbfsListing(Command):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
40 41
        self._header_re = re.compile(
            r'^.*: ([^,]+, bootblocksize [0-9]+, romsize [0-9]+, offset 0x[0-9A-Fa-f]+)$')
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

    @tool_required('cbfstool')
    def cmdline(self):
        return ['cbfstool', self.path, 'print']

    def filter(self, line):
        return self._header_re.sub('\\1', line.decode('utf-8')).encode('utf-8')


class CbfsContainer(Archive):
    @tool_required('cbfstool')
    def entries(self, path):
        cmd = ['cbfstool', path, 'print']
        output = subprocess.check_output(cmd, shell=False).decode('utf-8')
        header = True
        for line in output.rstrip('\n').split('\n'):
            if header:
                if line.startswith('Name'):
                    header = False
                continue
62
            name = line.split()[0]
63 64 65 66
            if name == '(empty)':
                continue
            yield name

67
    def open_archive(self):
68 69 70 71 72 73 74 75 76 77 78
        return self

    def close_archive(self):
        pass

    def get_member_names(self):
        return list(self.entries(self.source.path))

    @tool_required('cbfstool')
    def extract(self, member_name, dest_dir):
        dest_path = os.path.join(dest_dir, os.path.basename(member_name))
79 80
        cmd = ['cbfstool', self.source.path, 'extract',
            '-n', member_name, '-f', dest_path]
81
        logger.debug("cbfstool extract %s to %s", member_name, dest_path)
82 83
        subprocess.check_call(
            cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
84 85 86 87 88 89
        return dest_path


CBFS_HEADER_MAGIC = 0x4F524243
CBFS_HEADER_VERSION1 = 0x31313131
CBFS_HEADER_VERSION2 = 0x31313132
Mattia Rizzolo's avatar
Mattia Rizzolo committed
90
CBFS_HEADER_SIZE = 8 * 4  # 8 * uint32_t
91

92
# On 2015-12-15, the largest image produced by coreboot is 16 MiB
Mattia Rizzolo's avatar
Mattia Rizzolo committed
93
CBFS_MAXIMUM_FILE_SIZE = 24 * 2 ** 20  # 24 MiB
94

Mattia Rizzolo's avatar
Mattia Rizzolo committed
95

96
def is_header_valid(buf, size, offset=0):
97 98
    magic, version, romsize, bootblocksize, align, cbfs_offset, architecture, pad = struct.unpack_from(
        '!IIIIIIII', buf, offset)
99
    return magic == CBFS_HEADER_MAGIC and \
Mattia Rizzolo's avatar
Mattia Rizzolo committed
100 101 102
        (version == CBFS_HEADER_VERSION1 or version == CBFS_HEADER_VERSION2) and \
        (romsize <= size) and \
        (cbfs_offset < romsize)
103 104 105


class CbfsFile(File):
106
    DESCRIPTION = "Coreboot CBFS filesystem images"
107 108
    CONTAINER_CLASS = CbfsContainer

109 110
    @classmethod
    def recognizes(cls, file):
111
        size = os.stat(file.path).st_size
112
        if size < CBFS_HEADER_SIZE or size > CBFS_MAXIMUM_FILE_SIZE:
113 114 115 116 117 118 119 120 121 122 123 124 125
            return False
        with open(file.path, 'rb') as f:
            # pick at the latest byte as it should contain the relative offset of the header
            f.seek(-4, io.SEEK_END)
            # <pgeorgi> given the hardware we support so far, it looks like
            #           that field is now bound to be little endian
            #   -- #coreboot, 2015-10-14
            rel_offset = struct.unpack('<i', f.read(4))[0]
            if rel_offset < 0 and -rel_offset > CBFS_HEADER_SIZE and -rel_offset < size:
                f.seek(rel_offset, io.SEEK_END)
                logger.debug('looking for header at offset: %x', f.tell())
                if is_header_valid(f.read(CBFS_HEADER_SIZE), size):
                    return True
126 127 128
            elif not file.name.endswith('.rom'):
                return False
            else:
129 130
                logger.debug(
                    'CBFS relative offset seems wrong, scanning whole image')
131 132 133 134 135 136 137 138 139 140 141 142 143
            f.seek(0, io.SEEK_SET)
            offset = 0
            buf = f.read(CBFS_HEADER_SIZE)
            while len(buf) >= CBFS_HEADER_SIZE:
                if is_header_valid(buf, size, offset):
                    return True
                if len(buf) - offset <= CBFS_HEADER_SIZE:
                    buf = f.read(32768)
                    offset = 0
                else:
                    offset += 1
            return False

144
    def compare_details(self, other, source=None):
145
        return [Difference.from_command(CbfsListing, self.path, other.path)]