blob: 7eaa07ac8cace95c3f98eb92197c807e674c4ade [file] [log] [blame]
Amin Hassanif94b6432018-01-26 17:39:47 -08001#
2# Copyright (C) 2013 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
Gilad Arnold553b0ec2013-01-26 01:00:39 -080016
17"""Applying a Chrome OS update payload.
18
19This module is used internally by the main Payload class for applying an update
20payload. The interface for invoking the applier is as follows:
21
22 applier = PayloadApplier(payload)
23 applier.Run(...)
24
25"""
26
Andrew Lassalle165843c2019-11-05 13:30:34 -080027from __future__ import absolute_import
Allie Wood12f59aa2015-04-06 11:05:12 -070028from __future__ import print_function
29
Gilad Arnold553b0ec2013-01-26 01:00:39 -080030import array
31import bz2
32import hashlib
Amin Hassani0de7f782017-12-07 12:13:03 -080033# Not everywhere we can have the lzma library so we ignore it if we didn't have
34# it because it is not going to be used. For example, 'cros flash' uses
35# devserver code which eventually loads this file, but the lzma library is not
36# included in the client test devices, and it is not necessary to do so. But
37# lzma is not used in 'cros flash' so it should be fine. Python 3.x include
38# lzma, but for backward compatibility with Python 2.7, backports-lzma is
39# needed.
40try:
41 import lzma
42except ImportError:
43 try:
44 from backports import lzma
45 except ImportError:
46 pass
Gilad Arnold553b0ec2013-01-26 01:00:39 -080047import os
Gilad Arnold553b0ec2013-01-26 01:00:39 -080048import subprocess
49import sys
50import tempfile
51
Amin Hassanib05a65a2017-12-18 15:15:32 -080052from update_payload import common
53from update_payload.error import PayloadError
Gilad Arnold553b0ec2013-01-26 01:00:39 -080054
Andrew8a1de4b2019-11-23 20:32:35 -080055# buffer is not supported in python3, but memoryview has the same functionality
56if sys.version_info.major >= 3:
57 buffer = memoryview # pylint: disable=invalid-name, redefined-builtin
Gilad Arnold553b0ec2013-01-26 01:00:39 -080058#
59# Helper functions.
60#
Gilad Arnold382df5c2013-05-03 12:49:28 -070061def _VerifySha256(file_obj, expected_hash, name, length=-1):
Gilad Arnold553b0ec2013-01-26 01:00:39 -080062 """Verifies the SHA256 hash of a file.
63
64 Args:
65 file_obj: file object to read
66 expected_hash: the hash digest we expect to be getting
67 name: name string of this hash, for error reporting
Gilad Arnold382df5c2013-05-03 12:49:28 -070068 length: precise length of data to verify (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -070069
Gilad Arnold553b0ec2013-01-26 01:00:39 -080070 Raises:
Gilad Arnold382df5c2013-05-03 12:49:28 -070071 PayloadError if computed hash doesn't match expected one, or if fails to
72 read the specified length of data.
Gilad Arnold553b0ec2013-01-26 01:00:39 -080073 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -080074 hasher = hashlib.sha256()
75 block_length = 1024 * 1024
Andrew Lassalle165843c2019-11-05 13:30:34 -080076 max_length = length if length >= 0 else sys.maxsize
Gilad Arnold553b0ec2013-01-26 01:00:39 -080077
Gilad Arnold382df5c2013-05-03 12:49:28 -070078 while max_length > 0:
Gilad Arnold553b0ec2013-01-26 01:00:39 -080079 read_length = min(max_length, block_length)
80 data = file_obj.read(read_length)
81 if not data:
82 break
83 max_length -= len(data)
84 hasher.update(data)
85
Gilad Arnold382df5c2013-05-03 12:49:28 -070086 if length >= 0 and max_length > 0:
87 raise PayloadError(
88 'insufficient data (%d instead of %d) when verifying %s' %
89 (length - max_length, length, name))
90
Gilad Arnold553b0ec2013-01-26 01:00:39 -080091 actual_hash = hasher.digest()
92 if actual_hash != expected_hash:
93 raise PayloadError('%s hash (%s) not as expected (%s)' %
Gilad Arnold96405372013-05-04 00:24:58 -070094 (name, common.FormatSha256(actual_hash),
95 common.FormatSha256(expected_hash)))
Gilad Arnold553b0ec2013-01-26 01:00:39 -080096
97
98def _ReadExtents(file_obj, extents, block_size, max_length=-1):
99 """Reads data from file as defined by extent sequence.
100
101 This tries to be efficient by not copying data as it is read in chunks.
102
103 Args:
104 file_obj: file object
105 extents: sequence of block extents (offset and length)
106 block_size: size of each block
107 max_length: maximum length to read (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700108
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800109 Returns:
110 A character array containing the concatenated read data.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800111 """
Andrew8a1de4b2019-11-23 20:32:35 -0800112 data = array.array('B')
Gilad Arnold272a4992013-05-08 13:12:53 -0700113 if max_length < 0:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800114 max_length = sys.maxsize
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800115 for ex in extents:
116 if max_length == 0:
117 break
Gilad Arnold272a4992013-05-08 13:12:53 -0700118 read_length = min(max_length, ex.num_blocks * block_size)
Gilad Arnold658185a2013-05-08 17:57:54 -0700119
Amin Hassani55c75412019-10-07 11:20:39 -0700120 file_obj.seek(ex.start_block * block_size)
121 data.fromfile(file_obj, read_length)
Gilad Arnold658185a2013-05-08 17:57:54 -0700122
Gilad Arnold272a4992013-05-08 13:12:53 -0700123 max_length -= read_length
Gilad Arnold658185a2013-05-08 17:57:54 -0700124
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800125 return data
126
127
128def _WriteExtents(file_obj, data, extents, block_size, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -0700129 """Writes data to file as defined by extent sequence.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800130
131 This tries to be efficient by not copy data as it is written in chunks.
132
133 Args:
134 file_obj: file object
135 data: data to write
136 extents: sequence of block extents (offset and length)
137 block_size: size of each block
Gilad Arnold272a4992013-05-08 13:12:53 -0700138 base_name: name string of extent sequence for error reporting
Allie Wood12f59aa2015-04-06 11:05:12 -0700139
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800140 Raises:
141 PayloadError when things don't add up.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800142 """
143 data_offset = 0
144 data_length = len(data)
145 for ex, ex_name in common.ExtentIter(extents, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -0700146 if not data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800147 raise PayloadError('%s: more write extents than data' % ex_name)
Gilad Arnold272a4992013-05-08 13:12:53 -0700148 write_length = min(data_length, ex.num_blocks * block_size)
Gilad Arnold658185a2013-05-08 17:57:54 -0700149
Amin Hassani55c75412019-10-07 11:20:39 -0700150 file_obj.seek(ex.start_block * block_size)
151 data_view = buffer(data, data_offset, write_length)
152 file_obj.write(data_view)
Gilad Arnold658185a2013-05-08 17:57:54 -0700153
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800154 data_offset += write_length
Gilad Arnold272a4992013-05-08 13:12:53 -0700155 data_length -= write_length
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800156
Gilad Arnold272a4992013-05-08 13:12:53 -0700157 if data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800158 raise PayloadError('%s: more data than write extents' % base_name)
159
160
Gilad Arnold272a4992013-05-08 13:12:53 -0700161def _ExtentsToBspatchArg(extents, block_size, base_name, data_length=-1):
162 """Translates an extent sequence into a bspatch-compatible string argument.
163
164 Args:
165 extents: sequence of block extents (offset and length)
166 block_size: size of each block
167 base_name: name string of extent sequence for error reporting
168 data_length: the actual total length of the data in bytes (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700169
Gilad Arnold272a4992013-05-08 13:12:53 -0700170 Returns:
171 A tuple consisting of (i) a string of the form
172 "off_1:len_1,...,off_n:len_n", (ii) an offset where zero padding is needed
173 for filling the last extent, (iii) the length of the padding (zero means no
174 padding is needed and the extents cover the full length of data).
Allie Wood12f59aa2015-04-06 11:05:12 -0700175
Gilad Arnold272a4992013-05-08 13:12:53 -0700176 Raises:
177 PayloadError if data_length is too short or too long.
Gilad Arnold272a4992013-05-08 13:12:53 -0700178 """
179 arg = ''
180 pad_off = pad_len = 0
181 if data_length < 0:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800182 data_length = sys.maxsize
Gilad Arnold272a4992013-05-08 13:12:53 -0700183 for ex, ex_name in common.ExtentIter(extents, base_name):
184 if not data_length:
185 raise PayloadError('%s: more extents than total data length' % ex_name)
Gilad Arnold658185a2013-05-08 17:57:54 -0700186
Amin Hassani55c75412019-10-07 11:20:39 -0700187 start_byte = ex.start_block * block_size
Gilad Arnold272a4992013-05-08 13:12:53 -0700188 num_bytes = ex.num_blocks * block_size
189 if data_length < num_bytes:
Gilad Arnold658185a2013-05-08 17:57:54 -0700190 # We're only padding a real extent.
Amin Hassani55c75412019-10-07 11:20:39 -0700191 pad_off = start_byte + data_length
192 pad_len = num_bytes - data_length
Gilad Arnold272a4992013-05-08 13:12:53 -0700193 num_bytes = data_length
Gilad Arnold658185a2013-05-08 17:57:54 -0700194
Gilad Arnold272a4992013-05-08 13:12:53 -0700195 arg += '%s%d:%d' % (arg and ',', start_byte, num_bytes)
196 data_length -= num_bytes
197
198 if data_length:
199 raise PayloadError('%s: extents not covering full data length' % base_name)
200
201 return arg, pad_off, pad_len
202
203
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800204#
205# Payload application.
206#
207class PayloadApplier(object):
208 """Applying an update payload.
209
210 This is a short-lived object whose purpose is to isolate the logic used for
211 applying an update payload.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800212 """
213
Gilad Arnold21a02502013-08-22 16:59:48 -0700214 def __init__(self, payload, bsdiff_in_place=True, bspatch_path=None,
Amin Hassani5ef5d452017-08-04 13:10:59 -0700215 puffpatch_path=None, truncate_to_expected_size=True):
Gilad Arnold272a4992013-05-08 13:12:53 -0700216 """Initialize the applier.
217
218 Args:
219 payload: the payload object to check
220 bsdiff_in_place: whether to perform BSDIFF operation in-place (optional)
Gilad Arnold21a02502013-08-22 16:59:48 -0700221 bspatch_path: path to the bspatch binary (optional)
Amin Hassani5ef5d452017-08-04 13:10:59 -0700222 puffpatch_path: path to the puffpatch binary (optional)
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700223 truncate_to_expected_size: whether to truncate the resulting partitions
224 to their expected sizes, as specified in the
225 payload (optional)
Gilad Arnold272a4992013-05-08 13:12:53 -0700226 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800227 assert payload.is_init, 'uninitialized update payload'
228 self.payload = payload
229 self.block_size = payload.manifest.block_size
Allie Wood12f59aa2015-04-06 11:05:12 -0700230 self.minor_version = payload.manifest.minor_version
Gilad Arnold272a4992013-05-08 13:12:53 -0700231 self.bsdiff_in_place = bsdiff_in_place
Gilad Arnold21a02502013-08-22 16:59:48 -0700232 self.bspatch_path = bspatch_path or 'bspatch'
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700233 self.puffpatch_path = puffpatch_path or 'puffin'
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700234 self.truncate_to_expected_size = truncate_to_expected_size
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800235
236 def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
Amin Hassani0de7f782017-12-07 12:13:03 -0800237 """Applies a REPLACE{,_BZ,_XZ} operation.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800238
239 Args:
240 op: the operation object
241 op_name: name string for error reporting
242 out_data: the data to be written
243 part_file: the partition file object
244 part_size: the size of the partition
Allie Wood12f59aa2015-04-06 11:05:12 -0700245
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800246 Raises:
247 PayloadError if something goes wrong.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800248 """
249 block_size = self.block_size
250 data_length = len(out_data)
251
252 # Decompress data if needed.
253 if op.type == common.OpType.REPLACE_BZ:
254 out_data = bz2.decompress(out_data)
255 data_length = len(out_data)
Amin Hassani0de7f782017-12-07 12:13:03 -0800256 elif op.type == common.OpType.REPLACE_XZ:
257 # pylint: disable=no-member
258 out_data = lzma.decompress(out_data)
259 data_length = len(out_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800260
261 # Write data to blocks specified in dst extents.
262 data_start = 0
263 for ex, ex_name in common.ExtentIter(op.dst_extents,
264 '%s.dst_extents' % op_name):
265 start_block = ex.start_block
266 num_blocks = ex.num_blocks
267 count = num_blocks * block_size
268
Amin Hassani55c75412019-10-07 11:20:39 -0700269 data_end = data_start + count
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800270
Amin Hassani55c75412019-10-07 11:20:39 -0700271 # Make sure we're not running past partition boundary.
272 if (start_block + num_blocks) * block_size > part_size:
273 raise PayloadError(
274 '%s: extent (%s) exceeds partition size (%d)' %
275 (ex_name, common.FormatExtent(ex, block_size),
276 part_size))
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800277
Amin Hassani55c75412019-10-07 11:20:39 -0700278 # Make sure that we have enough data to write.
279 if data_end >= data_length + block_size:
280 raise PayloadError(
281 '%s: more dst blocks than data (even with padding)')
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800282
Amin Hassani55c75412019-10-07 11:20:39 -0700283 # Pad with zeros if necessary.
284 if data_end > data_length:
285 padding = data_end - data_length
286 out_data += '\0' * padding
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800287
Amin Hassani55c75412019-10-07 11:20:39 -0700288 self.payload.payload_file.seek(start_block * block_size)
289 part_file.seek(start_block * block_size)
290 part_file.write(out_data[data_start:data_end])
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800291
292 data_start += count
293
294 # Make sure we wrote all data.
295 if data_start < data_length:
296 raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
297 (op_name, data_start, data_length))
298
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700299 def _ApplyZeroOperation(self, op, op_name, part_file):
300 """Applies a ZERO operation.
301
302 Args:
303 op: the operation object
304 op_name: name string for error reporting
305 part_file: the partition file object
306
307 Raises:
308 PayloadError if something goes wrong.
309 """
310 block_size = self.block_size
311 base_name = '%s.dst_extents' % op_name
312
313 # Iterate over the extents and write zero.
Amin Hassanib05a65a2017-12-18 15:15:32 -0800314 # pylint: disable=unused-variable
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700315 for ex, ex_name in common.ExtentIter(op.dst_extents, base_name):
Amin Hassani55c75412019-10-07 11:20:39 -0700316 part_file.seek(ex.start_block * block_size)
317 part_file.write('\0' * (ex.num_blocks * block_size))
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700318
Allie Wood12f59aa2015-04-06 11:05:12 -0700319 def _ApplySourceCopyOperation(self, op, op_name, old_part_file,
320 new_part_file):
321 """Applies a SOURCE_COPY operation.
322
323 Args:
324 op: the operation object
325 op_name: name string for error reporting
326 old_part_file: the old partition file object
327 new_part_file: the new partition file object
328
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800329 Raises:
330 PayloadError if something goes wrong.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800331 """
Allie Wood12f59aa2015-04-06 11:05:12 -0700332 if not old_part_file:
333 raise PayloadError(
334 '%s: no source partition file provided for operation type (%d)' %
335 (op_name, op.type))
336
337 block_size = self.block_size
338
339 # Gather input raw data from src extents.
340 in_data = _ReadExtents(old_part_file, op.src_extents, block_size)
341
342 # Dump extracted data to dst extents.
343 _WriteExtents(new_part_file, in_data, op.dst_extents, block_size,
344 '%s.dst_extents' % op_name)
345
Amin Hassaniefa62d92017-11-09 13:46:56 -0800346 def _BytesInExtents(self, extents, base_name):
347 """Counts the length of extents in bytes.
348
349 Args:
350 extents: The list of Extents.
351 base_name: For error reporting.
352
353 Returns:
354 The number of bytes in extents.
355 """
356
357 length = 0
Amin Hassanib05a65a2017-12-18 15:15:32 -0800358 # pylint: disable=unused-variable
Amin Hassaniefa62d92017-11-09 13:46:56 -0800359 for ex, ex_name in common.ExtentIter(extents, base_name):
360 length += ex.num_blocks * self.block_size
361 return length
362
Sen Jiang92161a72016-06-28 16:09:38 -0700363 def _ApplyDiffOperation(self, op, op_name, patch_data, old_part_file,
364 new_part_file):
Amin Hassaniefa62d92017-11-09 13:46:56 -0800365 """Applies a SOURCE_BSDIFF, BROTLI_BSDIFF or PUFFDIFF operation.
Allie Wood12f59aa2015-04-06 11:05:12 -0700366
367 Args:
368 op: the operation object
369 op_name: name string for error reporting
370 patch_data: the binary patch content
371 old_part_file: the source partition file object
372 new_part_file: the target partition file object
373
374 Raises:
375 PayloadError if something goes wrong.
Allie Wood12f59aa2015-04-06 11:05:12 -0700376 """
377 if not old_part_file:
378 raise PayloadError(
379 '%s: no source partition file provided for operation type (%d)' %
380 (op_name, op.type))
381
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800382 block_size = self.block_size
383
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800384 # Dump patch data to file.
385 with tempfile.NamedTemporaryFile(delete=False) as patch_file:
386 patch_file_name = patch_file.name
387 patch_file.write(patch_data)
388
Allie Wood12f59aa2015-04-06 11:05:12 -0700389 if (hasattr(new_part_file, 'fileno') and
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700390 ((not old_part_file) or hasattr(old_part_file, 'fileno'))):
Gilad Arnold272a4992013-05-08 13:12:53 -0700391 # Construct input and output extents argument for bspatch.
Amin Hassaniefa62d92017-11-09 13:46:56 -0800392
Gilad Arnold272a4992013-05-08 13:12:53 -0700393 in_extents_arg, _, _ = _ExtentsToBspatchArg(
394 op.src_extents, block_size, '%s.src_extents' % op_name,
Amin Hassaniefa62d92017-11-09 13:46:56 -0800395 data_length=op.src_length if op.src_length else
396 self._BytesInExtents(op.src_extents, "%s.src_extents"))
Gilad Arnold272a4992013-05-08 13:12:53 -0700397 out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
398 op.dst_extents, block_size, '%s.dst_extents' % op_name,
Amin Hassaniefa62d92017-11-09 13:46:56 -0800399 data_length=op.dst_length if op.dst_length else
400 self._BytesInExtents(op.dst_extents, "%s.dst_extents"))
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800401
Allie Wood12f59aa2015-04-06 11:05:12 -0700402 new_file_name = '/dev/fd/%d' % new_part_file.fileno()
403 # Diff from source partition.
404 old_file_name = '/dev/fd/%d' % old_part_file.fileno()
405
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700406 if op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.BROTLI_BSDIFF):
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700407 # Invoke bspatch on partition file with extents args.
408 bspatch_cmd = [self.bspatch_path, old_file_name, new_file_name,
409 patch_file_name, in_extents_arg, out_extents_arg]
410 subprocess.check_call(bspatch_cmd)
411 elif op.type == common.OpType.PUFFDIFF:
412 # Invoke puffpatch on partition file with extents args.
413 puffpatch_cmd = [self.puffpatch_path,
414 "--operation=puffpatch",
415 "--src_file=%s" % old_file_name,
416 "--dst_file=%s" % new_file_name,
417 "--patch_file=%s" % patch_file_name,
418 "--src_extents=%s" % in_extents_arg,
419 "--dst_extents=%s" % out_extents_arg]
420 subprocess.check_call(puffpatch_cmd)
421 else:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800422 raise PayloadError("Unknown operation %s" % op.type)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800423
Gilad Arnold272a4992013-05-08 13:12:53 -0700424 # Pad with zeros past the total output length.
425 if pad_len:
Allie Wood12f59aa2015-04-06 11:05:12 -0700426 new_part_file.seek(pad_off)
427 new_part_file.write('\0' * pad_len)
Gilad Arnold272a4992013-05-08 13:12:53 -0700428 else:
429 # Gather input raw data and write to a temp file.
Allie Wood12f59aa2015-04-06 11:05:12 -0700430 input_part_file = old_part_file if old_part_file else new_part_file
431 in_data = _ReadExtents(input_part_file, op.src_extents, block_size,
Amin Hassaniefa62d92017-11-09 13:46:56 -0800432 max_length=op.src_length if op.src_length else
433 self._BytesInExtents(op.src_extents,
434 "%s.src_extents"))
Gilad Arnold272a4992013-05-08 13:12:53 -0700435 with tempfile.NamedTemporaryFile(delete=False) as in_file:
436 in_file_name = in_file.name
437 in_file.write(in_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800438
Allie Wood12f59aa2015-04-06 11:05:12 -0700439 # Allocate temporary output file.
Gilad Arnold272a4992013-05-08 13:12:53 -0700440 with tempfile.NamedTemporaryFile(delete=False) as out_file:
441 out_file_name = out_file.name
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800442
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700443 if op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.BROTLI_BSDIFF):
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700444 # Invoke bspatch.
445 bspatch_cmd = [self.bspatch_path, in_file_name, out_file_name,
446 patch_file_name]
447 subprocess.check_call(bspatch_cmd)
448 elif op.type == common.OpType.PUFFDIFF:
449 # Invoke puffpatch.
450 puffpatch_cmd = [self.puffpatch_path,
451 "--operation=puffpatch",
452 "--src_file=%s" % in_file_name,
453 "--dst_file=%s" % out_file_name,
454 "--patch_file=%s" % patch_file_name]
455 subprocess.check_call(puffpatch_cmd)
456 else:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800457 raise PayloadError("Unknown operation %s" % op.type)
Gilad Arnold272a4992013-05-08 13:12:53 -0700458
459 # Read output.
460 with open(out_file_name, 'rb') as out_file:
461 out_data = out_file.read()
462 if len(out_data) != op.dst_length:
463 raise PayloadError(
464 '%s: actual patched data length (%d) not as expected (%d)' %
465 (op_name, len(out_data), op.dst_length))
466
467 # Write output back to partition, with padding.
468 unaligned_out_len = len(out_data) % block_size
469 if unaligned_out_len:
470 out_data += '\0' * (block_size - unaligned_out_len)
Allie Wood12f59aa2015-04-06 11:05:12 -0700471 _WriteExtents(new_part_file, out_data, op.dst_extents, block_size,
Gilad Arnold272a4992013-05-08 13:12:53 -0700472 '%s.dst_extents' % op_name)
473
474 # Delete input/output files.
475 os.remove(in_file_name)
476 os.remove(out_file_name)
477
478 # Delete patch file.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800479 os.remove(patch_file_name)
480
Allie Wood12f59aa2015-04-06 11:05:12 -0700481 def _ApplyOperations(self, operations, base_name, old_part_file,
482 new_part_file, part_size):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800483 """Applies a sequence of update operations to a partition.
484
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800485 Args:
486 operations: the sequence of operations
487 base_name: the name of the operation sequence
Allie Wood12f59aa2015-04-06 11:05:12 -0700488 old_part_file: the old partition file object, open for reading/writing
489 new_part_file: the new partition file object, open for reading/writing
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800490 part_size: the partition size
Allie Wood12f59aa2015-04-06 11:05:12 -0700491
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800492 Raises:
493 PayloadError if anything goes wrong while processing the payload.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800494 """
495 for op, op_name in common.OperationIter(operations, base_name):
496 # Read data blob.
497 data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
498
Amin Hassani0de7f782017-12-07 12:13:03 -0800499 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ,
500 common.OpType.REPLACE_XZ):
Allie Wood12f59aa2015-04-06 11:05:12 -0700501 self._ApplyReplaceOperation(op, op_name, data, new_part_file, part_size)
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700502 elif op.type == common.OpType.ZERO:
503 self._ApplyZeroOperation(op, op_name, new_part_file)
Allie Wood12f59aa2015-04-06 11:05:12 -0700504 elif op.type == common.OpType.SOURCE_COPY:
505 self._ApplySourceCopyOperation(op, op_name, old_part_file,
506 new_part_file)
Amin Hassaniefa62d92017-11-09 13:46:56 -0800507 elif op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.PUFFDIFF,
508 common.OpType.BROTLI_BSDIFF):
Sen Jiang92161a72016-06-28 16:09:38 -0700509 self._ApplyDiffOperation(op, op_name, data, old_part_file,
510 new_part_file)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800511 else:
512 raise PayloadError('%s: unknown operation type (%d)' %
513 (op_name, op.type))
514
515 def _ApplyToPartition(self, operations, part_name, base_name,
Gilad Arnold16416602013-05-04 21:40:39 -0700516 new_part_file_name, new_part_info,
517 old_part_file_name=None, old_part_info=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800518 """Applies an update to a partition.
519
520 Args:
521 operations: the sequence of update operations to apply
522 part_name: the name of the partition, for error reporting
523 base_name: the name of the operation sequence
Gilad Arnold16416602013-05-04 21:40:39 -0700524 new_part_file_name: file name to write partition data to
525 new_part_info: size and expected hash of dest partition
526 old_part_file_name: file name of source partition (optional)
527 old_part_info: size and expected hash of source partition (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700528
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800529 Raises:
530 PayloadError if anything goes wrong with the update.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800531 """
532 # Do we have a source partition?
Gilad Arnold16416602013-05-04 21:40:39 -0700533 if old_part_file_name:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800534 # Verify the source partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700535 with open(old_part_file_name, 'rb') as old_part_file:
Gilad Arnold4b8f4c22015-07-16 11:45:39 -0700536 _VerifySha256(old_part_file, old_part_info.hash,
537 'old ' + part_name, length=old_part_info.size)
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700538 new_part_file_mode = 'r+b'
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700539 open(new_part_file_name, 'w').close()
540
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800541 else:
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700542 # We need to create/truncate the dst partition file.
543 new_part_file_mode = 'w+b'
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800544
545 # Apply operations.
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700546 with open(new_part_file_name, new_part_file_mode) as new_part_file:
Allie Wood12f59aa2015-04-06 11:05:12 -0700547 old_part_file = (open(old_part_file_name, 'r+b')
548 if old_part_file_name else None)
549 try:
550 self._ApplyOperations(operations, base_name, old_part_file,
551 new_part_file, new_part_info.size)
552 finally:
553 if old_part_file:
554 old_part_file.close()
555
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700556 # Truncate the result, if so instructed.
557 if self.truncate_to_expected_size:
558 new_part_file.seek(0, 2)
559 if new_part_file.tell() > new_part_info.size:
560 new_part_file.seek(new_part_info.size)
561 new_part_file.truncate()
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800562
563 # Verify the resulting partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700564 with open(new_part_file_name, 'rb') as new_part_file:
Gilad Arnold4b8f4c22015-07-16 11:45:39 -0700565 _VerifySha256(new_part_file, new_part_info.hash,
566 'new ' + part_name, length=new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800567
Tudor Brindus2d22c1a2018-06-15 13:07:13 -0700568 def Run(self, new_parts, old_parts=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800569 """Applier entry point, invoking all update operations.
570
571 Args:
Tudor Brindus2d22c1a2018-06-15 13:07:13 -0700572 new_parts: map of partition name to dest partition file
573 old_parts: map of partition name to source partition file (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700574
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800575 Raises:
576 PayloadError if payload application failed.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800577 """
Tudor Brindusacd20392018-06-19 11:46:16 -0700578 if old_parts is None:
579 old_parts = {}
580
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800581 self.payload.ResetFile()
582
Tudor Brindusacd20392018-06-19 11:46:16 -0700583 new_part_info = {}
584 old_part_info = {}
585 install_operations = []
586
587 manifest = self.payload.manifest
Amin Hassani55c75412019-10-07 11:20:39 -0700588 for part in manifest.partitions:
589 name = part.partition_name
590 new_part_info[name] = part.new_partition_info
591 old_part_info[name] = part.old_partition_info
592 install_operations.append((name, part.operations))
Tudor Brindusacd20392018-06-19 11:46:16 -0700593
594 part_names = set(new_part_info.keys()) # Equivalently, old_part_info.keys()
Tudor Brindus2d22c1a2018-06-15 13:07:13 -0700595
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800596 # Make sure the arguments are sane and match the payload.
Tudor Brindusacd20392018-06-19 11:46:16 -0700597 new_part_names = set(new_parts.keys())
598 if new_part_names != part_names:
599 raise PayloadError('missing dst partition(s) %s' %
600 ', '.join(part_names - new_part_names))
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800601
Tudor Brindusacd20392018-06-19 11:46:16 -0700602 old_part_names = set(old_parts.keys())
603 if part_names - old_part_names:
604 if self.payload.IsDelta():
605 raise PayloadError('trying to apply a delta update without src '
606 'partition(s) %s' %
607 ', '.join(part_names - old_part_names))
608 elif old_part_names == part_names:
609 if self.payload.IsFull():
610 raise PayloadError('trying to apply a full update onto src partitions')
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800611 else:
612 raise PayloadError('not all src partitions provided')
613
Tudor Brindusacd20392018-06-19 11:46:16 -0700614 for name, operations in install_operations:
615 # Apply update to partition.
616 self._ApplyToPartition(
617 operations, name, '%s_install_operations' % name, new_parts[name],
618 new_part_info[name], old_parts.get(name, None), old_part_info[name])