paycheck.py 13 KB

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