From f9f47395f18160e5511ffedfd4cd70914d11c3ae Mon Sep 17 00:00:00 2001 From: Chirayu Desai Date: Tue, 7 Dec 2021 03:07:13 +0530 Subject: [PATCH] Import fbpacktool from AOSP * qc_image_unpacker doesn't support Pixel 6, this does Source: https://developers.google.com/android/binary_transparency/pixel#fbpacktool Change-Id: If7a409ed9f84e9c8ff43a8aa3d26eaae2c8a0640 --- fbpacktool/.gitignore | 3 + fbpacktool/fbpack.py | 85 ++++++++++++ fbpacktool/fbpacktool | 1 + fbpacktool/fbpacktool.py | 256 +++++++++++++++++++++++++++++++++++++ fbpacktool/packedstruct.py | 61 +++++++++ 5 files changed, 406 insertions(+) create mode 100644 fbpacktool/.gitignore create mode 100644 fbpacktool/fbpack.py create mode 120000 fbpacktool/fbpacktool create mode 100755 fbpacktool/fbpacktool.py create mode 100644 fbpacktool/packedstruct.py diff --git a/fbpacktool/.gitignore b/fbpacktool/.gitignore new file mode 100644 index 0000000..6e4266f --- /dev/null +++ b/fbpacktool/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.py[cod] +*$py.class diff --git a/fbpacktool/fbpack.py b/fbpacktool/fbpack.py new file mode 100644 index 0000000..4d48834 --- /dev/null +++ b/fbpacktool/fbpack.py @@ -0,0 +1,85 @@ +# Copyright 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import struct +from packedstruct import PackedStruct +from collections import OrderedDict + +FBPACK_MAGIC = 0x4b504246 # "FBPK" FastBook PacK +FBPACK_VERSION = 2 +FBPACK_DEFAULT_DATA_ALIGN = 16 + +FBPACK_PARTITION_TABLE = 0 +FBPACK_PARTITION_DATA = 1 +FBPACK_SIDELOAD_DATA = 2 + + +class PackEntry(PackedStruct): + """Pack entry info.""" + + _FIELDS = OrderedDict([ + ('type', 'I'), + ('name', '36s'), + ('product', '40s'), + ('offset', 'Q'), + ('size', 'Q'), + ('slotted', 'I'), + ('crc32', 'I'), + ]) + + def __init__(self, type_=0, name=b'', prod=b'', offset=0, size=0, slotted=0, crc32=0): + super(PackEntry, self).__init__(type_, name, prod, offset, size, slotted, crc32) + + +class PackHeader(PackedStruct): + """ A packed image representation """ + + _FIELDS = OrderedDict([ + ('magic', 'I'), + ('version', 'I'), + ('header_size', 'I'), + ('entry_header_size', 'I'), + ('platform', '16s'), + ('pack_version', '64s'), + ('slot_type', 'I'), + ('data_align', 'I'), + ('total_entries', 'I'), + ('total_size', 'I'), + ]) + + def __init__( + self, + magic=FBPACK_MAGIC, + version=FBPACK_VERSION, + header_size=0, + entry_header_size=len(PackEntry()), + platform=b'', + pack_version=b'', + slot_type=0, + data_align=FBPACK_DEFAULT_DATA_ALIGN, + total_entries=0, + total_size=0): + super(PackHeader, self).__init__( + magic, + version, + header_size, + entry_header_size, + platform, + pack_version, + slot_type, + data_align, + total_entries, + total_size) + # update header size once we know all fields + self.header_size = len(self) diff --git a/fbpacktool/fbpacktool b/fbpacktool/fbpacktool new file mode 120000 index 0000000..0ac688a --- /dev/null +++ b/fbpacktool/fbpacktool @@ -0,0 +1 @@ +fbpacktool.py \ No newline at end of file diff --git a/fbpacktool/fbpacktool.py b/fbpacktool/fbpacktool.py new file mode 100755 index 0000000..4d1bdd4 --- /dev/null +++ b/fbpacktool/fbpacktool.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# +# Copyright 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import sys +import os +import argparse +import logging +from fbpack import * + + +def bytes_to_str(bstr): + return bstr.decode().rstrip('\x00') + + +def print_pack_header(pack): + print('magic: {:#x}'.format(pack.magic)) + print('version: {}'.format(pack.version)) + print('header size: {}'.format(pack.header_size)) + print('entry header size: {}'.format(pack.entry_header_size)) + platform = bytes_to_str(pack.platform) + print('platform: {}'.format(platform)) + pack_version = bytes_to_str(pack.pack_version) + print('pack version: {}'.format(pack_version)) + print('slock type: {}'.format(pack.slot_type)) + print('data align: {}'.format(pack.data_align)) + print('total entries: {}'.format(pack.total_entries)) + print('total size: {}'.format(pack.total_size)) + + +def print_pack_entry(entry, prefix): + name = bytes_to_str(entry.name) + print('{}name: {}'.format(prefix, name)) + etype = 'unknown' + if entry.type == FBPACK_PARTITION_TABLE: + etype = 'partiton table' + elif entry.type == FBPACK_PARTITION_DATA: + etype = 'partition' + elif entry.type == FBPACK_SIDELOAD_DATA: + etype = 'sideload' + else: + print("entry else") + print('{}type: {}'.format(prefix, etype)) + product = bytes_to_str(entry.product) + print('{}product: {}'.format(prefix, product)) + print('{}offset: {:#x} ({})'.format(prefix, entry.offset, entry.offset)) + print('{}size: {:#x} ({})'.format(prefix, entry.size, entry.size)) + print('{}slotted: {}'.format(entry.size, bool(entry.slotted))) + print('{}crc32: {:#08x}'.format(prefix, entry.crc32)) + + +def cmd_info(args): + with open(args.file, 'rb') as f: + pack = PackHeader.from_bytes(f.read(len(PackHeader()))) + + if pack.version != FBPACK_VERSION: + raise NotImplementedError('unsupported version {}'.format(pack.version)) + + print("Header:") + print_pack_header(pack) + + print('\nEntries:') + for i in range(1, pack.total_entries + 1): + entry = PackEntry.from_bytes(f.read(len(PackEntry()))) + print('Entry {}: {{'.format(i)) + print_pack_entry(entry, ' ') + print('}') + + +def align_up(val, align): + return (val + align - 1) & ~(align - 1) + + +def create_pack_file(file_name, in_dir_name, pack): + pack.total_entries = len(pack.entries) + offset = pack.header_size + pack.total_entries * pack.entry_header_size + with open(file_name, 'wb') as f: + # write entries data + for entry in pack.entries: + # align data + offset = align_up(offset, pack.data_align) + entry.offset = offset + f.seek(offset) + fin_name = os.path.join(in_dir_name, entry.filepath) + with open(fin_name, 'rb') as fin: + data = fin.read() + entry.size = len(data) + f.write(data) + offset += len(data) + + pack.total_size = offset + f.seek(0) + # write pack header + f.write(bytes(pack)) + # iterate over entries again to write entry header + for entry in pack.entries: + f.write(bytes(entry)) + + +def cmd_create(args): + if args.file.lower().endswith('.xml'): + import xmlparser as parser + elif args.file.lower().endswith('.yaml'): + import yamlparser as parser + else: + raise NotImplementedError('{} type not supported'.format(args.file)) + + pack = parser.parse(args.file) + pack.pack_version = bytes(str(args.pack_version).encode('ascii')) + pack.header_size = len(pack) + + # create output directory if missing + if not os.path.isdir(args.out_dir): + os.makedirs(args.out_dir, 0o755) + + file_name = os.path.join(args.out_dir, pack.name + '.img') + + create_pack_file(file_name, args.in_dir, pack) + + +def product_match(products, product): + return product in products.split('|') + + +def copyfileobj(src, dst, file_size): + while file_size > 0: + buf = src.read(min(128 * 1024, file_size)) + dst.write(buf) + file_size -= len(buf) + + +def cmd_unpack(args): + with open(args.file, 'rb') as f: + pack = PackHeader.from_bytes(f.read(len(PackHeader()))) + + if pack.version != FBPACK_VERSION: + raise NotImplementedError('unsupported version {}'.format(pack.version)) + + entries = [] + # create list of entries we want to extact + for _ in range(pack.total_entries): + entry = PackEntry.from_bytes(f.read(len(PackEntry()))) + name = bytes_to_str(entry.name) + if not args.partitions or name in args.partitions: + # if both product are valid then match product name too + if not args.product or not entry.product or product_match( + entry.product, args.product): + entries.append(entry) + + if not entries and not args.unpack_ver: + raise RuntimeError('no images to unpack') + + # create output directory if it does not exists + if not os.path.isdir(args.out_dir): + os.makedirs(args.out_dir, 0o755) + + out_files = {} + # write file per entry + for entry in entries: + name = bytes_to_str(entry.name) + logging.info( + 'Unpacking {} (size: {}, offset: {})'.format( + name, entry.size, entry.offset)) + f.seek(entry.offset) + entry_filename = os.path.join(args.out_dir, name + '.img') + instance = out_files.get(entry_filename, 0) + 1 + out_files[entry_filename] = instance + if instance > 1: + entry_filename = os.path.join(args.out_dir, name + '({}).img'.format(instance - 1)) + with open(entry_filename, 'wb') as entry_file: + copyfileobj(f, entry_file, entry.size) + + if args.unpack_ver: + ver_file_path = os.path.join(args.out_dir, 'version.txt') + with open(ver_file_path, 'w') as ver_file: + ver_file.write(bytes_to_str(pack.pack_version)) + + logging.info('Done') + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Tool to create/modify/inspect fastboot packed images") + parser.add_argument("-v", "--verbosity", action="count", default=0, + help="increase output verbosity") + + subparsers = parser.add_subparsers() + + # info command + info = subparsers.add_parser('info') + info.add_argument('file', help="packed image file") + info.set_defaults(func=cmd_info) + + # create command + create = subparsers.add_parser('create') + create.add_argument('-d', '--in_dir', help='directory to search for data files', default='.') + create.add_argument( + '-o', + '--out_dir', + help='output directory for the packed image', + default='.') + create.add_argument('-v', '--pack_version', help='Packed image version ', default='') + create.add_argument('file', help="config file describing packed image (yaml/xml)") + create.set_defaults(func=cmd_create) + + # unpack command + unpack = subparsers.add_parser('unpack') + unpack.add_argument('-o', '--out_dir', help='directory to store unpacked images', default='.') + unpack.add_argument('-p', '--product', help='filter images by product', default='') + unpack.add_argument('-v', '--unpack_ver', help='Unpack version to a file', action='store_true') + unpack.add_argument('file', help="packed image file") + unpack.add_argument('partitions', metavar='PART', type=str, nargs='*', + help='Partition names to extract (default all).') + unpack.set_defaults(func=cmd_unpack) + + args = parser.parse_args() + # make sure a command was passed + if not hasattr(args, 'func'): + parser.print_usage() + print("fbpacktool.py: error: no command was passed") + sys.exit(2) + + return args + + +def main(): + args = parse_args() + + log_level = logging.DEBUG + if args.verbosity >= 2: + log_level = logging.DEBUG + elif args.verbosity == 1: + log_level = logging.INFO + else: + log_level = logging.WARNING + + logging.basicConfig(level=log_level) + + # execute command + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/fbpacktool/packedstruct.py b/fbpacktool/packedstruct.py new file mode 100644 index 0000000..17c4f44 --- /dev/null +++ b/fbpacktool/packedstruct.py @@ -0,0 +1,61 @@ +# Copyright 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import struct + + +class PackedStruct(object): + """Class representing a C style packed structure + + Derived classes need to provide a dictionary with key will be the attributes and the values are + the format characters for each field. e.g. + + class Foo(PackedStruct): + _FIELDS = { + x: 'I', + name: '64s', + } + + In this case Foo.x will represent an "unsigned int" C value, while Foo.name will be a "char[64]" + C value + + """ + + def __init__(self, *args, **kwargs): + self._fmt = '<' + ''.join(fmt for fmt in self._FIELDS.values()) + for name in self._FIELDS: + setattr(self, name, None) + + for name, val in zip(self._FIELDS.keys(), args): + setattr(self, name, val) + for name, val in kwargs.items(): + setattr(self, name, val) + + def __repr__(self): + return '{} {{\n'.format(self.__class__.__name__) + ',\n'.join( + ' {!r}: {!r}'.format(k, getattr(self, k)) for k in self._FIELDS) + '\n}' + + def __str__(self): + return struct.pack(self._fmt, *(getattr(self, x) for x in self._FIELDS)) + + def __bytes__(self): + return struct.pack(self._fmt, *(getattr(self, x) for x in self._FIELDS)) + + def __len__(self): + return struct.calcsize(self._fmt) + + @classmethod + def from_bytes(cls, data): + fmt_str = '<' + ''.join(fmt for fmt in cls._FIELDS.values()) + return cls(*struct.unpack(fmt_str, data))