xref: /aosp_15_r20/system/update_engine/scripts/payload_info.py (revision 5a9231315b4521097b8dc3750bc806fcafe0c72f)
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2015 The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19"""payload_info: Show information about an update payload."""
20
21from __future__ import absolute_import
22from __future__ import print_function
23
24import argparse
25import sys
26import textwrap
27
28from six.moves import range
29import update_metadata_pb2
30import update_payload
31
32
33MAJOR_PAYLOAD_VERSION_BRILLO = 2
34
35def DisplayValue(key, value):
36  """Print out a key, value pair with values left-aligned."""
37  if value is not None:
38    print('%-*s %s' % (28, key + ':', value))
39  else:
40    raise ValueError('Cannot display an empty value.')
41
42
43def DisplayHexData(data, indent=0):
44  """Print out binary data as a hex values."""
45  for off in range(0, len(data), 16):
46    chunk = bytearray(data[off:off + 16])
47    print(' ' * indent +
48          ' '.join('%.2x' % c for c in chunk) +
49          '   ' * (16 - len(chunk)) +
50          ' | ' +
51          ''.join(chr(c) if 32 <= c < 127 else '.' for c in chunk))
52
53
54class PayloadCommand:
55  """Show basic information about an update payload.
56
57  This command parses an update payload and displays information from
58  its header and manifest.
59  """
60
61  def __init__(self, options):
62    self.options = options
63    self.payload = None
64
65  def _DisplayHeader(self):
66    """Show information from the payload header."""
67    header = self.payload.header
68    DisplayValue('Payload version', header.version)
69    DisplayValue('Manifest length', header.manifest_len)
70
71  def _DisplayManifest(self):
72    """Show information from the payload manifest."""
73    manifest = self.payload.manifest
74    # pylint: disable=no-member
75    DisplayValue('Number of partitions', len(manifest.partitions))
76    for partition in manifest.partitions:
77      DisplayValue('  Number of "%s" ops' % partition.partition_name,
78                   len(partition.operations))
79    for partition in manifest.partitions:
80      DisplayValue("  Timestamp for " +
81                   partition.partition_name, partition.version)
82    for partition in manifest.partitions:
83      DisplayValue("  COW Size for " +
84                   partition.partition_name, partition.estimate_cow_size)
85    DisplayValue('Block size', manifest.block_size)
86    DisplayValue('Minor version', manifest.minor_version)
87
88  def _DisplaySignatures(self):
89    """Show information about the signatures from the manifest."""
90    header = self.payload.header
91    if header.metadata_signature_len:
92      offset = header.size + header.manifest_len
93      DisplayValue('Metadata signatures blob',
94                   'file_offset=%d (%d bytes)' %
95                   (offset, header.metadata_signature_len))
96      # pylint: disable=invalid-unary-operand-type
97      signatures_blob = self.payload.ReadDataBlob(
98          -header.metadata_signature_len,
99          header.metadata_signature_len)
100      self._DisplaySignaturesBlob('Metadata', signatures_blob)
101    else:
102      print('No metadata signatures stored in the payload')
103
104    manifest = self.payload.manifest
105    if manifest.HasField('signatures_offset'):
106      # pylint: disable=no-member
107      signature_msg = 'blob_offset=%d' % manifest.signatures_offset
108      if manifest.signatures_size:
109        signature_msg += ' (%d bytes)' % manifest.signatures_size
110      DisplayValue('Payload signatures blob', signature_msg)
111      signatures_blob = self.payload.ReadDataBlob(manifest.signatures_offset,
112                                                  manifest.signatures_size)
113      self._DisplaySignaturesBlob('Payload', signatures_blob)
114    else:
115      print('No payload signatures stored in the payload')
116
117  @staticmethod
118  def _DisplaySignaturesBlob(signature_name, signatures_blob):
119    """Show information about the signatures blob."""
120    signatures = update_metadata_pb2.Signatures()
121    signatures.ParseFromString(signatures_blob)
122    # pylint: disable=no-member
123    print('%s signatures: (%d entries)' %
124          (signature_name, len(signatures.signatures)))
125    for signature in signatures.signatures:
126      print('  version=%s, hex_data: (%d bytes)' %
127            (signature.version if signature.HasField('version') else None,
128             len(signature.data)))
129      DisplayHexData(signature.data, indent=4)
130
131
132  def _DisplayOps(self, name, operations):
133    """Show information about the install operations from the manifest.
134
135    The list shown includes operation type, data offset, data length, source
136    extents, source length, destination extents, and destinations length.
137
138    Args:
139      name: The name you want displayed above the operation table.
140      operations: The operations object that you want to display information
141                  about.
142    """
143    def _DisplayExtents(extents, name):
144      """Show information about extents."""
145      num_blocks = sum([ext.num_blocks for ext in extents])
146      ext_str = ' '.join(
147          '(%s,%s)' % (ext.start_block, ext.num_blocks) for ext in extents)
148      # Make extent list wrap around at 80 chars.
149      ext_str = '\n      '.join(textwrap.wrap(ext_str, 74))
150      extent_plural = 's' if len(extents) > 1 else ''
151      block_plural = 's' if num_blocks > 1 else ''
152      print('    %s: %d extent%s (%d block%s)' %
153            (name, len(extents), extent_plural, num_blocks, block_plural))
154      print('      %s' % ext_str)
155
156    op_dict = update_payload.common.OpType.NAMES
157    print('%s:' % name)
158    for op_count, op in enumerate(operations):
159      print('  %d: %s' % (op_count, op_dict[op.type]))
160      if op.HasField('data_offset'):
161        print('    Data offset: %s' % op.data_offset)
162      if op.HasField('data_length'):
163        print('    Data length: %s' % op.data_length)
164      if op.src_extents:
165        _DisplayExtents(op.src_extents, 'Source')
166      if op.dst_extents:
167        _DisplayExtents(op.dst_extents, 'Destination')
168
169  def _GetStats(self, manifest):
170    """Returns various statistics about a payload file.
171
172    Returns a dictionary containing the number of blocks read during payload
173    application, the number of blocks written, and the number of seeks done
174    when writing during operation application.
175    """
176    read_blocks = 0
177    written_blocks = 0
178    num_write_seeks = 0
179    for partition in manifest.partitions:
180      last_ext = None
181      for curr_op in partition.operations:
182        read_blocks += sum([ext.num_blocks for ext in curr_op.src_extents])
183        written_blocks += sum([ext.num_blocks for ext in curr_op.dst_extents])
184        for curr_ext in curr_op.dst_extents:
185          # See if the extent is contiguous with the last extent seen.
186          if last_ext and (curr_ext.start_block !=
187                           last_ext.start_block + last_ext.num_blocks):
188            num_write_seeks += 1
189          last_ext = curr_ext
190
191      # Old and new partitions are read once during verification.
192      read_blocks += partition.old_partition_info.size // manifest.block_size
193      read_blocks += partition.new_partition_info.size // manifest.block_size
194
195    stats = {'read_blocks': read_blocks,
196             'written_blocks': written_blocks,
197             'num_write_seeks': num_write_seeks}
198    return stats
199
200  def _DisplayStats(self, manifest):
201    stats = self._GetStats(manifest)
202    DisplayValue('Blocks read', stats['read_blocks'])
203    DisplayValue('Blocks written', stats['written_blocks'])
204    DisplayValue('Seeks when writing', stats['num_write_seeks'])
205
206  def Run(self):
207    """Parse the update payload and display information from it."""
208    self.payload = update_payload.Payload(self.options.payload_file)
209    self.payload.Init()
210    self._DisplayHeader()
211    self._DisplayManifest()
212    if self.options.signatures:
213      self._DisplaySignatures()
214    if self.options.stats:
215      self._DisplayStats(self.payload.manifest)
216    if self.options.list_ops:
217      print()
218      # pylint: disable=no-member
219      for partition in self.payload.manifest.partitions:
220        self._DisplayOps('%s install operations' % partition.partition_name,
221                         partition.operations)
222
223
224def main():
225  parser = argparse.ArgumentParser(
226      description='Show information about an update payload.')
227  parser.add_argument('payload_file', type=argparse.FileType('rb'),
228                      help='The update payload file.')
229  parser.add_argument('--list_ops', default=False, action='store_true',
230                      help='List the install operations and their extents.')
231  parser.add_argument('--stats', default=False, action='store_true',
232                      help='Show information about overall input/output.')
233  parser.add_argument('--signatures', default=False, action='store_true',
234                      help='Show signatures stored in the payload.')
235  args = parser.parse_args()
236
237  PayloadCommand(args).Run()
238
239
240if __name__ == '__main__':
241  sys.exit(main())
242