blob: 7830c002971f68be7be6dbc3f1141b8a236621c5 [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
55
56#
57# Helper functions.
58#
Gilad Arnold382df5c2013-05-03 12:49:28 -070059def _VerifySha256(file_obj, expected_hash, name, length=-1):
Gilad Arnold553b0ec2013-01-26 01:00:39 -080060 """Verifies the SHA256 hash of a file.
61
62 Args:
63 file_obj: file object to read
64 expected_hash: the hash digest we expect to be getting
65 name: name string of this hash, for error reporting
Gilad Arnold382df5c2013-05-03 12:49:28 -070066 length: precise length of data to verify (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -070067
Gilad Arnold553b0ec2013-01-26 01:00:39 -080068 Raises:
Gilad Arnold382df5c2013-05-03 12:49:28 -070069 PayloadError if computed hash doesn't match expected one, or if fails to
70 read the specified length of data.
Gilad Arnold553b0ec2013-01-26 01:00:39 -080071 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -080072 hasher = hashlib.sha256()
73 block_length = 1024 * 1024
Andrew Lassalle165843c2019-11-05 13:30:34 -080074 max_length = length if length >= 0 else sys.maxsize
Gilad Arnold553b0ec2013-01-26 01:00:39 -080075
Gilad Arnold382df5c2013-05-03 12:49:28 -070076 while max_length > 0:
Gilad Arnold553b0ec2013-01-26 01:00:39 -080077 read_length = min(max_length, block_length)
78 data = file_obj.read(read_length)
79 if not data:
80 break
81 max_length -= len(data)
82 hasher.update(data)
83
Gilad Arnold382df5c2013-05-03 12:49:28 -070084 if length >= 0 and max_length > 0:
85 raise PayloadError(
86 'insufficient data (%d instead of %d) when verifying %s' %
87 (length - max_length, length, name))
88
Gilad Arnold553b0ec2013-01-26 01:00:39 -080089 actual_hash = hasher.digest()
90 if actual_hash != expected_hash:
91 raise PayloadError('%s hash (%s) not as expected (%s)' %
Gilad Arnold96405372013-05-04 00:24:58 -070092 (name, common.FormatSha256(actual_hash),
93 common.FormatSha256(expected_hash)))
Gilad Arnold553b0ec2013-01-26 01:00:39 -080094
95
96def _ReadExtents(file_obj, extents, block_size, max_length=-1):
97 """Reads data from file as defined by extent sequence.
98
99 This tries to be efficient by not copying data as it is read in chunks.
100
101 Args:
102 file_obj: file object
103 extents: sequence of block extents (offset and length)
104 block_size: size of each block
105 max_length: maximum length to read (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700106
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800107 Returns:
108 A character array containing the concatenated read data.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800109 """
110 data = array.array('c')
Gilad Arnold272a4992013-05-08 13:12:53 -0700111 if max_length < 0:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800112 max_length = sys.maxsize
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800113 for ex in extents:
114 if max_length == 0:
115 break
Gilad Arnold272a4992013-05-08 13:12:53 -0700116 read_length = min(max_length, ex.num_blocks * block_size)
Gilad Arnold658185a2013-05-08 17:57:54 -0700117
Amin Hassani55c75412019-10-07 11:20:39 -0700118 file_obj.seek(ex.start_block * block_size)
119 data.fromfile(file_obj, read_length)
Gilad Arnold658185a2013-05-08 17:57:54 -0700120
Gilad Arnold272a4992013-05-08 13:12:53 -0700121 max_length -= read_length
Gilad Arnold658185a2013-05-08 17:57:54 -0700122
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800123 return data
124
125
126def _WriteExtents(file_obj, data, extents, block_size, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -0700127 """Writes data to file as defined by extent sequence.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800128
129 This tries to be efficient by not copy data as it is written in chunks.
130
131 Args:
132 file_obj: file object
133 data: data to write
134 extents: sequence of block extents (offset and length)
135 block_size: size of each block
Gilad Arnold272a4992013-05-08 13:12:53 -0700136 base_name: name string of extent sequence for error reporting
Allie Wood12f59aa2015-04-06 11:05:12 -0700137
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800138 Raises:
139 PayloadError when things don't add up.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800140 """
141 data_offset = 0
142 data_length = len(data)
143 for ex, ex_name in common.ExtentIter(extents, base_name):
Gilad Arnold272a4992013-05-08 13:12:53 -0700144 if not data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800145 raise PayloadError('%s: more write extents than data' % ex_name)
Gilad Arnold272a4992013-05-08 13:12:53 -0700146 write_length = min(data_length, ex.num_blocks * block_size)
Gilad Arnold658185a2013-05-08 17:57:54 -0700147
Amin Hassani55c75412019-10-07 11:20:39 -0700148 file_obj.seek(ex.start_block * block_size)
149 data_view = buffer(data, data_offset, write_length)
150 file_obj.write(data_view)
Gilad Arnold658185a2013-05-08 17:57:54 -0700151
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800152 data_offset += write_length
Gilad Arnold272a4992013-05-08 13:12:53 -0700153 data_length -= write_length
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800154
Gilad Arnold272a4992013-05-08 13:12:53 -0700155 if data_length:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800156 raise PayloadError('%s: more data than write extents' % base_name)
157
158
Gilad Arnold272a4992013-05-08 13:12:53 -0700159def _ExtentsToBspatchArg(extents, block_size, base_name, data_length=-1):
160 """Translates an extent sequence into a bspatch-compatible string argument.
161
162 Args:
163 extents: sequence of block extents (offset and length)
164 block_size: size of each block
165 base_name: name string of extent sequence for error reporting
166 data_length: the actual total length of the data in bytes (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700167
Gilad Arnold272a4992013-05-08 13:12:53 -0700168 Returns:
169 A tuple consisting of (i) a string of the form
170 "off_1:len_1,...,off_n:len_n", (ii) an offset where zero padding is needed
171 for filling the last extent, (iii) the length of the padding (zero means no
172 padding is needed and the extents cover the full length of data).
Allie Wood12f59aa2015-04-06 11:05:12 -0700173
Gilad Arnold272a4992013-05-08 13:12:53 -0700174 Raises:
175 PayloadError if data_length is too short or too long.
Gilad Arnold272a4992013-05-08 13:12:53 -0700176 """
177 arg = ''
178 pad_off = pad_len = 0
179 if data_length < 0:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800180 data_length = sys.maxsize
Gilad Arnold272a4992013-05-08 13:12:53 -0700181 for ex, ex_name in common.ExtentIter(extents, base_name):
182 if not data_length:
183 raise PayloadError('%s: more extents than total data length' % ex_name)
Gilad Arnold658185a2013-05-08 17:57:54 -0700184
Amin Hassani55c75412019-10-07 11:20:39 -0700185 start_byte = ex.start_block * block_size
Gilad Arnold272a4992013-05-08 13:12:53 -0700186 num_bytes = ex.num_blocks * block_size
187 if data_length < num_bytes:
Gilad Arnold658185a2013-05-08 17:57:54 -0700188 # We're only padding a real extent.
Amin Hassani55c75412019-10-07 11:20:39 -0700189 pad_off = start_byte + data_length
190 pad_len = num_bytes - data_length
Gilad Arnold272a4992013-05-08 13:12:53 -0700191 num_bytes = data_length
Gilad Arnold658185a2013-05-08 17:57:54 -0700192
Gilad Arnold272a4992013-05-08 13:12:53 -0700193 arg += '%s%d:%d' % (arg and ',', start_byte, num_bytes)
194 data_length -= num_bytes
195
196 if data_length:
197 raise PayloadError('%s: extents not covering full data length' % base_name)
198
199 return arg, pad_off, pad_len
200
201
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800202#
203# Payload application.
204#
205class PayloadApplier(object):
206 """Applying an update payload.
207
208 This is a short-lived object whose purpose is to isolate the logic used for
209 applying an update payload.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800210 """
211
Gilad Arnold21a02502013-08-22 16:59:48 -0700212 def __init__(self, payload, bsdiff_in_place=True, bspatch_path=None,
Amin Hassani5ef5d452017-08-04 13:10:59 -0700213 puffpatch_path=None, truncate_to_expected_size=True):
Gilad Arnold272a4992013-05-08 13:12:53 -0700214 """Initialize the applier.
215
216 Args:
217 payload: the payload object to check
218 bsdiff_in_place: whether to perform BSDIFF operation in-place (optional)
Gilad Arnold21a02502013-08-22 16:59:48 -0700219 bspatch_path: path to the bspatch binary (optional)
Amin Hassani5ef5d452017-08-04 13:10:59 -0700220 puffpatch_path: path to the puffpatch binary (optional)
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700221 truncate_to_expected_size: whether to truncate the resulting partitions
222 to their expected sizes, as specified in the
223 payload (optional)
Gilad Arnold272a4992013-05-08 13:12:53 -0700224 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800225 assert payload.is_init, 'uninitialized update payload'
226 self.payload = payload
227 self.block_size = payload.manifest.block_size
Allie Wood12f59aa2015-04-06 11:05:12 -0700228 self.minor_version = payload.manifest.minor_version
Gilad Arnold272a4992013-05-08 13:12:53 -0700229 self.bsdiff_in_place = bsdiff_in_place
Gilad Arnold21a02502013-08-22 16:59:48 -0700230 self.bspatch_path = bspatch_path or 'bspatch'
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700231 self.puffpatch_path = puffpatch_path or 'puffin'
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700232 self.truncate_to_expected_size = truncate_to_expected_size
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800233
234 def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
Amin Hassani0de7f782017-12-07 12:13:03 -0800235 """Applies a REPLACE{,_BZ,_XZ} operation.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800236
237 Args:
238 op: the operation object
239 op_name: name string for error reporting
240 out_data: the data to be written
241 part_file: the partition file object
242 part_size: the size of the partition
Allie Wood12f59aa2015-04-06 11:05:12 -0700243
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800244 Raises:
245 PayloadError if something goes wrong.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800246 """
247 block_size = self.block_size
248 data_length = len(out_data)
249
250 # Decompress data if needed.
251 if op.type == common.OpType.REPLACE_BZ:
252 out_data = bz2.decompress(out_data)
253 data_length = len(out_data)
Amin Hassani0de7f782017-12-07 12:13:03 -0800254 elif op.type == common.OpType.REPLACE_XZ:
255 # pylint: disable=no-member
256 out_data = lzma.decompress(out_data)
257 data_length = len(out_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800258
259 # Write data to blocks specified in dst extents.
260 data_start = 0
261 for ex, ex_name in common.ExtentIter(op.dst_extents,
262 '%s.dst_extents' % op_name):
263 start_block = ex.start_block
264 num_blocks = ex.num_blocks
265 count = num_blocks * block_size
266
Amin Hassani55c75412019-10-07 11:20:39 -0700267 data_end = data_start + count
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800268
Amin Hassani55c75412019-10-07 11:20:39 -0700269 # Make sure we're not running past partition boundary.
270 if (start_block + num_blocks) * block_size > part_size:
271 raise PayloadError(
272 '%s: extent (%s) exceeds partition size (%d)' %
273 (ex_name, common.FormatExtent(ex, block_size),
274 part_size))
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800275
Amin Hassani55c75412019-10-07 11:20:39 -0700276 # Make sure that we have enough data to write.
277 if data_end >= data_length + block_size:
278 raise PayloadError(
279 '%s: more dst blocks than data (even with padding)')
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800280
Amin Hassani55c75412019-10-07 11:20:39 -0700281 # Pad with zeros if necessary.
282 if data_end > data_length:
283 padding = data_end - data_length
284 out_data += '\0' * padding
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800285
Amin Hassani55c75412019-10-07 11:20:39 -0700286 self.payload.payload_file.seek(start_block * block_size)
287 part_file.seek(start_block * block_size)
288 part_file.write(out_data[data_start:data_end])
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800289
290 data_start += count
291
292 # Make sure we wrote all data.
293 if data_start < data_length:
294 raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
295 (op_name, data_start, data_length))
296
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700297 def _ApplyZeroOperation(self, op, op_name, part_file):
298 """Applies a ZERO operation.
299
300 Args:
301 op: the operation object
302 op_name: name string for error reporting
303 part_file: the partition file object
304
305 Raises:
306 PayloadError if something goes wrong.
307 """
308 block_size = self.block_size
309 base_name = '%s.dst_extents' % op_name
310
311 # Iterate over the extents and write zero.
Amin Hassanib05a65a2017-12-18 15:15:32 -0800312 # pylint: disable=unused-variable
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700313 for ex, ex_name in common.ExtentIter(op.dst_extents, base_name):
Amin Hassani55c75412019-10-07 11:20:39 -0700314 part_file.seek(ex.start_block * block_size)
315 part_file.write('\0' * (ex.num_blocks * block_size))
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700316
Allie Wood12f59aa2015-04-06 11:05:12 -0700317 def _ApplySourceCopyOperation(self, op, op_name, old_part_file,
318 new_part_file):
319 """Applies a SOURCE_COPY operation.
320
321 Args:
322 op: the operation object
323 op_name: name string for error reporting
324 old_part_file: the old partition file object
325 new_part_file: the new partition file object
326
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800327 Raises:
328 PayloadError if something goes wrong.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800329 """
Allie Wood12f59aa2015-04-06 11:05:12 -0700330 if not old_part_file:
331 raise PayloadError(
332 '%s: no source partition file provided for operation type (%d)' %
333 (op_name, op.type))
334
335 block_size = self.block_size
336
337 # Gather input raw data from src extents.
338 in_data = _ReadExtents(old_part_file, op.src_extents, block_size)
339
340 # Dump extracted data to dst extents.
341 _WriteExtents(new_part_file, in_data, op.dst_extents, block_size,
342 '%s.dst_extents' % op_name)
343
Amin Hassaniefa62d92017-11-09 13:46:56 -0800344 def _BytesInExtents(self, extents, base_name):
345 """Counts the length of extents in bytes.
346
347 Args:
348 extents: The list of Extents.
349 base_name: For error reporting.
350
351 Returns:
352 The number of bytes in extents.
353 """
354
355 length = 0
Amin Hassanib05a65a2017-12-18 15:15:32 -0800356 # pylint: disable=unused-variable
Amin Hassaniefa62d92017-11-09 13:46:56 -0800357 for ex, ex_name in common.ExtentIter(extents, base_name):
358 length += ex.num_blocks * self.block_size
359 return length
360
Sen Jiang92161a72016-06-28 16:09:38 -0700361 def _ApplyDiffOperation(self, op, op_name, patch_data, old_part_file,
362 new_part_file):
Amin Hassaniefa62d92017-11-09 13:46:56 -0800363 """Applies a SOURCE_BSDIFF, BROTLI_BSDIFF or PUFFDIFF operation.
Allie Wood12f59aa2015-04-06 11:05:12 -0700364
365 Args:
366 op: the operation object
367 op_name: name string for error reporting
368 patch_data: the binary patch content
369 old_part_file: the source partition file object
370 new_part_file: the target partition file object
371
372 Raises:
373 PayloadError if something goes wrong.
Allie Wood12f59aa2015-04-06 11:05:12 -0700374 """
375 if not old_part_file:
376 raise PayloadError(
377 '%s: no source partition file provided for operation type (%d)' %
378 (op_name, op.type))
379
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800380 block_size = self.block_size
381
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800382 # Dump patch data to file.
383 with tempfile.NamedTemporaryFile(delete=False) as patch_file:
384 patch_file_name = patch_file.name
385 patch_file.write(patch_data)
386
Allie Wood12f59aa2015-04-06 11:05:12 -0700387 if (hasattr(new_part_file, 'fileno') and
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700388 ((not old_part_file) or hasattr(old_part_file, 'fileno'))):
Gilad Arnold272a4992013-05-08 13:12:53 -0700389 # Construct input and output extents argument for bspatch.
Amin Hassaniefa62d92017-11-09 13:46:56 -0800390
Gilad Arnold272a4992013-05-08 13:12:53 -0700391 in_extents_arg, _, _ = _ExtentsToBspatchArg(
392 op.src_extents, block_size, '%s.src_extents' % op_name,
Amin Hassaniefa62d92017-11-09 13:46:56 -0800393 data_length=op.src_length if op.src_length else
394 self._BytesInExtents(op.src_extents, "%s.src_extents"))
Gilad Arnold272a4992013-05-08 13:12:53 -0700395 out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
396 op.dst_extents, block_size, '%s.dst_extents' % op_name,
Amin Hassaniefa62d92017-11-09 13:46:56 -0800397 data_length=op.dst_length if op.dst_length else
398 self._BytesInExtents(op.dst_extents, "%s.dst_extents"))
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800399
Allie Wood12f59aa2015-04-06 11:05:12 -0700400 new_file_name = '/dev/fd/%d' % new_part_file.fileno()
401 # Diff from source partition.
402 old_file_name = '/dev/fd/%d' % old_part_file.fileno()
403
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700404 if op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.BROTLI_BSDIFF):
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700405 # Invoke bspatch on partition file with extents args.
406 bspatch_cmd = [self.bspatch_path, old_file_name, new_file_name,
407 patch_file_name, in_extents_arg, out_extents_arg]
408 subprocess.check_call(bspatch_cmd)
409 elif op.type == common.OpType.PUFFDIFF:
410 # Invoke puffpatch on partition file with extents args.
411 puffpatch_cmd = [self.puffpatch_path,
412 "--operation=puffpatch",
413 "--src_file=%s" % old_file_name,
414 "--dst_file=%s" % new_file_name,
415 "--patch_file=%s" % patch_file_name,
416 "--src_extents=%s" % in_extents_arg,
417 "--dst_extents=%s" % out_extents_arg]
418 subprocess.check_call(puffpatch_cmd)
419 else:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800420 raise PayloadError("Unknown operation %s" % op.type)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800421
Gilad Arnold272a4992013-05-08 13:12:53 -0700422 # Pad with zeros past the total output length.
423 if pad_len:
Allie Wood12f59aa2015-04-06 11:05:12 -0700424 new_part_file.seek(pad_off)
425 new_part_file.write('\0' * pad_len)
Gilad Arnold272a4992013-05-08 13:12:53 -0700426 else:
427 # Gather input raw data and write to a temp file.
Allie Wood12f59aa2015-04-06 11:05:12 -0700428 input_part_file = old_part_file if old_part_file else new_part_file
429 in_data = _ReadExtents(input_part_file, op.src_extents, block_size,
Amin Hassaniefa62d92017-11-09 13:46:56 -0800430 max_length=op.src_length if op.src_length else
431 self._BytesInExtents(op.src_extents,
432 "%s.src_extents"))
Gilad Arnold272a4992013-05-08 13:12:53 -0700433 with tempfile.NamedTemporaryFile(delete=False) as in_file:
434 in_file_name = in_file.name
435 in_file.write(in_data)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800436
Allie Wood12f59aa2015-04-06 11:05:12 -0700437 # Allocate temporary output file.
Gilad Arnold272a4992013-05-08 13:12:53 -0700438 with tempfile.NamedTemporaryFile(delete=False) as out_file:
439 out_file_name = out_file.name
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800440
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700441 if op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.BROTLI_BSDIFF):
Amin Hassanicdeb6e62017-10-11 10:15:11 -0700442 # Invoke bspatch.
443 bspatch_cmd = [self.bspatch_path, in_file_name, out_file_name,
444 patch_file_name]
445 subprocess.check_call(bspatch_cmd)
446 elif op.type == common.OpType.PUFFDIFF:
447 # Invoke puffpatch.
448 puffpatch_cmd = [self.puffpatch_path,
449 "--operation=puffpatch",
450 "--src_file=%s" % in_file_name,
451 "--dst_file=%s" % out_file_name,
452 "--patch_file=%s" % patch_file_name]
453 subprocess.check_call(puffpatch_cmd)
454 else:
Andrew Lassalle165843c2019-11-05 13:30:34 -0800455 raise PayloadError("Unknown operation %s" % op.type)
Gilad Arnold272a4992013-05-08 13:12:53 -0700456
457 # Read output.
458 with open(out_file_name, 'rb') as out_file:
459 out_data = out_file.read()
460 if len(out_data) != op.dst_length:
461 raise PayloadError(
462 '%s: actual patched data length (%d) not as expected (%d)' %
463 (op_name, len(out_data), op.dst_length))
464
465 # Write output back to partition, with padding.
466 unaligned_out_len = len(out_data) % block_size
467 if unaligned_out_len:
468 out_data += '\0' * (block_size - unaligned_out_len)
Allie Wood12f59aa2015-04-06 11:05:12 -0700469 _WriteExtents(new_part_file, out_data, op.dst_extents, block_size,
Gilad Arnold272a4992013-05-08 13:12:53 -0700470 '%s.dst_extents' % op_name)
471
472 # Delete input/output files.
473 os.remove(in_file_name)
474 os.remove(out_file_name)
475
476 # Delete patch file.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800477 os.remove(patch_file_name)
478
Allie Wood12f59aa2015-04-06 11:05:12 -0700479 def _ApplyOperations(self, operations, base_name, old_part_file,
480 new_part_file, part_size):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800481 """Applies a sequence of update operations to a partition.
482
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800483 Args:
484 operations: the sequence of operations
485 base_name: the name of the operation sequence
Allie Wood12f59aa2015-04-06 11:05:12 -0700486 old_part_file: the old partition file object, open for reading/writing
487 new_part_file: the new partition file object, open for reading/writing
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800488 part_size: the partition size
Allie Wood12f59aa2015-04-06 11:05:12 -0700489
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800490 Raises:
491 PayloadError if anything goes wrong while processing the payload.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800492 """
493 for op, op_name in common.OperationIter(operations, base_name):
494 # Read data blob.
495 data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
496
Amin Hassani0de7f782017-12-07 12:13:03 -0800497 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ,
498 common.OpType.REPLACE_XZ):
Allie Wood12f59aa2015-04-06 11:05:12 -0700499 self._ApplyReplaceOperation(op, op_name, data, new_part_file, part_size)
Amin Hassani8ad22ba2017-10-11 10:15:11 -0700500 elif op.type == common.OpType.ZERO:
501 self._ApplyZeroOperation(op, op_name, new_part_file)
Allie Wood12f59aa2015-04-06 11:05:12 -0700502 elif op.type == common.OpType.SOURCE_COPY:
503 self._ApplySourceCopyOperation(op, op_name, old_part_file,
504 new_part_file)
Amin Hassaniefa62d92017-11-09 13:46:56 -0800505 elif op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.PUFFDIFF,
506 common.OpType.BROTLI_BSDIFF):
Sen Jiang92161a72016-06-28 16:09:38 -0700507 self._ApplyDiffOperation(op, op_name, data, old_part_file,
508 new_part_file)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800509 else:
510 raise PayloadError('%s: unknown operation type (%d)' %
511 (op_name, op.type))
512
513 def _ApplyToPartition(self, operations, part_name, base_name,
Gilad Arnold16416602013-05-04 21:40:39 -0700514 new_part_file_name, new_part_info,
515 old_part_file_name=None, old_part_info=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800516 """Applies an update to a partition.
517
518 Args:
519 operations: the sequence of update operations to apply
520 part_name: the name of the partition, for error reporting
521 base_name: the name of the operation sequence
Gilad Arnold16416602013-05-04 21:40:39 -0700522 new_part_file_name: file name to write partition data to
523 new_part_info: size and expected hash of dest partition
524 old_part_file_name: file name of source partition (optional)
525 old_part_info: size and expected hash of source partition (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700526
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800527 Raises:
528 PayloadError if anything goes wrong with the update.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800529 """
530 # Do we have a source partition?
Gilad Arnold16416602013-05-04 21:40:39 -0700531 if old_part_file_name:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800532 # Verify the source partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700533 with open(old_part_file_name, 'rb') as old_part_file:
Gilad Arnold4b8f4c22015-07-16 11:45:39 -0700534 _VerifySha256(old_part_file, old_part_info.hash,
535 'old ' + part_name, length=old_part_info.size)
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700536 new_part_file_mode = 'r+b'
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700537 open(new_part_file_name, 'w').close()
538
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800539 else:
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700540 # We need to create/truncate the dst partition file.
541 new_part_file_mode = 'w+b'
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800542
543 # Apply operations.
Gilad Arnoldf69065c2013-05-27 16:54:59 -0700544 with open(new_part_file_name, new_part_file_mode) as new_part_file:
Allie Wood12f59aa2015-04-06 11:05:12 -0700545 old_part_file = (open(old_part_file_name, 'r+b')
546 if old_part_file_name else None)
547 try:
548 self._ApplyOperations(operations, base_name, old_part_file,
549 new_part_file, new_part_info.size)
550 finally:
551 if old_part_file:
552 old_part_file.close()
553
Gilad Arnolde5fdf182013-05-23 16:13:38 -0700554 # Truncate the result, if so instructed.
555 if self.truncate_to_expected_size:
556 new_part_file.seek(0, 2)
557 if new_part_file.tell() > new_part_info.size:
558 new_part_file.seek(new_part_info.size)
559 new_part_file.truncate()
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800560
561 # Verify the resulting partition.
Gilad Arnold16416602013-05-04 21:40:39 -0700562 with open(new_part_file_name, 'rb') as new_part_file:
Gilad Arnold4b8f4c22015-07-16 11:45:39 -0700563 _VerifySha256(new_part_file, new_part_info.hash,
564 'new ' + part_name, length=new_part_info.size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800565
Tudor Brindus2d22c1a2018-06-15 13:07:13 -0700566 def Run(self, new_parts, old_parts=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800567 """Applier entry point, invoking all update operations.
568
569 Args:
Tudor Brindus2d22c1a2018-06-15 13:07:13 -0700570 new_parts: map of partition name to dest partition file
571 old_parts: map of partition name to source partition file (optional)
Allie Wood12f59aa2015-04-06 11:05:12 -0700572
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800573 Raises:
574 PayloadError if payload application failed.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800575 """
Tudor Brindusacd20392018-06-19 11:46:16 -0700576 if old_parts is None:
577 old_parts = {}
578
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800579 self.payload.ResetFile()
580
Tudor Brindusacd20392018-06-19 11:46:16 -0700581 new_part_info = {}
582 old_part_info = {}
583 install_operations = []
584
585 manifest = self.payload.manifest
Amin Hassani55c75412019-10-07 11:20:39 -0700586 for part in manifest.partitions:
587 name = part.partition_name
588 new_part_info[name] = part.new_partition_info
589 old_part_info[name] = part.old_partition_info
590 install_operations.append((name, part.operations))
Tudor Brindusacd20392018-06-19 11:46:16 -0700591
592 part_names = set(new_part_info.keys()) # Equivalently, old_part_info.keys()
Tudor Brindus2d22c1a2018-06-15 13:07:13 -0700593
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800594 # Make sure the arguments are sane and match the payload.
Tudor Brindusacd20392018-06-19 11:46:16 -0700595 new_part_names = set(new_parts.keys())
596 if new_part_names != part_names:
597 raise PayloadError('missing dst partition(s) %s' %
598 ', '.join(part_names - new_part_names))
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800599
Tudor Brindusacd20392018-06-19 11:46:16 -0700600 old_part_names = set(old_parts.keys())
601 if part_names - old_part_names:
602 if self.payload.IsDelta():
603 raise PayloadError('trying to apply a delta update without src '
604 'partition(s) %s' %
605 ', '.join(part_names - old_part_names))
606 elif old_part_names == part_names:
607 if self.payload.IsFull():
608 raise PayloadError('trying to apply a full update onto src partitions')
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800609 else:
610 raise PayloadError('not all src partitions provided')
611
Tudor Brindusacd20392018-06-19 11:46:16 -0700612 for name, operations in install_operations:
613 # Apply update to partition.
614 self._ApplyToPartition(
615 operations, name, '%s_install_operations' % name, new_parts[name],
616 new_part_info[name], old_parts.get(name, None), old_part_info[name])