#!/usr/bin/env python # # Copyright (C) 2014 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. from __future__ import print_function import argparse import bisect import logging import os import struct import threading from hashlib import sha1 import rangelib logger = logging.getLogger(__name__) class SparseImage(object): """Wraps a sparse image file into an image object. Wraps a sparse image file (and optional file map and clobbered_blocks) into an image object suitable for passing to BlockImageDiff. file_map contains the mapping between files and their blocks. clobbered_blocks contains the set of blocks that should be always written to the target regardless of the old contents (i.e. copying instead of patching). clobbered_blocks should be in the form of a string like "0" or "0 1-5 8". """ def __init__(self, simg_fn, file_map_fn=None, clobbered_blocks=None, mode="rb", build_map=True, allow_shared_blocks=False, hashtree_info_generator=None): self.simg_f = f = open(simg_fn, mode) header_bin = f.read(28) header = struct.unpack("> 2)) to_read -= this_read while to_read > 0: # continue with following chunks if this range spans multiple chunks. idx += 1 chunk_start, chunk_len, filepos, fill_data = self.offset_map[idx] this_read = min(chunk_len, to_read) if filepos is not None: f.seek(filepos, os.SEEK_SET) yield f.read(this_read * self.blocksize) else: yield fill_data * (this_read * (self.blocksize >> 2)) to_read -= this_read def LoadFileBlockMap(self, fn, clobbered_blocks, allow_shared_blocks): """Loads the given block map file. Args: fn: The filename of the block map file. clobbered_blocks: A RangeSet instance for the clobbered blocks. allow_shared_blocks: Whether having shared blocks is allowed. """ remaining = self.care_map self.file_map = out = {} with open(fn) as f: for line in f: fn, ranges_text = line.rstrip().split(None, 1) raw_ranges = rangelib.RangeSet.parse(ranges_text) # Note: e2fsdroid records holes in the extent tree as "0" blocks. # This causes confusion because clobbered_blocks always includes # the superblock (physical block #0). Since the 0 blocks here do # not represent actual physical blocks, remove them from the set. ranges = raw_ranges.subtract(rangelib.RangeSet("0")) # b/150334561 we need to perserve the monotonic property of the raw # range. Otherwise, the validation script will read the blocks with # wrong order when pulling files from the image. ranges.monotonic = raw_ranges.monotonic ranges.extra['text_str'] = ranges_text if allow_shared_blocks: # Find the shared blocks that have been claimed by others. If so, tag # the entry so that we can skip applying imgdiff on this file. shared_blocks = ranges.subtract(remaining) if shared_blocks: non_shared = ranges.subtract(shared_blocks) if not non_shared: continue # Put the non-shared RangeSet as the value in the block map, which # has a copy of the original RangeSet. non_shared.extra['uses_shared_blocks'] = ranges ranges = non_shared out[fn] = ranges assert ranges.size() == ranges.intersect(remaining).size() # Currently we assume that blocks in clobbered_blocks are not part of # any file. assert not clobbered_blocks.overlaps(ranges) remaining = remaining.subtract(ranges) remaining = remaining.subtract(clobbered_blocks) if self.hashtree_info: remaining = remaining.subtract(self.hashtree_info.hashtree_range) # For all the remaining blocks in the care_map (ie, those that # aren't part of the data for any file nor part of the clobbered_blocks), # divide them into blocks that are all zero and blocks that aren't. # (Zero blocks are handled specially because (1) there are usually # a lot of them and (2) bsdiff handles files with long sequences of # repeated bytes especially poorly.) zero_blocks = [] nonzero_blocks = [] reference = '\0' * self.blocksize # Workaround for bug 23227672. For squashfs, we don't have a system.map. So # the whole system image will be treated as a single file. But for some # unknown bug, the updater will be killed due to OOM when writing back the # patched image to flash (observed on lenok-userdebug MEA49). Prior to # getting a real fix, we evenly divide the non-zero blocks into smaller # groups (currently 1024 blocks or 4MB per group). # Bug: 23227672 MAX_BLOCKS_PER_GROUP = 1024 nonzero_groups = [] f = self.simg_f for s, e in remaining: for b in range(s, e): idx = bisect.bisect_right(self.offset_index, b) - 1 chunk_start, _, filepos, fill_data = self.offset_map[idx] if filepos is not None: filepos += (b-chunk_start) * self.blocksize f.seek(filepos, os.SEEK_SET) data = f.read(self.blocksize) else: if fill_data == reference[:4]: # fill with all zeros data = reference else: data = None if data == reference: zero_blocks.append(b) zero_blocks.append(b+1) else: nonzero_blocks.append(b) nonzero_blocks.append(b+1) if len(nonzero_blocks) >= MAX_BLOCKS_PER_GROUP: nonzero_groups.append(nonzero_blocks) # Clear the list. nonzero_blocks = [] if nonzero_blocks: nonzero_groups.append(nonzero_blocks) nonzero_blocks = [] assert zero_blocks or nonzero_groups or clobbered_blocks if zero_blocks: out["__ZERO"] = rangelib.RangeSet(data=zero_blocks) if nonzero_groups: for i, blocks in enumerate(nonzero_groups): out["__NONZERO-%d" % i] = rangelib.RangeSet(data=blocks) if clobbered_blocks: out["__COPY"] = clobbered_blocks if self.hashtree_info: out["__HASHTREE"] = self.hashtree_info.hashtree_range def ResetFileMap(self): """Throw away the file map and treat the entire image as undifferentiated data.""" self.file_map = {"__DATA": self.care_map} def GetImagePartitionSize(img): try: simg = SparseImage(img, build_map=False) return simg.blocksize * simg.total_blocks except ValueError: return os.path.getsize(img) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('image') parser.add_argument('--get_partition_size', action='store_true', help='Return partition size of the image') args = parser.parse_args() if args.get_partition_size: print(GetImagePartitionSize(args.image))