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