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