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    for partition in manifest.partitions:
78      DisplayValue("  Timestamp for " +
79                   partition.partition_name, partition.version)
80    for partition in manifest.partitions:
81      DisplayValue("  COW Size for " +
82                   partition.partition_name, partition.estimate_cow_size)
83    DisplayValue('Block size', manifest.block_size)
84    DisplayValue('Minor version', manifest.minor_version)
85
86  def _DisplaySignatures(self):
87    """Show information about the signatures from the manifest."""
88    header = self.payload.header
89    if header.metadata_signature_len:
90      offset = header.size + header.manifest_len
91      DisplayValue('Metadata signatures blob',
92                   'file_offset=%d (%d bytes)' %
93                   (offset, header.metadata_signature_len))
94      # pylint: disable=invalid-unary-operand-type
95      signatures_blob = self.payload.ReadDataBlob(
96          -header.metadata_signature_len,
97          header.metadata_signature_len)
98      self._DisplaySignaturesBlob('Metadata', signatures_blob)
99    else:
100      print('No metadata signatures stored in the payload')
101
102    manifest = self.payload.manifest
103    if manifest.HasField('signatures_offset'):
104      signature_msg = 'blob_offset=%d' % manifest.signatures_offset
105      if manifest.signatures_size:
106        signature_msg += ' (%d bytes)' % manifest.signatures_size
107      DisplayValue('Payload signatures blob', signature_msg)
108      signatures_blob = self.payload.ReadDataBlob(manifest.signatures_offset,
109                                                  manifest.signatures_size)
110      self._DisplaySignaturesBlob('Payload', signatures_blob)
111    else:
112      print('No payload signatures stored in the payload')
113
114  @staticmethod
115  def _DisplaySignaturesBlob(signature_name, signatures_blob):
116    """Show information about the signatures blob."""
117    signatures = update_payload.update_metadata_pb2.Signatures()
118    signatures.ParseFromString(signatures_blob)
119    print('%s signatures: (%d entries)' %
120          (signature_name, len(signatures.signatures)))
121    for signature in signatures.signatures:
122      print('  version=%s, hex_data: (%d bytes)' %
123            (signature.version if signature.HasField('version') else None,
124             len(signature.data)))
125      DisplayHexData(signature.data, indent=4)
126
127
128  def _DisplayOps(self, name, operations):
129    """Show information about the install operations from the manifest.
130
131    The list shown includes operation type, data offset, data length, source
132    extents, source length, destination extents, and destinations length.
133
134    Args:
135      name: The name you want displayed above the operation table.
136      operations: The operations object that you want to display information
137                  about.
138    """
139    def _DisplayExtents(extents, name):
140      """Show information about extents."""
141      num_blocks = sum([ext.num_blocks for ext in extents])
142      ext_str = ' '.join(
143          '(%s,%s)' % (ext.start_block, ext.num_blocks) for ext in extents)
144      # Make extent list wrap around at 80 chars.
145      ext_str = '\n      '.join(textwrap.wrap(ext_str, 74))
146      extent_plural = 's' if len(extents) > 1 else ''
147      block_plural = 's' if num_blocks > 1 else ''
148      print('    %s: %d extent%s (%d block%s)' %
149            (name, len(extents), extent_plural, num_blocks, block_plural))
150      print('      %s' % ext_str)
151
152    op_dict = update_payload.common.OpType.NAMES
153    print('%s:' % name)
154    for op_count, op in enumerate(operations):
155      print('  %d: %s' % (op_count, op_dict[op.type]))
156      if op.HasField('data_offset'):
157        print('    Data offset: %s' % op.data_offset)
158      if op.HasField('data_length'):
159        print('    Data length: %s' % op.data_length)
160      if op.src_extents:
161        _DisplayExtents(op.src_extents, 'Source')
162      if op.dst_extents:
163        _DisplayExtents(op.dst_extents, 'Destination')
164
165  def _GetStats(self, manifest):
166    """Returns various statistics about a payload file.
167
168    Returns a dictionary containing the number of blocks read during payload
169    application, the number of blocks written, and the number of seeks done
170    when writing during operation application.
171    """
172    read_blocks = 0
173    written_blocks = 0
174    num_write_seeks = 0
175    for partition in manifest.partitions:
176      last_ext = None
177      for curr_op in partition.operations:
178        read_blocks += sum([ext.num_blocks for ext in curr_op.src_extents])
179        written_blocks += sum([ext.num_blocks for ext in curr_op.dst_extents])
180        for curr_ext in curr_op.dst_extents:
181          # See if the extent is contiguous with the last extent seen.
182          if last_ext and (curr_ext.start_block !=
183                           last_ext.start_block + last_ext.num_blocks):
184            num_write_seeks += 1
185          last_ext = curr_ext
186
187      # Old and new partitions are read once during verification.
188      read_blocks += partition.old_partition_info.size // manifest.block_size
189      read_blocks += partition.new_partition_info.size // manifest.block_size
190
191    stats = {'read_blocks': read_blocks,
192             'written_blocks': written_blocks,
193             'num_write_seeks': num_write_seeks}
194    return stats
195
196  def _DisplayStats(self, manifest):
197    stats = self._GetStats(manifest)
198    DisplayValue('Blocks read', stats['read_blocks'])
199    DisplayValue('Blocks written', stats['written_blocks'])
200    DisplayValue('Seeks when writing', stats['num_write_seeks'])
201
202  def Run(self):
203    """Parse the update payload and display information from it."""
204    self.payload = update_payload.Payload(self.options.payload_file)
205    self.payload.Init()
206    self._DisplayHeader()
207    self._DisplayManifest()
208    if self.options.signatures:
209      self._DisplaySignatures()
210    if self.options.stats:
211      self._DisplayStats(self.payload.manifest)
212    if self.options.list_ops:
213      print()
214      for partition in self.payload.manifest.partitions:
215        self._DisplayOps('%s install operations' % partition.partition_name,
216                         partition.operations)
217
218
219def main():
220  parser = argparse.ArgumentParser(
221      description='Show information about an update payload.')
222  parser.add_argument('payload_file', type=argparse.FileType('rb'),
223                      help='The update payload file.')
224  parser.add_argument('--list_ops', default=False, action='store_true',
225                      help='List the install operations and their extents.')
226  parser.add_argument('--stats', default=False, action='store_true',
227                      help='Show information about overall input/output.')
228  parser.add_argument('--signatures', default=False, action='store_true',
229                      help='Show signatures stored in the payload.')
230  args = parser.parse_args()
231
232  PayloadCommand(args).Run()
233
234
235if __name__ == '__main__':
236  sys.exit(main())
237