1#!/usr/bin/python2 2# 3# Copyright (C) 2013 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18"""Command-line tool for checking and applying Chrome OS update payloads.""" 19 20from __future__ import print_function 21 22# pylint: disable=import-error 23import argparse 24import filecmp 25import os 26import sys 27import tempfile 28 29from update_payload import common 30from update_payload import error 31 32lib_dir = os.path.join(os.path.dirname(__file__), 'lib') 33if os.path.exists(lib_dir) and os.path.isdir(lib_dir): 34 sys.path.insert(1, lib_dir) 35import update_payload 36 37 38_TYPE_FULL = 'full' 39_TYPE_DELTA = 'delta' 40 41 42def ParseArguments(argv): 43 """Parse and validate command-line arguments. 44 45 Args: 46 argv: command-line arguments to parse (excluding the program name) 47 48 Returns: 49 Returns the arguments returned by the argument parser. 50 """ 51 parser = argparse.ArgumentParser( 52 description=('Applies a Chrome OS update PAYLOAD to src_kern and ' 53 'src_root emitting dst_kern and dst_root, respectively. ' 54 'src_kern and src_root are only needed for delta payloads. ' 55 'When no partitions are provided, verifies the payload ' 56 'integrity.'), 57 epilog=('Note: a payload may verify correctly but fail to apply, and ' 58 'vice versa; this is by design and can be thought of as static ' 59 'vs dynamic correctness. A payload that both verifies and ' 60 'applies correctly should be safe for use by the Chrome OS ' 61 'Update Engine. Use --check to verify a payload prior to ' 62 'applying it.'), 63 formatter_class=argparse.RawDescriptionHelpFormatter 64 ) 65 66 check_args = parser.add_argument_group('Checking payload integrity') 67 check_args.add_argument('-c', '--check', action='store_true', default=False, 68 help=('force payload integrity check (e.g. before ' 69 'applying)')) 70 check_args.add_argument('-D', '--describe', action='store_true', 71 default=False, 72 help='Print a friendly description of the payload.') 73 check_args.add_argument('-r', '--report', metavar='FILE', 74 help="dump payload report (`-' for stdout)") 75 check_args.add_argument('-t', '--type', dest='assert_type', 76 help='assert the payload type', 77 choices=[_TYPE_FULL, _TYPE_DELTA]) 78 check_args.add_argument('-z', '--block-size', metavar='NUM', default=0, 79 type=int, 80 help='assert a non-default (4096) payload block size') 81 check_args.add_argument('-u', '--allow-unhashed', action='store_true', 82 default=False, help='allow unhashed operations') 83 check_args.add_argument('-d', '--disabled_tests', default=(), metavar='', 84 help=('space separated list of tests to disable. ' 85 'allowed options include: ' + 86 ', '.join(update_payload.CHECKS_TO_DISABLE)), 87 choices=update_payload.CHECKS_TO_DISABLE) 88 check_args.add_argument('-k', '--key', metavar='FILE', 89 help=('override standard key used for signature ' 90 'validation')) 91 check_args.add_argument('-m', '--meta-sig', metavar='FILE', 92 help='verify metadata against its signature') 93 check_args.add_argument('-s', '--metadata-size', metavar='NUM', default=0, 94 help='the metadata size to verify with the one in' 95 ' payload') 96 # TODO(tbrindus): deprecated in favour of --part_sizes 97 check_args.add_argument('-p', '--root-part-size', metavar='NUM', 98 default=0, type=int, 99 help='override rootfs partition size auto-inference') 100 check_args.add_argument('-P', '--kern-part-size', metavar='NUM', 101 default=0, type=int, 102 help='override kernel partition size auto-inference') 103 check_args.add_argument('--part_sizes', metavar='NUM', nargs='+', type=int, 104 help='override partition size auto-inference') 105 106 apply_args = parser.add_argument_group('Applying payload') 107 # TODO(ahassani): Extent extract-bsdiff to puffdiff too. 108 apply_args.add_argument('-x', '--extract-bsdiff', action='store_true', 109 default=False, 110 help=('use temp input/output files with BSDIFF ' 111 'operations (not in-place)')) 112 apply_args.add_argument('--bspatch-path', metavar='FILE', 113 help='use the specified bspatch binary') 114 apply_args.add_argument('--puffpatch-path', metavar='FILE', 115 help='use the specified puffpatch binary') 116 # TODO(tbrindus): deprecated in favour of --dst_part_paths 117 apply_args.add_argument('--dst_kern', metavar='FILE', 118 help='destination kernel partition file') 119 apply_args.add_argument('--dst_root', metavar='FILE', 120 help='destination root partition file') 121 # TODO(tbrindus): deprecated in favour of --src_part_paths 122 apply_args.add_argument('--src_kern', metavar='FILE', 123 help='source kernel partition file') 124 apply_args.add_argument('--src_root', metavar='FILE', 125 help='source root partition file') 126 # TODO(tbrindus): deprecated in favour of --out_dst_part_paths 127 apply_args.add_argument('--out_dst_kern', metavar='FILE', 128 help='created destination kernel partition file') 129 apply_args.add_argument('--out_dst_root', metavar='FILE', 130 help='created destination root partition file') 131 132 apply_args.add_argument('--src_part_paths', metavar='FILE', nargs='+', 133 help='source partitition files') 134 apply_args.add_argument('--dst_part_paths', metavar='FILE', nargs='+', 135 help='destination partition files') 136 apply_args.add_argument('--out_dst_part_paths', metavar='FILE', nargs='+', 137 help='created destination partition files') 138 139 parser.add_argument('payload', metavar='PAYLOAD', help='the payload file') 140 parser.add_argument('--part_names', metavar='NAME', nargs='+', 141 help='names of partitions') 142 143 # Parse command-line arguments. 144 args = parser.parse_args(argv) 145 146 # TODO(tbrindus): temporary workaround to keep old-style flags from breaking 147 # without having to handle both types in our code. Remove after flag usage is 148 # removed from calling scripts. 149 args.part_names = args.part_names or [common.KERNEL, common.ROOTFS] 150 args.part_sizes = args.part_sizes or [args.kern_part_size, 151 args.root_part_size] 152 args.src_part_paths = args.src_part_paths or [args.src_kern, args.src_root] 153 args.dst_part_paths = args.dst_part_paths or [args.dst_kern, args.dst_root] 154 args.out_dst_part_paths = args.out_dst_part_paths or [args.out_dst_kern, 155 args.out_dst_root] 156 157 # Make sure we don't have new dependencies on old flags by deleting them from 158 # the namespace here. 159 for old in ['kern_part_size', 'root_part_size', 'src_kern', 'src_root', 160 'dst_kern', 'dst_root', 'out_dst_kern', 'out_dst_root']: 161 delattr(args, old) 162 163 # There are several options that imply --check. 164 args.check = (args.check or args.report or args.assert_type or 165 args.block_size or args.allow_unhashed or 166 args.disabled_tests or args.meta_sig or args.key or 167 any(args.part_sizes) or args.metadata_size) 168 169 for arg in ['part_sizes', 'src_part_paths', 'dst_part_paths', 170 'out_dst_part_paths']: 171 if len(args.part_names) != len(getattr(args, arg, [])): 172 parser.error('partitions in --%s do not match --part_names' % arg) 173 174 if all(args.dst_part_paths) or all(args.out_dst_part_paths): 175 if all(args.src_part_paths): 176 if args.assert_type == _TYPE_FULL: 177 parser.error('%s payload does not accept source partition arguments' 178 % _TYPE_FULL) 179 else: 180 args.assert_type = _TYPE_DELTA 181 else: 182 if args.assert_type == _TYPE_DELTA: 183 parser.error('%s payload requires source partitions arguments' 184 % _TYPE_DELTA) 185 else: 186 args.assert_type = _TYPE_FULL 187 else: 188 # Not applying payload. 189 if args.extract_bsdiff: 190 parser.error('--extract-bsdiff can only be used when applying payloads') 191 if args.bspatch_path: 192 parser.error('--bspatch-path can only be used when applying payloads') 193 if args.puffpatch_path: 194 parser.error('--puffpatch-path can only be used when applying payloads') 195 196 # By default, look for a metadata-signature file with a name based on the name 197 # of the payload we are checking. We only do it if check was triggered. 198 if args.check and not args.meta_sig: 199 default_meta_sig = args.payload + '.metadata-signature' 200 if os.path.isfile(default_meta_sig): 201 args.meta_sig = default_meta_sig 202 print('Using default metadata signature', args.meta_sig, file=sys.stderr) 203 204 return args 205 206 207def main(argv): 208 # Parse and validate arguments. 209 args = ParseArguments(argv[1:]) 210 211 with open(args.payload) as payload_file: 212 payload = update_payload.Payload(payload_file) 213 try: 214 # Initialize payload. 215 payload.Init() 216 217 if args.describe: 218 payload.Describe() 219 220 # Perform payload integrity checks. 221 if args.check: 222 report_file = None 223 do_close_report_file = False 224 metadata_sig_file = None 225 try: 226 if args.report: 227 if args.report == '-': 228 report_file = sys.stdout 229 else: 230 report_file = open(args.report, 'w') 231 do_close_report_file = True 232 233 part_sizes = dict(zip(args.part_names, args.part_sizes)) 234 metadata_sig_file = args.meta_sig and open(args.meta_sig) 235 payload.Check( 236 pubkey_file_name=args.key, 237 metadata_sig_file=metadata_sig_file, 238 metadata_size=int(args.metadata_size), 239 report_out_file=report_file, 240 assert_type=args.assert_type, 241 block_size=int(args.block_size), 242 part_sizes=part_sizes, 243 allow_unhashed=args.allow_unhashed, 244 disabled_tests=args.disabled_tests) 245 finally: 246 if metadata_sig_file: 247 metadata_sig_file.close() 248 if do_close_report_file: 249 report_file.close() 250 251 # Apply payload. 252 if all(args.dst_part_paths) or all(args.out_dst_part_paths): 253 dargs = {'bsdiff_in_place': not args.extract_bsdiff} 254 if args.bspatch_path: 255 dargs['bspatch_path'] = args.bspatch_path 256 if args.puffpatch_path: 257 dargs['puffpatch_path'] = args.puffpatch_path 258 if args.assert_type == _TYPE_DELTA: 259 dargs['old_parts'] = dict(zip(args.part_names, args.src_part_paths)) 260 261 out_dst_parts = {} 262 file_handles = [] 263 if all(args.out_dst_part_paths): 264 for name, path in zip(args.part_names, args.out_dst_part_paths): 265 handle = open(path, 'w+') 266 file_handles.append(handle) 267 out_dst_parts[name] = handle.name 268 else: 269 for name in args.part_names: 270 handle = tempfile.NamedTemporaryFile() 271 file_handles.append(handle) 272 out_dst_parts[name] = handle.name 273 274 payload.Apply(out_dst_parts, **dargs) 275 276 # If destination kernel and rootfs partitions are not given, then this 277 # just becomes an apply operation with no check. 278 if all(args.dst_part_paths): 279 # Prior to comparing, add the unused space past the filesystem 280 # boundary in the new target partitions to become the same size as 281 # the given partitions. This will truncate to larger size. 282 for part_name, out_dst_part, dst_part in zip(args.part_names, 283 file_handles, 284 args.dst_part_paths): 285 out_dst_part.truncate(os.path.getsize(dst_part)) 286 287 # Compare resulting partitions with the ones from the target image. 288 if not filecmp.cmp(out_dst_part.name, dst_part): 289 raise error.PayloadError( 290 'Resulting %s partition corrupted.' % part_name) 291 292 # Close the output files. If args.out_dst_* was not given, then these 293 # files are created as temp files and will be deleted upon close(). 294 for handle in file_handles: 295 handle.close() 296 except error.PayloadError, e: 297 sys.stderr.write('Error: %s\n' % e) 298 return 1 299 300 return 0 301 302 303if __name__ == '__main__': 304 sys.exit(main(sys.argv)) 305