#!/usr/bin/env python import argparse import bisect import copy import json import math import numbers import os import struct import sys import uuid import zlib from ctypes import create_string_buffer sys.path.append(os.path.join(os.getcwd(), 'pactool')) from sfs_module import SFSImageHandler import traceback from functools import cmp_to_key def error(str): callstack = traceback.format_exc() sys.stderr.write('\033[1;31m%s\033[0m\n' %str) sys.stderr.write('\033[1;31m%s\033[0m\n' %callstack) sys.exit(1) # Python 2.6 required for modern exception syntax if sys.hexversion < 0x02060000: error("Python 2.6 or newer is required.") # Keywords used in JSON files. JSON_KEYWORD_SETTINGS = 'settings' JSON_KEYWORD_SETTINGS_AB_SUFFIXES = 'ab_suffixes' JSON_KEYWORD_SETTINGS_DISK_SIZE = 'disk_size' JSON_KEYWORD_SETTINGS_DISK_ALIGNMENT = 'disk_alignment' JSON_KEYWORD_SETTINGS_DISK_GUID = 'disk_guid' JSON_KEYWORD_SETTINGS_PARTITIONS_OFFSET_BEGIN = 'partitions_offset_begin' JSON_KEYWORD_PARTITIONS = 'partitions' JSON_KEYWORD_PARTITIONS_LABEL = 'label' JSON_KEYWORD_PARTITIONS_OFFSET = 'offset' JSON_KEYWORD_PARTITIONS_SIZE = 'size' JSON_KEYWORD_PARTITIONS_GROW = 'grow' JSON_KEYWORD_PARTITIONS_GUID = 'guid' JSON_KEYWORD_PARTITIONS_TYPE_GUID = 'type_guid' JSON_KEYWORD_PARTITIONS_FLAGS = 'flags' JSON_KEYWORD_PARTITIONS_PERSIST = 'persist' JSON_KEYWORD_PARTITIONS_IGNORE = 'ignore' JSON_KEYWORD_PARTITIONS_AB = 'ab' JSON_KEYWORD_PARTITIONS_AB_EXPANDED = 'ab_expanded' JSON_KEYWORD_PARTITIONS_POSITION = 'position' JSON_KEYWORD_AUTO = 'auto' SAFETY_PATITION_NAME = ["os", "dil", 'boot'] SAFETY_BAK_PATITION_NAME = ["os_bak", "dil_bak", 'boot_back'] SAFETY_THIRD_PATITION_NAME = ['boot_third'] SFS_IMG_MAX_SIZE = 128 # Possible values for the --type option of the query_partition # sub-command. QUERY_PARTITION_TYPES = ['size', 'offset', 'guid', 'type_guid', 'flags', 'persist'] PACKTOOL_VERSION_MAJOR = 1 PACKTOOL_VERSION_MINOR = 0 DISK_SECTOR_SIZE = 512 GPT_NUM_LBAS = 33 GPT_MIN_PART_NUM = 1 GPT_MAX_PART_NUM = 128 PACKET_MAGIC = 0x5041434B PACKET_STRUCT_VER = 0x0 PACKET_HEADER_SIZE = 0x400 # 1K PACKET_FILETS_SIZE = 0x4000 # 16K = 128 * 128 PACKET_INFO_SIZE = PACKET_HEADER_SIZE + PACKET_FILETS_SIZE E_PACK_FIRST_DA_FILE = 0 E_PACK_MID_DA_FILE = 1 E_PACK_DLOADER_FILE = 2 E_PACK_SPL_FILE = 3 E_PACK_SFS_FILE = 4 E_PACK_GPT_FILE = 5 E_PACK_IMG_FILE = 6 E_PACK_PAC_FILE = 7 E_PACK_FBL_FILE = 8 # boot0 E_PACK_SBL_FILE = 9 # boot1 E_PACK_TBL_FILE = 10 # boot2 E_PACK_NFS_FILE = 11 # NFS E_PACK_RFD_FILE = 12 # RFD NEW_PRELOAD_NAMES = ["boot0", "boot1", "boot2", "nfs", "rfd"] PRELOAD_NAMES = ["spl", "sfs"] + NEW_PRELOAD_NAMES PRELOAD_TYPES = [E_PACK_SPL_FILE, E_PACK_SFS_FILE, E_PACK_FBL_FILE, E_PACK_SBL_FILE, E_PACK_TBL_FILE, E_PACK_NFS_FILE, E_PACK_RFD_FILE] STR_PACK_TYPE = ["FDA", "OSPIDA", "DLOADER", "SPL", "SFS", "GPT", "IMG", "PAC", "BOOT0", "BOOT1", "BOOT2", "NFS", "RFD"] DA_ID_LIST = ["FDA", "OPSIDA", "DLOADER"] MAX_BUFFER_SIZE = 0x4000000 # 64M KNOWN_TYPE_GUIDS = { 'brillo_boot': 'bb499290-b57e-49f6-bf41-190386693794', 'brillo_bootloader': '4892aeb3-a45f-4c5f-875f-da3303c0795c', 'brillo_system': '0f2778c4-5cc1-4300-8670-6c88b7e57ed6', 'brillo_odm': 'e99d84d7-2c1b-44cf-8c58-effae2dc2558', 'brillo_oem': 'aa3434b2-ddc3-4065-8b1a-18e99ea15cb7', 'brillo_userdata': '0bb7e6ed-4424-49c0-9372-7fbab465ab4c', 'brillo_misc': '6b2378b0-0fbc-4aa9-a4f6-4d6e17281c47', 'brillo_vbmeta': 'b598858a-5fe3-418e-b8c4-824b41f4adfc', 'brillo_vendor_specific': '314f99d5-b2bf-4883-8d03-e2f2ce507d6a', 'linux_fs': '0fc63daf-8483-4772-8e79-3d69d8477de4', 'ms_basic_data': 'ebd0a0a2-b9e5-4433-87c0-68b6b72699c7', 'efi_system': 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b', 'secondary_partition_identifier': '7d06a189-1fef-4d89-8940-e8c2a6832ecf' } def get_file_extension(path): return os.path.splitext(path)[1] def RoundToMultiple(number, size, round_down=False): """Rounds a number up (or down) to nearest multiple of another number. Args: number: The number to round up. size: The multiple to round up to. round_down: If True, the number will be rounded down. Returns: If |number| is a multiple of |size|, returns |number|, otherwise returns |number| + |size| - |remainder| (if |round_down| is False) or |number| - |remainder| (if |round_down| is True). Always returns an integer. """ remainder = number % size if remainder == 0: return int(number) if round_down: return int(number - remainder) return int(number + size - remainder) def ParseNumber(arg): """Number parser. If |arg| is an integer, that value is returned. Otherwise int(arg, 0) is returned. This function is suitable for use in the |type| parameter of |ArgumentParser|'s add_argument() function. An improvement to just using type=int is that this function supports numbers in other bases, e.g. "0x1234". Arguments: arg: Argument (int or string) to parse. Returns: The parsed value, as an integer. Raises: ValueError: If the argument could not be parsed. """ if isinstance(arg, numbers.Integral): return arg return int(arg, 0) def ParseGuid(arg): """Parser for RFC 4122 GUIDs. Arguments: arg: The argument, as a string. Returns: UUID in hyphenated format. Raises: ValueError: If the given string cannot be parsed. """ return str(uuid.UUID(arg)) def ParseSize(arg): """Parser for size strings with decimal and binary unit support. This supports both integers and strings. Arguments: arg: The string to parse. Returns: The parsed size in bytes as an integer. Raises: ValueError: If the given string cannot be parsed. """ if isinstance(arg, numbers.Integral): return arg ws_index = arg.find(' ') if ws_index != -1: num = float(arg[0:ws_index]) factor = 1 if arg.endswith('KiB'): factor = 1024 elif arg.endswith('MiB'): factor = 1024*1024 elif arg.endswith('GiB'): factor = 1024*1024*1024 elif arg.endswith('TiB'): factor = 1024*1024*1024*1024 elif arg.endswith('PiB'): factor = 1024*1024*1024*1024*1024 elif arg.endswith('kB'): factor = 1000 elif arg.endswith('MB'): factor = 1000*1000 elif arg.endswith('GB'): factor = 1000*1000*1000 elif arg.endswith('TB'): factor = 1000*1000*1000*1000 elif arg.endswith('PB'): factor = 1000*1000*1000*1000*1000 else: raise ValueError('Cannot parse string "{}"'.format(arg)) value = num*factor # If the resulting value isn't an integer, round up. if not value.is_integer(): value = int(math.ceil(value)) else: value = int(arg, 0) return int(value) class ImageHandler(object): """Abstraction for image I/O with support for Android sparse images. This class provides an interface for working with image files that may be using the Android Sparse Image format. When an instance is constructed, we test whether it's an Android sparse file. If so, operations will be on the sparse file by interpreting the sparse format, otherwise they will be directly on the file. Either way the operations do the same. For reading, this interface mimics a file object - it has seek(), tell(), and read() methods. For writing, only truncation (truncate()) and appending is supported (append_raw(), append_fill(), and append_dont_care()). Additionally, data can only be written in units of the block size. Attributes: is_sparse: Whether the file being operated on is sparse. block_size: The block size, typically 4096. image_size: The size of the unsparsified file. """ # See system/core/libsparse/sparse_format.h for details. MAGIC = 0xed26ff3a HEADER_FORMAT = ' max_lba: return [255, 255, 255] c = (int)(lba / (num_heads*num_sectors)) h = (lba / num_sectors) % num_heads s = (int)(lba % num_sectors) return [(int)(h), (int)((((c>>8) & 0x03)<<6) | (s & 0x3f)), (int)(c & 0xff)] def _generate_protective_mbr(self, settings): """Generate Protective MBR. Arguments: settings: A Settings object. Returns: A string with the binary protective MBR (512 bytes). """ # See https://en.wikipedia.org/wiki/Master_boot_record for MBR layout. # # The first partition starts at offset 446 (0x1be). lba_start = 1 lba_end = (int)(settings.disk_size/DISK_SECTOR_SIZE - 1) start_chs = self._lba_to_chs(lba_start) end_chs = self._lba_to_chs(lba_end) pmbr = struct.pack(' (disk_size - GPT_NUM_LBAS*DISK_SECTOR_SIZE): raise PackError('Partition with label "{}" exceeds the disk ' 'image size.'.format(p.label)) def make_table(self, inputs, ab_suffixes=None, partitions_offset_begin=None, disk_size=None, disk_alignment=None, disk_guid=None, guid_generator=None): """Implementation of the 'make_table' command. This function takes a list of input partition definition files, flattens them, expands A/B partitions, grows partitions, and lays out partitions according to alignment constraints. Arguments: inputs: List of JSON files to parse. ab_suffixes: List of the A/B suffixes (as a comma-separated string) to use or None to not override. partitions_offset_begin: Size of disk partitions offset begin or None to not override. disk_size: Size of disk or None to not override. disk_alignment: Disk alignment or None to not override. disk_guid: Disk GUID as a string or None to not override. guid_generator: A GuidGenerator or None to use the default. Returns: A tuple where the first argument is a JSON string for the resulting partitions and the second argument is the binary partition tables. Raises: BptParsingError: If an input file has an error. PackError: If another application-specific error occurs """ partitions, settings = self._read_json(inputs) # Command-line arguments override anything specified in input # files. if disk_size: settings.disk_size = int(math.ceil(disk_size)) if disk_alignment: settings.disk_alignment = int(disk_alignment) if partitions_offset_begin: settings.partitions_offset_begin = int(partitions_offset_begin) if ab_suffixes: settings.ab_suffixes = ab_suffixes.split(',') if disk_guid: settings.disk_guid = disk_guid if not guid_generator: guid_generator = GuidGenerator() # We need to know the disk size. Also round it down to ensure it's # a multiple of the sector size. if not settings.disk_size: raise PackError('Disk size not specified. Use --disk_size option ' 'or specify it in an input file.\n') settings.disk_size = RoundToMultiple(settings.disk_size, DISK_SECTOR_SIZE, round_down=True) # Alignment must be divisible by disk sector size. if settings.disk_alignment % DISK_SECTOR_SIZE != 0: raise PackError( 'Disk alignment size of {} is not divisible by {}.\n'.format( settings.disk_alignment, DISK_SECTOR_SIZE)) if settings.partitions_offset_begin != 0: # Disk partitions offset begin size must be # divisible by disk sector size. if settings.partitions_offset_begin % settings.disk_alignment != 0: raise PackError( 'Disk Partitions offset begin size of {} ' 'is not divisible by {}.\n'.format( settings.partitions_offset_begin, settings.disk_alignment)) settings.partitions_offset_begin = max(settings.partitions_offset_begin, DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS)) settings.partitions_offset_begin = RoundToMultiple( settings.partitions_offset_begin, settings.disk_alignment) # Expand A/B partitions and skip ignored partitions. expanded_partitions = [] for p in partitions: if p.ignore: continue if p.ab and not p.ab_expanded: p.ab_expanded = True for suffix in settings.ab_suffixes: new_p = copy.deepcopy(p) new_p.label += suffix expanded_partitions.append(new_p) else: expanded_partitions.append(p) partitions = expanded_partitions # Expand Disk GUID if needed. if not settings.disk_guid or settings.disk_guid == JSON_KEYWORD_AUTO: settings.disk_guid = guid_generator.dispense_guid(0) # Sort according to 'position' attribute. partitions = sorted(partitions, key=cmp_to_key(lambda x, y: x.position - y.position)) # Automatically generate GUIDs if the GUID is unset or set to # 'auto'. Also validate the rest of the fields. part_no = 1 for p in partitions: p.expand_guid(guid_generator, part_no) p.validate() part_no += 1 # Idenfify partition to grow and lay out partitions, ignoring the # one to grow. This way we can figure out how much space is left. # # Right now we only support a single 'grow' partition but we could # support more in the future by splitting up the available bytes # between them. grow_part = None # offset minimal size: DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS) offset = max(settings.partitions_offset_begin, DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS)) for p in partitions: if p.grow: if grow_part: raise PackError('Only a single partition can be automatically ' 'grown.\n') grow_part = p else: # Ensure size is a multiple of DISK_SECTOR_SIZE by rounding up # (user may specify it as e.g. "1.5 GB" which is not divisible # by 512). p.size = RoundToMultiple(p.size, DISK_SECTOR_SIZE) # Align offset to disk alignment. offset = RoundToMultiple(offset, settings.disk_alignment) offset += p.size # After laying out (respecting alignment) all non-grow # partitions, check that the given disk size is big enough. if offset > settings.disk_size - DISK_SECTOR_SIZE*GPT_NUM_LBAS: raise PackError('Disk size of {} bytes is too small for partitions ' 'totaling {} bytes.\n'.format( settings.disk_size, offset)) # If we have a grow partition, it'll starts at the next # available alignment offset and we can calculate its size as # follows. if grow_part: offset = RoundToMultiple(offset, settings.disk_alignment) grow_part.size = RoundToMultiple( settings.disk_size - DISK_SECTOR_SIZE*GPT_NUM_LBAS - offset, settings.disk_alignment, round_down=True) if grow_part.size < DISK_SECTOR_SIZE: raise PackError('Not enough space for partition "{}" to be ' 'automatically grown.\n'.format(grow_part.label)) # Now we can assign partition start offsets for all partitions, # including the grow partition. # offset minimal size: DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS) offset = max(settings.partitions_offset_begin, DISK_SECTOR_SIZE*(1 + GPT_NUM_LBAS)) for p in partitions: # Align offset. offset = RoundToMultiple(offset, settings.disk_alignment) p.offset = offset offset += p.size assert offset <= settings.disk_size - DISK_SECTOR_SIZE*GPT_NUM_LBAS json_str = self._generate_json(partitions, settings) gpt_bin = self._generate_gpt_bin(partitions, settings) return json_str, gpt_bin def _generate_pack_header(self, image_count, preload_size = 0, product_name = "", product_ver = "" ): preload_offset = 0 if preload_size: preload_offset = PACKET_INFO_SIZE header_data = struct.pack( ' eval(_name_next): min_index = j _name_current = _name_next if not i == min_index: pack_file_array[i], pack_file_array[min_index] = pack_file_array[min_index], pack_file_array[i] for file_t in pack_file_array: output.write(file_t) def _words2string(self, words): string = "" for i in range(len(words)): if i % 2 == 0: if not words[i] == 0: string += "%c" % words[i] return string.rstrip('\x00') def _str_to_number(self, str): str = str.lower().strip() _number = 0 if str.find('0x') == -1: _number = int(str) else: _number = int(str, 16) return _number class PackTool(object): """Object for pack tool command-line tool.""" def __init__(self): """Initializer method.""" self.packet = Packet() def run(self, argv): """Command-line processor. Arguments: argv: Pass sys.argv from main. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(title='subcommands') sub_parser = subparsers.add_parser( 'version', help='Prints version of packtool.') sub_parser.set_defaults(func=self.version) sub_parser = subparsers.add_parser( 'make_table', help='Lays out partitions and creates partition table.') sub_parser.add_argument('--input', help='Path to partition definition file.', type=argparse.FileType('r'), action='append') sub_parser.add_argument('--ab_suffixes', help='Set or override A/B suffixes.') sub_parser.add_argument('--partitions_offset_begin', help='Set or override disk partitions ' 'offset begin size.', type=ParseSize) sub_parser.add_argument('--disk_size', help='Set or override disk size.', type=ParseSize) sub_parser.add_argument('--disk_alignment', help='Set or override disk alignment.', type=ParseSize) sub_parser.add_argument('--disk_guid', help='Set or override disk GUID.', type=ParseGuid) sub_parser.add_argument('--output_json', help='JSON output file name.', type=argparse.FileType('w')) sub_parser.add_argument('--output_gpt', help='Output file name for MBR/GPT/GPT file.', type=argparse.FileType('wb')) sub_parser.set_defaults(func=self.make_table) sub_parser = subparsers.add_parser( 'make_pac_image', help='Creates pack image for program tool.') sub_parser.add_argument('--output', help='Path to image output.', type=argparse.FileType('wb'), required=True) sub_parser.add_argument('--input', help='Path to bpt file input.', type=argparse.FileType('r'), required=True) sub_parser.add_argument('--image', help='Partition name and path to image file.', metavar='PARTITION_NAME:PATH', action='append') # FDA : spl.img # OPSIDA : ospi_prog # DLOADER : dloader sub_parser.add_argument('--da', help='Download agent name and path to image file.', metavar='DA_NAME:PATH', action='append') sub_parser.add_argument('--preload', help='Partition name and path to image file.[spl/sfs:file]', metavar='PARTITION_NAME:PATH', action='append' ) sub_parser.add_argument('--product', help='product name.', type=str, default="" ) sub_parser.add_argument('--version', help='Product version.', type=str, default="" ) sub_parser.add_argument('--allow_empty_partitions', help='Allow skipping partitions in bpt file.', action='store_true') sub_parser.set_defaults(func=self.make_pac_image) # no gpt solution sub_parser = subparsers.add_parser( 'make_pac_image_no_gpt', help='Creates no partition table pack image for program tool.') sub_parser.add_argument('--output', help='Path to image output.', type=argparse.FileType('wb'), required=True) sub_parser.add_argument('--image', help='address and path to image file.', metavar='ADDRESS:PATH', action='append') # FDA : spl.img # OPSIDA : ospi_prog # DLOADER : dloader sub_parser.add_argument('--da', help='Download agent name and path to image file.', metavar='DA_NAME:PATH', action='append') sub_parser.add_argument('--preload', help='Partition name and path to image file.[spl/sfs:file]', metavar='PARTITION_NAME:PATH', action='append' ) sub_parser.add_argument('--product', help='product name.', type=str, default="" ) sub_parser.add_argument('--version', help='Product version.', type=str, default="" ) sub_parser.add_argument('--allow_empty_partitions', help='Allow skipping partitions in bpt file.', action='store_true') sub_parser.set_defaults(func=self.make_pac_image_no_gpt) args = parser.parse_args(argv[1:]) args.func(args) def version(self, _): """Implements the 'version' sub-command.""" print("{}.{}".format(PACKTOOL_VERSION_MAJOR, PACKTOOL_VERSION_MINOR)) def make_table(self, args): """Implements the 'make_table' sub-command.""" if not args.input: print('Option --input is required one or more times.\n') return False try: (json_str, gpt_bin) = self.packet.make_table(args.input, args.ab_suffixes, args.partitions_offset_begin, args.disk_size, args.disk_alignment, args.disk_guid) except PackParsingError as e: print('{}: Error parsing: {}\n'.format(e.filename, e.message)) return False except PackError as e: print('Error : {}\n'.format(e.message)) return False if args.output_json: args.output_json.write(json_str) if args.output_gpt: args.output_gpt.write(gpt_bin) return True def make_pac_image(self, args): """Implements the 'make_pac_image' sub-command.""" if not args.input: error('Option --input is required.\n') if not args.output: error('Option --ouptut is required.\n') try: self.packet.make_pac_image(args.output, args.input, args.image, args.da, args.preload, args.product, args.version, args.allow_empty_partitions ) except PackParsingError as e: error('{}: Error parsing: {}\n'.format(e.filename, e.message)) except 'PackError' as e: error('Error : {}\n'.format(e.message)) def make_pac_image_no_gpt(self, args): """Implements the 'make_pac_image_no_gpt' sub-command.""" if not args.output: error('Option --ouptut is required.\n') try: self.packet.make_pac_image_no_gpt( args.output, args.image, args.da, args.preload, args.product, args.version, args.allow_empty_partitions) except PackParsingError as e: error('{}: Error parsing: {}\n'.format(e.filename, e.message)) except 'PackError' as e: error('Error : {}\n'.format(e.message)) if __name__ == '__main__': tool = PackTool() tool.run(sys.argv)