github.com/westcoastroms/westcoastroms-build@v0.0.0-20190928114312-2350e5a73030/build/make/tools/releasetools/ota_package_parser.py (about)

     1  #!/usr/bin/env python
     2  # Copyright (C) 2017 The Android Open Source Project
     3  #
     4  # Licensed under the Apache License, Version 2.0 (the "License");
     5  # you may not use this file except in compliance with the License.
     6  # You may obtain a copy of the License at
     7  #
     8  #      http://www.apache.org/licenses/LICENSE-2.0
     9  #
    10  # Unless required by applicable law or agreed to in writing, software
    11  # distributed under the License is distributed on an "AS IS" BASIS,
    12  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  # See the License for the specific language governing permissions and
    14  # limitations under the License.
    15  
    16  import argparse
    17  import logging
    18  import sys
    19  import traceback
    20  import zipfile
    21  
    22  from rangelib import RangeSet
    23  
    24  class Stash(object):
    25    """Build a map to track stashed blocks during update simulation."""
    26  
    27    def __init__(self):
    28      self.blocks_stashed = 0
    29      self.overlap_blocks_stashed = 0
    30      self.max_stash_needed = 0
    31      self.current_stash_size = 0
    32      self.stash_map = {}
    33  
    34    def StashBlocks(self, SHA1, blocks):
    35      if SHA1 in self.stash_map:
    36        logging.info("already stashed {}: {}".format(SHA1, blocks))
    37        return
    38      self.blocks_stashed += blocks.size()
    39      self.current_stash_size += blocks.size()
    40      self.max_stash_needed = max(self.current_stash_size, self.max_stash_needed)
    41      self.stash_map[SHA1] = blocks
    42  
    43    def FreeBlocks(self, SHA1):
    44      assert self.stash_map.has_key(SHA1), "stash {} not found".format(SHA1)
    45      self.current_stash_size -= self.stash_map[SHA1].size()
    46      del self.stash_map[SHA1]
    47  
    48    def HandleOverlapBlocks(self, SHA1, blocks):
    49      self.StashBlocks(SHA1, blocks)
    50      self.overlap_blocks_stashed += blocks.size()
    51      self.FreeBlocks(SHA1)
    52  
    53  
    54  class OtaPackageParser(object):
    55    """Parse a block-based OTA package."""
    56  
    57    def __init__(self, package):
    58      self.package = package
    59      self.new_data_size = 0
    60      self.patch_data_size = 0
    61      self.block_written = 0
    62      self.block_stashed = 0
    63  
    64    @staticmethod
    65    def GetSizeString(size):
    66      assert size >= 0
    67      base = 1024.0
    68      if size <= base:
    69        return "{} bytes".format(size)
    70      for units in ['K', 'M', 'G']:
    71        if size <= base * 1024 or units == 'G':
    72          return "{:.1f}{}".format(size / base, units)
    73        base *= 1024
    74  
    75    def ParseTransferList(self, name):
    76      """Simulate the transfer commands and calculate the amout of I/O."""
    77  
    78      logging.info("\nSimulating commands in '{}':".format(name))
    79      lines = self.package.read(name).strip().splitlines()
    80      assert len(lines) >= 4, "{} is too short; Transfer list expects at least" \
    81          "4 lines, it has {}".format(name, len(lines))
    82      assert int(lines[0]) >= 3
    83      logging.info("(version: {})".format(lines[0]))
    84  
    85      blocks_written = 0
    86      my_stash = Stash()
    87      for line in lines[4:]:
    88        cmd_list = line.strip().split(" ")
    89        cmd_name = cmd_list[0]
    90        try:
    91          if cmd_name == "new" or cmd_name == "zero":
    92            assert len(cmd_list) == 2, "command format error: {}".format(line)
    93            target_range = RangeSet.parse_raw(cmd_list[1])
    94            blocks_written += target_range.size()
    95          elif cmd_name == "move":
    96            # Example:  move <onehash> <tgt_range> <src_blk_count> <src_range>
    97            # [<loc_range> <stashed_blocks>]
    98            assert len(cmd_list) >= 5, "command format error: {}".format(line)
    99            target_range = RangeSet.parse_raw(cmd_list[2])
   100            blocks_written += target_range.size()
   101            if cmd_list[4] == '-':
   102              continue
   103            SHA1 = cmd_list[1]
   104            source_range = RangeSet.parse_raw(cmd_list[4])
   105            if target_range.overlaps(source_range):
   106              my_stash.HandleOverlapBlocks(SHA1, source_range)
   107          elif cmd_name == "bsdiff" or cmd_name == "imgdiff":
   108            # Example:  bsdiff <offset> <len> <src_hash> <tgt_hash> <tgt_range>
   109            # <src_blk_count> <src_range> [<loc_range> <stashed_blocks>]
   110            assert len(cmd_list) >= 8, "command format error: {}".format(line)
   111            target_range = RangeSet.parse_raw(cmd_list[5])
   112            blocks_written += target_range.size()
   113            if cmd_list[7] == '-':
   114              continue
   115            source_SHA1 = cmd_list[3]
   116            source_range = RangeSet.parse_raw(cmd_list[7])
   117            if target_range.overlaps(source_range):
   118              my_stash.HandleOverlapBlocks(source_SHA1, source_range)
   119          elif cmd_name == "stash":
   120            assert len(cmd_list) == 3, "command format error: {}".format(line)
   121            SHA1 = cmd_list[1]
   122            source_range = RangeSet.parse_raw(cmd_list[2])
   123            my_stash.StashBlocks(SHA1, source_range)
   124          elif cmd_name == "free":
   125            assert len(cmd_list) == 2, "command format error: {}".format(line)
   126            SHA1 = cmd_list[1]
   127            my_stash.FreeBlocks(SHA1)
   128        except:
   129          logging.error("failed to parse command in: " + line)
   130          raise
   131  
   132      self.block_written += blocks_written
   133      self.block_stashed += my_stash.blocks_stashed
   134  
   135      logging.info("blocks written: {}  (expected: {})".format(
   136          blocks_written, lines[1]))
   137      logging.info("max blocks stashed simultaneously: {}  (expected: {})".
   138          format(my_stash.max_stash_needed, lines[3]))
   139      logging.info("total blocks stashed: {}".format(my_stash.blocks_stashed))
   140      logging.info("blocks stashed implicitly: {}".format(
   141          my_stash.overlap_blocks_stashed))
   142  
   143    def PrintDataInfo(self, partition):
   144      logging.info("\nReading data info for {} partition:".format(partition))
   145      new_data = self.package.getinfo(partition + ".new.dat")
   146      patch_data = self.package.getinfo(partition + ".patch.dat")
   147      logging.info("{:<40}{:<40}".format(new_data.filename, patch_data.filename))
   148      logging.info("{:<40}{:<40}".format(
   149            "compress_type: " + str(new_data.compress_type),
   150            "compress_type: " + str(patch_data.compress_type)))
   151      logging.info("{:<40}{:<40}".format(
   152            "compressed_size: " + OtaPackageParser.GetSizeString(
   153                new_data.compress_size),
   154            "compressed_size: " + OtaPackageParser.GetSizeString(
   155                patch_data.compress_size)))
   156      logging.info("{:<40}{:<40}".format(
   157          "file_size: " + OtaPackageParser.GetSizeString(new_data.file_size),
   158          "file_size: " + OtaPackageParser.GetSizeString(patch_data.file_size)))
   159  
   160      self.new_data_size += new_data.file_size
   161      self.patch_data_size += patch_data.file_size
   162  
   163    def AnalyzePartition(self, partition):
   164      assert partition in ("system", "vendor")
   165      assert partition + ".new.dat" in self.package.namelist()
   166      assert partition + ".patch.dat" in self.package.namelist()
   167      assert partition + ".transfer.list" in self.package.namelist()
   168  
   169      self.PrintDataInfo(partition)
   170      self.ParseTransferList(partition + ".transfer.list")
   171  
   172    def PrintMetadata(self):
   173      metadata_path = "META-INF/com/android/metadata"
   174      logging.info("\nMetadata info:")
   175      metadata_info = {}
   176      for line in self.package.read(metadata_path).strip().splitlines():
   177        index = line.find("=")
   178        metadata_info[line[0 : index].strip()] = line[index + 1:].strip()
   179      assert metadata_info.get("ota-type") == "BLOCK"
   180      assert "pre-device" in metadata_info
   181      logging.info("device: {}".format(metadata_info["pre-device"]))
   182      if "pre-build" in metadata_info:
   183        logging.info("pre-build: {}".format(metadata_info["pre-build"]))
   184      assert "post-build" in metadata_info
   185      logging.info("post-build: {}".format(metadata_info["post-build"]))
   186  
   187    def Analyze(self):
   188      logging.info("Analyzing ota package: " + self.package.filename)
   189      self.PrintMetadata()
   190      assert "system.new.dat" in self.package.namelist()
   191      self.AnalyzePartition("system")
   192      if "vendor.new.dat" in self.package.namelist():
   193        self.AnalyzePartition("vendor")
   194  
   195      #TODO Add analysis of other partitions(e.g. bootloader, boot, radio)
   196  
   197      BLOCK_SIZE = 4096
   198      logging.info("\nOTA package analyzed:")
   199      logging.info("new data size (uncompressed): " +
   200          OtaPackageParser.GetSizeString(self.new_data_size))
   201      logging.info("patch data size (uncompressed): " +
   202          OtaPackageParser.GetSizeString(self.patch_data_size))
   203      logging.info("total data written: " +
   204          OtaPackageParser.GetSizeString(self.block_written * BLOCK_SIZE))
   205      logging.info("total data stashed: " +
   206          OtaPackageParser.GetSizeString(self.block_stashed * BLOCK_SIZE))
   207  
   208  
   209  def main(argv):
   210    parser = argparse.ArgumentParser(description='Analyze an OTA package.')
   211    parser.add_argument("ota_package", help='Path of the OTA package.')
   212    args = parser.parse_args(argv)
   213  
   214    logging_format = '%(message)s'
   215    logging.basicConfig(level=logging.INFO, format=logging_format)
   216  
   217    try:
   218      with zipfile.ZipFile(args.ota_package, 'r') as package:
   219        package_parser = OtaPackageParser(package)
   220        package_parser.Analyze()
   221    except:
   222      logging.error("Failed to read " + args.ota_package)
   223      traceback.print_exc()
   224      sys.exit(1)
   225  
   226  
   227  if __name__ == '__main__':
   228    main(sys.argv[1:])