diff --git a/scripts/extract-partitions.py b/scripts/extract-partitions.py index 8a7137a..e4c15c2 100755 --- a/scripts/extract-partitions.py +++ b/scripts/extract-partitions.py @@ -5,108 +5,57 @@ # Copyright (C) 2015 Peter Wu <peter@lekensteyn.nl> # Licensed under the MIT license <http://opensource.org/licenses/MIT>. -from collections import OrderedDict -from contextlib import closing, contextmanager +from contextlib import closing import argparse, logging, os, struct -import lglaf +import lglaf, partitions -_logger = logging.getLogger(__name__) +_logger = logging.getLogger("extract-partitions") -def read_uint32(data, offset): - return struct.unpack_from('<I', data, 4)[0] - -def read_partitions(comm): +def read_partition_numbers(comm): output = comm.call(lglaf.make_exec_request('cat /proc/partitions'))[1] - partitions = OrderedDict() + partitions = [] for line in output.decode('ascii').split('\n'): - if not 'mmcblk0p' in line: + if not line: continue - major, minor, blocks, name = line.split() - partitions[name] = int(blocks) + name = line.split()[-1] + if not name.startswith('mmcblk0p'): + continue + part_num = int(name[len('mmcblk0p'):]) + partitions.append(part_num) return partitions -@contextmanager -def laf_open_ro(comm, path): - # Avoid opening the whole partition in read/write mode. - assert path, "Path must not be empty" - path_bin = path.encode('ascii') + b'\0' - open_cmd = lglaf.make_request(b'OPEN', body=path_bin) - open_header = comm.call(open_cmd)[0] - fd_num = read_uint32(open_header, 4) - try: - yield fd_num - finally: - close_cmd = lglaf.make_request(b'CLSE', args=[fd_num]) - comm.call(close_cmd) - -def laf_read(comm, fd_num, offset, size): - """Read size bytes at the given block offset.""" - read_cmd = lglaf.make_request(b'READ', args=[fd_num, offset, size]) - header, response = comm.call(read_cmd) - # Ensure that response fd, offset and length are sane (match the request) - assert read_cmd[4:4+12] == header[4:4+12], "Unexpected read response" - assert len(response) == size - return response - parser = argparse.ArgumentParser() parser.add_argument("-d", "--outdir", default=".", help="Output directory for disk images.") # Do not dump partitions larger than this size # (userdata 11728 MiB, system 2064 MiB, cache 608 MiB, cust 256 MiB) -parser.add_argument("--max-size", type=int, default=65536, +parser.add_argument("--max-size", metavar="kbytes", type=int, default=65536, help="Maximum partition size to dump (in KiB)") parser.add_argument("--debug", action='store_true', help="Enable debug messages") -# On Linux, one bulk read returns at most 16 KiB. 32 bytes are part of the first -# header, so remove one block size (512 bytes) to stay within that margin. -# This ensures that whenever the USB communication gets out of sync, it will -# always start with a message header, making recovery easier. -MAX_BLOCK_SIZE = (16 * 1024 - 512) // 512 - -def dump_file(comm, remote_path, local_path, size): - try: - offset = os.path.getsize(local_path) - except OSError: - offset = 0 - if offset >= size: - if offset > size: - _logger.warn("%s: unexpected size %dK > %dK", - local_path, offset, size) - else: - _logger.info("%s: already retrieved %dK", - local_path, size) - return - - # Read offsets must be a multiple of 512 bytes, enforce this - BLOCK_SIZE = 512 - unaligned_bytes = offset % BLOCK_SIZE - offset = BLOCK_SIZE * (offset // BLOCK_SIZE) - - with laf_open_ro(comm, remote_path) as fd_num: - _logger.debug("Opened fd %d for %s (final size %.2fK, offset %.2fK)", - fd_num, remote_path, size / 1024, offset / 1024) - with open(local_path, 'ab') as f: - # Offset should be aligned to block size. If not, read at most a - # whole block and drop the leading bytes. - if unaligned_bytes: - chunksize = min(size - offset, BLOCK_SIZE) - data = laf_read(comm, fd_num, offset // BLOCK_SIZE, chunksize) - f.write(data[unaligned_bytes:]) - offset += BLOCK_SIZE - while offset < size: - chunksize = min(size - offset, BLOCK_SIZE * MAX_BLOCK_SIZE) - data = laf_read(comm, fd_num, offset // BLOCK_SIZE, chunksize) - f.write(data) - offset += chunksize - -def dump_partitions(comm, outdir, max_size): - parts = read_partitions(comm) - for name, size in parts.items(): - if size > max_size: - _logger.info("Ignoring large partition %s of size %dK" % (name, size)) +def dump_partitions(comm, disk_fd, outdir, max_size): + part_nums = read_partition_numbers(comm) + for part_num in part_nums: + part_offset, part_size = partitions.partition_info(comm, part_num) + if part_size > max_size: + _logger.info("Ignoring large partition %s of size %dK" % (part_num, + part_size / 1024)) continue - out_path = os.path.join(outdir, "%s.bin" % name) - dump_file(comm, "/dev/block/%s" % name, out_path, 1024 * size) + out_path = os.path.join(outdir, "mmcblk0p%d.bin" % part_num) + try: + current_size = os.path.getsize(out_path) + if current_size > part_size: + _logger.warn("%s: unexpected size %dK, larger than %dK", + out_path, current_size / 1024, part_size / 1024) + continue + elif current_size == part_size: + _logger.info("%s: already retrieved %dK", + out_path, part_size / 1024) + continue + except OSError: pass + _logger.info("Dumping partition %d to %s (%d bytes)", + part_num, out_path, part_size) + partitions.dump_partition(comm, disk_fd, out_path, part_offset, part_size) def main(): args = parser.parse_args() @@ -119,7 +68,9 @@ def main(): comm = lglaf.autodetect_device() with closing(comm): lglaf.try_hello(comm) - dump_partitions(comm, args.outdir, args.max_size) + with partitions.laf_open_disk(comm) as disk_fd: + _logger.debug("Opened fd %d for disk", disk_fd) + dump_partitions(comm, disk_fd, args.outdir, args.max_size * 1024) if __name__ == '__main__': main()