blob: 52d9f9cc3618500cd27c9f9121311553c2398e25 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2010, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Alternatively, this software may be distributed under the terms of the
# GNU General Public License ("GPL") version 2 as published by the Free
# Software Foundation.
"""Basic encode/decode functionality of flashrom memory map (FMAP) structures.
Usage:
(decode)
obj = fmap_decode(blob)
print obj
(encode)
blob = fmap_encode(obj)
open('output.bin', 'w').write(blob)
The object returned by fmap_decode is a dictionary with names defined in
fmap.h. A special property 'FLAGS' is provided as a readable and read-only
tuple of decoded area flags.
"""
import argparse
import copy
import logging
import pprint
import struct
import sys
# constants imported from lib/fmap.h
FMAP_SIGNATURE = b"__FMAP__"
FMAP_VER_MAJOR = 1
FMAP_VER_MINOR_MIN = 0
FMAP_VER_MINOR_MAX = 1
FMAP_STRLEN = 32
FMAP_SEARCH_STRIDE = 4
FMAP_FLAGS = {
"FMAP_AREA_STATIC": 1 << 0,
"FMAP_AREA_COMPRESSED": 1 << 1,
"FMAP_AREA_RO": 1 << 2,
"FMAP_AREA_PRESERVE": 1 << 3,
}
FMAP_HEADER_NAMES = (
"signature",
"ver_major",
"ver_minor",
"base",
"size",
"name",
"nareas",
)
FMAP_AREA_NAMES = (
"offset",
"size",
"name",
"flags",
)
# format string
FMAP_HEADER_FORMAT = "<8sBBQI%dsH" % (FMAP_STRLEN)
FMAP_AREA_FORMAT = "<II%dsH" % (FMAP_STRLEN)
def _fmap_decode_header(blob, offset):
"""(internal) Decodes a FMAP header from blob by offset"""
header = {}
for name, value in zip(
FMAP_HEADER_NAMES, struct.unpack_from(FMAP_HEADER_FORMAT, blob, offset)
):
header[name] = value
if header["signature"] != FMAP_SIGNATURE:
raise struct.error("Invalid signature")
if (
header["ver_major"] != FMAP_VER_MAJOR
or header["ver_minor"] < FMAP_VER_MINOR_MIN
or header["ver_minor"] > FMAP_VER_MINOR_MAX
):
raise struct.error("Incompatible version")
# convert null-terminated names
header["name"] = header["name"].strip(b"\x00")
# In Python 2, binary==string, so we don't need to convert.
if sys.version_info.major >= 3:
# Decode after verifying to avoid errors due to corruption.
for name in FMAP_HEADER_NAMES:
if hasattr(header[name], "decode"):
header[name] = header[name].decode("utf-8")
return (header, struct.calcsize(FMAP_HEADER_FORMAT))
def _fmap_decode_area(blob, offset):
"""(internal) Decodes a FMAP area record from blob by offset"""
area = {}
for name, value in zip(
FMAP_AREA_NAMES, struct.unpack_from(FMAP_AREA_FORMAT, blob, offset)
):
area[name] = value
# convert null-terminated names
area["name"] = area["name"].strip(b"\x00")
# add a (readonly) readable FLAGS
area["FLAGS"] = _fmap_decode_area_flags(area["flags"])
# In Python 2, binary==string, so we don't need to convert.
if sys.version_info.major >= 3:
for name in FMAP_AREA_NAMES:
if hasattr(area[name], "decode"):
area[name] = area[name].decode("utf-8")
return (area, struct.calcsize(FMAP_AREA_FORMAT))
def _fmap_decode_area_flags(area_flags):
"""(internal) Decodes a FMAP flags property"""
# Since FMAP_FLAGS is a dict with arbitrary ordering, sort the list so the
# output is stable. Also sorting is nicer for humans.
return tuple(sorted(x for x in FMAP_FLAGS if area_flags & FMAP_FLAGS[x]))
def _fmap_check_name(fmap, name):
"""Checks if the FMAP structure has correct name.
Args:
fmap: A decoded FMAP structure.
name: A string to specify expected FMAP name.
Raises:
struct.error if the name does not match.
"""
if fmap["name"] != name:
raise struct.error(
'Incorrect FMAP (found: "%s", expected: "%s")'
% (fmap["name"], name)
)
def _fmap_search_header(blob, fmap_name=None):
"""Searches FMAP headers in given blob.
Uses same logic from vboot_reference/host/lib/fmap.c.
Args:
blob: A string containing FMAP data.
fmap_name: A string to specify target FMAP name.
Returns:
A tuple of (fmap, size, offset).
"""
lim = len(blob) - struct.calcsize(FMAP_HEADER_FORMAT)
align = FMAP_SEARCH_STRIDE
# Search large alignments before small ones to find "right" FMAP.
while align <= lim:
align *= 2
while align >= FMAP_SEARCH_STRIDE:
for offset in range(align, lim + 1, align * 2):
if not blob.startswith(FMAP_SIGNATURE, offset):
continue
try:
(fmap, size) = _fmap_decode_header(blob, offset)
if fmap_name is not None:
_fmap_check_name(fmap, fmap_name)
return (fmap, size, offset)
except struct.error as e:
# Search for next FMAP candidate.
logging.debug("Continue searching FMAP due to exception %r", e)
align //= 2
raise struct.error("No valid FMAP signatures.")
def fmap_decode(blob, offset=None, fmap_name=None):
"""Decodes a blob to FMAP dictionary object.
Args:
blob: a binary data containing FMAP structure.
offset: starting offset of FMAP. When omitted, fmap_decode will search
in the blob.
fmap_name: A string to specify target FMAP name.
"""
fmap = {}
if offset is None:
(fmap, size, offset) = _fmap_search_header(blob, fmap_name)
else:
(fmap, size) = _fmap_decode_header(blob, offset)
if fmap_name is not None:
_fmap_check_name(fmap, fmap_name)
fmap["areas"] = []
offset = offset + size
for _ in range(fmap["nareas"]):
(area, size) = _fmap_decode_area(blob, offset)
offset = offset + size
fmap["areas"].append(area)
return fmap
def _fmap_encode_header(obj):
"""(internal) Encodes a FMAP header"""
# Convert strings to bytes.
obj = copy.deepcopy(obj)
for name in FMAP_HEADER_NAMES:
if hasattr(obj[name], "encode"):
obj[name] = obj[name].encode("utf-8")
values = [obj[name] for name in FMAP_HEADER_NAMES]
return struct.pack(FMAP_HEADER_FORMAT, *values)
def _fmap_encode_area(obj):
"""(internal) Encodes a FMAP area entry"""
# Convert strings to bytes.
obj = copy.deepcopy(obj)
for name in FMAP_AREA_NAMES:
if hasattr(obj[name], "encode"):
obj[name] = obj[name].encode("utf-8")
values = [obj[name] for name in FMAP_AREA_NAMES]
return struct.pack(FMAP_AREA_FORMAT, *values)
def fmap_encode(obj):
"""Encodes a FMAP dictionary object to blob.
Args:
obj: a FMAP dictionary object.
"""
# fix up values
obj["nareas"] = len(obj["areas"])
# TODO(hungte) re-assign signature / version?
blob = _fmap_encode_header(obj)
for area in obj["areas"]:
blob = blob + _fmap_encode_area(area)
return blob
def get_parser():
"""Return a command line parser."""
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument("file", help="The file to decode & print.")
parser.add_argument(
"--raw", action="store_true", help="Dump the object output for scripts."
)
return parser
def main(argv):
"""Decode FMAP from supplied file and print."""
parser = get_parser()
opts = parser.parse_args(argv)
if not opts.raw:
print("Decoding FMAP from: %s" % opts.file)
blob = open(opts.file, "rb").read()
obj = fmap_decode(blob)
if opts.raw:
print(obj)
else:
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(obj)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))