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