Tianjie Xu | ce9d78f | 2017-04-29 23:24:50 -0700 | [diff] [blame] | 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: |
Kelvin Zhang | 928c234 | 2020-09-22 16:15:57 -0400 | [diff] [blame^] | 218 | with zipfile.ZipFile(args.ota_package, 'r', allowZip64=True) as package: |
Tianjie Xu | ce9d78f | 2017-04-29 23:24:50 -0700 | [diff] [blame] | 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:]) |