| #!/usr/bin/python2 |
| # |
| # Copyright (C) 2013 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| """Command-line tool for checking and applying Chrome OS update payloads.""" |
| |
| from __future__ import print_function |
| |
| # pylint: disable=import-error |
| import argparse |
| import filecmp |
| import os |
| import sys |
| import tempfile |
| |
| from update_payload import error |
| |
| lib_dir = os.path.join(os.path.dirname(__file__), 'lib') |
| if os.path.exists(lib_dir) and os.path.isdir(lib_dir): |
| sys.path.insert(1, lib_dir) |
| import update_payload |
| |
| |
| _TYPE_FULL = 'full' |
| _TYPE_DELTA = 'delta' |
| |
| |
| def ParseArguments(argv): |
| """Parse and validate command-line arguments. |
| |
| Args: |
| argv: command-line arguments to parse (excluding the program name) |
| |
| Returns: |
| Returns the arguments returned by the argument parser. |
| """ |
| parser = argparse.ArgumentParser( |
| description=('Applies a Chrome OS update PAYLOAD to src_kern and ' |
| 'src_root emitting dst_kern and dst_root, respectively. ' |
| 'src_kern and src_root are only needed for delta payloads. ' |
| 'When no partitions are provided, verifies the payload ' |
| 'integrity.'), |
| epilog=('Note: a payload may verify correctly but fail to apply, and ' |
| 'vice versa; this is by design and can be thought of as static ' |
| 'vs dynamic correctness. A payload that both verifies and ' |
| 'applies correctly should be safe for use by the Chrome OS ' |
| 'Update Engine. Use --check to verify a payload prior to ' |
| 'applying it.'), |
| formatter_class=argparse.RawDescriptionHelpFormatter |
| ) |
| |
| check_args = parser.add_argument_group('Checking payload integrity') |
| check_args.add_argument('-c', '--check', action='store_true', default=False, |
| help=('force payload integrity check (e.g. before ' |
| 'applying)')) |
| check_args.add_argument('-D', '--describe', action='store_true', |
| default=False, |
| help='Print a friendly description of the payload.') |
| check_args.add_argument('-r', '--report', metavar='FILE', |
| help="dump payload report (`-' for stdout)") |
| check_args.add_argument('-t', '--type', dest='assert_type', |
| help='assert the payload type', |
| choices=[_TYPE_FULL, _TYPE_DELTA]) |
| check_args.add_argument('-z', '--block-size', metavar='NUM', default=0, |
| type=int, |
| help='assert a non-default (4096) payload block size') |
| check_args.add_argument('-u', '--allow-unhashed', action='store_true', |
| default=False, help='allow unhashed operations') |
| check_args.add_argument('-d', '--disabled_tests', default=(), metavar='', |
| help=('space separated list of tests to disable. ' |
| 'allowed options include: ' + |
| ', '.join(update_payload.CHECKS_TO_DISABLE)), |
| choices=update_payload.CHECKS_TO_DISABLE) |
| check_args.add_argument('-k', '--key', metavar='FILE', |
| help=('override standard key used for signature ' |
| 'validation')) |
| check_args.add_argument('-m', '--meta-sig', metavar='FILE', |
| help='verify metadata against its signature') |
| check_args.add_argument('-s', '--metadata-size', metavar='NUM', default=0, |
| help='the metadata size to verify with the one in' |
| ' payload') |
| check_args.add_argument('-p', '--root-part-size', metavar='NUM', |
| default=0, type=int, |
| help='override rootfs partition size auto-inference') |
| check_args.add_argument('-P', '--kern-part-size', metavar='NUM', |
| default=0, type=int, |
| help='override kernel partition size auto-inference') |
| |
| apply_args = parser.add_argument_group('Applying payload') |
| # TODO(ahassani): Extent extract-bsdiff to puffdiff too. |
| apply_args.add_argument('-x', '--extract-bsdiff', action='store_true', |
| default=False, |
| help=('use temp input/output files with BSDIFF ' |
| 'operations (not in-place)')) |
| apply_args.add_argument('--bspatch-path', metavar='FILE', |
| help='use the specified bspatch binary') |
| apply_args.add_argument('--puffpatch-path', metavar='FILE', |
| help='use the specified puffpatch binary') |
| apply_args.add_argument('--dst_kern', metavar='FILE', |
| help='destination kernel partition file') |
| apply_args.add_argument('--dst_root', metavar='FILE', |
| help='destination root partition file') |
| apply_args.add_argument('--src_kern', metavar='FILE', |
| help='source kernel partition file') |
| apply_args.add_argument('--src_root', metavar='FILE', |
| help='source root partition file') |
| apply_args.add_argument('--out_dst_kern', metavar='FILE', |
| help='created destination kernel partition file') |
| apply_args.add_argument('--out_dst_root', metavar='FILE', |
| help='created destination root partition file') |
| |
| parser.add_argument('payload', metavar='PAYLOAD', help='the payload file') |
| |
| # Parse command-line arguments. |
| args = parser.parse_args(argv) |
| |
| # There are several options that imply --check. |
| args.check = (args.check or args.report or args.assert_type or |
| args.block_size or args.allow_unhashed or |
| args.disabled_tests or args.meta_sig or args.key or |
| args.root_part_size or args.kern_part_size or |
| args.metadata_size) |
| |
| # Check the arguments, enforce payload type accordingly. |
| if (args.src_kern is None) != (args.src_root is None): |
| parser.error('--src_kern and --src_root should be given together') |
| if (args.dst_kern is None) != (args.dst_root is None): |
| parser.error('--dst_kern and --dst_root should be given together') |
| if (args.out_dst_kern is None) != (args.out_dst_root is None): |
| parser.error('--out_dst_kern and --out_dst_root should be given together') |
| |
| if (args.dst_kern and args.dst_root) or \ |
| (args.out_dst_kern and args.out_dst_root): |
| if args.src_kern and args.src_root: |
| if args.assert_type == _TYPE_FULL: |
| parser.error('%s payload does not accept source partition arguments' |
| % _TYPE_FULL) |
| else: |
| args.assert_type = _TYPE_DELTA |
| else: |
| if args.assert_type == _TYPE_DELTA: |
| parser.error('%s payload requires source partitions arguments' |
| % _TYPE_DELTA) |
| else: |
| args.assert_type = _TYPE_FULL |
| else: |
| # Not applying payload. |
| if args.extract_bsdiff: |
| parser.error('--extract-bsdiff can only be used when applying payloads') |
| if args.bspatch_path: |
| parser.error('--bspatch-path can only be used when applying payloads') |
| if args.puffpatch_path: |
| parser.error('--puffpatch-path can only be used when applying payloads') |
| |
| # By default, look for a metadata-signature file with a name based on the name |
| # of the payload we are checking. We only do it if check was triggered. |
| if args.check and not args.meta_sig: |
| default_meta_sig = args.payload + '.metadata-signature' |
| if os.path.isfile(default_meta_sig): |
| args.meta_sig = default_meta_sig |
| print('Using default metadata signature', args.meta_sig, file=sys.stderr) |
| |
| return args |
| |
| |
| def main(argv): |
| # Parse and validate arguments. |
| args = ParseArguments(argv[1:]) |
| |
| with open(args.payload) as payload_file: |
| payload = update_payload.Payload(payload_file) |
| try: |
| # Initialize payload. |
| payload.Init() |
| |
| if args.describe: |
| payload.Describe() |
| |
| # Perform payload integrity checks. |
| if args.check: |
| report_file = None |
| do_close_report_file = False |
| metadata_sig_file = None |
| try: |
| if args.report: |
| if args.report == '-': |
| report_file = sys.stdout |
| else: |
| report_file = open(args.report, 'w') |
| do_close_report_file = True |
| |
| metadata_sig_file = args.meta_sig and open(args.meta_sig) |
| payload.Check( |
| pubkey_file_name=args.key, |
| metadata_sig_file=metadata_sig_file, |
| metadata_size=int(args.metadata_size), |
| report_out_file=report_file, |
| assert_type=args.assert_type, |
| block_size=int(args.block_size), |
| rootfs_part_size=args.root_part_size, |
| kernel_part_size=args.kern_part_size, |
| allow_unhashed=args.allow_unhashed, |
| disabled_tests=args.disabled_tests) |
| finally: |
| if metadata_sig_file: |
| metadata_sig_file.close() |
| if do_close_report_file: |
| report_file.close() |
| |
| # Apply payload. |
| if (args.dst_root and args.dst_kern) or \ |
| (args.out_dst_root and args.out_dst_kern): |
| dargs = {'bsdiff_in_place': not args.extract_bsdiff} |
| if args.bspatch_path: |
| dargs['bspatch_path'] = args.bspatch_path |
| if args.puffpatch_path: |
| dargs['puffpatch_path'] = args.puffpatch_path |
| if args.assert_type == _TYPE_DELTA: |
| dargs['old_kernel_part'] = args.src_kern |
| dargs['old_rootfs_part'] = args.src_root |
| |
| if args.out_dst_kern and args.out_dst_root: |
| out_dst_kern = open(args.out_dst_kern, 'w+') |
| out_dst_root = open(args.out_dst_root, 'w+') |
| else: |
| out_dst_kern = tempfile.NamedTemporaryFile() |
| out_dst_root = tempfile.NamedTemporaryFile() |
| |
| payload.Apply(out_dst_kern.name, out_dst_root.name, **dargs) |
| |
| # If destination kernel and rootfs partitions are not given, then this |
| # just becomes an apply operation with no check. |
| if args.dst_kern and args.dst_root: |
| # Prior to comparing, add the unused space past the filesystem |
| # boundary in the new target partitions to become the same size as |
| # the given partitions. This will truncate to larger size. |
| out_dst_kern.truncate(os.path.getsize(args.dst_kern)) |
| out_dst_root.truncate(os.path.getsize(args.dst_root)) |
| |
| # Compare resulting partitions with the ones from the target image. |
| if not filecmp.cmp(out_dst_kern.name, args.dst_kern): |
| raise error.PayloadError('Resulting kernel partition corrupted.') |
| if not filecmp.cmp(out_dst_root.name, args.dst_root): |
| raise error.PayloadError('Resulting rootfs partition corrupted.') |
| |
| # Close the output files. If args.out_dst_* was not given, then these |
| # files are created as temp files and will be deleted upon close(). |
| out_dst_kern.close() |
| out_dst_root.close() |
| |
| except error.PayloadError, e: |
| sys.stderr.write('Error: %s\n' % e) |
| return 1 |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv)) |