Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright 2021 The Android Open Source Project |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 16 | import argparse |
| 17 | import logging |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 18 | import os |
| 19 | import sys |
| 20 | from lxml.etree import XMLParser |
| 21 | import yaml |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 22 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 23 | #from google3.third_party.devsite.androidsource.en.docs.core.architecture.bootloader.tools.pixel.fw_unpack import fbpack |
| 24 | import fbpack |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 25 | |
| 26 | def bytes_to_str(bstr): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 27 | return bstr.decode().rstrip('\x00') |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 28 | |
| 29 | |
| 30 | def print_pack_header(pack): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 31 | print('magic: {:#x}'.format(pack.magic)) |
| 32 | print('version: {}'.format(pack.version)) |
| 33 | print('header size: {}'.format(pack.header_size)) |
| 34 | print('entry header size: {}'.format(pack.entry_header_size)) |
| 35 | platform = bytes_to_str(pack.platform) |
| 36 | print('platform: {}'.format(platform)) |
| 37 | pack_version = bytes_to_str(pack.pack_version) |
| 38 | print('pack version: {}'.format(pack_version)) |
| 39 | print('slock type: {}'.format(pack.slot_type)) |
| 40 | print('data align: {}'.format(pack.data_align)) |
| 41 | print('total entries: {}'.format(pack.total_entries)) |
| 42 | print('total size: {}'.format(pack.total_size)) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 43 | |
| 44 | |
| 45 | def print_pack_entry(entry, prefix): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 46 | name = bytes_to_str(entry.name) |
| 47 | print('{}name: {}'.format(prefix, name)) |
| 48 | etype = 'unknown' |
| 49 | if entry.type == fbpack.FBPACK_PARTITION_TABLE: |
| 50 | etype = 'partiton table' |
| 51 | elif entry.type == fbpack.FBPACK_PARTITION_DATA: |
| 52 | etype = 'partition' |
| 53 | elif entry.type == fbpack.FBPACK_SIDELOAD_DATA: |
| 54 | etype = 'sideload' |
| 55 | else: |
| 56 | print('entry else') |
| 57 | print('{}type: {}'.format(prefix, etype)) |
| 58 | product = bytes_to_str(entry.product) |
| 59 | print('{}product: {}'.format(prefix, product)) |
| 60 | print('{}offset: {:#x} ({})'.format(prefix, entry.offset, entry.offset)) |
| 61 | print('{}size: {:#x} ({})'.format(prefix, entry.size, entry.size)) |
| 62 | print('{}slotted: {}'.format(entry.size, bool(entry.slotted))) |
| 63 | print('{}crc32: {:#08x}'.format(prefix, entry.crc32)) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 64 | |
| 65 | |
| 66 | def cmd_info(args): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 67 | with open(args.file, 'rb') as f: |
| 68 | pack = fbpack.PackHeader.from_bytes(f.read(len(fbpack.PackHeader()))) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 69 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 70 | if pack.version != fbpack.FBPACK_VERSION: |
| 71 | raise NotImplementedError('unsupported version {}'.format(pack.version)) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 72 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 73 | print('Header:') |
| 74 | print_pack_header(pack) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 75 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 76 | print('\nEntries:') |
| 77 | for i in range(1, pack.total_entries + 1): |
| 78 | entry = fbpack.PackEntry.from_bytes(f.read(len(fbpack.PackEntry()))) |
| 79 | print('Entry {}: {{'.format(i)) |
| 80 | print_pack_entry(entry, ' ') |
| 81 | print('}') |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 82 | |
| 83 | |
| 84 | def align_up(val, align): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 85 | return (val + align - 1) & ~(align - 1) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 86 | |
| 87 | |
| 88 | def create_pack_file(file_name, in_dir_name, pack): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 89 | pack.total_entries = len(pack.entries) |
| 90 | offset = pack.header_size + pack.total_entries * pack.entry_header_size |
| 91 | with open(file_name, 'wb') as f: |
| 92 | # write entries data |
| 93 | for entry in pack.entries: |
| 94 | # align data |
| 95 | offset = align_up(offset, pack.data_align) |
| 96 | entry.offset = offset |
| 97 | f.seek(offset) |
| 98 | fin_name = os.path.join(in_dir_name, entry.filepath) |
| 99 | with open(fin_name, 'rb') as fin: |
| 100 | data = fin.read() |
| 101 | entry.size = len(data) |
| 102 | f.write(data) |
| 103 | offset += len(data) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 104 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 105 | pack.total_size = offset |
| 106 | f.seek(0) |
| 107 | # write pack header |
| 108 | f.write(bytes(pack)) |
| 109 | # iterate over entries again to write entry header |
| 110 | for entry in pack.entries: |
| 111 | f.write(bytes(entry)) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 112 | |
| 113 | |
| 114 | def cmd_create(args): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 115 | if not (args.file.lower().endswith('.xml') or |
| 116 | args.file.lower().endswith('.yaml')): |
| 117 | raise NotImplementedError('{} type not supported'.format(args.file)) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 118 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 119 | pack = None |
| 120 | if args.file.lower().endswith('.yaml'): |
| 121 | pack = yaml.parse(args.file) |
| 122 | else: |
| 123 | pack = XMLParser.parse(args.file) |
| 124 | pack.pack_version = bytes(str(args.pack_version).encode('ascii')) |
| 125 | pack.header_size = len(pack) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 126 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 127 | # create output directory if missing |
| 128 | if not os.path.isdir(args.out_dir): |
| 129 | os.makedirs(args.out_dir, 0o755) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 130 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 131 | file_name = os.path.join(args.out_dir, pack.name + '.img') |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 132 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 133 | create_pack_file(file_name, args.in_dir, pack) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 134 | |
| 135 | |
| 136 | def product_match(products, product): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 137 | return product in products.split(b'|') |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 138 | |
| 139 | |
| 140 | def copyfileobj(src, dst, file_size): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 141 | while file_size > 0: |
| 142 | buf = src.read(min(128 * 1024, file_size)) |
| 143 | dst.write(buf) |
| 144 | file_size -= len(buf) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 145 | |
| 146 | |
| 147 | def cmd_unpack(args): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 148 | with open(args.file, 'rb') as f: |
| 149 | pack = fbpack.PackHeader.from_bytes(f.read(len(fbpack.PackHeader()))) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 150 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 151 | if pack.version != fbpack.FBPACK_VERSION: |
| 152 | raise NotImplementedError('unsupported version {}'.format(pack.version)) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 153 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 154 | entries = [] |
| 155 | # create list of entries we want to extact |
| 156 | for _ in range(pack.total_entries): |
| 157 | entry = fbpack.PackEntry.from_bytes(f.read(len(fbpack.PackEntry()))) |
| 158 | name = bytes_to_str(entry.name) |
| 159 | if not args.partitions or name in args.partitions: |
| 160 | # if both product are valid then match product name too |
| 161 | if not args.product or not entry.product or product_match( |
| 162 | entry.product, args.product): |
| 163 | entries.append(entry) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 164 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 165 | if not entries and not args.unpack_ver: |
| 166 | raise RuntimeError('no images to unpack') |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 167 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 168 | # create output directory if it does not exist |
| 169 | if not os.path.isdir(args.out_dir): |
| 170 | os.makedirs(args.out_dir, 0o755) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 171 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 172 | out_files = {} |
| 173 | # write file per entry |
| 174 | for entry in entries: |
| 175 | name = bytes_to_str(entry.name) |
| 176 | logging.info('Unpacking {} (size: {}, offset: {})'.format( |
| 177 | name, entry.size, entry.offset)) |
| 178 | f.seek(entry.offset) |
| 179 | entry_filename = os.path.join(args.out_dir, name + '.img') |
| 180 | instance = out_files.get(entry_filename, 0) + 1 |
| 181 | out_files[entry_filename] = instance |
| 182 | if instance > 1: |
| 183 | entry_filename = os.path.join(args.out_dir, |
| 184 | name + '({}).img'.format(instance - 1)) |
| 185 | with open(entry_filename, 'wb') as entry_file: |
| 186 | copyfileobj(f, entry_file, entry.size) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 187 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 188 | if args.unpack_ver: |
| 189 | ver_file_path = os.path.join(args.out_dir, 'version.txt') |
| 190 | with open(ver_file_path, 'w') as ver_file: |
| 191 | ver_file.write(bytes_to_str(pack.pack_version)) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 192 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 193 | logging.info('Done') |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 194 | |
| 195 | |
| 196 | def parse_args(): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 197 | parser = argparse.ArgumentParser( |
| 198 | description='Tool to create/modify/inspect fastboot packed images') |
| 199 | parser.add_argument( |
| 200 | '-v', |
| 201 | '--verbosity', |
| 202 | action='count', |
| 203 | default=0, |
| 204 | help='increase output verbosity') |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 205 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 206 | subparsers = parser.add_subparsers() |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 207 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 208 | # info command |
| 209 | info = subparsers.add_parser('info') |
| 210 | info.add_argument('file', help='packed image file') |
| 211 | info.set_defaults(func=cmd_info) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 212 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 213 | # create command |
| 214 | create = subparsers.add_parser('create') |
| 215 | create.add_argument( |
| 216 | '-d', '--in_dir', help='directory to search for data files', default='.') |
| 217 | create.add_argument( |
| 218 | '-o', |
| 219 | '--out_dir', |
| 220 | help='output directory for the packed image', |
| 221 | default='.') |
| 222 | create.add_argument( |
| 223 | '-v', '--pack_version', help='Packed image version ', default='') |
| 224 | create.add_argument( |
| 225 | 'file', help='config file describing packed image (yaml/xml)') |
| 226 | create.set_defaults(func=cmd_create) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 227 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 228 | # unpack command |
| 229 | unpack = subparsers.add_parser('unpack') |
| 230 | unpack.add_argument( |
| 231 | '-o', '--out_dir', help='directory to store unpacked images', default='.') |
| 232 | unpack.add_argument( |
| 233 | '-p', '--product', help='filter images by product', default='') |
| 234 | unpack.add_argument( |
| 235 | '-v', |
| 236 | '--unpack_ver', |
| 237 | help='Unpack version to a file', |
| 238 | action='store_true') |
| 239 | unpack.add_argument('file', help='packed image file') |
| 240 | unpack.add_argument( |
| 241 | 'partitions', |
| 242 | metavar='PART', |
| 243 | type=str, |
| 244 | nargs='*', |
| 245 | help='Partition names to extract (default all).') |
| 246 | unpack.set_defaults(func=cmd_unpack) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 247 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 248 | args = parser.parse_args() |
| 249 | # make sure a command was passed |
| 250 | if not hasattr(args, 'func'): |
| 251 | parser.print_usage() |
| 252 | print('fbpacktool.py: error: no command was passed') |
| 253 | sys.exit(2) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 254 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 255 | return args |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 256 | |
| 257 | |
| 258 | def main(): |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 259 | args = parse_args() |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 260 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 261 | if args.verbosity >= 2: |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 262 | log_level = logging.DEBUG |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 263 | elif args.verbosity == 1: |
| 264 | log_level = logging.INFO |
| 265 | else: |
| 266 | log_level = logging.WARNING |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 267 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 268 | logging.basicConfig(level=log_level) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 269 | |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 270 | # execute command |
| 271 | args.func(args) |
Chirayu Desai | 7119eb6 | 2021-12-07 03:07:13 +0530 | [diff] [blame] | 272 | |
| 273 | |
| 274 | if __name__ == '__main__': |
Michael Bestas | 0382f30 | 2023-06-12 02:22:51 +0300 | [diff] [blame] | 275 | main() |