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