summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGabriele M <moto.falcon.git@gmail.com>2018-03-24 09:11:21 +0100
committerGabriele M <moto.falcon.git@gmail.com>2018-03-24 17:45:00 +0100
commit8ad65076b68f4f96a81810195d347b49149d9c20 (patch)
treec196e949a1881516f4055cf515b9b24a6a4da5de
downloadscripts-8ad65076b68f4f96a81810195d347b49149d9c20.tar.gz
scripts-8ad65076b68f4f96a81810195d347b49149d9c20.tar.bz2
scripts-8ad65076b68f4f96a81810195d347b49149d9c20.zip
Import update_payload library code
From https://android.googlesource.com/platform/system/update_engine at ee67a8d57519112f62f439a8644ea497ab6898c2.
-rw-r--r--update_payload/__init__.py22
-rw-r--r--update_payload/applier.py667
-rw-r--r--update_payload/checker.py1336
-rwxr-xr-xupdate_payload/checker_unittest.py1364
-rw-r--r--update_payload/common.py218
-rw-r--r--update_payload/error.py21
-rw-r--r--update_payload/format_utils.py109
-rwxr-xr-xupdate_payload/format_utils_unittest.py89
-rw-r--r--update_payload/histogram.py129
-rwxr-xr-xupdate_payload/histogram_unittest.py72
-rw-r--r--update_payload/payload-test-key.pem27
-rw-r--r--update_payload/payload-test-key.pub9
-rw-r--r--update_payload/payload.py336
-rw-r--r--update_payload/test_utils.py369
-rw-r--r--update_payload/update-payload-key.pub.pem9
-rw-r--r--update_payload/update_metadata_pb2.py631
16 files changed, 5408 insertions, 0 deletions
diff --git a/update_payload/__init__.py b/update_payload/__init__.py
new file mode 100644
index 0000000..8ee95e2
--- /dev/null
+++ b/update_payload/__init__.py
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+"""Library for processing, verifying and applying Chrome OS update payloads."""
+
+# Just raise the interface classes to the root namespace.
+from update_payload.checker import CHECKS_TO_DISABLE
+from update_payload.error import PayloadError
+from update_payload.payload import Payload
diff --git a/update_payload/applier.py b/update_payload/applier.py
new file mode 100644
index 0000000..9582b3d
--- /dev/null
+++ b/update_payload/applier.py
@@ -0,0 +1,667 @@
+#
+# 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.
+#
+
+"""Applying a Chrome OS update payload.
+
+This module is used internally by the main Payload class for applying an update
+payload. The interface for invoking the applier is as follows:
+
+ applier = PayloadApplier(payload)
+ applier.Run(...)
+
+"""
+
+from __future__ import print_function
+
+import array
+import bz2
+import hashlib
+import itertools
+# Not everywhere we can have the lzma library so we ignore it if we didn't have
+# it because it is not going to be used. For example, 'cros flash' uses
+# devserver code which eventually loads this file, but the lzma library is not
+# included in the client test devices, and it is not necessary to do so. But
+# lzma is not used in 'cros flash' so it should be fine. Python 3.x include
+# lzma, but for backward compatibility with Python 2.7, backports-lzma is
+# needed.
+try:
+ import lzma
+except ImportError:
+ try:
+ from backports import lzma
+ except ImportError:
+ pass
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from update_payload import common
+from update_payload.error import PayloadError
+
+
+#
+# Helper functions.
+#
+def _VerifySha256(file_obj, expected_hash, name, length=-1):
+ """Verifies the SHA256 hash of a file.
+
+ Args:
+ file_obj: file object to read
+ expected_hash: the hash digest we expect to be getting
+ name: name string of this hash, for error reporting
+ length: precise length of data to verify (optional)
+
+ Raises:
+ PayloadError if computed hash doesn't match expected one, or if fails to
+ read the specified length of data.
+ """
+ hasher = hashlib.sha256()
+ block_length = 1024 * 1024
+ max_length = length if length >= 0 else sys.maxint
+
+ while max_length > 0:
+ read_length = min(max_length, block_length)
+ data = file_obj.read(read_length)
+ if not data:
+ break
+ max_length -= len(data)
+ hasher.update(data)
+
+ if length >= 0 and max_length > 0:
+ raise PayloadError(
+ 'insufficient data (%d instead of %d) when verifying %s' %
+ (length - max_length, length, name))
+
+ actual_hash = hasher.digest()
+ if actual_hash != expected_hash:
+ raise PayloadError('%s hash (%s) not as expected (%s)' %
+ (name, common.FormatSha256(actual_hash),
+ common.FormatSha256(expected_hash)))
+
+
+def _ReadExtents(file_obj, extents, block_size, max_length=-1):
+ """Reads data from file as defined by extent sequence.
+
+ This tries to be efficient by not copying data as it is read in chunks.
+
+ Args:
+ file_obj: file object
+ extents: sequence of block extents (offset and length)
+ block_size: size of each block
+ max_length: maximum length to read (optional)
+
+ Returns:
+ A character array containing the concatenated read data.
+ """
+ data = array.array('c')
+ if max_length < 0:
+ max_length = sys.maxint
+ for ex in extents:
+ if max_length == 0:
+ break
+ read_length = min(max_length, ex.num_blocks * block_size)
+
+ # Fill with zeros or read from file, depending on the type of extent.
+ if ex.start_block == common.PSEUDO_EXTENT_MARKER:
+ data.extend(itertools.repeat('\0', read_length))
+ else:
+ file_obj.seek(ex.start_block * block_size)
+ data.fromfile(file_obj, read_length)
+
+ max_length -= read_length
+
+ return data
+
+
+def _WriteExtents(file_obj, data, extents, block_size, base_name):
+ """Writes data to file as defined by extent sequence.
+
+ This tries to be efficient by not copy data as it is written in chunks.
+
+ Args:
+ file_obj: file object
+ data: data to write
+ extents: sequence of block extents (offset and length)
+ block_size: size of each block
+ base_name: name string of extent sequence for error reporting
+
+ Raises:
+ PayloadError when things don't add up.
+ """
+ data_offset = 0
+ data_length = len(data)
+ for ex, ex_name in common.ExtentIter(extents, base_name):
+ if not data_length:
+ raise PayloadError('%s: more write extents than data' % ex_name)
+ write_length = min(data_length, ex.num_blocks * block_size)
+
+ # Only do actual writing if this is not a pseudo-extent.
+ if ex.start_block != common.PSEUDO_EXTENT_MARKER:
+ file_obj.seek(ex.start_block * block_size)
+ data_view = buffer(data, data_offset, write_length)
+ file_obj.write(data_view)
+
+ data_offset += write_length
+ data_length -= write_length
+
+ if data_length:
+ raise PayloadError('%s: more data than write extents' % base_name)
+
+
+def _ExtentsToBspatchArg(extents, block_size, base_name, data_length=-1):
+ """Translates an extent sequence into a bspatch-compatible string argument.
+
+ Args:
+ extents: sequence of block extents (offset and length)
+ block_size: size of each block
+ base_name: name string of extent sequence for error reporting
+ data_length: the actual total length of the data in bytes (optional)
+
+ Returns:
+ A tuple consisting of (i) a string of the form
+ "off_1:len_1,...,off_n:len_n", (ii) an offset where zero padding is needed
+ for filling the last extent, (iii) the length of the padding (zero means no
+ padding is needed and the extents cover the full length of data).
+
+ Raises:
+ PayloadError if data_length is too short or too long.
+ """
+ arg = ''
+ pad_off = pad_len = 0
+ if data_length < 0:
+ data_length = sys.maxint
+ for ex, ex_name in common.ExtentIter(extents, base_name):
+ if not data_length:
+ raise PayloadError('%s: more extents than total data length' % ex_name)
+
+ is_pseudo = ex.start_block == common.PSEUDO_EXTENT_MARKER
+ start_byte = -1 if is_pseudo else ex.start_block * block_size
+ num_bytes = ex.num_blocks * block_size
+ if data_length < num_bytes:
+ # We're only padding a real extent.
+ if not is_pseudo:
+ pad_off = start_byte + data_length
+ pad_len = num_bytes - data_length
+
+ num_bytes = data_length
+
+ arg += '%s%d:%d' % (arg and ',', start_byte, num_bytes)
+ data_length -= num_bytes
+
+ if data_length:
+ raise PayloadError('%s: extents not covering full data length' % base_name)
+
+ return arg, pad_off, pad_len
+
+
+#
+# Payload application.
+#
+class PayloadApplier(object):
+ """Applying an update payload.
+
+ This is a short-lived object whose purpose is to isolate the logic used for
+ applying an update payload.
+ """
+
+ def __init__(self, payload, bsdiff_in_place=True, bspatch_path=None,
+ puffpatch_path=None, truncate_to_expected_size=True):
+ """Initialize the applier.
+
+ Args:
+ payload: the payload object to check
+ bsdiff_in_place: whether to perform BSDIFF operation in-place (optional)
+ bspatch_path: path to the bspatch binary (optional)
+ puffpatch_path: path to the puffpatch binary (optional)
+ truncate_to_expected_size: whether to truncate the resulting partitions
+ to their expected sizes, as specified in the
+ payload (optional)
+ """
+ assert payload.is_init, 'uninitialized update payload'
+ self.payload = payload
+ self.block_size = payload.manifest.block_size
+ self.minor_version = payload.manifest.minor_version
+ self.bsdiff_in_place = bsdiff_in_place
+ self.bspatch_path = bspatch_path or 'bspatch'
+ self.puffpatch_path = puffpatch_path or 'puffin'
+ self.truncate_to_expected_size = truncate_to_expected_size
+
+ def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size):
+ """Applies a REPLACE{,_BZ,_XZ} operation.
+
+ Args:
+ op: the operation object
+ op_name: name string for error reporting
+ out_data: the data to be written
+ part_file: the partition file object
+ part_size: the size of the partition
+
+ Raises:
+ PayloadError if something goes wrong.
+ """
+ block_size = self.block_size
+ data_length = len(out_data)
+
+ # Decompress data if needed.
+ if op.type == common.OpType.REPLACE_BZ:
+ out_data = bz2.decompress(out_data)
+ data_length = len(out_data)
+ elif op.type == common.OpType.REPLACE_XZ:
+ # pylint: disable=no-member
+ out_data = lzma.decompress(out_data)
+ data_length = len(out_data)
+
+ # Write data to blocks specified in dst extents.
+ data_start = 0
+ for ex, ex_name in common.ExtentIter(op.dst_extents,
+ '%s.dst_extents' % op_name):
+ start_block = ex.start_block
+ num_blocks = ex.num_blocks
+ count = num_blocks * block_size
+
+ # Make sure it's not a fake (signature) operation.
+ if start_block != common.PSEUDO_EXTENT_MARKER:
+ data_end = data_start + count
+
+ # Make sure we're not running past partition boundary.
+ if (start_block + num_blocks) * block_size > part_size:
+ raise PayloadError(
+ '%s: extent (%s) exceeds partition size (%d)' %
+ (ex_name, common.FormatExtent(ex, block_size),
+ part_size))
+
+ # Make sure that we have enough data to write.
+ if data_end >= data_length + block_size:
+ raise PayloadError(
+ '%s: more dst blocks than data (even with padding)')
+
+ # Pad with zeros if necessary.
+ if data_end > data_length:
+ padding = data_end - data_length
+ out_data += '\0' * padding
+
+ self.payload.payload_file.seek(start_block * block_size)
+ part_file.seek(start_block * block_size)
+ part_file.write(out_data[data_start:data_end])
+
+ data_start += count
+
+ # Make sure we wrote all data.
+ if data_start < data_length:
+ raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' %
+ (op_name, data_start, data_length))
+
+ def _ApplyMoveOperation(self, op, op_name, part_file):
+ """Applies a MOVE operation.
+
+ Note that this operation must read the whole block data from the input and
+ only then dump it, due to our in-place update semantics; otherwise, it
+ might clobber data midway through.
+
+ Args:
+ op: the operation object
+ op_name: name string for error reporting
+ part_file: the partition file object
+
+ Raises:
+ PayloadError if something goes wrong.
+ """
+ block_size = self.block_size
+
+ # Gather input raw data from src extents.
+ in_data = _ReadExtents(part_file, op.src_extents, block_size)
+
+ # Dump extracted data to dst extents.
+ _WriteExtents(part_file, in_data, op.dst_extents, block_size,
+ '%s.dst_extents' % op_name)
+
+ def _ApplyZeroOperation(self, op, op_name, part_file):
+ """Applies a ZERO operation.
+
+ Args:
+ op: the operation object
+ op_name: name string for error reporting
+ part_file: the partition file object
+
+ Raises:
+ PayloadError if something goes wrong.
+ """
+ block_size = self.block_size
+ base_name = '%s.dst_extents' % op_name
+
+ # Iterate over the extents and write zero.
+ # pylint: disable=unused-variable
+ for ex, ex_name in common.ExtentIter(op.dst_extents, base_name):
+ # Only do actual writing if this is not a pseudo-extent.
+ if ex.start_block != common.PSEUDO_EXTENT_MARKER:
+ part_file.seek(ex.start_block * block_size)
+ part_file.write('\0' * (ex.num_blocks * block_size))
+
+ def _ApplySourceCopyOperation(self, op, op_name, old_part_file,
+ new_part_file):
+ """Applies a SOURCE_COPY operation.
+
+ Args:
+ op: the operation object
+ op_name: name string for error reporting
+ old_part_file: the old partition file object
+ new_part_file: the new partition file object
+
+ Raises:
+ PayloadError if something goes wrong.
+ """
+ if not old_part_file:
+ raise PayloadError(
+ '%s: no source partition file provided for operation type (%d)' %
+ (op_name, op.type))
+
+ block_size = self.block_size
+
+ # Gather input raw data from src extents.
+ in_data = _ReadExtents(old_part_file, op.src_extents, block_size)
+
+ # Dump extracted data to dst extents.
+ _WriteExtents(new_part_file, in_data, op.dst_extents, block_size,
+ '%s.dst_extents' % op_name)
+
+ def _BytesInExtents(self, extents, base_name):
+ """Counts the length of extents in bytes.
+
+ Args:
+ extents: The list of Extents.
+ base_name: For error reporting.
+
+ Returns:
+ The number of bytes in extents.
+ """
+
+ length = 0
+ # pylint: disable=unused-variable
+ for ex, ex_name in common.ExtentIter(extents, base_name):
+ length += ex.num_blocks * self.block_size
+ return length
+
+ def _ApplyDiffOperation(self, op, op_name, patch_data, old_part_file,
+ new_part_file):
+ """Applies a SOURCE_BSDIFF, BROTLI_BSDIFF or PUFFDIFF operation.
+
+ Args:
+ op: the operation object
+ op_name: name string for error reporting
+ patch_data: the binary patch content
+ old_part_file: the source partition file object
+ new_part_file: the target partition file object
+
+ Raises:
+ PayloadError if something goes wrong.
+ """
+ if not old_part_file:
+ raise PayloadError(
+ '%s: no source partition file provided for operation type (%d)' %
+ (op_name, op.type))
+
+ block_size = self.block_size
+
+ # Dump patch data to file.
+ with tempfile.NamedTemporaryFile(delete=False) as patch_file:
+ patch_file_name = patch_file.name
+ patch_file.write(patch_data)
+
+ if (hasattr(new_part_file, 'fileno') and
+ ((not old_part_file) or hasattr(old_part_file, 'fileno'))):
+ # Construct input and output extents argument for bspatch.
+
+ in_extents_arg, _, _ = _ExtentsToBspatchArg(
+ op.src_extents, block_size, '%s.src_extents' % op_name,
+ data_length=op.src_length if op.src_length else
+ self._BytesInExtents(op.src_extents, "%s.src_extents"))
+ out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg(
+ op.dst_extents, block_size, '%s.dst_extents' % op_name,
+ data_length=op.dst_length if op.dst_length else
+ self._BytesInExtents(op.dst_extents, "%s.dst_extents"))
+
+ new_file_name = '/dev/fd/%d' % new_part_file.fileno()
+ # Diff from source partition.
+ old_file_name = '/dev/fd/%d' % old_part_file.fileno()
+
+ if op.type in (common.OpType.BSDIFF, common.OpType.SOURCE_BSDIFF,
+ common.OpType.BROTLI_BSDIFF):
+ # Invoke bspatch on partition file with extents args.
+ bspatch_cmd = [self.bspatch_path, old_file_name, new_file_name,
+ patch_file_name, in_extents_arg, out_extents_arg]
+ subprocess.check_call(bspatch_cmd)
+ elif op.type == common.OpType.PUFFDIFF:
+ # Invoke puffpatch on partition file with extents args.
+ puffpatch_cmd = [self.puffpatch_path,
+ "--operation=puffpatch",
+ "--src_file=%s" % old_file_name,
+ "--dst_file=%s" % new_file_name,
+ "--patch_file=%s" % patch_file_name,
+ "--src_extents=%s" % in_extents_arg,
+ "--dst_extents=%s" % out_extents_arg]
+ subprocess.check_call(puffpatch_cmd)
+ else:
+ raise PayloadError("Unknown operation %s", op.type)
+
+ # Pad with zeros past the total output length.
+ if pad_len:
+ new_part_file.seek(pad_off)
+ new_part_file.write('\0' * pad_len)
+ else:
+ # Gather input raw data and write to a temp file.
+ input_part_file = old_part_file if old_part_file else new_part_file
+ in_data = _ReadExtents(input_part_file, op.src_extents, block_size,
+ max_length=op.src_length if op.src_length else
+ self._BytesInExtents(op.src_extents,
+ "%s.src_extents"))
+ with tempfile.NamedTemporaryFile(delete=False) as in_file:
+ in_file_name = in_file.name
+ in_file.write(in_data)
+
+ # Allocate temporary output file.
+ with tempfile.NamedTemporaryFile(delete=False) as out_file:
+ out_file_name = out_file.name
+
+ if op.type in (common.OpType.BSDIFF, common.OpType.SOURCE_BSDIFF,
+ common.OpType.BROTLI_BSDIFF):
+ # Invoke bspatch.
+ bspatch_cmd = [self.bspatch_path, in_file_name, out_file_name,
+ patch_file_name]
+ subprocess.check_call(bspatch_cmd)
+ elif op.type == common.OpType.PUFFDIFF:
+ # Invoke puffpatch.
+ puffpatch_cmd = [self.puffpatch_path,
+ "--operation=puffpatch",
+ "--src_file=%s" % in_file_name,
+ "--dst_file=%s" % out_file_name,
+ "--patch_file=%s" % patch_file_name]
+ subprocess.check_call(puffpatch_cmd)
+ else:
+ raise PayloadError("Unknown operation %s", op.type)
+
+ # Read output.
+ with open(out_file_name, 'rb') as out_file:
+ out_data = out_file.read()
+ if len(out_data) != op.dst_length:
+ raise PayloadError(
+ '%s: actual patched data length (%d) not as expected (%d)' %
+ (op_name, len(out_data), op.dst_length))
+
+ # Write output back to partition, with padding.
+ unaligned_out_len = len(out_data) % block_size
+ if unaligned_out_len:
+ out_data += '\0' * (block_size - unaligned_out_len)
+ _WriteExtents(new_part_file, out_data, op.dst_extents, block_size,
+ '%s.dst_extents' % op_name)
+
+ # Delete input/output files.
+ os.remove(in_file_name)
+ os.remove(out_file_name)
+
+ # Delete patch file.
+ os.remove(patch_file_name)
+
+ def _ApplyOperations(self, operations, base_name, old_part_file,
+ new_part_file, part_size):
+ """Applies a sequence of update operations to a partition.
+
+ This assumes an in-place update semantics for MOVE and BSDIFF, namely all
+ reads are performed first, then the data is processed and written back to
+ the same file.
+
+ Args:
+ operations: the sequence of operations
+ base_name: the name of the operation sequence
+ old_part_file: the old partition file object, open for reading/writing
+ new_part_file: the new partition file object, open for reading/writing
+ part_size: the partition size
+
+ Raises:
+ PayloadError if anything goes wrong while processing the payload.
+ """
+ for op, op_name in common.OperationIter(operations, base_name):
+ # Read data blob.
+ data = self.payload.ReadDataBlob(op.data_offset, op.data_length)
+
+ if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ,
+ common.OpType.REPLACE_XZ):
+ self._ApplyReplaceOperation(op, op_name, data, new_part_file, part_size)
+ elif op.type == common.OpType.MOVE:
+ self._ApplyMoveOperation(op, op_name, new_part_file)
+ elif op.type == common.OpType.ZERO:
+ self._ApplyZeroOperation(op, op_name, new_part_file)
+ elif op.type == common.OpType.BSDIFF:
+ self._ApplyDiffOperation(op, op_name, data, new_part_file,
+ new_part_file)
+ elif op.type == common.OpType.SOURCE_COPY:
+ self._ApplySourceCopyOperation(op, op_name, old_part_file,
+ new_part_file)
+ elif op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.PUFFDIFF,
+ common.OpType.BROTLI_BSDIFF):
+ self._ApplyDiffOperation(op, op_name, data, old_part_file,
+ new_part_file)
+ else:
+ raise PayloadError('%s: unknown operation type (%d)' %
+ (op_name, op.type))
+
+ def _ApplyToPartition(self, operations, part_name, base_name,
+ new_part_file_name, new_part_info,
+ old_part_file_name=None, old_part_info=None):
+ """Applies an update to a partition.
+
+ Args:
+ operations: the sequence of update operations to apply
+ part_name: the name of the partition, for error reporting
+ base_name: the name of the operation sequence
+ new_part_file_name: file name to write partition data to
+ new_part_info: size and expected hash of dest partition
+ old_part_file_name: file name of source partition (optional)
+ old_part_info: size and expected hash of source partition (optional)
+
+ Raises:
+ PayloadError if anything goes wrong with the update.
+ """
+ # Do we have a source partition?
+ if old_part_file_name:
+ # Verify the source partition.
+ with open(old_part_file_name, 'rb') as old_part_file:
+ _VerifySha256(old_part_file, old_part_info.hash,
+ 'old ' + part_name, length=old_part_info.size)
+ new_part_file_mode = 'r+b'
+ if self.minor_version == common.INPLACE_MINOR_PAYLOAD_VERSION:
+ # Copy the src partition to the dst one; make sure we don't truncate it.
+ shutil.copyfile(old_part_file_name, new_part_file_name)
+ elif (self.minor_version == common.SOURCE_MINOR_PAYLOAD_VERSION or
+ self.minor_version == common.OPSRCHASH_MINOR_PAYLOAD_VERSION or
+ self.minor_version == common.BROTLI_BSDIFF_MINOR_PAYLOAD_VERSION or
+ self.minor_version == common.PUFFDIFF_MINOR_PAYLOAD_VERSION):
+ # In minor version >= 2, we don't want to copy the partitions, so
+ # instead just make the new partition file.
+ open(new_part_file_name, 'w').close()
+ else:
+ raise PayloadError("Unknown minor version: %d" % self.minor_version)
+ else:
+ # We need to create/truncate the dst partition file.
+ new_part_file_mode = 'w+b'
+
+ # Apply operations.
+ with open(new_part_file_name, new_part_file_mode) as new_part_file:
+ old_part_file = (open(old_part_file_name, 'r+b')
+ if old_part_file_name else None)
+ try:
+ self._ApplyOperations(operations, base_name, old_part_file,
+ new_part_file, new_part_info.size)
+ finally:
+ if old_part_file:
+ old_part_file.close()
+
+ # Truncate the result, if so instructed.
+ if self.truncate_to_expected_size:
+ new_part_file.seek(0, 2)
+ if new_part_file.tell() > new_part_info.size:
+ new_part_file.seek(new_part_info.size)
+ new_part_file.truncate()
+
+ # Verify the resulting partition.
+ with open(new_part_file_name, 'rb') as new_part_file:
+ _VerifySha256(new_part_file, new_part_info.hash,
+ 'new ' + part_name, length=new_part_info.size)
+
+ def Run(self, new_kernel_part, new_rootfs_part, old_kernel_part=None,
+ old_rootfs_part=None):
+ """Applier entry point, invoking all update operations.
+
+ Args:
+ new_kernel_part: name of dest kernel partition file
+ new_rootfs_part: name of dest rootfs partition file
+ old_kernel_part: name of source kernel partition file (optional)
+ old_rootfs_part: name of source rootfs partition file (optional)
+
+ Raises:
+ PayloadError if payload application failed.
+ """
+ self.payload.ResetFile()
+
+ # Make sure the arguments are sane and match the payload.
+ if not (new_kernel_part and new_rootfs_part):
+ raise PayloadError('missing dst {kernel,rootfs} partitions')
+
+ if not (old_kernel_part or old_rootfs_part):
+ if not self.payload.IsFull():
+ raise PayloadError('trying to apply a non-full update without src '
+ '{kernel,rootfs} partitions')
+ elif old_kernel_part and old_rootfs_part:
+ if not self.payload.IsDelta():
+ raise PayloadError('trying to apply a non-delta update onto src '
+ '{kernel,rootfs} partitions')
+ else:
+ raise PayloadError('not all src partitions provided')
+
+ # Apply update to rootfs.
+ self._ApplyToPartition(
+ self.payload.manifest.install_operations, 'rootfs',
+ 'install_operations', new_rootfs_part,
+ self.payload.manifest.new_rootfs_info, old_rootfs_part,
+ self.payload.manifest.old_rootfs_info)
+
+ # Apply update to kernel update.
+ self._ApplyToPartition(
+ self.payload.manifest.kernel_install_operations, 'kernel',
+ 'kernel_install_operations', new_kernel_part,
+ self.payload.manifest.new_kernel_info, old_kernel_part,
+ self.payload.manifest.old_kernel_info)
diff --git a/update_payload/checker.py b/update_payload/checker.py
new file mode 100644
index 0000000..e241b0b
--- /dev/null
+++ b/update_payload/checker.py
@@ -0,0 +1,1336 @@
+#
+# 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.
+#
+
+"""Verifying the integrity of a Chrome OS update payload.
+
+This module is used internally by the main Payload class for verifying the
+integrity of an update payload. The interface for invoking the checks is as
+follows:
+
+ checker = PayloadChecker(payload)
+ checker.Run(...)
+"""
+
+from __future__ import print_function
+
+import array
+import base64
+import hashlib
+import itertools
+import os
+import subprocess
+
+from update_payload import common
+from update_payload import error
+from update_payload import format_utils
+from update_payload import histogram
+from update_payload import update_metadata_pb2
+
+
+#
+# Constants.
+#
+
+_CHECK_DST_PSEUDO_EXTENTS = 'dst-pseudo-extents'
+_CHECK_MOVE_SAME_SRC_DST_BLOCK = 'move-same-src-dst-block'
+_CHECK_PAYLOAD_SIG = 'payload-sig'
+CHECKS_TO_DISABLE = (
+ _CHECK_DST_PSEUDO_EXTENTS,
+ _CHECK_MOVE_SAME_SRC_DST_BLOCK,
+ _CHECK_PAYLOAD_SIG,
+)
+
+_TYPE_FULL = 'full'
+_TYPE_DELTA = 'delta'
+
+_DEFAULT_BLOCK_SIZE = 4096
+
+_DEFAULT_PUBKEY_BASE_NAME = 'update-payload-key.pub.pem'
+_DEFAULT_PUBKEY_FILE_NAME = os.path.join(os.path.dirname(__file__),
+ _DEFAULT_PUBKEY_BASE_NAME)
+
+# Supported minor version map to payload types allowed to be using them.
+_SUPPORTED_MINOR_VERSIONS = {
+ 0: (_TYPE_FULL,),
+ 1: (_TYPE_DELTA,),
+ 2: (_TYPE_DELTA,),
+ 3: (_TYPE_DELTA,),
+ 4: (_TYPE_DELTA,),
+ 5: (_TYPE_DELTA,),
+}
+
+_OLD_DELTA_USABLE_PART_SIZE = 2 * 1024 * 1024 * 1024
+
+#
+# Helper functions.
+#
+
+def _IsPowerOfTwo(val):
+ """Returns True iff val is a power of two."""
+ return val > 0 and (val & (val - 1)) == 0
+
+
+def _AddFormat(format_func, value):
+ """Adds a custom formatted representation to ordinary string representation.
+
+ Args:
+ format_func: A value formatter.
+ value: Value to be formatted and returned.
+
+ Returns:
+ A string 'x (y)' where x = str(value) and y = format_func(value).
+ """
+ ret = str(value)
+ formatted_str = format_func(value)
+ if formatted_str:
+ ret += ' (%s)' % formatted_str
+ return ret
+
+
+def _AddHumanReadableSize(size):
+ """Adds a human readable representation to a byte size value."""
+ return _AddFormat(format_utils.BytesToHumanReadable, size)
+
+
+#
+# Payload report generator.
+#
+
+class _PayloadReport(object):
+ """A payload report generator.
+
+ A report is essentially a sequence of nodes, which represent data points. It
+ is initialized to have a "global", untitled section. A node may be a
+ sub-report itself.
+ """
+
+ # Report nodes: Field, sub-report, section.
+ class Node(object):
+ """A report node interface."""
+
+ @staticmethod
+ def _Indent(indent, line):
+ """Indents a line by a given indentation amount.
+
+ Args:
+ indent: The indentation amount.
+ line: The line content (string).
+
+ Returns:
+ The properly indented line (string).
+ """
+ return '%*s%s' % (indent, '', line)
+
+ def GenerateLines(self, base_indent, sub_indent, curr_section):
+ """Generates the report lines for this node.
+
+ Args:
+ base_indent: Base indentation for each line.
+ sub_indent: Additional indentation for sub-nodes.
+ curr_section: The current report section object.
+
+ Returns:
+ A pair consisting of a list of properly indented report lines and a new
+ current section object.
+ """
+ raise NotImplementedError
+
+ class FieldNode(Node):
+ """A field report node, representing a (name, value) pair."""
+
+ def __init__(self, name, value, linebreak, indent):
+ super(_PayloadReport.FieldNode, self).__init__()
+ self.name = name
+ self.value = value
+ self.linebreak = linebreak
+ self.indent = indent
+
+ def GenerateLines(self, base_indent, sub_indent, curr_section):
+ """Generates a properly formatted 'name : value' entry."""
+ report_output = ''
+ if self.name:
+ report_output += self.name.ljust(curr_section.max_field_name_len) + ' :'
+ value_lines = str(self.value).splitlines()
+ if self.linebreak and self.name:
+ report_output += '\n' + '\n'.join(
+ ['%*s%s' % (self.indent, '', line) for line in value_lines])
+ else:
+ if self.name:
+ report_output += ' '
+ report_output += '%*s' % (self.indent, '')
+ cont_line_indent = len(report_output)
+ indented_value_lines = [value_lines[0]]
+ indented_value_lines.extend(['%*s%s' % (cont_line_indent, '', line)
+ for line in value_lines[1:]])
+ report_output += '\n'.join(indented_value_lines)
+
+ report_lines = [self._Indent(base_indent, line + '\n')
+ for line in report_output.split('\n')]
+ return report_lines, curr_section
+
+ class SubReportNode(Node):
+ """A sub-report node, representing a nested report."""
+
+ def __init__(self, title, report):
+ super(_PayloadReport.SubReportNode, self).__init__()
+ self.title = title
+ self.report = report
+
+ def GenerateLines(self, base_indent, sub_indent, curr_section):
+ """Recurse with indentation."""
+ report_lines = [self._Indent(base_indent, self.title + ' =>\n')]
+ report_lines.extend(self.report.GenerateLines(base_indent + sub_indent,
+ sub_indent))
+ return report_lines, curr_section
+
+ class SectionNode(Node):
+ """A section header node."""
+
+ def __init__(self, title=None):
+ super(_PayloadReport.SectionNode, self).__init__()
+ self.title = title
+ self.max_field_name_len = 0
+
+ def GenerateLines(self, base_indent, sub_indent, curr_section):
+ """Dump a title line, return self as the (new) current section."""
+ report_lines = []
+ if self.title:
+ report_lines.append(self._Indent(base_indent,
+ '=== %s ===\n' % self.title))
+ return report_lines, self
+
+ def __init__(self):
+ self.report = []
+ self.last_section = self.global_section = self.SectionNode()
+ self.is_finalized = False
+
+ def GenerateLines(self, base_indent, sub_indent):
+ """Generates the lines in the report, properly indented.
+
+ Args:
+ base_indent: The indentation used for root-level report lines.
+ sub_indent: The indentation offset used for sub-reports.
+
+ Returns:
+ A list of indented report lines.
+ """
+ report_lines = []
+ curr_section = self.global_section
+ for node in self.report:
+ node_report_lines, curr_section = node.GenerateLines(
+ base_indent, sub_indent, curr_section)
+ report_lines.extend(node_report_lines)
+
+ return report_lines
+
+ def Dump(self, out_file, base_indent=0, sub_indent=2):
+ """Dumps the report to a file.
+
+ Args:
+ out_file: File object to output the content to.
+ base_indent: Base indentation for report lines.
+ sub_indent: Added indentation for sub-reports.
+ """
+ report_lines = self.GenerateLines(base_indent, sub_indent)
+ if report_lines and not self.is_finalized:
+ report_lines.append('(incomplete report)\n')
+
+ for line in report_lines:
+ out_file.write(line)
+
+ def AddField(self, name, value, linebreak=False, indent=0):
+ """Adds a field/value pair to the payload report.
+
+ Args:
+ name: The field's name.
+ value: The field's value.
+ linebreak: Whether the value should be printed on a new line.
+ indent: Amount of extra indent for each line of the value.
+ """
+ assert not self.is_finalized
+ if name and self.last_section.max_field_name_len < len(name):
+ self.last_section.max_field_name_len = len(name)
+ self.report.append(self.FieldNode(name, value, linebreak, indent))
+
+ def AddSubReport(self, title):
+ """Adds and returns a sub-report with a title."""
+ assert not self.is_finalized
+ sub_report = self.SubReportNode(title, type(self)())
+ self.report.append(sub_report)
+ return sub_report.report
+
+ def AddSection(self, title):
+ """Adds a new section title."""
+ assert not self.is_finalized
+ self.last_section = self.SectionNode(title)
+ self.report.append(self.last_section)
+
+ def Finalize(self):
+ """Seals the report, marking it as complete."""
+ self.is_finalized = True
+
+
+#
+# Payload verification.
+#
+
+class PayloadChecker(object):
+ """Checking the integrity of an update payload.
+
+ This is a short-lived object whose purpose is to isolate the logic used for
+ verifying the integrity of an update payload.
+ """
+
+ def __init__(self, payload, assert_type=None, block_size=0,
+ allow_unhashed=False, disabled_tests=()):
+ """Initialize the checker.
+
+ Args:
+ payload: The payload object to check.
+ assert_type: Assert that payload is either 'full' or 'delta' (optional).
+ block_size: Expected filesystem / payload block size (optional).
+ allow_unhashed: Allow operations with unhashed data blobs.
+ disabled_tests: Sequence of tests to disable.
+ """
+ if not payload.is_init:
+ raise ValueError('Uninitialized update payload.')
+
+ # Set checker configuration.
+ self.payload = payload
+ self.block_size = block_size if block_size else _DEFAULT_BLOCK_SIZE
+ if not _IsPowerOfTwo(self.block_size):
+ raise error.PayloadError(
+ 'Expected block (%d) size is not a power of two.' % self.block_size)
+ if assert_type not in (None, _TYPE_FULL, _TYPE_DELTA):
+ raise error.PayloadError('Invalid assert_type value (%r).' %
+ assert_type)
+ self.payload_type = assert_type
+ self.allow_unhashed = allow_unhashed
+
+ # Disable specific tests.
+ self.check_dst_pseudo_extents = (
+ _CHECK_DST_PSEUDO_EXTENTS not in disabled_tests)
+ self.check_move_same_src_dst_block = (
+ _CHECK_MOVE_SAME_SRC_DST_BLOCK not in disabled_tests)
+ self.check_payload_sig = _CHECK_PAYLOAD_SIG not in disabled_tests
+
+ # Reset state; these will be assigned when the manifest is checked.
+ self.sigs_offset = 0
+ self.sigs_size = 0
+ self.old_rootfs_fs_size = 0
+ self.old_kernel_fs_size = 0
+ self.new_rootfs_fs_size = 0
+ self.new_kernel_fs_size = 0
+ self.minor_version = None
+ # TODO(*): When fixing crbug.com/794404, the major version should be
+ # correclty handled in update_payload scripts. So stop forcing
+ # major_verions=1 here and set it to the correct value.
+ self.major_version = 1
+
+ @staticmethod
+ def _CheckElem(msg, name, report, is_mandatory, is_submsg, convert=str,
+ msg_name=None, linebreak=False, indent=0):
+ """Adds an element from a protobuf message to the payload report.
+
+ Checks to see whether a message contains a given element, and if so adds
+ the element value to the provided report. A missing mandatory element
+ causes an exception to be raised.
+
+ Args:
+ msg: The message containing the element.
+ name: The name of the element.
+ report: A report object to add the element name/value to.
+ is_mandatory: Whether or not this element must be present.
+ is_submsg: Whether this element is itself a message.
+ convert: A function for converting the element value for reporting.
+ msg_name: The name of the message object (for error reporting).
+ linebreak: Whether the value report should induce a line break.
+ indent: Amount of indent used for reporting the value.
+
+ Returns:
+ A pair consisting of the element value and the generated sub-report for
+ it (if the element is a sub-message, None otherwise). If the element is
+ missing, returns (None, None).
+
+ Raises:
+ error.PayloadError if a mandatory element is missing.
+ """
+ if not msg.HasField(name):
+ if is_mandatory:
+ raise error.PayloadError('%smissing mandatory %s %r.' %
+ (msg_name + ' ' if msg_name else '',
+ 'sub-message' if is_submsg else 'field',
+ name))
+ return None, None
+
+ value = getattr(msg, name)
+ if is_submsg:
+ return value, report and report.AddSubReport(name)
+ else:
+ if report:
+ report.AddField(name, convert(value), linebreak=linebreak,
+ indent=indent)
+ return value, None
+
+ @staticmethod
+ def _CheckMandatoryField(msg, field_name, report, msg_name, convert=str,
+ linebreak=False, indent=0):
+ """Adds a mandatory field; returning first component from _CheckElem."""
+ return PayloadChecker._CheckElem(msg, field_name, report, True, False,
+ convert=convert, msg_name=msg_name,
+ linebreak=linebreak, indent=indent)[0]
+
+ @staticmethod
+ def _CheckOptionalField(msg, field_name, report, convert=str,
+ linebreak=False, indent=0):
+ """Adds an optional field; returning first component from _CheckElem."""
+ return PayloadChecker._CheckElem(msg, field_name, report, False, False,
+ convert=convert, linebreak=linebreak,
+ indent=indent)[0]
+
+ @staticmethod
+ def _CheckMandatorySubMsg(msg, submsg_name, report, msg_name):
+ """Adds a mandatory sub-message; wrapper for _CheckElem."""
+ return PayloadChecker._CheckElem(msg, submsg_name, report, True, True,
+ msg_name)
+
+ @staticmethod
+ def _CheckOptionalSubMsg(msg, submsg_name, report):
+ """Adds an optional sub-message; wrapper for _CheckElem."""
+ return PayloadChecker._CheckElem(msg, submsg_name, report, False, True)
+
+ @staticmethod
+ def _CheckPresentIff(val1, val2, name1, name2, obj_name):
+ """Checks that val1 is None iff val2 is None.
+
+ Args:
+ val1: first value to be compared.
+ val2: second value to be compared.
+ name1: name of object holding the first value.
+ name2: name of object holding the second value.
+ obj_name: Name of the object containing these values.
+
+ Raises:
+ error.PayloadError if assertion does not hold.
+ """
+ if None in (val1, val2) and val1 is not val2:
+ present, missing = (name1, name2) if val2 is None else (name2, name1)
+ raise error.PayloadError('%r present without %r%s.' %
+ (present, missing,
+ ' in ' + obj_name if obj_name else ''))
+
+ @staticmethod
+ def _Run(cmd, send_data=None):
+ """Runs a subprocess, returns its output.
+
+ Args:
+ cmd: Sequence of command-line argument for invoking the subprocess.
+ send_data: Data to feed to the process via its stdin.
+
+ Returns:
+ A tuple containing the stdout and stderr output of the process.
+ """
+ run_process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ try:
+ result = run_process.communicate(input=send_data)
+ finally:
+ exit_code = run_process.wait()
+
+ if exit_code:
+ raise RuntimeError('Subprocess %r failed with code %r.' %
+ (cmd, exit_code))
+
+ return result
+
+ @staticmethod
+ def _CheckSha256Signature(sig_data, pubkey_file_name, actual_hash, sig_name):
+ """Verifies an actual hash against a signed one.
+
+ Args:
+ sig_data: The raw signature data.
+ pubkey_file_name: Public key used for verifying signature.
+ actual_hash: The actual hash digest.
+ sig_name: Signature name for error reporting.
+
+ Raises:
+ error.PayloadError if signature could not be verified.
+ """
+ if len(sig_data) != 256:
+ raise error.PayloadError(
+ '%s: signature size (%d) not as expected (256).' %
+ (sig_name, len(sig_data)))
+ signed_data, _ = PayloadChecker._Run(
+ ['openssl', 'rsautl', '-verify', '-pubin', '-inkey', pubkey_file_name],
+ send_data=sig_data)
+
+ if len(signed_data) != len(common.SIG_ASN1_HEADER) + 32:
+ raise error.PayloadError('%s: unexpected signed data length (%d).' %
+ (sig_name, len(signed_data)))
+
+ if not signed_data.startswith(common.SIG_ASN1_HEADER):
+ raise error.PayloadError('%s: not containing standard ASN.1 prefix.' %
+ sig_name)
+
+ signed_hash = signed_data[len(common.SIG_ASN1_HEADER):]
+ if signed_hash != actual_hash:
+ raise error.PayloadError(
+ '%s: signed hash (%s) different from actual (%s).' %
+ (sig_name, common.FormatSha256(signed_hash),
+ common.FormatSha256(actual_hash)))
+
+ @staticmethod
+ def _CheckBlocksFitLength(length, num_blocks, block_size, length_name,
+ block_name=None):
+ """Checks that a given length fits given block space.
+
+ This ensures that the number of blocks allocated is appropriate for the
+ length of the data residing in these blocks.
+
+ Args:
+ length: The actual length of the data.
+ num_blocks: The number of blocks allocated for it.
+ block_size: The size of each block in bytes.
+ length_name: Name of length (used for error reporting).
+ block_name: Name of block (used for error reporting).
+
+ Raises:
+ error.PayloadError if the aforementioned invariant is not satisfied.
+ """
+ # Check: length <= num_blocks * block_size.
+ if length > num_blocks * block_size:
+ raise error.PayloadError(
+ '%s (%d) > num %sblocks (%d) * block_size (%d).' %
+ (length_name, length, block_name or '', num_blocks, block_size))
+
+ # Check: length > (num_blocks - 1) * block_size.
+ if length <= (num_blocks - 1) * block_size:
+ raise error.PayloadError(
+ '%s (%d) <= (num %sblocks - 1 (%d)) * block_size (%d).' %
+ (length_name, length, block_name or '', num_blocks - 1, block_size))
+
+ def _CheckManifestMinorVersion(self, report):
+ """Checks the payload manifest minor_version field.
+
+ Args:
+ report: The report object to add to.
+
+ Raises:
+ error.PayloadError if any of the checks fail.
+ """
+ self.minor_version = self._CheckOptionalField(self.payload.manifest,
+ 'minor_version', report)
+ if self.minor_version in _SUPPORTED_MINOR_VERSIONS:
+ if self.payload_type not in _SUPPORTED_MINOR_VERSIONS[self.minor_version]:
+ raise error.PayloadError(
+ 'Minor version %d not compatible with payload type %s.' %
+ (self.minor_version, self.payload_type))
+ elif self.minor_version is None:
+ raise error.PayloadError('Minor version is not set.')
+ else:
+ raise error.PayloadError('Unsupported minor version: %d' %
+ self.minor_version)
+
+ def _CheckManifest(self, report, rootfs_part_size=0, kernel_part_size=0):
+ """Checks the payload manifest.
+
+ Args:
+ report: A report object to add to.
+ rootfs_part_size: Size of the rootfs partition in bytes.
+ kernel_part_size: Size of the kernel partition in bytes.
+
+ Returns:
+ A tuple consisting of the partition block size used during the update
+ (integer), the signatures block offset and size.
+
+ Raises:
+ error.PayloadError if any of the checks fail.
+ """
+ manifest = self.payload.manifest
+ report.AddSection('manifest')
+
+ # Check: block_size must exist and match the expected value.
+ actual_block_size = self._CheckMandatoryField(manifest, 'block_size',
+ report, 'manifest')
+ if actual_block_size != self.block_size:
+ raise error.PayloadError('Block_size (%d) not as expected (%d).' %
+ (actual_block_size, self.block_size))
+
+ # Check: signatures_offset <==> signatures_size.
+ self.sigs_offset = self._CheckOptionalField(manifest, 'signatures_offset',
+ report)
+ self.sigs_size = self._CheckOptionalField(manifest, 'signatures_size',
+ report)
+ self._CheckPresentIff(self.sigs_offset, self.sigs_size,
+ 'signatures_offset', 'signatures_size', 'manifest')
+
+ # Check: old_kernel_info <==> old_rootfs_info.
+ oki_msg, oki_report = self._CheckOptionalSubMsg(manifest,
+ 'old_kernel_info', report)
+ ori_msg, ori_report = self._CheckOptionalSubMsg(manifest,
+ 'old_rootfs_info', report)
+ self._CheckPresentIff(oki_msg, ori_msg, 'old_kernel_info',
+ 'old_rootfs_info', 'manifest')
+ if oki_msg: # equivalently, ori_msg
+ # Assert/mark delta payload.
+ if self.payload_type == _TYPE_FULL:
+ raise error.PayloadError(
+ 'Apparent full payload contains old_{kernel,rootfs}_info.')
+ self.payload_type = _TYPE_DELTA
+
+ # Check: {size, hash} present in old_{kernel,rootfs}_info.
+ self.old_kernel_fs_size = self._CheckMandatoryField(
+ oki_msg, 'size', oki_report, 'old_kernel_info')
+ self._CheckMandatoryField(oki_msg, 'hash', oki_report, 'old_kernel_info',
+ convert=common.FormatSha256)
+ self.old_rootfs_fs_size = self._CheckMandatoryField(
+ ori_msg, 'size', ori_report, 'old_rootfs_info')
+ self._CheckMandatoryField(ori_msg, 'hash', ori_report, 'old_rootfs_info',
+ convert=common.FormatSha256)
+
+ # Check: old_{kernel,rootfs} size must fit in respective partition.
+ if kernel_part_size and self.old_kernel_fs_size > kernel_part_size:
+ raise error.PayloadError(
+ 'Old kernel content (%d) exceed partition size (%d).' %
+ (self.old_kernel_fs_size, kernel_part_size))
+ if rootfs_part_size and self.old_rootfs_fs_size > rootfs_part_size:
+ raise error.PayloadError(
+ 'Old rootfs content (%d) exceed partition size (%d).' %
+ (self.old_rootfs_fs_size, rootfs_part_size))
+ else:
+ # Assert/mark full payload.
+ if self.payload_type == _TYPE_DELTA:
+ raise error.PayloadError(
+ 'Apparent delta payload missing old_{kernel,rootfs}_info.')
+ self.payload_type = _TYPE_FULL
+
+ # Check: new_kernel_info present; contains {size, hash}.
+ nki_msg, nki_report = self._CheckMandatorySubMsg(
+ manifest, 'new_kernel_info', report, 'manifest')
+ self.new_kernel_fs_size = self._CheckMandatoryField(
+ nki_msg, 'size', nki_report, 'new_kernel_info')
+ self._CheckMandatoryField(nki_msg, 'hash', nki_report, 'new_kernel_info',
+ convert=common.FormatSha256)
+
+ # Check: new_rootfs_info present; contains {size, hash}.
+ nri_msg, nri_report = self._CheckMandatorySubMsg(
+ manifest, 'new_rootfs_info', report, 'manifest')
+ self.new_rootfs_fs_size = self._CheckMandatoryField(
+ nri_msg, 'size', nri_report, 'new_rootfs_info')
+ self._CheckMandatoryField(nri_msg, 'hash', nri_report, 'new_rootfs_info',
+ convert=common.FormatSha256)
+
+ # Check: new_{kernel,rootfs} size must fit in respective partition.
+ if kernel_part_size and self.new_kernel_fs_size > kernel_part_size:
+ raise error.PayloadError(
+ 'New kernel content (%d) exceed partition size (%d).' %
+ (self.new_kernel_fs_size, kernel_part_size))
+ if rootfs_part_size and self.new_rootfs_fs_size > rootfs_part_size:
+ raise error.PayloadError(
+ 'New rootfs content (%d) exceed partition size (%d).' %
+ (self.new_rootfs_fs_size, rootfs_part_size))
+
+ # Check: minor_version makes sense for the payload type. This check should
+ # run after the payload type has been set.
+ self._CheckManifestMinorVersion(report)
+
+ def _CheckLength(self, length, total_blocks, op_name, length_name):
+ """Checks whether a length matches the space designated in extents.
+
+ Args:
+ length: The total length of the data.
+ total_blocks: The total number of blocks in extents.
+ op_name: Operation name (for error reporting).
+ length_name: Length name (for error reporting).
+
+ Raises:
+ error.PayloadError is there a problem with the length.
+ """
+ # Check: length is non-zero.
+ if length == 0:
+ raise error.PayloadError('%s: %s is zero.' % (op_name, length_name))
+
+ # Check that length matches number of blocks.
+ self._CheckBlocksFitLength(length, total_blocks, self.block_size,
+ '%s: %s' % (op_name, length_name))
+
+ def _CheckExtents(self, extents, usable_size, block_counters, name,
+ allow_pseudo=False, allow_signature=False):
+ """Checks a sequence of extents.
+
+ Args:
+ extents: The sequence of extents to check.
+ usable_size: The usable size of the partition to which the extents apply.
+ block_counters: Array of counters corresponding to the number of blocks.
+ name: The name of the extent block.
+ allow_pseudo: Whether or not pseudo block numbers are allowed.
+ allow_signature: Whether or not the extents are used for a signature.
+
+ Returns:
+ The total number of blocks in the extents.
+
+ Raises:
+ error.PayloadError if any of the entailed checks fails.
+ """
+ total_num_blocks = 0
+ for ex, ex_name in common.ExtentIter(extents, name):
+ # Check: Mandatory fields.
+ start_block = PayloadChecker._CheckMandatoryField(ex, 'start_block',
+ None, ex_name)
+ num_blocks = PayloadChecker._CheckMandatoryField(ex, 'num_blocks', None,
+ ex_name)
+ end_block = start_block + num_blocks
+
+ # Check: num_blocks > 0.
+ if num_blocks == 0:
+ raise error.PayloadError('%s: extent length is zero.' % ex_name)
+
+ if start_block != common.PSEUDO_EXTENT_MARKER:
+ # Check: Make sure we're within the partition limit.
+ if usable_size and end_block * self.block_size > usable_size:
+ raise error.PayloadError(
+ '%s: extent (%s) exceeds usable partition size (%d).' %
+ (ex_name, common.FormatExtent(ex, self.block_size), usable_size))
+
+ # Record block usage.
+ for i in xrange(start_block, end_block):
+ block_counters[i] += 1
+ elif not (allow_pseudo or (allow_signature and len(extents) == 1)):
+ # Pseudo-extents must be allowed explicitly, or otherwise be part of a
+ # signature operation (in which case there has to be exactly one).
+ raise error.PayloadError('%s: unexpected pseudo-extent.' % ex_name)
+
+ total_num_blocks += num_blocks
+
+ return total_num_blocks
+
+ def _CheckReplaceOperation(self, op, data_length, total_dst_blocks, op_name):
+ """Specific checks for REPLACE/REPLACE_BZ/REPLACE_XZ operations.
+
+ Args:
+ op: The operation object from the manifest.
+ data_length: The length of the data blob associated with the operation.
+ total_dst_blocks: Total number of blocks in dst_extents.
+ op_name: Operation name for error reporting.
+
+ Raises:
+ error.PayloadError if any check fails.
+ """
+ # Check: Does not contain src extents.
+ if op.src_extents:
+ raise error.PayloadError('%s: contains src_extents.' % op_name)
+
+ # Check: Contains data.
+ if data_length is None:
+ raise error.PayloadError('%s: missing data_{offset,length}.' % op_name)
+
+ if op.type == common.OpType.REPLACE:
+ PayloadChecker._CheckBlocksFitLength(data_length, total_dst_blocks,
+ self.block_size,
+ op_name + '.data_length', 'dst')
+ else:
+ # Check: data_length must be smaller than the alotted dst blocks.
+ if data_length >= total_dst_blocks * self.block_size:
+ raise error.PayloadError(
+ '%s: data_length (%d) must be less than allotted dst block '
+ 'space (%d * %d).' %
+ (op_name, data_length, total_dst_blocks, self.block_size))
+
+ def _CheckMoveOperation(self, op, data_offset, total_src_blocks,
+ total_dst_blocks, op_name):
+ """Specific checks for MOVE operations.
+
+ Args:
+ op: The operation object from the manifest.
+ data_offset: The offset of a data blob for the operation.
+ total_src_blocks: Total number of blocks in src_extents.
+ total_dst_blocks: Total number of blocks in dst_extents.
+ op_name: Operation name for error reporting.
+
+ Raises:
+ error.PayloadError if any check fails.
+ """
+ # Check: No data_{offset,length}.
+ if data_offset is not None:
+ raise error.PayloadError('%s: contains data_{offset,length}.' % op_name)
+
+ # Check: total_src_blocks == total_dst_blocks.
+ if total_src_blocks != total_dst_blocks:
+ raise error.PayloadError(
+ '%s: total src blocks (%d) != total dst blocks (%d).' %
+ (op_name, total_src_blocks, total_dst_blocks))
+
+ # Check: For all i, i-th src block index != i-th dst block index.
+ i = 0
+ src_extent_iter = iter(op.src_extents)
+ dst_extent_iter = iter(op.dst_extents)
+ src_extent = dst_extent = None
+ src_idx = src_num = dst_idx = dst_num = 0
+ while i < total_src_blocks:
+ # Get the next source extent, if needed.
+ if not src_extent:
+ try:
+ src_extent = src_extent_iter.next()
+ except StopIteration:
+ raise error.PayloadError('%s: ran out of src extents (%d/%d).' %
+ (op_name, i, total_src_blocks))
+ src_idx = src_extent.start_block
+ src_num = src_extent.num_blocks
+
+ # Get the next dest extent, if needed.
+ if not dst_extent:
+ try:
+ dst_extent = dst_extent_iter.next()
+ except StopIteration:
+ raise error.PayloadError('%s: ran out of dst extents (%d/%d).' %
+ (op_name, i, total_dst_blocks))
+ dst_idx = dst_extent.start_block
+ dst_num = dst_extent.num_blocks
+
+ # Check: start block is not 0. See crbug/480751; there are still versions
+ # of update_engine which fail when seeking to 0 in PReadAll and PWriteAll,
+ # so we need to fail payloads that try to MOVE to/from block 0.
+ if src_idx == 0 or dst_idx == 0:
+ raise error.PayloadError(
+ '%s: MOVE operation cannot have extent with start block 0' %
+ op_name)
+
+ if self.check_move_same_src_dst_block and src_idx == dst_idx:
+ raise error.PayloadError(
+ '%s: src/dst block number %d is the same (%d).' %
+ (op_name, i, src_idx))
+
+ advance = min(src_num, dst_num)
+ i += advance
+
+ src_idx += advance
+ src_num -= advance
+ if src_num == 0:
+ src_extent = None
+
+ dst_idx += advance
+ dst_num -= advance
+ if dst_num == 0:
+ dst_extent = None
+
+ # Make sure we've exhausted all src/dst extents.
+ if src_extent:
+ raise error.PayloadError('%s: excess src blocks.' % op_name)
+ if dst_extent:
+ raise error.PayloadError('%s: excess dst blocks.' % op_name)
+
+ def _CheckZeroOperation(self, op, op_name):
+ """Specific checks for ZERO operations.
+
+ Args:
+ op: The operation object from the manifest.
+ op_name: Operation name for error reporting.
+
+ Raises:
+ error.PayloadError if any check fails.
+ """
+ # Check: Does not contain src extents, data_length and data_offset.
+ if op.src_extents:
+ raise error.PayloadError('%s: contains src_extents.' % op_name)
+ if op.data_length:
+ raise error.PayloadError('%s: contains data_length.' % op_name)
+ if op.data_offset:
+ raise error.PayloadError('%s: contains data_offset.' % op_name)
+
+ def _CheckAnyDiffOperation(self, op, data_length, total_dst_blocks, op_name):
+ """Specific checks for BSDIFF, SOURCE_BSDIFF, PUFFDIFF and BROTLI_BSDIFF
+ operations.
+
+ Args:
+ op: The operation.
+ data_length: The length of the data blob associated with the operation.
+ total_dst_blocks: Total number of blocks in dst_extents.
+ op_name: Operation name for error reporting.
+
+ Raises:
+ error.PayloadError if any check fails.
+ """
+ # Check: data_{offset,length} present.
+ if data_length is None:
+ raise error.PayloadError('%s: missing data_{offset,length}.' % op_name)
+
+ # Check: data_length is strictly smaller than the alotted dst blocks.
+ if data_length >= total_dst_blocks * self.block_size:
+ raise error.PayloadError(
+ '%s: data_length (%d) must be smaller than allotted dst space '
+ '(%d * %d = %d).' %
+ (op_name, data_length, total_dst_blocks, self.block_size,
+ total_dst_blocks * self.block_size))
+
+ # Check the existence of src_length and dst_length for legacy bsdiffs.
+ if (op.type == common.OpType.BSDIFF or
+ (op.type == common.OpType.SOURCE_BSDIFF and self.minor_version <= 3)):
+ if not op.HasField('src_length') or not op.HasField('dst_length'):
+ raise error.PayloadError('%s: require {src,dst}_length.' % op_name)
+ else:
+ if op.HasField('src_length') or op.HasField('dst_length'):
+ raise error.PayloadError('%s: unneeded {src,dst}_length.' % op_name)
+
+ def _CheckSourceCopyOperation(self, data_offset, total_src_blocks,
+ total_dst_blocks, op_name):
+ """Specific checks for SOURCE_COPY.
+
+ Args:
+ data_offset: The offset of a data blob for the operation.
+ total_src_blocks: Total number of blocks in src_extents.
+ total_dst_blocks: Total number of blocks in dst_extents.
+ op_name: Operation name for error reporting.
+
+ Raises:
+ error.PayloadError if any check fails.
+ """
+ # Check: No data_{offset,length}.
+ if data_offset is not None:
+ raise error.PayloadError('%s: contains data_{offset,length}.' % op_name)
+
+ # Check: total_src_blocks == total_dst_blocks.
+ if total_src_blocks != total_dst_blocks:
+ raise error.PayloadError(
+ '%s: total src blocks (%d) != total dst blocks (%d).' %
+ (op_name, total_src_blocks, total_dst_blocks))
+
+ def _CheckAnySourceOperation(self, op, total_src_blocks, op_name):
+ """Specific checks for SOURCE_* operations.
+
+ Args:
+ op: The operation object from the manifest.
+ total_src_blocks: Total number of blocks in src_extents.
+ op_name: Operation name for error reporting.
+
+ Raises:
+ error.PayloadError if any check fails.
+ """
+ # Check: total_src_blocks != 0.
+ if total_src_blocks == 0:
+ raise error.PayloadError('%s: no src blocks in a source op.' % op_name)
+
+ # Check: src_sha256_hash present in minor version >= 3.
+ if self.minor_version >= 3 and op.src_sha256_hash is None:
+ raise error.PayloadError('%s: source hash missing.' % op_name)
+
+ def _CheckOperation(self, op, op_name, is_last, old_block_counters,
+ new_block_counters, old_usable_size, new_usable_size,
+ prev_data_offset, allow_signature, blob_hash_counts):
+ """Checks a single update operation.
+
+ Args:
+ op: The operation object.
+ op_name: Operation name string for error reporting.
+ is_last: Whether this is the last operation in the sequence.
+ old_block_counters: Arrays of block read counters.
+ new_block_counters: Arrays of block write counters.
+ old_usable_size: The overall usable size for src data in bytes.
+ new_usable_size: The overall usable size for dst data in bytes.
+ prev_data_offset: Offset of last used data bytes.
+ allow_signature: Whether this may be a signature operation.
+ blob_hash_counts: Counters for hashed/unhashed blobs.
+
+ Returns:
+ The amount of data blob associated with the operation.
+
+ Raises:
+ error.PayloadError if any check has failed.
+ """
+ # Check extents.
+ total_src_blocks = self._CheckExtents(
+ op.src_extents, old_usable_size, old_block_counters,
+ op_name + '.src_extents', allow_pseudo=True)
+ allow_signature_in_extents = (allow_signature and is_last and
+ op.type == common.OpType.REPLACE)
+ total_dst_blocks = self._CheckExtents(
+ op.dst_extents, new_usable_size, new_block_counters,
+ op_name + '.dst_extents',
+ allow_pseudo=(not self.check_dst_pseudo_extents),
+ allow_signature=allow_signature_in_extents)
+
+ # Check: data_offset present <==> data_length present.
+ data_offset = self._CheckOptionalField(op, 'data_offset', None)
+ data_length = self._CheckOptionalField(op, 'data_length', None)
+ self._CheckPresentIff(data_offset, data_length, 'data_offset',
+ 'data_length', op_name)
+
+ # Check: At least one dst_extent.
+ if not op.dst_extents:
+ raise error.PayloadError('%s: dst_extents is empty.' % op_name)
+
+ # Check {src,dst}_length, if present.
+ if op.HasField('src_length'):
+ self._CheckLength(op.src_length, total_src_blocks, op_name, 'src_length')
+ if op.HasField('dst_length'):
+ self._CheckLength(op.dst_length, total_dst_blocks, op_name, 'dst_length')
+
+ if op.HasField('data_sha256_hash'):
+ blob_hash_counts['hashed'] += 1
+
+ # Check: Operation carries data.
+ if data_offset is None:
+ raise error.PayloadError(
+ '%s: data_sha256_hash present but no data_{offset,length}.' %
+ op_name)
+
+ # Check: Hash verifies correctly.
+ actual_hash = hashlib.sha256(self.payload.ReadDataBlob(data_offset,
+ data_length))
+ if op.data_sha256_hash != actual_hash.digest():
+ raise error.PayloadError(
+ '%s: data_sha256_hash (%s) does not match actual hash (%s).' %
+ (op_name, common.FormatSha256(op.data_sha256_hash),
+ common.FormatSha256(actual_hash.digest())))
+ elif data_offset is not None:
+ if allow_signature_in_extents:
+ blob_hash_counts['signature'] += 1
+ elif self.allow_unhashed:
+ blob_hash_counts['unhashed'] += 1
+ else:
+ raise error.PayloadError('%s: unhashed operation not allowed.' %
+ op_name)
+
+ if data_offset is not None:
+ # Check: Contiguous use of data section.
+ if data_offset != prev_data_offset:
+ raise error.PayloadError(
+ '%s: data offset (%d) not matching amount used so far (%d).' %
+ (op_name, data_offset, prev_data_offset))
+
+ # Type-specific checks.
+ if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
+ self._CheckReplaceOperation(op, data_length, total_dst_blocks, op_name)
+ elif op.type == common.OpType.REPLACE_XZ and (self.minor_version >= 3 or
+ self.major_version >= 2):
+ self._CheckReplaceOperation(op, data_length, total_dst_blocks, op_name)
+ elif op.type == common.OpType.MOVE and self.minor_version == 1:
+ self._CheckMoveOperation(op, data_offset, total_src_blocks,
+ total_dst_blocks, op_name)
+ elif op.type == common.OpType.ZERO and self.minor_version >= 4:
+ self._CheckZeroOperation(op, op_name)
+ elif op.type == common.OpType.BSDIFF and self.minor_version == 1:
+ self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name)
+ elif op.type == common.OpType.SOURCE_COPY and self.minor_version >= 2:
+ self._CheckSourceCopyOperation(data_offset, total_src_blocks,
+ total_dst_blocks, op_name)
+ self._CheckAnySourceOperation(op, total_src_blocks, op_name)
+ elif op.type == common.OpType.SOURCE_BSDIFF and self.minor_version >= 2:
+ self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name)
+ self._CheckAnySourceOperation(op, total_src_blocks, op_name)
+ elif op.type == common.OpType.BROTLI_BSDIFF and self.minor_version >= 4:
+ self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name)
+ self._CheckAnySourceOperation(op, total_src_blocks, op_name)
+ elif op.type == common.OpType.PUFFDIFF and self.minor_version >= 5:
+ self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name)
+ self._CheckAnySourceOperation(op, total_src_blocks, op_name)
+ else:
+ raise error.PayloadError(
+ 'Operation %s (type %d) not allowed in minor version %d' %
+ (op_name, op.type, self.minor_version))
+ return data_length if data_length is not None else 0
+
+ def _SizeToNumBlocks(self, size):
+ """Returns the number of blocks needed to contain a given byte size."""
+ return (size + self.block_size - 1) / self.block_size
+
+ def _AllocBlockCounters(self, total_size):
+ """Returns a freshly initialized array of block counters.
+
+ Note that the generated array is not portable as is due to byte-ordering
+ issues, hence it should not be serialized.
+
+ Args:
+ total_size: The total block size in bytes.
+
+ Returns:
+ An array of unsigned short elements initialized to zero, one for each of
+ the blocks necessary for containing the partition.
+ """
+ return array.array('H',
+ itertools.repeat(0, self._SizeToNumBlocks(total_size)))
+
+ def _CheckOperations(self, operations, report, base_name, old_fs_size,
+ new_fs_size, old_usable_size, new_usable_size,
+ prev_data_offset, allow_signature):
+ """Checks a sequence of update operations.
+
+ Args:
+ operations: The sequence of operations to check.
+ report: The report object to add to.
+ base_name: The name of the operation block.
+ old_fs_size: The old filesystem size in bytes.
+ new_fs_size: The new filesystem size in bytes.
+ old_usable_size: The overall usable size of the old partition in bytes.
+ new_usable_size: The overall usable size of the new partition in bytes.
+ prev_data_offset: Offset of last used data bytes.
+ allow_signature: Whether this sequence may contain signature operations.
+
+ Returns:
+ The total data blob size used.
+
+ Raises:
+ error.PayloadError if any of the checks fails.
+ """
+ # The total size of data blobs used by operations scanned thus far.
+ total_data_used = 0
+ # Counts of specific operation types.
+ op_counts = {
+ common.OpType.REPLACE: 0,
+ common.OpType.REPLACE_BZ: 0,
+ common.OpType.REPLACE_XZ: 0,
+ common.OpType.MOVE: 0,
+ common.OpType.ZERO: 0,
+ common.OpType.BSDIFF: 0,
+ common.OpType.SOURCE_COPY: 0,
+ common.OpType.SOURCE_BSDIFF: 0,
+ common.OpType.PUFFDIFF: 0,
+ common.OpType.BROTLI_BSDIFF: 0,
+ }
+ # Total blob sizes for each operation type.
+ op_blob_totals = {
+ common.OpType.REPLACE: 0,
+ common.OpType.REPLACE_BZ: 0,
+ common.OpType.REPLACE_XZ: 0,
+ # MOVE operations don't have blobs.
+ common.OpType.BSDIFF: 0,
+ # SOURCE_COPY operations don't have blobs.
+ common.OpType.SOURCE_BSDIFF: 0,
+ common.OpType.PUFFDIFF: 0,
+ common.OpType.BROTLI_BSDIFF: 0,
+ }
+ # Counts of hashed vs unhashed operations.
+ blob_hash_counts = {
+ 'hashed': 0,
+ 'unhashed': 0,
+ }
+ if allow_signature:
+ blob_hash_counts['signature'] = 0
+
+ # Allocate old and new block counters.
+ old_block_counters = (self._AllocBlockCounters(old_usable_size)
+ if old_fs_size else None)
+ new_block_counters = self._AllocBlockCounters(new_usable_size)
+
+ # Process and verify each operation.
+ op_num = 0
+ for op, op_name in common.OperationIter(operations, base_name):
+ op_num += 1
+
+ # Check: Type is valid.
+ if op.type not in op_counts.keys():
+ raise error.PayloadError('%s: invalid type (%d).' % (op_name, op.type))
+ op_counts[op.type] += 1
+
+ is_last = op_num == len(operations)
+ curr_data_used = self._CheckOperation(
+ op, op_name, is_last, old_block_counters, new_block_counters,
+ old_usable_size, new_usable_size,
+ prev_data_offset + total_data_used, allow_signature,
+ blob_hash_counts)
+ if curr_data_used:
+ op_blob_totals[op.type] += curr_data_used
+ total_data_used += curr_data_used
+
+ # Report totals and breakdown statistics.
+ report.AddField('total operations', op_num)
+ report.AddField(
+ None,
+ histogram.Histogram.FromCountDict(op_counts,
+ key_names=common.OpType.NAMES),
+ indent=1)
+ report.AddField('total blobs', sum(blob_hash_counts.values()))
+ report.AddField(None,
+ histogram.Histogram.FromCountDict(blob_hash_counts),
+ indent=1)
+ report.AddField('total blob size', _AddHumanReadableSize(total_data_used))
+ report.AddField(
+ None,
+ histogram.Histogram.FromCountDict(op_blob_totals,
+ formatter=_AddHumanReadableSize,
+ key_names=common.OpType.NAMES),
+ indent=1)
+
+ # Report read/write histograms.
+ if old_block_counters:
+ report.AddField('block read hist',
+ histogram.Histogram.FromKeyList(old_block_counters),
+ linebreak=True, indent=1)
+
+ new_write_hist = histogram.Histogram.FromKeyList(
+ new_block_counters[:self._SizeToNumBlocks(new_fs_size)])
+ report.AddField('block write hist', new_write_hist, linebreak=True,
+ indent=1)
+
+ # Check: Full update must write each dst block once.
+ if self.payload_type == _TYPE_FULL and new_write_hist.GetKeys() != [1]:
+ raise error.PayloadError(
+ '%s: not all blocks written exactly once during full update.' %
+ base_name)
+
+ return total_data_used
+
+ def _CheckSignatures(self, report, pubkey_file_name):
+ """Checks a payload's signature block."""
+ sigs_raw = self.payload.ReadDataBlob(self.sigs_offset, self.sigs_size)
+ sigs = update_metadata_pb2.Signatures()
+ sigs.ParseFromString(sigs_raw)
+ report.AddSection('signatures')
+
+ # Check: At least one signature present.
+ if not sigs.signatures:
+ raise error.PayloadError('Signature block is empty.')
+
+ last_ops_section = (self.payload.manifest.kernel_install_operations or
+ self.payload.manifest.install_operations)
+ fake_sig_op = last_ops_section[-1]
+ # Check: signatures_{offset,size} must match the last (fake) operation.
+ if not (fake_sig_op.type == common.OpType.REPLACE and
+ self.sigs_offset == fake_sig_op.data_offset and
+ self.sigs_size == fake_sig_op.data_length):
+ raise error.PayloadError(
+ 'Signatures_{offset,size} (%d+%d) does not match last operation '
+ '(%d+%d).' %
+ (self.sigs_offset, self.sigs_size, fake_sig_op.data_offset,
+ fake_sig_op.data_length))
+
+ # Compute the checksum of all data up to signature blob.
+ # TODO(garnold) we're re-reading the whole data section into a string
+ # just to compute the checksum; instead, we could do it incrementally as
+ # we read the blobs one-by-one, under the assumption that we're reading
+ # them in order (which currently holds). This should be reconsidered.
+ payload_hasher = self.payload.manifest_hasher.copy()
+ common.Read(self.payload.payload_file, self.sigs_offset,
+ offset=self.payload.data_offset, hasher=payload_hasher)
+
+ for sig, sig_name in common.SignatureIter(sigs.signatures, 'signatures'):
+ sig_report = report.AddSubReport(sig_name)
+
+ # Check: Signature contains mandatory fields.
+ self._CheckMandatoryField(sig, 'version', sig_report, sig_name)
+ self._CheckMandatoryField(sig, 'data', None, sig_name)
+ sig_report.AddField('data len', len(sig.data))
+
+ # Check: Signatures pertains to actual payload hash.
+ if sig.version == 1:
+ self._CheckSha256Signature(sig.data, pubkey_file_name,
+ payload_hasher.digest(), sig_name)
+ else:
+ raise error.PayloadError('Unknown signature version (%d).' %
+ sig.version)
+
+ def Run(self, pubkey_file_name=None, metadata_sig_file=None,
+ rootfs_part_size=0, kernel_part_size=0, report_out_file=None):
+ """Checker entry point, invoking all checks.
+
+ Args:
+ pubkey_file_name: Public key used for signature verification.
+ metadata_sig_file: Metadata signature, if verification is desired.
+ rootfs_part_size: The size of rootfs partitions in bytes (default: infer
+ based on payload type and version).
+ kernel_part_size: The size of kernel partitions in bytes (default: use
+ reported filesystem size).
+ report_out_file: File object to dump the report to.
+
+ Raises:
+ error.PayloadError if payload verification failed.
+ """
+ if not pubkey_file_name:
+ pubkey_file_name = _DEFAULT_PUBKEY_FILE_NAME
+
+ report = _PayloadReport()
+
+ # Get payload file size.
+ self.payload.payload_file.seek(0, 2)
+ payload_file_size = self.payload.payload_file.tell()
+ self.payload.ResetFile()
+
+ try:
+ # Check metadata signature (if provided).
+ if metadata_sig_file:
+ metadata_sig = base64.b64decode(metadata_sig_file.read())
+ self._CheckSha256Signature(metadata_sig, pubkey_file_name,
+ self.payload.manifest_hasher.digest(),
+ 'metadata signature')
+
+ # Part 1: Check the file header.
+ report.AddSection('header')
+ # Check: Payload version is valid.
+ if self.payload.header.version != 1:
+ raise error.PayloadError('Unknown payload version (%d).' %
+ self.payload.header.version)
+ report.AddField('version', self.payload.header.version)
+ report.AddField('manifest len', self.payload.header.manifest_len)
+
+ # Part 2: Check the manifest.
+ self._CheckManifest(report, rootfs_part_size, kernel_part_size)
+ assert self.payload_type, 'payload type should be known by now'
+
+ # Infer the usable partition size when validating rootfs operations:
+ # - If rootfs partition size was provided, use that.
+ # - Otherwise, if this is an older delta (minor version < 2), stick with
+ # a known constant size. This is necessary because older deltas may
+ # exceed the filesystem size when moving data blocks around.
+ # - Otherwise, use the encoded filesystem size.
+ new_rootfs_usable_size = self.new_rootfs_fs_size
+ old_rootfs_usable_size = self.old_rootfs_fs_size
+ if rootfs_part_size:
+ new_rootfs_usable_size = rootfs_part_size
+ old_rootfs_usable_size = rootfs_part_size
+ elif self.payload_type == _TYPE_DELTA and self.minor_version in (None, 1):
+ new_rootfs_usable_size = _OLD_DELTA_USABLE_PART_SIZE
+ old_rootfs_usable_size = _OLD_DELTA_USABLE_PART_SIZE
+
+ # Part 3: Examine rootfs operations.
+ # TODO(garnold)(chromium:243559) only default to the filesystem size if
+ # no explicit size provided *and* the partition size is not embedded in
+ # the payload; see issue for more details.
+ report.AddSection('rootfs operations')
+ total_blob_size = self._CheckOperations(
+ self.payload.manifest.install_operations, report,
+ 'install_operations', self.old_rootfs_fs_size,
+ self.new_rootfs_fs_size, old_rootfs_usable_size,
+ new_rootfs_usable_size, 0, False)
+
+ # Part 4: Examine kernel operations.
+ # TODO(garnold)(chromium:243559) as above.
+ report.AddSection('kernel operations')
+ total_blob_size += self._CheckOperations(
+ self.payload.manifest.kernel_install_operations, report,
+ 'kernel_install_operations', self.old_kernel_fs_size,
+ self.new_kernel_fs_size,
+ kernel_part_size if kernel_part_size else self.old_kernel_fs_size,
+ kernel_part_size if kernel_part_size else self.new_kernel_fs_size,
+ total_blob_size, True)
+
+ # Check: Operations data reach the end of the payload file.
+ used_payload_size = self.payload.data_offset + total_blob_size
+ if used_payload_size != payload_file_size:
+ raise error.PayloadError(
+ 'Used payload size (%d) different from actual file size (%d).' %
+ (used_payload_size, payload_file_size))
+
+ # Part 5: Handle payload signatures message.
+ if self.check_payload_sig and self.sigs_size:
+ self._CheckSignatures(report, pubkey_file_name)
+
+ # Part 6: Summary.
+ report.AddSection('summary')
+ report.AddField('update type', self.payload_type)
+
+ report.Finalize()
+ finally:
+ if report_out_file:
+ report.Dump(report_out_file)
diff --git a/update_payload/checker_unittest.py b/update_payload/checker_unittest.py
new file mode 100755
index 0000000..f718234
--- /dev/null
+++ b/update_payload/checker_unittest.py
@@ -0,0 +1,1364 @@
+#!/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.
+#
+
+"""Unit testing checker.py."""
+
+from __future__ import print_function
+
+import array
+import collections
+import cStringIO
+import hashlib
+import itertools
+import os
+import unittest
+
+# pylint cannot find mox.
+# pylint: disable=F0401
+import mox
+
+from update_payload import checker
+from update_payload import common
+from update_payload import test_utils
+from update_payload import update_metadata_pb2
+from update_payload.error import PayloadError
+from update_payload.payload import Payload # Avoid name conflicts later.
+
+
+def _OpTypeByName(op_name):
+ """Returns the type of an operation from itsname."""
+ op_name_to_type = {
+ 'REPLACE': common.OpType.REPLACE,
+ 'REPLACE_BZ': common.OpType.REPLACE_BZ,
+ 'MOVE': common.OpType.MOVE,
+ 'BSDIFF': common.OpType.BSDIFF,
+ 'SOURCE_COPY': common.OpType.SOURCE_COPY,
+ 'SOURCE_BSDIFF': common.OpType.SOURCE_BSDIFF,
+ 'ZERO': common.OpType.ZERO,
+ 'DISCARD': common.OpType.DISCARD,
+ 'REPLACE_XZ': common.OpType.REPLACE_XZ,
+ 'PUFFDIFF': common.OpType.PUFFDIFF,
+ 'BROTLI_BSDIFF': common.OpType.BROTLI_BSDIFF,
+ }
+ return op_name_to_type[op_name]
+
+
+def _GetPayloadChecker(payload_gen_write_to_file_func, payload_gen_dargs=None,
+ checker_init_dargs=None):
+ """Returns a payload checker from a given payload generator."""
+ if payload_gen_dargs is None:
+ payload_gen_dargs = {}
+ if checker_init_dargs is None:
+ checker_init_dargs = {}
+
+ payload_file = cStringIO.StringIO()
+ payload_gen_write_to_file_func(payload_file, **payload_gen_dargs)
+ payload_file.seek(0)
+ payload = Payload(payload_file)
+ payload.Init()
+ return checker.PayloadChecker(payload, **checker_init_dargs)
+
+
+def _GetPayloadCheckerWithData(payload_gen):
+ """Returns a payload checker from a given payload generator."""
+ payload_file = cStringIO.StringIO()
+ payload_gen.WriteToFile(payload_file)
+ payload_file.seek(0)
+ payload = Payload(payload_file)
+ payload.Init()
+ return checker.PayloadChecker(payload)
+
+
+# This class doesn't need an __init__().
+# pylint: disable=W0232
+# Unit testing is all about running protected methods.
+# pylint: disable=W0212
+# Don't bark about missing members of classes you cannot import.
+# pylint: disable=E1101
+class PayloadCheckerTest(mox.MoxTestBase):
+ """Tests the PayloadChecker class.
+
+ In addition to ordinary testFoo() methods, which are automatically invoked by
+ the unittest framework, in this class we make use of DoBarTest() calls that
+ implement parametric tests of certain features. In order to invoke each test,
+ which embodies a unique combination of parameter values, as a complete unit
+ test, we perform explicit enumeration of the parameter space and create
+ individual invocation contexts for each, which are then bound as
+ testBar__param1=val1__param2=val2(). The enumeration of parameter spaces for
+ all such tests is done in AddAllParametricTests().
+ """
+
+ def MockPayload(self):
+ """Create a mock payload object, complete with a mock manifest."""
+ payload = self.mox.CreateMock(Payload)
+ payload.is_init = True
+ payload.manifest = self.mox.CreateMock(
+ update_metadata_pb2.DeltaArchiveManifest)
+ return payload
+
+ @staticmethod
+ def NewExtent(start_block, num_blocks):
+ """Returns an Extent message.
+
+ Each of the provided fields is set iff it is >= 0; otherwise, it's left at
+ its default state.
+
+ Args:
+ start_block: The starting block of the extent.
+ num_blocks: The number of blocks in the extent.
+
+ Returns:
+ An Extent message.
+ """
+ ex = update_metadata_pb2.Extent()
+ if start_block >= 0:
+ ex.start_block = start_block
+ if num_blocks >= 0:
+ ex.num_blocks = num_blocks
+ return ex
+
+ @staticmethod
+ def NewExtentList(*args):
+ """Returns an list of extents.
+
+ Args:
+ *args: (start_block, num_blocks) pairs defining the extents.
+
+ Returns:
+ A list of Extent objects.
+ """
+ ex_list = []
+ for start_block, num_blocks in args:
+ ex_list.append(PayloadCheckerTest.NewExtent(start_block, num_blocks))
+ return ex_list
+
+ @staticmethod
+ def AddToMessage(repeated_field, field_vals):
+ for field_val in field_vals:
+ new_field = repeated_field.add()
+ new_field.CopyFrom(field_val)
+
+ def SetupAddElemTest(self, is_present, is_submsg, convert=str,
+ linebreak=False, indent=0):
+ """Setup for testing of _CheckElem() and its derivatives.
+
+ Args:
+ is_present: Whether or not the element is found in the message.
+ is_submsg: Whether the element is a sub-message itself.
+ convert: A representation conversion function.
+ linebreak: Whether or not a linebreak is to be used in the report.
+ indent: Indentation used for the report.
+
+ Returns:
+ msg: A mock message object.
+ report: A mock report object.
+ subreport: A mock sub-report object.
+ name: An element name to check.
+ val: Expected element value.
+ """
+ name = 'foo'
+ val = 'fake submsg' if is_submsg else 'fake field'
+ subreport = 'fake subreport'
+
+ # Create a mock message.
+ msg = self.mox.CreateMock(update_metadata_pb2._message.Message)
+ msg.HasField(name).AndReturn(is_present)
+ setattr(msg, name, val)
+
+ # Create a mock report.
+ report = self.mox.CreateMock(checker._PayloadReport)
+ if is_present:
+ if is_submsg:
+ report.AddSubReport(name).AndReturn(subreport)
+ else:
+ report.AddField(name, convert(val), linebreak=linebreak, indent=indent)
+
+ self.mox.ReplayAll()
+ return (msg, report, subreport, name, val)
+
+ def DoAddElemTest(self, is_present, is_mandatory, is_submsg, convert,
+ linebreak, indent):
+ """Parametric testing of _CheckElem().
+
+ Args:
+ is_present: Whether or not the element is found in the message.
+ is_mandatory: Whether or not it's a mandatory element.
+ is_submsg: Whether the element is a sub-message itself.
+ convert: A representation conversion function.
+ linebreak: Whether or not a linebreak is to be used in the report.
+ indent: Indentation used for the report.
+ """
+ msg, report, subreport, name, val = self.SetupAddElemTest(
+ is_present, is_submsg, convert, linebreak, indent)
+
+ args = (msg, name, report, is_mandatory, is_submsg)
+ kwargs = {'convert': convert, 'linebreak': linebreak, 'indent': indent}
+ if is_mandatory and not is_present:
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckElem, *args, **kwargs)
+ else:
+ ret_val, ret_subreport = checker.PayloadChecker._CheckElem(*args,
+ **kwargs)
+ self.assertEquals(val if is_present else None, ret_val)
+ self.assertEquals(subreport if is_present and is_submsg else None,
+ ret_subreport)
+
+ def DoAddFieldTest(self, is_mandatory, is_present, convert, linebreak,
+ indent):
+ """Parametric testing of _Check{Mandatory,Optional}Field().
+
+ Args:
+ is_mandatory: Whether we're testing a mandatory call.
+ is_present: Whether or not the element is found in the message.
+ convert: A representation conversion function.
+ linebreak: Whether or not a linebreak is to be used in the report.
+ indent: Indentation used for the report.
+ """
+ msg, report, _, name, val = self.SetupAddElemTest(
+ is_present, False, convert, linebreak, indent)
+
+ # Prepare for invocation of the tested method.
+ args = [msg, name, report]
+ kwargs = {'convert': convert, 'linebreak': linebreak, 'indent': indent}
+ if is_mandatory:
+ args.append('bar')
+ tested_func = checker.PayloadChecker._CheckMandatoryField
+ else:
+ tested_func = checker.PayloadChecker._CheckOptionalField
+
+ # Test the method call.
+ if is_mandatory and not is_present:
+ self.assertRaises(PayloadError, tested_func, *args, **kwargs)
+ else:
+ ret_val = tested_func(*args, **kwargs)
+ self.assertEquals(val if is_present else None, ret_val)
+
+ def DoAddSubMsgTest(self, is_mandatory, is_present):
+ """Parametrized testing of _Check{Mandatory,Optional}SubMsg().
+
+ Args:
+ is_mandatory: Whether we're testing a mandatory call.
+ is_present: Whether or not the element is found in the message.
+ """
+ msg, report, subreport, name, val = self.SetupAddElemTest(is_present, True)
+
+ # Prepare for invocation of the tested method.
+ args = [msg, name, report]
+ if is_mandatory:
+ args.append('bar')
+ tested_func = checker.PayloadChecker._CheckMandatorySubMsg
+ else:
+ tested_func = checker.PayloadChecker._CheckOptionalSubMsg
+
+ # Test the method call.
+ if is_mandatory and not is_present:
+ self.assertRaises(PayloadError, tested_func, *args)
+ else:
+ ret_val, ret_subreport = tested_func(*args)
+ self.assertEquals(val if is_present else None, ret_val)
+ self.assertEquals(subreport if is_present else None, ret_subreport)
+
+ def testCheckPresentIff(self):
+ """Tests _CheckPresentIff()."""
+ self.assertIsNone(checker.PayloadChecker._CheckPresentIff(
+ None, None, 'foo', 'bar', 'baz'))
+ self.assertIsNone(checker.PayloadChecker._CheckPresentIff(
+ 'a', 'b', 'foo', 'bar', 'baz'))
+ self.assertRaises(PayloadError, checker.PayloadChecker._CheckPresentIff,
+ 'a', None, 'foo', 'bar', 'baz')
+ self.assertRaises(PayloadError, checker.PayloadChecker._CheckPresentIff,
+ None, 'b', 'foo', 'bar', 'baz')
+
+ def DoCheckSha256SignatureTest(self, expect_pass, expect_subprocess_call,
+ sig_data, sig_asn1_header,
+ returned_signed_hash, expected_signed_hash):
+ """Parametric testing of _CheckSha256SignatureTest().
+
+ Args:
+ expect_pass: Whether or not it should pass.
+ expect_subprocess_call: Whether to expect the openssl call to happen.
+ sig_data: The signature raw data.
+ sig_asn1_header: The ASN1 header.
+ returned_signed_hash: The signed hash data retuned by openssl.
+ expected_signed_hash: The signed hash data to compare against.
+ """
+ try:
+ # Stub out the subprocess invocation.
+ self.mox.StubOutWithMock(checker.PayloadChecker, '_Run')
+ if expect_subprocess_call:
+ checker.PayloadChecker._Run(
+ mox.IsA(list), send_data=sig_data).AndReturn(
+ (sig_asn1_header + returned_signed_hash, None))
+
+ self.mox.ReplayAll()
+ if expect_pass:
+ self.assertIsNone(checker.PayloadChecker._CheckSha256Signature(
+ sig_data, 'foo', expected_signed_hash, 'bar'))
+ else:
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckSha256Signature,
+ sig_data, 'foo', expected_signed_hash, 'bar')
+ finally:
+ self.mox.UnsetStubs()
+
+ def testCheckSha256Signature_Pass(self):
+ """Tests _CheckSha256Signature(); pass case."""
+ sig_data = 'fake-signature'.ljust(256)
+ signed_hash = hashlib.sha256('fake-data').digest()
+ self.DoCheckSha256SignatureTest(True, True, sig_data,
+ common.SIG_ASN1_HEADER, signed_hash,
+ signed_hash)
+
+ def testCheckSha256Signature_FailBadSignature(self):
+ """Tests _CheckSha256Signature(); fails due to malformed signature."""
+ sig_data = 'fake-signature' # Malformed (not 256 bytes in length).
+ signed_hash = hashlib.sha256('fake-data').digest()
+ self.DoCheckSha256SignatureTest(False, False, sig_data,
+ common.SIG_ASN1_HEADER, signed_hash,
+ signed_hash)
+
+ def testCheckSha256Signature_FailBadOutputLength(self):
+ """Tests _CheckSha256Signature(); fails due to unexpected output length."""
+ sig_data = 'fake-signature'.ljust(256)
+ signed_hash = 'fake-hash' # Malformed (not 32 bytes in length).
+ self.DoCheckSha256SignatureTest(False, True, sig_data,
+ common.SIG_ASN1_HEADER, signed_hash,
+ signed_hash)
+
+ def testCheckSha256Signature_FailBadAsnHeader(self):
+ """Tests _CheckSha256Signature(); fails due to bad ASN1 header."""
+ sig_data = 'fake-signature'.ljust(256)
+ signed_hash = hashlib.sha256('fake-data').digest()
+ bad_asn1_header = 'bad-asn-header'.ljust(len(common.SIG_ASN1_HEADER))
+ self.DoCheckSha256SignatureTest(False, True, sig_data, bad_asn1_header,
+ signed_hash, signed_hash)
+
+ def testCheckSha256Signature_FailBadHash(self):
+ """Tests _CheckSha256Signature(); fails due to bad hash returned."""
+ sig_data = 'fake-signature'.ljust(256)
+ expected_signed_hash = hashlib.sha256('fake-data').digest()
+ returned_signed_hash = hashlib.sha256('bad-fake-data').digest()
+ self.DoCheckSha256SignatureTest(False, True, sig_data,
+ common.SIG_ASN1_HEADER,
+ expected_signed_hash, returned_signed_hash)
+
+ def testCheckBlocksFitLength_Pass(self):
+ """Tests _CheckBlocksFitLength(); pass case."""
+ self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength(
+ 64, 4, 16, 'foo'))
+ self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength(
+ 60, 4, 16, 'foo'))
+ self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength(
+ 49, 4, 16, 'foo'))
+ self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength(
+ 48, 3, 16, 'foo'))
+
+ def testCheckBlocksFitLength_TooManyBlocks(self):
+ """Tests _CheckBlocksFitLength(); fails due to excess blocks."""
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 64, 5, 16, 'foo')
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 60, 5, 16, 'foo')
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 49, 5, 16, 'foo')
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 48, 4, 16, 'foo')
+
+ def testCheckBlocksFitLength_TooFewBlocks(self):
+ """Tests _CheckBlocksFitLength(); fails due to insufficient blocks."""
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 64, 3, 16, 'foo')
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 60, 3, 16, 'foo')
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 49, 3, 16, 'foo')
+ self.assertRaises(PayloadError,
+ checker.PayloadChecker._CheckBlocksFitLength,
+ 48, 2, 16, 'foo')
+
+ def DoCheckManifestTest(self, fail_mismatched_block_size, fail_bad_sigs,
+ fail_mismatched_oki_ori, fail_bad_oki, fail_bad_ori,
+ fail_bad_nki, fail_bad_nri, fail_old_kernel_fs_size,
+ fail_old_rootfs_fs_size, fail_new_kernel_fs_size,
+ fail_new_rootfs_fs_size):
+ """Parametric testing of _CheckManifest().
+
+ Args:
+ fail_mismatched_block_size: Simulate a missing block_size field.
+ fail_bad_sigs: Make signatures descriptor inconsistent.
+ fail_mismatched_oki_ori: Make old rootfs/kernel info partially present.
+ fail_bad_oki: Tamper with old kernel info.
+ fail_bad_ori: Tamper with old rootfs info.
+ fail_bad_nki: Tamper with new kernel info.
+ fail_bad_nri: Tamper with new rootfs info.
+ fail_old_kernel_fs_size: Make old kernel fs size too big.
+ fail_old_rootfs_fs_size: Make old rootfs fs size too big.
+ fail_new_kernel_fs_size: Make new kernel fs size too big.
+ fail_new_rootfs_fs_size: Make new rootfs fs size too big.
+ """
+ # Generate a test payload. For this test, we only care about the manifest
+ # and don't need any data blobs, hence we can use a plain paylaod generator
+ # (which also gives us more control on things that can be screwed up).
+ payload_gen = test_utils.PayloadGenerator()
+
+ # Tamper with block size, if required.
+ if fail_mismatched_block_size:
+ payload_gen.SetBlockSize(test_utils.KiB(1))
+ else:
+ payload_gen.SetBlockSize(test_utils.KiB(4))
+
+ # Add some operations.
+ payload_gen.AddOperation(False, common.OpType.MOVE,
+ src_extents=[(0, 16), (16, 497)],
+ dst_extents=[(16, 496), (0, 16)])
+ payload_gen.AddOperation(True, common.OpType.MOVE,
+ src_extents=[(0, 8), (8, 8)],
+ dst_extents=[(8, 8), (0, 8)])
+
+ # Set an invalid signatures block (offset but no size), if required.
+ if fail_bad_sigs:
+ payload_gen.SetSignatures(32, None)
+
+ # Set partition / filesystem sizes.
+ rootfs_part_size = test_utils.MiB(8)
+ kernel_part_size = test_utils.KiB(512)
+ old_rootfs_fs_size = new_rootfs_fs_size = rootfs_part_size
+ old_kernel_fs_size = new_kernel_fs_size = kernel_part_size
+ if fail_old_kernel_fs_size:
+ old_kernel_fs_size += 100
+ if fail_old_rootfs_fs_size:
+ old_rootfs_fs_size += 100
+ if fail_new_kernel_fs_size:
+ new_kernel_fs_size += 100
+ if fail_new_rootfs_fs_size:
+ new_rootfs_fs_size += 100
+
+ # Add old kernel/rootfs partition info, as required.
+ if fail_mismatched_oki_ori or fail_old_kernel_fs_size or fail_bad_oki:
+ oki_hash = (None if fail_bad_oki
+ else hashlib.sha256('fake-oki-content').digest())
+ payload_gen.SetPartInfo(True, False, old_kernel_fs_size, oki_hash)
+ if not fail_mismatched_oki_ori and (fail_old_rootfs_fs_size or
+ fail_bad_ori):
+ ori_hash = (None if fail_bad_ori
+ else hashlib.sha256('fake-ori-content').digest())
+ payload_gen.SetPartInfo(False, False, old_rootfs_fs_size, ori_hash)
+
+ # Add new kernel/rootfs partition info.
+ payload_gen.SetPartInfo(
+ True, True, new_kernel_fs_size,
+ None if fail_bad_nki else hashlib.sha256('fake-nki-content').digest())
+ payload_gen.SetPartInfo(
+ False, True, new_rootfs_fs_size,
+ None if fail_bad_nri else hashlib.sha256('fake-nri-content').digest())
+
+ # Set the minor version.
+ payload_gen.SetMinorVersion(0)
+
+ # Create the test object.
+ payload_checker = _GetPayloadChecker(payload_gen.WriteToFile)
+ report = checker._PayloadReport()
+
+ should_fail = (fail_mismatched_block_size or fail_bad_sigs or
+ fail_mismatched_oki_ori or fail_bad_oki or fail_bad_ori or
+ fail_bad_nki or fail_bad_nri or fail_old_kernel_fs_size or
+ fail_old_rootfs_fs_size or fail_new_kernel_fs_size or
+ fail_new_rootfs_fs_size)
+ if should_fail:
+ self.assertRaises(PayloadError, payload_checker._CheckManifest, report,
+ rootfs_part_size, kernel_part_size)
+ else:
+ self.assertIsNone(payload_checker._CheckManifest(report,
+ rootfs_part_size,
+ kernel_part_size))
+
+ def testCheckLength(self):
+ """Tests _CheckLength()."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ block_size = payload_checker.block_size
+
+ # Passes.
+ self.assertIsNone(payload_checker._CheckLength(
+ int(3.5 * block_size), 4, 'foo', 'bar'))
+ # Fails, too few blocks.
+ self.assertRaises(PayloadError, payload_checker._CheckLength,
+ int(3.5 * block_size), 3, 'foo', 'bar')
+ # Fails, too many blocks.
+ self.assertRaises(PayloadError, payload_checker._CheckLength,
+ int(3.5 * block_size), 5, 'foo', 'bar')
+
+ def testCheckExtents(self):
+ """Tests _CheckExtents()."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ block_size = payload_checker.block_size
+
+ # Passes w/ all real extents.
+ extents = self.NewExtentList((0, 4), (8, 3), (1024, 16))
+ self.assertEquals(
+ 23,
+ payload_checker._CheckExtents(extents, (1024 + 16) * block_size,
+ collections.defaultdict(int), 'foo'))
+
+ # Passes w/ pseudo-extents (aka sparse holes).
+ extents = self.NewExtentList((0, 4), (common.PSEUDO_EXTENT_MARKER, 5),
+ (8, 3))
+ self.assertEquals(
+ 12,
+ payload_checker._CheckExtents(extents, (1024 + 16) * block_size,
+ collections.defaultdict(int), 'foo',
+ allow_pseudo=True))
+
+ # Passes w/ pseudo-extent due to a signature.
+ extents = self.NewExtentList((common.PSEUDO_EXTENT_MARKER, 2))
+ self.assertEquals(
+ 2,
+ payload_checker._CheckExtents(extents, (1024 + 16) * block_size,
+ collections.defaultdict(int), 'foo',
+ allow_signature=True))
+
+ # Fails, extent missing a start block.
+ extents = self.NewExtentList((-1, 4), (8, 3), (1024, 16))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckExtents, extents,
+ (1024 + 16) * block_size, collections.defaultdict(int), 'foo')
+
+ # Fails, extent missing block count.
+ extents = self.NewExtentList((0, -1), (8, 3), (1024, 16))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckExtents, extents,
+ (1024 + 16) * block_size, collections.defaultdict(int), 'foo')
+
+ # Fails, extent has zero blocks.
+ extents = self.NewExtentList((0, 4), (8, 3), (1024, 0))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckExtents, extents,
+ (1024 + 16) * block_size, collections.defaultdict(int), 'foo')
+
+ # Fails, extent exceeds partition boundaries.
+ extents = self.NewExtentList((0, 4), (8, 3), (1024, 16))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckExtents, extents,
+ (1024 + 15) * block_size, collections.defaultdict(int), 'foo')
+
+ def testCheckReplaceOperation(self):
+ """Tests _CheckReplaceOperation() where op.type == REPLACE."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ block_size = payload_checker.block_size
+ data_length = 10000
+
+ op = self.mox.CreateMock(
+ update_metadata_pb2.InstallOperation)
+ op.type = common.OpType.REPLACE
+
+ # Pass.
+ op.src_extents = []
+ self.assertIsNone(
+ payload_checker._CheckReplaceOperation(
+ op, data_length, (data_length + block_size - 1) / block_size,
+ 'foo'))
+
+ # Fail, src extents founds.
+ op.src_extents = ['bar']
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, data_length, (data_length + block_size - 1) / block_size, 'foo')
+
+ # Fail, missing data.
+ op.src_extents = []
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, None, (data_length + block_size - 1) / block_size, 'foo')
+
+ # Fail, length / block number mismatch.
+ op.src_extents = ['bar']
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, data_length, (data_length + block_size - 1) / block_size + 1, 'foo')
+
+ def testCheckReplaceBzOperation(self):
+ """Tests _CheckReplaceOperation() where op.type == REPLACE_BZ."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ block_size = payload_checker.block_size
+ data_length = block_size * 3
+
+ op = self.mox.CreateMock(
+ update_metadata_pb2.InstallOperation)
+ op.type = common.OpType.REPLACE_BZ
+
+ # Pass.
+ op.src_extents = []
+ self.assertIsNone(
+ payload_checker._CheckReplaceOperation(
+ op, data_length, (data_length + block_size - 1) / block_size + 5,
+ 'foo'))
+
+ # Fail, src extents founds.
+ op.src_extents = ['bar']
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, data_length, (data_length + block_size - 1) / block_size + 5, 'foo')
+
+ # Fail, missing data.
+ op.src_extents = []
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, None, (data_length + block_size - 1) / block_size, 'foo')
+
+ # Fail, too few blocks to justify BZ.
+ op.src_extents = []
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, data_length, (data_length + block_size - 1) / block_size, 'foo')
+
+ def testCheckReplaceXzOperation(self):
+ """Tests _CheckReplaceOperation() where op.type == REPLACE_XZ."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ block_size = payload_checker.block_size
+ data_length = block_size * 3
+
+ op = self.mox.CreateMock(
+ update_metadata_pb2.InstallOperation)
+ op.type = common.OpType.REPLACE_XZ
+
+ # Pass.
+ op.src_extents = []
+ self.assertIsNone(
+ payload_checker._CheckReplaceOperation(
+ op, data_length, (data_length + block_size - 1) / block_size + 5,
+ 'foo'))
+
+ # Fail, src extents founds.
+ op.src_extents = ['bar']
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, data_length, (data_length + block_size - 1) / block_size + 5, 'foo')
+
+ # Fail, missing data.
+ op.src_extents = []
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, None, (data_length + block_size - 1) / block_size, 'foo')
+
+ # Fail, too few blocks to justify XZ.
+ op.src_extents = []
+ self.assertRaises(
+ PayloadError, payload_checker._CheckReplaceOperation,
+ op, data_length, (data_length + block_size - 1) / block_size, 'foo')
+
+ def testCheckMoveOperation_Pass(self):
+ """Tests _CheckMoveOperation(); pass case."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((16, 128), (512, 6)))
+ self.assertIsNone(
+ payload_checker._CheckMoveOperation(op, None, 134, 134, 'foo'))
+
+ def testCheckMoveOperation_FailContainsData(self):
+ """Tests _CheckMoveOperation(); fails, message contains data."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((16, 128), (512, 6)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, 1024, 134, 134, 'foo')
+
+ def testCheckMoveOperation_FailInsufficientSrcBlocks(self):
+ """Tests _CheckMoveOperation(); fails, not enough actual src blocks."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 127)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((16, 128), (512, 6)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+
+ def testCheckMoveOperation_FailInsufficientDstBlocks(self):
+ """Tests _CheckMoveOperation(); fails, not enough actual dst blocks."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((16, 128), (512, 5)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+
+ def testCheckMoveOperation_FailExcessSrcBlocks(self):
+ """Tests _CheckMoveOperation(); fails, too many actual src blocks."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((16, 128), (512, 5)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 129)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((16, 128), (512, 6)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+
+ def testCheckMoveOperation_FailExcessDstBlocks(self):
+ """Tests _CheckMoveOperation(); fails, too many actual dst blocks."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((16, 128), (512, 7)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+
+ def testCheckMoveOperation_FailStagnantBlocks(self):
+ """Tests _CheckMoveOperation(); fails, there are blocks that do not move."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((8, 128), (512, 6)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+
+ def testCheckMoveOperation_FailZeroStartBlock(self):
+ """Tests _CheckMoveOperation(); fails, has extent with start block 0."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+ op.type = common.OpType.MOVE
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((0, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((8, 128), (512, 6)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 4), (12, 2), (1024, 128)))
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((0, 128), (512, 6)))
+ self.assertRaises(
+ PayloadError, payload_checker._CheckMoveOperation,
+ op, None, 134, 134, 'foo')
+
+ def testCheckAnyDiff(self):
+ """Tests _CheckAnyDiffOperation()."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ op = update_metadata_pb2.InstallOperation()
+
+ # Pass.
+ self.assertIsNone(
+ payload_checker._CheckAnyDiffOperation(op, 10000, 3, 'foo'))
+
+ # Fail, missing data blob.
+ self.assertRaises(
+ PayloadError, payload_checker._CheckAnyDiffOperation,
+ op, None, 3, 'foo')
+
+ # Fail, too big of a diff blob (unjustified).
+ self.assertRaises(
+ PayloadError, payload_checker._CheckAnyDiffOperation,
+ op, 10000, 2, 'foo')
+
+ def testCheckSourceCopyOperation_Pass(self):
+ """Tests _CheckSourceCopyOperation(); pass case."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ self.assertIsNone(
+ payload_checker._CheckSourceCopyOperation(None, 134, 134, 'foo'))
+
+ def testCheckSourceCopyOperation_FailContainsData(self):
+ """Tests _CheckSourceCopyOperation(); message contains data."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ self.assertRaises(PayloadError, payload_checker._CheckSourceCopyOperation,
+ 134, 0, 0, 'foo')
+
+ def testCheckSourceCopyOperation_FailBlockCountsMismatch(self):
+ """Tests _CheckSourceCopyOperation(); src and dst block totals not equal."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ self.assertRaises(PayloadError, payload_checker._CheckSourceCopyOperation,
+ None, 0, 1, 'foo')
+
+ def DoCheckOperationTest(self, op_type_name, is_last, allow_signature,
+ allow_unhashed, fail_src_extents, fail_dst_extents,
+ fail_mismatched_data_offset_length,
+ fail_missing_dst_extents, fail_src_length,
+ fail_dst_length, fail_data_hash,
+ fail_prev_data_offset, fail_bad_minor_version):
+ """Parametric testing of _CheckOperation().
+
+ Args:
+ op_type_name: 'REPLACE', 'REPLACE_BZ', 'REPLACE_XZ', 'MOVE', 'BSDIFF',
+ 'SOURCE_COPY', 'SOURCE_BSDIFF', BROTLI_BSDIFF or 'PUFFDIFF'.
+ is_last: Whether we're testing the last operation in a sequence.
+ allow_signature: Whether we're testing a signature-capable operation.
+ allow_unhashed: Whether we're allowing to not hash the data.
+ fail_src_extents: Tamper with src extents.
+ fail_dst_extents: Tamper with dst extents.
+ fail_mismatched_data_offset_length: Make data_{offset,length}
+ inconsistent.
+ fail_missing_dst_extents: Do not include dst extents.
+ fail_src_length: Make src length inconsistent.
+ fail_dst_length: Make dst length inconsistent.
+ fail_data_hash: Tamper with the data blob hash.
+ fail_prev_data_offset: Make data space uses incontiguous.
+ fail_bad_minor_version: Make minor version incompatible with op.
+ """
+ op_type = _OpTypeByName(op_type_name)
+
+ # Create the test object.
+ payload = self.MockPayload()
+ payload_checker = checker.PayloadChecker(payload,
+ allow_unhashed=allow_unhashed)
+ block_size = payload_checker.block_size
+
+ # Create auxiliary arguments.
+ old_part_size = test_utils.MiB(4)
+ new_part_size = test_utils.MiB(8)
+ old_block_counters = array.array(
+ 'B', [0] * ((old_part_size + block_size - 1) / block_size))
+ new_block_counters = array.array(
+ 'B', [0] * ((new_part_size + block_size - 1) / block_size))
+ prev_data_offset = 1876
+ blob_hash_counts = collections.defaultdict(int)
+
+ # Create the operation object for the test.
+ op = update_metadata_pb2.InstallOperation()
+ op.type = op_type
+
+ total_src_blocks = 0
+ if op_type in (common.OpType.MOVE, common.OpType.BSDIFF,
+ common.OpType.SOURCE_COPY, common.OpType.SOURCE_BSDIFF,
+ common.OpType.PUFFDIFF, common.OpType.BROTLI_BSDIFF):
+ if fail_src_extents:
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 0)))
+ else:
+ self.AddToMessage(op.src_extents,
+ self.NewExtentList((1, 16)))
+ total_src_blocks = 16
+
+ if op_type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
+ payload_checker.minor_version = 0
+ elif op_type in (common.OpType.MOVE, common.OpType.BSDIFF):
+ payload_checker.minor_version = 2 if fail_bad_minor_version else 1
+ elif op_type in (common.OpType.SOURCE_COPY, common.OpType.SOURCE_BSDIFF):
+ payload_checker.minor_version = 1 if fail_bad_minor_version else 2
+ if op_type == common.OpType.REPLACE_XZ:
+ payload_checker.minor_version = 2 if fail_bad_minor_version else 3
+ elif op_type in (common.OpType.ZERO, common.OpType.DISCARD,
+ common.OpType.BROTLI_BSDIFF):
+ payload_checker.minor_version = 3 if fail_bad_minor_version else 4
+ elif op_type == common.OpType.PUFFDIFF:
+ payload_checker.minor_version = 4 if fail_bad_minor_version else 5
+
+ if op_type not in (common.OpType.MOVE, common.OpType.SOURCE_COPY):
+ if not fail_mismatched_data_offset_length:
+ op.data_length = 16 * block_size - 8
+ if fail_prev_data_offset:
+ op.data_offset = prev_data_offset + 16
+ else:
+ op.data_offset = prev_data_offset
+
+ fake_data = 'fake-data'.ljust(op.data_length)
+ if not (allow_unhashed or (is_last and allow_signature and
+ op_type == common.OpType.REPLACE)):
+ if not fail_data_hash:
+ # Create a valid data blob hash.
+ op.data_sha256_hash = hashlib.sha256(fake_data).digest()
+ payload.ReadDataBlob(op.data_offset, op.data_length).AndReturn(
+ fake_data)
+
+ elif fail_data_hash:
+ # Create an invalid data blob hash.
+ op.data_sha256_hash = hashlib.sha256(
+ fake_data.replace(' ', '-')).digest()
+ payload.ReadDataBlob(op.data_offset, op.data_length).AndReturn(
+ fake_data)
+
+ total_dst_blocks = 0
+ if not fail_missing_dst_extents:
+ total_dst_blocks = 16
+ if fail_dst_extents:
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((4, 16), (32, 0)))
+ else:
+ self.AddToMessage(op.dst_extents,
+ self.NewExtentList((4, 8), (64, 8)))
+
+ if total_src_blocks:
+ if fail_src_length:
+ op.src_length = total_src_blocks * block_size + 8
+ elif (op_type in (common.OpType.MOVE, common.OpType.BSDIFF,
+ common.OpType.SOURCE_BSDIFF) and
+ payload_checker.minor_version <= 3):
+ op.src_length = total_src_blocks * block_size
+ elif fail_src_length:
+ # Add an orphaned src_length.
+ op.src_length = 16
+
+ if total_dst_blocks:
+ if fail_dst_length:
+ op.dst_length = total_dst_blocks * block_size + 8
+ elif (op_type in (common.OpType.MOVE, common.OpType.BSDIFF,
+ common.OpType.SOURCE_BSDIFF) and
+ payload_checker.minor_version <= 3):
+ op.dst_length = total_dst_blocks * block_size
+
+ self.mox.ReplayAll()
+ should_fail = (fail_src_extents or fail_dst_extents or
+ fail_mismatched_data_offset_length or
+ fail_missing_dst_extents or fail_src_length or
+ fail_dst_length or fail_data_hash or fail_prev_data_offset or
+ fail_bad_minor_version)
+ args = (op, 'foo', is_last, old_block_counters, new_block_counters,
+ old_part_size, new_part_size, prev_data_offset, allow_signature,
+ blob_hash_counts)
+ if should_fail:
+ self.assertRaises(PayloadError, payload_checker._CheckOperation, *args)
+ else:
+ self.assertEqual(op.data_length if op.HasField('data_length') else 0,
+ payload_checker._CheckOperation(*args))
+
+ def testAllocBlockCounters(self):
+ """Tests _CheckMoveOperation()."""
+ payload_checker = checker.PayloadChecker(self.MockPayload())
+ block_size = payload_checker.block_size
+
+ # Check allocation for block-aligned partition size, ensure it's integers.
+ result = payload_checker._AllocBlockCounters(16 * block_size)
+ self.assertEqual(16, len(result))
+ self.assertEqual(int, type(result[0]))
+
+ # Check allocation of unaligned partition sizes.
+ result = payload_checker._AllocBlockCounters(16 * block_size - 1)
+ self.assertEqual(16, len(result))
+ result = payload_checker._AllocBlockCounters(16 * block_size + 1)
+ self.assertEqual(17, len(result))
+
+ def DoCheckOperationsTest(self, fail_nonexhaustive_full_update):
+ """Tests _CheckOperations()."""
+ # Generate a test payload. For this test, we only care about one
+ # (arbitrary) set of operations, so we'll only be generating kernel and
+ # test with them.
+ payload_gen = test_utils.PayloadGenerator()
+
+ block_size = test_utils.KiB(4)
+ payload_gen.SetBlockSize(block_size)
+
+ rootfs_part_size = test_utils.MiB(8)
+
+ # Fake rootfs operations in a full update, tampered with as required.
+ rootfs_op_type = common.OpType.REPLACE
+ rootfs_data_length = rootfs_part_size
+ if fail_nonexhaustive_full_update:
+ rootfs_data_length -= block_size
+
+ payload_gen.AddOperation(False, rootfs_op_type,
+ dst_extents=[(0, rootfs_data_length / block_size)],
+ data_offset=0,
+ data_length=rootfs_data_length)
+
+ # Create the test object.
+ payload_checker = _GetPayloadChecker(payload_gen.WriteToFile,
+ checker_init_dargs={
+ 'allow_unhashed': True})
+ payload_checker.payload_type = checker._TYPE_FULL
+ report = checker._PayloadReport()
+
+ args = (payload_checker.payload.manifest.install_operations, report, 'foo',
+ 0, rootfs_part_size, rootfs_part_size, rootfs_part_size, 0, False)
+ if fail_nonexhaustive_full_update:
+ self.assertRaises(PayloadError, payload_checker._CheckOperations, *args)
+ else:
+ self.assertEqual(rootfs_data_length,
+ payload_checker._CheckOperations(*args))
+
+ def DoCheckSignaturesTest(self, fail_empty_sigs_blob, fail_missing_pseudo_op,
+ fail_mismatched_pseudo_op, fail_sig_missing_fields,
+ fail_unknown_sig_version, fail_incorrect_sig):
+ """Tests _CheckSignatures()."""
+ # Generate a test payload. For this test, we only care about the signature
+ # block and how it relates to the payload hash. Therefore, we're generating
+ # a random (otherwise useless) payload for this purpose.
+ payload_gen = test_utils.EnhancedPayloadGenerator()
+ block_size = test_utils.KiB(4)
+ payload_gen.SetBlockSize(block_size)
+ rootfs_part_size = test_utils.MiB(2)
+ kernel_part_size = test_utils.KiB(16)
+ payload_gen.SetPartInfo(False, True, rootfs_part_size,
+ hashlib.sha256('fake-new-rootfs-content').digest())
+ payload_gen.SetPartInfo(True, True, kernel_part_size,
+ hashlib.sha256('fake-new-kernel-content').digest())
+ payload_gen.SetMinorVersion(0)
+ payload_gen.AddOperationWithData(
+ False, common.OpType.REPLACE,
+ dst_extents=[(0, rootfs_part_size / block_size)],
+ data_blob=os.urandom(rootfs_part_size))
+
+ do_forge_pseudo_op = (fail_missing_pseudo_op or fail_mismatched_pseudo_op)
+ do_forge_sigs_data = (do_forge_pseudo_op or fail_empty_sigs_blob or
+ fail_sig_missing_fields or fail_unknown_sig_version
+ or fail_incorrect_sig)
+
+ sigs_data = None
+ if do_forge_sigs_data:
+ sigs_gen = test_utils.SignaturesGenerator()
+ if not fail_empty_sigs_blob:
+ if fail_sig_missing_fields:
+ sig_data = None
+ else:
+ sig_data = test_utils.SignSha256('fake-payload-content',
+ test_utils._PRIVKEY_FILE_NAME)
+ sigs_gen.AddSig(5 if fail_unknown_sig_version else 1, sig_data)
+
+ sigs_data = sigs_gen.ToBinary()
+ payload_gen.SetSignatures(payload_gen.curr_offset, len(sigs_data))
+
+ if do_forge_pseudo_op:
+ assert sigs_data is not None, 'should have forged signatures blob by now'
+ sigs_len = len(sigs_data)
+ payload_gen.AddOperation(
+ False, common.OpType.REPLACE,
+ data_offset=payload_gen.curr_offset / 2,
+ data_length=sigs_len / 2,
+ dst_extents=[(0, (sigs_len / 2 + block_size - 1) / block_size)])
+
+ # Generate payload (complete w/ signature) and create the test object.
+ payload_checker = _GetPayloadChecker(
+ payload_gen.WriteToFileWithData,
+ payload_gen_dargs={
+ 'sigs_data': sigs_data,
+ 'privkey_file_name': test_utils._PRIVKEY_FILE_NAME,
+ 'do_add_pseudo_operation': not do_forge_pseudo_op})
+ payload_checker.payload_type = checker._TYPE_FULL
+ report = checker._PayloadReport()
+
+ # We have to check the manifest first in order to set signature attributes.
+ payload_checker._CheckManifest(report, rootfs_part_size, kernel_part_size)
+
+ should_fail = (fail_empty_sigs_blob or fail_missing_pseudo_op or
+ fail_mismatched_pseudo_op or fail_sig_missing_fields or
+ fail_unknown_sig_version or fail_incorrect_sig)
+ args = (report, test_utils._PUBKEY_FILE_NAME)
+ if should_fail:
+ self.assertRaises(PayloadError, payload_checker._CheckSignatures, *args)
+ else:
+ self.assertIsNone(payload_checker._CheckSignatures(*args))
+
+ def DoCheckManifestMinorVersionTest(self, minor_version, payload_type):
+ """Parametric testing for CheckManifestMinorVersion().
+
+ Args:
+ minor_version: The payload minor version to test with.
+ payload_type: The type of the payload we're testing, delta or full.
+ """
+ # Create the test object.
+ payload = self.MockPayload()
+ payload.manifest.minor_version = minor_version
+ payload_checker = checker.PayloadChecker(payload)
+ payload_checker.payload_type = payload_type
+ report = checker._PayloadReport()
+
+ should_succeed = (
+ (minor_version == 0 and payload_type == checker._TYPE_FULL) or
+ (minor_version == 1 and payload_type == checker._TYPE_DELTA) or
+ (minor_version == 2 and payload_type == checker._TYPE_DELTA) or
+ (minor_version == 3 and payload_type == checker._TYPE_DELTA) or
+ (minor_version == 4 and payload_type == checker._TYPE_DELTA) or
+ (minor_version == 5 and payload_type == checker._TYPE_DELTA))
+ args = (report,)
+
+ if should_succeed:
+ self.assertIsNone(payload_checker._CheckManifestMinorVersion(*args))
+ else:
+ self.assertRaises(PayloadError,
+ payload_checker._CheckManifestMinorVersion, *args)
+
+ def DoRunTest(self, rootfs_part_size_provided, kernel_part_size_provided,
+ fail_wrong_payload_type, fail_invalid_block_size,
+ fail_mismatched_block_size, fail_excess_data,
+ fail_rootfs_part_size_exceeded,
+ fail_kernel_part_size_exceeded):
+ """Tests Run()."""
+ # Generate a test payload. For this test, we generate a full update that
+ # has sample kernel and rootfs operations. Since most testing is done with
+ # internal PayloadChecker methods that are tested elsewhere, here we only
+ # tamper with what's actually being manipulated and/or tested in the Run()
+ # method itself. Note that the checker doesn't verify partition hashes, so
+ # they're safe to fake.
+ payload_gen = test_utils.EnhancedPayloadGenerator()
+ block_size = test_utils.KiB(4)
+ payload_gen.SetBlockSize(block_size)
+ kernel_filesystem_size = test_utils.KiB(16)
+ rootfs_filesystem_size = test_utils.MiB(2)
+ payload_gen.SetPartInfo(False, True, rootfs_filesystem_size,
+ hashlib.sha256('fake-new-rootfs-content').digest())
+ payload_gen.SetPartInfo(True, True, kernel_filesystem_size,
+ hashlib.sha256('fake-new-kernel-content').digest())
+ payload_gen.SetMinorVersion(0)
+
+ rootfs_part_size = 0
+ if rootfs_part_size_provided:
+ rootfs_part_size = rootfs_filesystem_size + block_size
+ rootfs_op_size = rootfs_part_size or rootfs_filesystem_size
+ if fail_rootfs_part_size_exceeded:
+ rootfs_op_size += block_size
+ payload_gen.AddOperationWithData(
+ False, common.OpType.REPLACE,
+ dst_extents=[(0, rootfs_op_size / block_size)],
+ data_blob=os.urandom(rootfs_op_size))
+
+ kernel_part_size = 0
+ if kernel_part_size_provided:
+ kernel_part_size = kernel_filesystem_size + block_size
+ kernel_op_size = kernel_part_size or kernel_filesystem_size
+ if fail_kernel_part_size_exceeded:
+ kernel_op_size += block_size
+ payload_gen.AddOperationWithData(
+ True, common.OpType.REPLACE,
+ dst_extents=[(0, kernel_op_size / block_size)],
+ data_blob=os.urandom(kernel_op_size))
+
+ # Generate payload (complete w/ signature) and create the test object.
+ if fail_invalid_block_size:
+ use_block_size = block_size + 5 # Not a power of two.
+ elif fail_mismatched_block_size:
+ use_block_size = block_size * 2 # Different that payload stated.
+ else:
+ use_block_size = block_size
+
+ kwargs = {
+ 'payload_gen_dargs': {
+ 'privkey_file_name': test_utils._PRIVKEY_FILE_NAME,
+ 'do_add_pseudo_operation': True,
+ 'is_pseudo_in_kernel': True,
+ 'padding': os.urandom(1024) if fail_excess_data else None},
+ 'checker_init_dargs': {
+ 'assert_type': 'delta' if fail_wrong_payload_type else 'full',
+ 'block_size': use_block_size}}
+ if fail_invalid_block_size:
+ self.assertRaises(PayloadError, _GetPayloadChecker,
+ payload_gen.WriteToFileWithData, **kwargs)
+ else:
+ payload_checker = _GetPayloadChecker(payload_gen.WriteToFileWithData,
+ **kwargs)
+
+ kwargs = {'pubkey_file_name': test_utils._PUBKEY_FILE_NAME,
+ 'rootfs_part_size': rootfs_part_size,
+ 'kernel_part_size': kernel_part_size}
+ should_fail = (fail_wrong_payload_type or fail_mismatched_block_size or
+ fail_excess_data or
+ fail_rootfs_part_size_exceeded or
+ fail_kernel_part_size_exceeded)
+ if should_fail:
+ self.assertRaises(PayloadError, payload_checker.Run, **kwargs)
+ else:
+ self.assertIsNone(payload_checker.Run(**kwargs))
+
+# This implements a generic API, hence the occasional unused args.
+# pylint: disable=W0613
+def ValidateCheckOperationTest(op_type_name, is_last, allow_signature,
+ allow_unhashed, fail_src_extents,
+ fail_dst_extents,
+ fail_mismatched_data_offset_length,
+ fail_missing_dst_extents, fail_src_length,
+ fail_dst_length, fail_data_hash,
+ fail_prev_data_offset, fail_bad_minor_version):
+ """Returns True iff the combination of arguments represents a valid test."""
+ op_type = _OpTypeByName(op_type_name)
+
+ # REPLACE/REPLACE_BZ/REPLACE_XZ operations don't read data from src
+ # partition. They are compatible with all valid minor versions, so we don't
+ # need to check that.
+ if (op_type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ,
+ common.OpType.REPLACE_XZ) and (fail_src_extents or
+ fail_src_length or
+ fail_bad_minor_version)):
+ return False
+
+ # MOVE and SOURCE_COPY operations don't carry data.
+ if (op_type in (common.OpType.MOVE, common.OpType.SOURCE_COPY) and (
+ fail_mismatched_data_offset_length or fail_data_hash or
+ fail_prev_data_offset)):
+ return False
+
+ return True
+
+
+def TestMethodBody(run_method_name, run_dargs):
+ """Returns a function that invokes a named method with named arguments."""
+ return lambda self: getattr(self, run_method_name)(**run_dargs)
+
+
+def AddParametricTests(tested_method_name, arg_space, validate_func=None):
+ """Enumerates and adds specific parametric tests to PayloadCheckerTest.
+
+ This function enumerates a space of test parameters (defined by arg_space),
+ then binds a new, unique method name in PayloadCheckerTest to a test function
+ that gets handed the said parameters. This is a preferable approach to doing
+ the enumeration and invocation during the tests because this way each test is
+ treated as a complete run by the unittest framework, and so benefits from the
+ usual setUp/tearDown mechanics.
+
+ Args:
+ tested_method_name: Name of the tested PayloadChecker method.
+ arg_space: A dictionary containing variables (keys) and lists of values
+ (values) associated with them.
+ validate_func: A function used for validating test argument combinations.
+ """
+ for value_tuple in itertools.product(*arg_space.itervalues()):
+ run_dargs = dict(zip(arg_space.iterkeys(), value_tuple))
+ if validate_func and not validate_func(**run_dargs):
+ continue
+ run_method_name = 'Do%sTest' % tested_method_name
+ test_method_name = 'test%s' % tested_method_name
+ for arg_key, arg_val in run_dargs.iteritems():
+ if arg_val or type(arg_val) is int:
+ test_method_name += '__%s=%s' % (arg_key, arg_val)
+ setattr(PayloadCheckerTest, test_method_name,
+ TestMethodBody(run_method_name, run_dargs))
+
+
+def AddAllParametricTests():
+ """Enumerates and adds all parametric tests to PayloadCheckerTest."""
+ # Add all _CheckElem() test cases.
+ AddParametricTests('AddElem',
+ {'linebreak': (True, False),
+ 'indent': (0, 1, 2),
+ 'convert': (str, lambda s: s[::-1]),
+ 'is_present': (True, False),
+ 'is_mandatory': (True, False),
+ 'is_submsg': (True, False)})
+
+ # Add all _Add{Mandatory,Optional}Field tests.
+ AddParametricTests('AddField',
+ {'is_mandatory': (True, False),
+ 'linebreak': (True, False),
+ 'indent': (0, 1, 2),
+ 'convert': (str, lambda s: s[::-1]),
+ 'is_present': (True, False)})
+
+ # Add all _Add{Mandatory,Optional}SubMsg tests.
+ AddParametricTests('AddSubMsg',
+ {'is_mandatory': (True, False),
+ 'is_present': (True, False)})
+
+ # Add all _CheckManifest() test cases.
+ AddParametricTests('CheckManifest',
+ {'fail_mismatched_block_size': (True, False),
+ 'fail_bad_sigs': (True, False),
+ 'fail_mismatched_oki_ori': (True, False),
+ 'fail_bad_oki': (True, False),
+ 'fail_bad_ori': (True, False),
+ 'fail_bad_nki': (True, False),
+ 'fail_bad_nri': (True, False),
+ 'fail_old_kernel_fs_size': (True, False),
+ 'fail_old_rootfs_fs_size': (True, False),
+ 'fail_new_kernel_fs_size': (True, False),
+ 'fail_new_rootfs_fs_size': (True, False)})
+
+ # Add all _CheckOperation() test cases.
+ AddParametricTests('CheckOperation',
+ {'op_type_name': ('REPLACE', 'REPLACE_BZ', 'REPLACE_XZ',
+ 'MOVE', 'BSDIFF', 'SOURCE_COPY',
+ 'SOURCE_BSDIFF', 'PUFFDIFF',
+ 'BROTLI_BSDIFF'),
+ 'is_last': (True, False),
+ 'allow_signature': (True, False),
+ 'allow_unhashed': (True, False),
+ 'fail_src_extents': (True, False),
+ 'fail_dst_extents': (True, False),
+ 'fail_mismatched_data_offset_length': (True, False),
+ 'fail_missing_dst_extents': (True, False),
+ 'fail_src_length': (True, False),
+ 'fail_dst_length': (True, False),
+ 'fail_data_hash': (True, False),
+ 'fail_prev_data_offset': (True, False),
+ 'fail_bad_minor_version': (True, False)},
+ validate_func=ValidateCheckOperationTest)
+
+ # Add all _CheckOperations() test cases.
+ AddParametricTests('CheckOperations',
+ {'fail_nonexhaustive_full_update': (True, False)})
+
+ # Add all _CheckOperations() test cases.
+ AddParametricTests('CheckSignatures',
+ {'fail_empty_sigs_blob': (True, False),
+ 'fail_missing_pseudo_op': (True, False),
+ 'fail_mismatched_pseudo_op': (True, False),
+ 'fail_sig_missing_fields': (True, False),
+ 'fail_unknown_sig_version': (True, False),
+ 'fail_incorrect_sig': (True, False)})
+
+ # Add all _CheckManifestMinorVersion() test cases.
+ AddParametricTests('CheckManifestMinorVersion',
+ {'minor_version': (None, 0, 1, 2, 3, 4, 5, 555),
+ 'payload_type': (checker._TYPE_FULL,
+ checker._TYPE_DELTA)})
+
+ # Add all Run() test cases.
+ AddParametricTests('Run',
+ {'rootfs_part_size_provided': (True, False),
+ 'kernel_part_size_provided': (True, False),
+ 'fail_wrong_payload_type': (True, False),
+ 'fail_invalid_block_size': (True, False),
+ 'fail_mismatched_block_size': (True, False),
+ 'fail_excess_data': (True, False),
+ 'fail_rootfs_part_size_exceeded': (True, False),
+ 'fail_kernel_part_size_exceeded': (True, False)})
+
+
+if __name__ == '__main__':
+ AddAllParametricTests()
+ unittest.main()
diff --git a/update_payload/common.py b/update_payload/common.py
new file mode 100644
index 0000000..4e7b2e3
--- /dev/null
+++ b/update_payload/common.py
@@ -0,0 +1,218 @@
+#
+# 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.
+#
+
+"""Utilities for update payload processing."""
+
+from __future__ import print_function
+
+from update_payload import update_metadata_pb2
+from update_payload.error import PayloadError
+
+
+#
+# Constants.
+#
+PSEUDO_EXTENT_MARKER = (1L << 64) - 1 # UINT64_MAX
+
+SIG_ASN1_HEADER = (
+ '\x30\x31\x30\x0d\x06\x09\x60\x86'
+ '\x48\x01\x65\x03\x04\x02\x01\x05'
+ '\x00\x04\x20'
+)
+
+CHROMEOS_MAJOR_PAYLOAD_VERSION = 1
+BRILLO_MAJOR_PAYLOAD_VERSION = 2
+
+INPLACE_MINOR_PAYLOAD_VERSION = 1
+SOURCE_MINOR_PAYLOAD_VERSION = 2
+OPSRCHASH_MINOR_PAYLOAD_VERSION = 3
+BROTLI_BSDIFF_MINOR_PAYLOAD_VERSION = 4
+PUFFDIFF_MINOR_PAYLOAD_VERSION = 5
+
+#
+# Payload operation types.
+#
+class OpType(object):
+ """Container for operation type constants."""
+ _CLASS = update_metadata_pb2.InstallOperation
+ REPLACE = _CLASS.REPLACE
+ REPLACE_BZ = _CLASS.REPLACE_BZ
+ MOVE = _CLASS.MOVE
+ BSDIFF = _CLASS.BSDIFF
+ SOURCE_COPY = _CLASS.SOURCE_COPY
+ SOURCE_BSDIFF = _CLASS.SOURCE_BSDIFF
+ ZERO = _CLASS.ZERO
+ DISCARD = _CLASS.DISCARD
+ REPLACE_XZ = _CLASS.REPLACE_XZ
+ PUFFDIFF = _CLASS.PUFFDIFF
+ BROTLI_BSDIFF = _CLASS.BROTLI_BSDIFF
+ ALL = (REPLACE, REPLACE_BZ, MOVE, BSDIFF, SOURCE_COPY, SOURCE_BSDIFF, ZERO,
+ DISCARD, REPLACE_XZ, PUFFDIFF, BROTLI_BSDIFF)
+ NAMES = {
+ REPLACE: 'REPLACE',
+ REPLACE_BZ: 'REPLACE_BZ',
+ MOVE: 'MOVE',
+ BSDIFF: 'BSDIFF',
+ SOURCE_COPY: 'SOURCE_COPY',
+ SOURCE_BSDIFF: 'SOURCE_BSDIFF',
+ ZERO: 'ZERO',
+ DISCARD: 'DISCARD',
+ REPLACE_XZ: 'REPLACE_XZ',
+ PUFFDIFF: 'PUFFDIFF',
+ BROTLI_BSDIFF: 'BROTLI_BSDIFF',
+ }
+
+ def __init__(self):
+ pass
+
+
+#
+# Checked and hashed reading of data.
+#
+def IntPackingFmtStr(size, is_unsigned):
+ """Returns an integer format string for use by the struct module.
+
+ Args:
+ size: the integer size in bytes (2, 4 or 8)
+ is_unsigned: whether it is signed or not
+
+ Returns:
+ A format string for packing/unpacking integer values; assumes network byte
+ order (big-endian).
+
+ Raises:
+ PayloadError if something is wrong with the arguments.
+ """
+ # Determine the base conversion format.
+ if size == 2:
+ fmt = 'h'
+ elif size == 4:
+ fmt = 'i'
+ elif size == 8:
+ fmt = 'q'
+ else:
+ raise PayloadError('unsupport numeric field size (%s)' % size)
+
+ # Signed or unsigned?
+ if is_unsigned:
+ fmt = fmt.upper()
+
+ # Make it network byte order (big-endian).
+ fmt = '!' + fmt
+
+ return fmt
+
+
+def Read(file_obj, length, offset=None, hasher=None):
+ """Reads binary data from a file.
+
+ Args:
+ file_obj: an open file object
+ length: the length of the data to read
+ offset: an offset to seek to prior to reading; this is an absolute offset
+ from either the beginning (non-negative) or end (negative) of the
+ file. (optional)
+ hasher: a hashing object to pass the read data through (optional)
+
+ Returns:
+ A string containing the read data.
+
+ Raises:
+ PayloadError if a read error occurred or not enough data was read.
+ """
+ if offset is not None:
+ if offset >= 0:
+ file_obj.seek(offset)
+ else:
+ file_obj.seek(offset, 2)
+
+ try:
+ data = file_obj.read(length)
+ except IOError, e:
+ raise PayloadError('error reading from file (%s): %s' % (file_obj.name, e))
+
+ if len(data) != length:
+ raise PayloadError(
+ 'reading from file (%s) too short (%d instead of %d bytes)' %
+ (file_obj.name, len(data), length))
+
+ if hasher:
+ hasher.update(data)
+
+ return data
+
+
+#
+# Formatting functions.
+#
+def FormatExtent(ex, block_size=0):
+ end_block = ex.start_block + ex.num_blocks
+ if block_size:
+ return '%d->%d * %d' % (ex.start_block, end_block, block_size)
+ else:
+ return '%d->%d' % (ex.start_block, end_block)
+
+
+def FormatSha256(digest):
+ """Returns a canonical string representation of a SHA256 digest."""
+ return digest.encode('base64').strip()
+
+
+#
+# Useful iterators.
+#
+def _ObjNameIter(items, base_name, reverse=False, name_format_func=None):
+ """A generic (item, name) tuple iterators.
+
+ Args:
+ items: the sequence of objects to iterate on
+ base_name: the base name for all objects
+ reverse: whether iteration should be in reverse order
+ name_format_func: a function to apply to the name string
+
+ Yields:
+ An iterator whose i-th invocation returns (items[i], name), where name ==
+ base_name + '[i]' (with a formatting function optionally applied to it).
+ """
+ idx, inc = (len(items), -1) if reverse else (1, 1)
+ if reverse:
+ items = reversed(items)
+ for item in items:
+ item_name = '%s[%d]' % (base_name, idx)
+ if name_format_func:
+ item_name = name_format_func(item, item_name)
+ yield (item, item_name)
+ idx += inc
+
+
+def _OperationNameFormatter(op, op_name):
+ return '%s(%s)' % (op_name, OpType.NAMES.get(op.type, '?'))
+
+
+def OperationIter(operations, base_name, reverse=False):
+ """An (item, name) iterator for update operations."""
+ return _ObjNameIter(operations, base_name, reverse=reverse,
+ name_format_func=_OperationNameFormatter)
+
+
+def ExtentIter(extents, base_name, reverse=False):
+ """An (item, name) iterator for operation extents."""
+ return _ObjNameIter(extents, base_name, reverse=reverse)
+
+
+def SignatureIter(sigs, base_name, reverse=False):
+ """An (item, name) iterator for signatures."""
+ return _ObjNameIter(sigs, base_name, reverse=reverse)
diff --git a/update_payload/error.py b/update_payload/error.py
new file mode 100644
index 0000000..6f95433
--- /dev/null
+++ b/update_payload/error.py
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+"""Payload handling errors."""
+
+
+class PayloadError(Exception):
+ """An update payload general processing error."""
diff --git a/update_payload/format_utils.py b/update_payload/format_utils.py
new file mode 100644
index 0000000..6248ba9
--- /dev/null
+++ b/update_payload/format_utils.py
@@ -0,0 +1,109 @@
+#
+# 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.
+#
+
+"""Various formatting functions."""
+
+
+def NumToPercent(num, total, min_precision=1, max_precision=5):
+ """Returns the percentage (string) of |num| out of |total|.
+
+ If the percentage includes a fraction, it will be computed down to the least
+ precision that yields a non-zero and ranging between |min_precision| and
+ |max_precision|. Values are always rounded down. All arithmetic operations
+ are integer built-ins. Examples (using default precision):
+
+ (1, 1) => 100%
+ (3, 10) => 30%
+ (3, 9) => 33.3%
+ (3, 900) => 0.3%
+ (3, 9000000) => 0.00003%
+ (3, 900000000) => 0%
+ (5, 2) => 250%
+
+ Args:
+ num: the value of the part
+ total: the value of the whole
+ min_precision: minimum precision for fractional percentage
+ max_precision: maximum precision for fractional percentage
+ Returns:
+ Percentage string, or None if percent cannot be computed (i.e. total is
+ zero).
+
+ """
+ if total == 0:
+ return None
+
+ percent = 0
+ precision = min(min_precision, max_precision)
+ factor = 10 ** precision
+ while precision <= max_precision:
+ percent = num * 100 * factor / total
+ if percent:
+ break
+ factor *= 10
+ precision += 1
+
+ whole, frac = divmod(percent, factor)
+ while frac and not frac % 10:
+ frac /= 10
+ precision -= 1
+
+ return '%d%s%%' % (whole, '.%0*d' % (precision, frac) if frac else '')
+
+
+def BytesToHumanReadable(size, precision=1, decimal=False):
+ """Returns a human readable representation of a given |size|.
+
+ The returned string includes unit notations in either binary (KiB, MiB, etc)
+ or decimal (kB, MB, etc), based on the value of |decimal|. The chosen unit is
+ the largest that yields a whole (or mixed) number. It may contain up to
+ |precision| fractional digits. Values are always rounded down. Largest unit
+ is an exabyte. All arithmetic operations are integer built-ins. Examples
+ (using default precision and binary units):
+
+ 4096 => 4 KiB
+ 5000 => 4.8 KiB
+ 500000 => 488.2 KiB
+ 5000000 => 4.7 MiB
+
+ Args:
+ size: the size in bytes
+ precision: the number of digits past the decimal point
+ decimal: whether to compute/present decimal or binary units
+ Returns:
+ Readable size string, or None if no conversion is applicable (i.e. size is
+ less than the smallest unit).
+
+ """
+ constants = (
+ (('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'), 1024),
+ (('kB', 'MB', 'GB', 'TB', 'PB', 'EB'), 1000)
+ )
+ suffixes, base = constants[decimal]
+ exp, magnitude = 0, 1
+ while exp < len(suffixes):
+ next_magnitude = magnitude * base
+ if size < next_magnitude:
+ break
+ exp += 1
+ magnitude = next_magnitude
+
+ if exp != 0:
+ whole = size / magnitude
+ frac = (size % magnitude) * (10 ** precision) / magnitude
+ while frac and not frac % 10:
+ frac /= 10
+ return '%d%s %s' % (whole, '.%d' % frac if frac else '', suffixes[exp - 1])
diff --git a/update_payload/format_utils_unittest.py b/update_payload/format_utils_unittest.py
new file mode 100755
index 0000000..42ea621
--- /dev/null
+++ b/update_payload/format_utils_unittest.py
@@ -0,0 +1,89 @@
+#!/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.
+#
+
+"""Unit tests for format_utils.py."""
+
+import unittest
+
+from update_payload import format_utils
+
+
+class NumToPercentTest(unittest.TestCase):
+ """ Tests number conversion to percentage format."""
+ def testHundredPercent(self):
+ self.assertEqual(format_utils.NumToPercent(1, 1), '100%')
+
+ def testOverHundredPercent(self):
+ self.assertEqual(format_utils.NumToPercent(5, 2), '250%')
+
+ def testWholePercent(self):
+ self.assertEqual(format_utils.NumToPercent(3, 10), '30%')
+
+ def testDefaultMinPrecision(self):
+ self.assertEqual(format_utils.NumToPercent(3, 9), '33.3%')
+ self.assertEqual(format_utils.NumToPercent(3, 900), '0.3%')
+
+ def testDefaultMaxPrecision(self):
+ self.assertEqual(format_utils.NumToPercent(3, 9000000), '0.00003%')
+ self.assertEqual(format_utils.NumToPercent(3, 90000000), '0%')
+
+ def testCustomMinPrecision(self):
+ self.assertEqual(format_utils.NumToPercent(3, 9, min_precision=3),
+ '33.333%')
+ self.assertEqual(format_utils.NumToPercent(3, 9, min_precision=0),
+ '33%')
+
+ def testCustomMaxPrecision(self):
+ self.assertEqual(format_utils.NumToPercent(3, 900, max_precision=1),
+ '0.3%')
+ self.assertEqual(format_utils.NumToPercent(3, 9000, max_precision=1),
+ '0%')
+
+
+class BytesToHumanReadableTest(unittest.TestCase):
+ """ Tests number conversion to human readable format."""
+ def testBaseTwo(self):
+ self.assertEqual(format_utils.BytesToHumanReadable(0x1000), '4 KiB')
+ self.assertEqual(format_utils.BytesToHumanReadable(0x400000), '4 MiB')
+ self.assertEqual(format_utils.BytesToHumanReadable(0x100000000), '4 GiB')
+ self.assertEqual(format_utils.BytesToHumanReadable(0x40000000000), '4 TiB')
+
+ def testDecimal(self):
+ self.assertEqual(format_utils.BytesToHumanReadable(5000, decimal=True),
+ '5 kB')
+ self.assertEqual(format_utils.BytesToHumanReadable(5000000, decimal=True),
+ '5 MB')
+ self.assertEqual(format_utils.BytesToHumanReadable(5000000000,
+ decimal=True),
+ '5 GB')
+
+ def testDefaultPrecision(self):
+ self.assertEqual(format_utils.BytesToHumanReadable(5000), '4.8 KiB')
+ self.assertEqual(format_utils.BytesToHumanReadable(500000), '488.2 KiB')
+ self.assertEqual(format_utils.BytesToHumanReadable(5000000), '4.7 MiB')
+
+ def testCustomPrecision(self):
+ self.assertEqual(format_utils.BytesToHumanReadable(5000, precision=3),
+ '4.882 KiB')
+ self.assertEqual(format_utils.BytesToHumanReadable(500000, precision=0),
+ '488 KiB')
+ self.assertEqual(format_utils.BytesToHumanReadable(5000000, precision=5),
+ '4.76837 MiB')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/update_payload/histogram.py b/update_payload/histogram.py
new file mode 100644
index 0000000..1ac2ab5
--- /dev/null
+++ b/update_payload/histogram.py
@@ -0,0 +1,129 @@
+#
+# 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.
+#
+
+"""Histogram generation tools."""
+
+from collections import defaultdict
+
+from update_payload import format_utils
+
+
+class Histogram(object):
+ """A histogram generating object.
+
+ This object serves the sole purpose of formatting (key, val) pairs as an
+ ASCII histogram, including bars and percentage markers, and taking care of
+ label alignment, scaling, etc. In addition to the standard __init__
+ interface, two static methods are provided for conveniently converting data
+ in different formats into a histogram. Histogram generation is exported via
+ its __str__ method, and looks as follows:
+
+ Yes |################ | 5 (83.3%)
+ No |### | 1 (16.6%)
+
+ TODO(garnold) we may want to add actual methods for adding data or tweaking
+ the output layout and formatting. For now, though, this is fine.
+
+ """
+
+ def __init__(self, data, scale=20, formatter=None):
+ """Initialize a histogram object.
+
+ Args:
+ data: list of (key, count) pairs constituting the histogram
+ scale: number of characters used to indicate 100%
+ formatter: function used for formatting raw histogram values
+
+ """
+ self.data = data
+ self.scale = scale
+ self.formatter = formatter or str
+ self.max_key_len = max([len(str(key)) for key, count in self.data])
+ self.total = sum([count for key, count in self.data])
+
+ @staticmethod
+ def FromCountDict(count_dict, scale=20, formatter=None, key_names=None):
+ """Takes a dictionary of counts and returns a histogram object.
+
+ This simply converts a mapping from names to counts into a list of (key,
+ count) pairs, optionally translating keys into name strings, then
+ generating and returning a histogram for them. This is a useful convenience
+ call for clients that update a dictionary of counters as they (say) scan a
+ data stream.
+
+ Args:
+ count_dict: dictionary mapping keys to occurrence counts
+ scale: number of characters used to indicate 100%
+ formatter: function used for formatting raw histogram values
+ key_names: dictionary mapping keys to name strings
+ Returns:
+ A histogram object based on the given data.
+
+ """
+ namer = None
+ if key_names:
+ namer = lambda key: key_names[key]
+ else:
+ namer = lambda key: key
+
+ hist = [(namer(key), count) for key, count in count_dict.items()]
+ return Histogram(hist, scale, formatter)
+
+ @staticmethod
+ def FromKeyList(key_list, scale=20, formatter=None, key_names=None):
+ """Takes a list of (possibly recurring) keys and returns a histogram object.
+
+ This converts the list into a dictionary of counters, then uses
+ FromCountDict() to generate the actual histogram. For example:
+
+ ['a', 'a', 'b', 'a', 'b'] --> {'a': 3, 'b': 2} --> ...
+
+ Args:
+ key_list: list of (possibly recurring) keys
+ scale: number of characters used to indicate 100%
+ formatter: function used for formatting raw histogram values
+ key_names: dictionary mapping keys to name strings
+ Returns:
+ A histogram object based on the given data.
+
+ """
+ count_dict = defaultdict(int) # Unset items default to zero
+ for key in key_list:
+ count_dict[key] += 1
+ return Histogram.FromCountDict(count_dict, scale, formatter, key_names)
+
+ def __str__(self):
+ hist_lines = []
+ hist_bar = '|'
+ for key, count in self.data:
+ if self.total:
+ bar_len = count * self.scale / self.total
+ hist_bar = '|%s|' % ('#' * bar_len).ljust(self.scale)
+
+ line = '%s %s %s' % (
+ str(key).ljust(self.max_key_len),
+ hist_bar,
+ self.formatter(count))
+ percent_str = format_utils.NumToPercent(count, self.total)
+ if percent_str:
+ line += ' (%s)' % percent_str
+ hist_lines.append(line)
+
+ return '\n'.join(hist_lines)
+
+ def GetKeys(self):
+ """Returns the keys of the histogram."""
+ return [key for key, _ in self.data]
diff --git a/update_payload/histogram_unittest.py b/update_payload/histogram_unittest.py
new file mode 100755
index 0000000..e757dd0
--- /dev/null
+++ b/update_payload/histogram_unittest.py
@@ -0,0 +1,72 @@
+#!/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.
+#
+
+"""Unit tests for histogram.py."""
+
+import unittest
+
+from update_payload import format_utils
+from update_payload import histogram
+
+
+class HistogramTest(unittest.TestCase):
+ """ Tests histogram"""
+
+ @staticmethod
+ def AddHumanReadableSize(size):
+ fmt = format_utils.BytesToHumanReadable(size)
+ return '%s (%s)' % (size, fmt) if fmt else str(size)
+
+ def CompareToExpectedDefault(self, actual_str):
+ expected_str = (
+ 'Yes |################ | 5 (83.3%)\n'
+ 'No |### | 1 (16.6%)'
+ )
+ self.assertEqual(actual_str, expected_str)
+
+ def testExampleHistogram(self):
+ self.CompareToExpectedDefault(str(histogram.Histogram(
+ [('Yes', 5), ('No', 1)])))
+
+ def testFromCountDict(self):
+ self.CompareToExpectedDefault(str(histogram.Histogram.FromCountDict(
+ {'Yes': 5, 'No': 1})))
+
+ def testFromKeyList(self):
+ self.CompareToExpectedDefault(str(histogram.Histogram.FromKeyList(
+ ['Yes', 'Yes', 'No', 'Yes', 'Yes', 'Yes'])))
+
+ def testCustomScale(self):
+ expected_str = (
+ 'Yes |#### | 5 (83.3%)\n'
+ 'No | | 1 (16.6%)'
+ )
+ actual_str = str(histogram.Histogram([('Yes', 5), ('No', 1)], scale=5))
+ self.assertEqual(actual_str, expected_str)
+
+ def testCustomFormatter(self):
+ expected_str = (
+ 'Yes |################ | 5000 (4.8 KiB) (83.3%)\n'
+ 'No |### | 1000 (16.6%)'
+ )
+ actual_str = str(histogram.Histogram(
+ [('Yes', 5000), ('No', 1000)], formatter=self.AddHumanReadableSize))
+ self.assertEqual(actual_str, expected_str)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/update_payload/payload-test-key.pem b/update_payload/payload-test-key.pem
new file mode 100644
index 0000000..342e923
--- /dev/null
+++ b/update_payload/payload-test-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAvtGHtqO21Uhy2wGz9fluIpIUR8G7dZoCZhZukGkm4mlfgL71
+xPSArjx02/w/FhYxOusV6/XQeKgL3i8cni3HCkCOurZLpi2L5Ver6qrxKFh6WBVZ
+0Dj7N6P/Mf5jZdhfvVyweLlsNK8Ypeb+RazfrsXhd4cy3dBMxouGwH7R7QQXTFCo
+Cc8kgJBTxILl3jfvY8OrNKgYiCETa7tQdFkP0bfPwH9cAXuMjHXiZatim0tF+ivp
+kM2v/6LTxtD6Rq1wks/N6CHi8efrRaviFp7c0mNmBNFaV54cHEUW2SlNIiRun7L0
+1nAz/D8kuoHfx4E3Mtj0DbvngZJMX/X+rJQ5cQIDAQABAoIBADmE2X7hbJxwAUcp
+BUExFdTP6dMTf9lcOjrhqiRXvgPjtYkOhvD+rsdWq/cf2zhiKibTdEEzUMr+BM3N
+r7eyntvlR+DaUIVgF1pjigvryVPbD837aZ5NftRv194PC5FInttq1Dsf0ZEz8p8X
+uS/xg1+ggG1SUK/yOSJkLpNZ5xelbclQJ9bnJST8PR8XbEieA83xt5M2DcooPzq0
+/99m/daA5hmSWs6n8sFrIZDQxDhLyyW4J72jjoNTE87eCpwK855yXMelpEPDZNQi
+nB3x5Y/bGbl81PInqL2q14lekrVYdYZ7bOBVlsmyvz6f1e4OOE1aaAM+w6ArA4az
+6elZQE0CgYEA4GOU6BBu9jLqFdqV9jIkWsgz5ZWINz8PLJPtZzk5I9KO1m+GAUy2
+h/1IGGR6qRQR49hMtq4C0lUifxquq0xivzJ87U9oxKC9yEeTxkmDe5csVHsnAtqT
+xRgVM7Ysrut5NLU1zm0q3jBmkDu7d99LvscM/3n7eJ6RiYpnA54O6I8CgYEA2bNA
+34PTvxBS2deRoxKQNlVU14FtirE+q0+k0wcE85wr7wIMpR13al8T1TpE8J1yvvZM
+92HMGFGfYNDB46b8VfJ5AxEUFwdruec6sTVVfkMZMOqM/A08yiaLzQ1exDxNwaja
+fLuG5FAVRD/2g7fLBcsmosyNgcgNr1XA8Q/nvf8CgYEAwaSOg7py19rWcqehlMZu
+4z00tCNYWzz7LmA2l0clzYlPJTU3MvXt6+ujhRFpXXJpgfRPN7Nx0ewQihoPtNqF
+uTSr5OwLoOyK+0Tx/UPByS2L3xgscWUJ8yQ2X9sOMqIZhmf/mDZTsU2ZpU03GlrE
+dk43JF4zq0NEm6qp/dAwU3cCgYEAvECl+KKmmLIk8vvWlI2Y52Mi2rixYR2kc7+L
+aHDJd1+1HhlHlgDFItbU765Trz5322phZArN0rnCeJYNFC9yRWBIBL7gAIoKPdgW
+iOb15xlez04EXHGV/7kVa1wEdu0u0CiTxwjivMwDl+E36u8kQP5LirwYIgI800H0
+doCqhUECgYEAjvA38OS7hy56Q4LQtmHFBuRIn4E5SrIGMwNIH6TGbEKQix3ajTCQ
+0fSoLDGTkU6dH+T4v0WheveN2a2Kofqm0UQx5V2rfnY/Ut1fAAWgL/lsHLDnzPUZ
+bvTOANl8TbT49xAfNXTaGWe7F7nYz+bK0UDif1tJNDLQw7USD5I8lbQ=
+-----END RSA PRIVATE KEY-----
diff --git a/update_payload/payload-test-key.pub b/update_payload/payload-test-key.pub
new file mode 100644
index 0000000..fdae963
--- /dev/null
+++ b/update_payload/payload-test-key.pub
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtGHtqO21Uhy2wGz9flu
+IpIUR8G7dZoCZhZukGkm4mlfgL71xPSArjx02/w/FhYxOusV6/XQeKgL3i8cni3H
+CkCOurZLpi2L5Ver6qrxKFh6WBVZ0Dj7N6P/Mf5jZdhfvVyweLlsNK8Ypeb+Razf
+rsXhd4cy3dBMxouGwH7R7QQXTFCoCc8kgJBTxILl3jfvY8OrNKgYiCETa7tQdFkP
+0bfPwH9cAXuMjHXiZatim0tF+ivpkM2v/6LTxtD6Rq1wks/N6CHi8efrRaviFp7c
+0mNmBNFaV54cHEUW2SlNIiRun7L01nAz/D8kuoHfx4E3Mtj0DbvngZJMX/X+rJQ5
+cQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/update_payload/payload.py b/update_payload/payload.py
new file mode 100644
index 0000000..380d6d0
--- /dev/null
+++ b/update_payload/payload.py
@@ -0,0 +1,336 @@
+#
+# 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.
+#
+
+"""Tools for reading, verifying and applying Chrome OS update payloads."""
+
+from __future__ import print_function
+
+import hashlib
+import struct
+
+from update_payload import applier
+from update_payload import checker
+from update_payload import common
+from update_payload import update_metadata_pb2
+from update_payload.error import PayloadError
+
+
+#
+# Helper functions.
+#
+def _ReadInt(file_obj, size, is_unsigned, hasher=None):
+ """Reads a binary-encoded integer from a file.
+
+ It will do the correct conversion based on the reported size and whether or
+ not a signed number is expected. Assumes a network (big-endian) byte
+ ordering.
+
+ Args:
+ file_obj: a file object
+ size: the integer size in bytes (2, 4 or 8)
+ is_unsigned: whether it is signed or not
+ hasher: an optional hasher to pass the value through
+
+ Returns:
+ An "unpacked" (Python) integer value.
+
+ Raises:
+ PayloadError if an read error occurred.
+ """
+ return struct.unpack(common.IntPackingFmtStr(size, is_unsigned),
+ common.Read(file_obj, size, hasher=hasher))[0]
+
+
+#
+# Update payload.
+#
+class Payload(object):
+ """Chrome OS update payload processor."""
+
+ class _PayloadHeader(object):
+ """Update payload header struct."""
+
+ # Header constants; sizes are in bytes.
+ _MAGIC = 'CrAU'
+ _VERSION_SIZE = 8
+ _MANIFEST_LEN_SIZE = 8
+ _METADATA_SIGNATURE_LEN_SIZE = 4
+
+ def __init__(self):
+ self.version = None
+ self.manifest_len = None
+ self.metadata_signature_len = None
+ self.size = None
+
+ def ReadFromPayload(self, payload_file, hasher=None):
+ """Reads the payload header from a file.
+
+ Reads the payload header from the |payload_file| and updates the |hasher|
+ if one is passed. The parsed header is stored in the _PayloadHeader
+ instance attributes.
+
+ Args:
+ payload_file: a file object
+ hasher: an optional hasher to pass the value through
+
+ Returns:
+ None.
+
+ Raises:
+ PayloadError if a read error occurred or the header is invalid.
+ """
+ # Verify magic
+ magic = common.Read(payload_file, len(self._MAGIC), hasher=hasher)
+ if magic != self._MAGIC:
+ raise PayloadError('invalid payload magic: %s' % magic)
+
+ self.version = _ReadInt(payload_file, self._VERSION_SIZE, True,
+ hasher=hasher)
+ self.manifest_len = _ReadInt(payload_file, self._MANIFEST_LEN_SIZE, True,
+ hasher=hasher)
+ self.size = (len(self._MAGIC) + self._VERSION_SIZE +
+ self._MANIFEST_LEN_SIZE)
+ self.metadata_signature_len = 0
+
+ if self.version == common.BRILLO_MAJOR_PAYLOAD_VERSION:
+ self.size += self._METADATA_SIGNATURE_LEN_SIZE
+ self.metadata_signature_len = _ReadInt(
+ payload_file, self._METADATA_SIGNATURE_LEN_SIZE, True,
+ hasher=hasher)
+
+
+ def __init__(self, payload_file, payload_file_offset=0):
+ """Initialize the payload object.
+
+ Args:
+ payload_file: update payload file object open for reading
+ payload_file_offset: the offset of the actual payload
+ """
+ self.payload_file = payload_file
+ self.payload_file_offset = payload_file_offset
+ self.manifest_hasher = None
+ self.is_init = False
+ self.header = None
+ self.manifest = None
+ self.data_offset = None
+ self.metadata_signature = None
+ self.metadata_size = None
+
+ def _ReadHeader(self):
+ """Reads and returns the payload header.
+
+ Returns:
+ A payload header object.
+
+ Raises:
+ PayloadError if a read error occurred.
+ """
+ header = self._PayloadHeader()
+ header.ReadFromPayload(self.payload_file, self.manifest_hasher)
+ return header
+
+ def _ReadManifest(self):
+ """Reads and returns the payload manifest.
+
+ Returns:
+ A string containing the payload manifest in binary form.
+
+ Raises:
+ PayloadError if a read error occurred.
+ """
+ if not self.header:
+ raise PayloadError('payload header not present')
+
+ return common.Read(self.payload_file, self.header.manifest_len,
+ hasher=self.manifest_hasher)
+
+ def _ReadMetadataSignature(self):
+ """Reads and returns the metadata signatures.
+
+ Returns:
+ A string containing the metadata signatures protobuf in binary form or
+ an empty string if no metadata signature found in the payload.
+
+ Raises:
+ PayloadError if a read error occurred.
+ """
+ if not self.header:
+ raise PayloadError('payload header not present')
+
+ return common.Read(
+ self.payload_file, self.header.metadata_signature_len,
+ offset=self.payload_file_offset + self.header.size +
+ self.header.manifest_len)
+
+ def ReadDataBlob(self, offset, length):
+ """Reads and returns a single data blob from the update payload.
+
+ Args:
+ offset: offset to the beginning of the blob from the end of the manifest
+ length: the blob's length
+
+ Returns:
+ A string containing the raw blob data.
+
+ Raises:
+ PayloadError if a read error occurred.
+ """
+ return common.Read(self.payload_file, length,
+ offset=self.payload_file_offset + self.data_offset +
+ offset)
+
+ def Init(self):
+ """Initializes the payload object.
+
+ This is a prerequisite for any other public API call.
+
+ Raises:
+ PayloadError if object already initialized or fails to initialize
+ correctly.
+ """
+ if self.is_init:
+ raise PayloadError('payload object already initialized')
+
+ self.manifest_hasher = hashlib.sha256()
+
+ # Read the file header.
+ self.payload_file.seek(self.payload_file_offset)
+ self.header = self._ReadHeader()
+
+ # Read the manifest.
+ manifest_raw = self._ReadManifest()
+ self.manifest = update_metadata_pb2.DeltaArchiveManifest()
+ self.manifest.ParseFromString(manifest_raw)
+
+ # Read the metadata signature (if any).
+ metadata_signature_raw = self._ReadMetadataSignature()
+ if metadata_signature_raw:
+ self.metadata_signature = update_metadata_pb2.Signatures()
+ self.metadata_signature.ParseFromString(metadata_signature_raw)
+
+ self.metadata_size = self.header.size + self.header.manifest_len
+ self.data_offset = self.metadata_size + self.header.metadata_signature_len
+
+ self.is_init = True
+
+ def Describe(self):
+ """Emits the payload embedded description data to standard output."""
+ def _DescribeImageInfo(description, image_info):
+ """Display info about the image."""
+ def _DisplayIndentedValue(name, value):
+ print(' {:<14} {}'.format(name+':', value))
+
+ print('%s:' % description)
+ _DisplayIndentedValue('Channel', image_info.channel)
+ _DisplayIndentedValue('Board', image_info.board)
+ _DisplayIndentedValue('Version', image_info.version)
+ _DisplayIndentedValue('Key', image_info.key)
+
+ if image_info.build_channel != image_info.channel:
+ _DisplayIndentedValue('Build channel', image_info.build_channel)
+
+ if image_info.build_version != image_info.version:
+ _DisplayIndentedValue('Build version', image_info.build_version)
+
+ if self.manifest.HasField('old_image_info'):
+ _DescribeImageInfo('Old Image', self.manifest.old_image_info)
+
+ if self.manifest.HasField('new_image_info'):
+ _DescribeImageInfo('New Image', self.manifest.new_image_info)
+
+ def _AssertInit(self):
+ """Raises an exception if the object was not initialized."""
+ if not self.is_init:
+ raise PayloadError('payload object not initialized')
+
+ def ResetFile(self):
+ """Resets the offset of the payload file to right past the manifest."""
+ self.payload_file.seek(self.payload_file_offset + self.data_offset)
+
+ def IsDelta(self):
+ """Returns True iff the payload appears to be a delta."""
+ self._AssertInit()
+ return (self.manifest.HasField('old_kernel_info') or
+ self.manifest.HasField('old_rootfs_info') or
+ any(partition.HasField('old_partition_info')
+ for partition in self.manifest.partitions))
+
+ def IsFull(self):
+ """Returns True iff the payload appears to be a full."""
+ return not self.IsDelta()
+
+ def Check(self, pubkey_file_name=None, metadata_sig_file=None,
+ report_out_file=None, assert_type=None, block_size=0,
+ rootfs_part_size=0, kernel_part_size=0, allow_unhashed=False,
+ disabled_tests=()):
+ """Checks the payload integrity.
+
+ Args:
+ pubkey_file_name: public key used for signature verification
+ metadata_sig_file: metadata signature, if verification is desired
+ report_out_file: file object to dump the report to
+ assert_type: assert that payload is either 'full' or 'delta'
+ block_size: expected filesystem / payload block size
+ rootfs_part_size: the size of (physical) rootfs partitions in bytes
+ kernel_part_size: the size of (physical) kernel partitions in bytes
+ allow_unhashed: allow unhashed operation blobs
+ disabled_tests: list of tests to disable
+
+ Raises:
+ PayloadError if payload verification failed.
+ """
+ self._AssertInit()
+
+ # Create a short-lived payload checker object and run it.
+ helper = checker.PayloadChecker(
+ self, assert_type=assert_type, block_size=block_size,
+ allow_unhashed=allow_unhashed, disabled_tests=disabled_tests)
+ helper.Run(pubkey_file_name=pubkey_file_name,
+ metadata_sig_file=metadata_sig_file,
+ rootfs_part_size=rootfs_part_size,
+ kernel_part_size=kernel_part_size,
+ report_out_file=report_out_file)
+
+ def Apply(self, new_kernel_part, new_rootfs_part, old_kernel_part=None,
+ old_rootfs_part=None, bsdiff_in_place=True, bspatch_path=None,
+ puffpatch_path=None, truncate_to_expected_size=True):
+ """Applies the update payload.
+
+ Args:
+ new_kernel_part: name of dest kernel partition file
+ new_rootfs_part: name of dest rootfs partition file
+ old_kernel_part: name of source kernel partition file (optional)
+ old_rootfs_part: name of source rootfs partition file (optional)
+ bsdiff_in_place: whether to perform BSDIFF operations in-place (optional)
+ bspatch_path: path to the bspatch binary (optional)
+ puffpatch_path: path to the puffpatch binary (optional)
+ truncate_to_expected_size: whether to truncate the resulting partitions
+ to their expected sizes, as specified in the
+ payload (optional)
+
+ Raises:
+ PayloadError if payload application failed.
+ """
+ self._AssertInit()
+
+ # Create a short-lived payload applier object and run it.
+ helper = applier.PayloadApplier(
+ self, bsdiff_in_place=bsdiff_in_place, bspatch_path=bspatch_path,
+ puffpatch_path=puffpatch_path,
+ truncate_to_expected_size=truncate_to_expected_size)
+ helper.Run(new_kernel_part, new_rootfs_part,
+ old_kernel_part=old_kernel_part,
+ old_rootfs_part=old_rootfs_part)
diff --git a/update_payload/test_utils.py b/update_payload/test_utils.py
new file mode 100644
index 0000000..1e2259d
--- /dev/null
+++ b/update_payload/test_utils.py
@@ -0,0 +1,369 @@
+#
+# 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.
+#
+
+"""Utilities for unit testing."""
+
+from __future__ import print_function
+
+import cStringIO
+import hashlib
+import os
+import struct
+import subprocess
+
+from update_payload import common
+from update_payload import payload
+from update_payload import update_metadata_pb2
+
+
+class TestError(Exception):
+ """An error during testing of update payload code."""
+
+
+# Private/public RSA keys used for testing.
+_PRIVKEY_FILE_NAME = os.path.join(os.path.dirname(__file__),
+ 'payload-test-key.pem')
+_PUBKEY_FILE_NAME = os.path.join(os.path.dirname(__file__),
+ 'payload-test-key.pub')
+
+
+def KiB(count):
+ return count << 10
+
+
+def MiB(count):
+ return count << 20
+
+
+def GiB(count):
+ return count << 30
+
+
+def _WriteInt(file_obj, size, is_unsigned, val):
+ """Writes a binary-encoded integer to a file.
+
+ It will do the correct conversion based on the reported size and whether or
+ not a signed number is expected. Assumes a network (big-endian) byte
+ ordering.
+
+ Args:
+ file_obj: a file object
+ size: the integer size in bytes (2, 4 or 8)
+ is_unsigned: whether it is signed or not
+ val: integer value to encode
+
+ Raises:
+ PayloadError if a write error occurred.
+ """
+ try:
+ file_obj.write(struct.pack(common.IntPackingFmtStr(size, is_unsigned), val))
+ except IOError, e:
+ raise payload.PayloadError('error writing to file (%s): %s' %
+ (file_obj.name, e))
+
+
+def _SetMsgField(msg, field_name, val):
+ """Sets or clears a field in a protobuf message."""
+ if val is None:
+ msg.ClearField(field_name)
+ else:
+ setattr(msg, field_name, val)
+
+
+def SignSha256(data, privkey_file_name):
+ """Signs the data's SHA256 hash with an RSA private key.
+
+ Args:
+ data: the data whose SHA256 hash we want to sign
+ privkey_file_name: private key used for signing data
+
+ Returns:
+ The signature string, prepended with an ASN1 header.
+
+ Raises:
+ TestError if something goes wrong.
+ """
+ data_sha256_hash = common.SIG_ASN1_HEADER + hashlib.sha256(data).digest()
+ sign_cmd = ['openssl', 'rsautl', '-sign', '-inkey', privkey_file_name]
+ try:
+ sign_process = subprocess.Popen(sign_cmd, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ sig, _ = sign_process.communicate(input=data_sha256_hash)
+ except Exception as e:
+ raise TestError('signing subprocess failed: %s' % e)
+
+ return sig
+
+
+class SignaturesGenerator(object):
+ """Generates a payload signatures data block."""
+
+ def __init__(self):
+ self.sigs = update_metadata_pb2.Signatures()
+
+ def AddSig(self, version, data):
+ """Adds a signature to the signature sequence.
+
+ Args:
+ version: signature version (None means do not assign)
+ data: signature binary data (None means do not assign)
+ """
+ sig = self.sigs.signatures.add()
+ if version is not None:
+ sig.version = version
+ if data is not None:
+ sig.data = data
+
+ def ToBinary(self):
+ """Returns the binary representation of the signature block."""
+ return self.sigs.SerializeToString()
+
+
+class PayloadGenerator(object):
+ """Generates an update payload allowing low-level control.
+
+ Attributes:
+ manifest: the protobuf containing the payload manifest
+ version: the payload version identifier
+ block_size: the block size pertaining to update operations
+
+ """
+
+ def __init__(self, version=1):
+ self.manifest = update_metadata_pb2.DeltaArchiveManifest()
+ self.version = version
+ self.block_size = 0
+
+ @staticmethod
+ def _WriteExtent(ex, val):
+ """Returns an Extent message."""
+ start_block, num_blocks = val
+ _SetMsgField(ex, 'start_block', start_block)
+ _SetMsgField(ex, 'num_blocks', num_blocks)
+
+ @staticmethod
+ def _AddValuesToRepeatedField(repeated_field, values, write_func):
+ """Adds values to a repeated message field."""
+ if values:
+ for val in values:
+ new_item = repeated_field.add()
+ write_func(new_item, val)
+
+ @staticmethod
+ def _AddExtents(extents_field, values):
+ """Adds extents to an extents field."""
+ PayloadGenerator._AddValuesToRepeatedField(
+ extents_field, values, PayloadGenerator._WriteExtent)
+
+ def SetBlockSize(self, block_size):
+ """Sets the payload's block size."""
+ self.block_size = block_size
+ _SetMsgField(self.manifest, 'block_size', block_size)
+
+ def SetPartInfo(self, is_kernel, is_new, part_size, part_hash):
+ """Set the partition info entry.
+
+ Args:
+ is_kernel: whether this is kernel partition info
+ is_new: whether to set old (False) or new (True) info
+ part_size: the partition size (in fact, filesystem size)
+ part_hash: the partition hash
+ """
+ if is_kernel:
+ part_info = (self.manifest.new_kernel_info if is_new
+ else self.manifest.old_kernel_info)
+ else:
+ part_info = (self.manifest.new_rootfs_info if is_new
+ else self.manifest.old_rootfs_info)
+ _SetMsgField(part_info, 'size', part_size)
+ _SetMsgField(part_info, 'hash', part_hash)
+
+ def AddOperation(self, is_kernel, op_type, data_offset=None,
+ data_length=None, src_extents=None, src_length=None,
+ dst_extents=None, dst_length=None, data_sha256_hash=None):
+ """Adds an InstallOperation entry."""
+ operations = (self.manifest.kernel_install_operations if is_kernel
+ else self.manifest.install_operations)
+
+ op = operations.add()
+ op.type = op_type
+
+ _SetMsgField(op, 'data_offset', data_offset)
+ _SetMsgField(op, 'data_length', data_length)
+
+ self._AddExtents(op.src_extents, src_extents)
+ _SetMsgField(op, 'src_length', src_length)
+
+ self._AddExtents(op.dst_extents, dst_extents)
+ _SetMsgField(op, 'dst_length', dst_length)
+
+ _SetMsgField(op, 'data_sha256_hash', data_sha256_hash)
+
+ def SetSignatures(self, sigs_offset, sigs_size):
+ """Set the payload's signature block descriptors."""
+ _SetMsgField(self.manifest, 'signatures_offset', sigs_offset)
+ _SetMsgField(self.manifest, 'signatures_size', sigs_size)
+
+ def SetMinorVersion(self, minor_version):
+ """Set the payload's minor version field."""
+ _SetMsgField(self.manifest, 'minor_version', minor_version)
+
+ def _WriteHeaderToFile(self, file_obj, manifest_len):
+ """Writes a payload heaer to a file."""
+ # We need to access protected members in Payload for writing the header.
+ # pylint: disable=W0212
+ file_obj.write(payload.Payload._PayloadHeader._MAGIC)
+ _WriteInt(file_obj, payload.Payload._PayloadHeader._VERSION_SIZE, True,
+ self.version)
+ _WriteInt(file_obj, payload.Payload._PayloadHeader._MANIFEST_LEN_SIZE, True,
+ manifest_len)
+
+ def WriteToFile(self, file_obj, manifest_len=-1, data_blobs=None,
+ sigs_data=None, padding=None):
+ """Writes the payload content to a file.
+
+ Args:
+ file_obj: a file object open for writing
+ manifest_len: manifest len to dump (otherwise computed automatically)
+ data_blobs: a list of data blobs to be concatenated to the payload
+ sigs_data: a binary Signatures message to be concatenated to the payload
+ padding: stuff to dump past the normal data blobs provided (optional)
+ """
+ manifest = self.manifest.SerializeToString()
+ if manifest_len < 0:
+ manifest_len = len(manifest)
+ self._WriteHeaderToFile(file_obj, manifest_len)
+ file_obj.write(manifest)
+ if data_blobs:
+ for data_blob in data_blobs:
+ file_obj.write(data_blob)
+ if sigs_data:
+ file_obj.write(sigs_data)
+ if padding:
+ file_obj.write(padding)
+
+
+class EnhancedPayloadGenerator(PayloadGenerator):
+ """Payload generator with automatic handling of data blobs.
+
+ Attributes:
+ data_blobs: a list of blobs, in the order they were added
+ curr_offset: the currently consumed offset of blobs added to the payload
+ """
+
+ def __init__(self):
+ super(EnhancedPayloadGenerator, self).__init__()
+ self.data_blobs = []
+ self.curr_offset = 0
+
+ def AddData(self, data_blob):
+ """Adds a (possibly orphan) data blob."""
+ data_length = len(data_blob)
+ data_offset = self.curr_offset
+ self.curr_offset += data_length
+ self.data_blobs.append(data_blob)
+ return data_length, data_offset
+
+ def AddOperationWithData(self, is_kernel, op_type, src_extents=None,
+ src_length=None, dst_extents=None, dst_length=None,
+ data_blob=None, do_hash_data_blob=True):
+ """Adds an install operation and associated data blob.
+
+ This takes care of obtaining a hash of the data blob (if so instructed)
+ and appending it to the internally maintained list of blobs, including the
+ necessary offset/length accounting.
+
+ Args:
+ is_kernel: whether this is a kernel (True) or rootfs (False) operation
+ op_type: one of REPLACE, REPLACE_BZ, REPLACE_XZ, MOVE or BSDIFF
+ src_extents: list of (start, length) pairs indicating src block ranges
+ src_length: size of the src data in bytes (needed for BSDIFF)
+ dst_extents: list of (start, length) pairs indicating dst block ranges
+ dst_length: size of the dst data in bytes (needed for BSDIFF)
+ data_blob: a data blob associated with this operation
+ do_hash_data_blob: whether or not to compute and add a data blob hash
+ """
+ data_offset = data_length = data_sha256_hash = None
+ if data_blob is not None:
+ if do_hash_data_blob:
+ data_sha256_hash = hashlib.sha256(data_blob).digest()
+ data_length, data_offset = self.AddData(data_blob)
+
+ self.AddOperation(is_kernel, op_type, data_offset=data_offset,
+ data_length=data_length, src_extents=src_extents,
+ src_length=src_length, dst_extents=dst_extents,
+ dst_length=dst_length, data_sha256_hash=data_sha256_hash)
+
+ def WriteToFileWithData(self, file_obj, sigs_data=None,
+ privkey_file_name=None,
+ do_add_pseudo_operation=False,
+ is_pseudo_in_kernel=False, padding=None):
+ """Writes the payload content to a file, optionally signing the content.
+
+ Args:
+ file_obj: a file object open for writing
+ sigs_data: signatures blob to be appended to the payload (optional;
+ payload signature fields assumed to be preset by the caller)
+ privkey_file_name: key used for signing the payload (optional; used only
+ if explicit signatures blob not provided)
+ do_add_pseudo_operation: whether a pseudo-operation should be added to
+ account for the signature blob
+ is_pseudo_in_kernel: whether the pseudo-operation should be added to
+ kernel (True) or rootfs (False) operations
+ padding: stuff to dump past the normal data blobs provided (optional)
+
+ Raises:
+ TestError: if arguments are inconsistent or something goes wrong.
+ """
+ sigs_len = len(sigs_data) if sigs_data else 0
+
+ # Do we need to generate a genuine signatures blob?
+ do_generate_sigs_data = sigs_data is None and privkey_file_name
+
+ if do_generate_sigs_data:
+ # First, sign some arbitrary data to obtain the size of a signature blob.
+ fake_sig = SignSha256('fake-payload-data', privkey_file_name)
+ fake_sigs_gen = SignaturesGenerator()
+ fake_sigs_gen.AddSig(1, fake_sig)
+ sigs_len = len(fake_sigs_gen.ToBinary())
+
+ # Update the payload with proper signature attributes.
+ self.SetSignatures(self.curr_offset, sigs_len)
+
+ # Add a pseudo-operation to account for the signature blob, if requested.
+ if do_add_pseudo_operation:
+ if not self.block_size:
+ raise TestError('cannot add pseudo-operation without knowing the '
+ 'payload block size')
+ self.AddOperation(
+ is_pseudo_in_kernel, common.OpType.REPLACE,
+ data_offset=self.curr_offset, data_length=sigs_len,
+ dst_extents=[(common.PSEUDO_EXTENT_MARKER,
+ (sigs_len + self.block_size - 1) / self.block_size)])
+
+ if do_generate_sigs_data:
+ # Once all payload fields are updated, dump and sign it.
+ temp_payload_file = cStringIO.StringIO()
+ self.WriteToFile(temp_payload_file, data_blobs=self.data_blobs)
+ sig = SignSha256(temp_payload_file.getvalue(), privkey_file_name)
+ sigs_gen = SignaturesGenerator()
+ sigs_gen.AddSig(1, sig)
+ sigs_data = sigs_gen.ToBinary()
+ assert len(sigs_data) == sigs_len, 'signature blob lengths mismatch'
+
+ # Dump the whole thing, complete with data and signature blob, to a file.
+ self.WriteToFile(file_obj, data_blobs=self.data_blobs, sigs_data=sigs_data,
+ padding=padding)
diff --git a/update_payload/update-payload-key.pub.pem b/update_payload/update-payload-key.pub.pem
new file mode 100644
index 0000000..7ac369f
--- /dev/null
+++ b/update_payload/update-payload-key.pub.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Bg9BnjWhX3jJyECeXqF
+O28nkYTF1NHWLlFHgzAGg+ysva22BL3S5LlsNejnYVg/xzx3izvAQyOF3I1TJVOy
+2fH1DoZOWyKuckMyUrFQbO6OV1VIvPUPKckHadWcXSsHj2lBdDPH9xRDEBsXeztf
+nAGBD8GlAyTU7iH+Bf+xzyK9k4BmITf4Nx4xWhRZ6gm2Fc2SEP3x5N5fohkLv5ZP
+kFr0fj5wUK+0XF95rkGFBLIq2XACS3dmxMFToFl1HMM1HonUg9TAH+3dVH93zue1
+y81mkTuGnNX+zYya5ov2kD8zW1V10iTOSJfOlho5T8FpKbG37o3yYcUiyMHKO1Iv
+PQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/update_payload/update_metadata_pb2.py b/update_payload/update_metadata_pb2.py
new file mode 100644
index 0000000..595f2f6
--- /dev/null
+++ b/update_payload/update_metadata_pb2.py
@@ -0,0 +1,631 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: update_metadata.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='update_metadata.proto',
+ package='chromeos_update_engine',
+ serialized_pb='\n\x15update_metadata.proto\x12\x16\x63hromeos_update_engine\"1\n\x06\x45xtent\x12\x13\n\x0bstart_block\x18\x01 \x01(\x04\x12\x12\n\nnum_blocks\x18\x02 \x01(\x04\"z\n\nSignatures\x12@\n\nsignatures\x18\x01 \x03(\x0b\x32,.chromeos_update_engine.Signatures.Signature\x1a*\n\tSignature\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"+\n\rPartitionInfo\x12\x0c\n\x04size\x18\x01 \x01(\x04\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\"w\n\tImageInfo\x12\r\n\x05\x62oard\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x15\n\rbuild_channel\x18\x05 \x01(\t\x12\x15\n\rbuild_version\x18\x06 \x01(\t\"\xe6\x03\n\x10InstallOperation\x12;\n\x04type\x18\x01 \x02(\x0e\x32-.chromeos_update_engine.InstallOperation.Type\x12\x13\n\x0b\x64\x61ta_offset\x18\x02 \x01(\r\x12\x13\n\x0b\x64\x61ta_length\x18\x03 \x01(\r\x12\x33\n\x0bsrc_extents\x18\x04 \x03(\x0b\x32\x1e.chromeos_update_engine.Extent\x12\x12\n\nsrc_length\x18\x05 \x01(\x04\x12\x33\n\x0b\x64st_extents\x18\x06 \x03(\x0b\x32\x1e.chromeos_update_engine.Extent\x12\x12\n\ndst_length\x18\x07 \x01(\x04\x12\x18\n\x10\x64\x61ta_sha256_hash\x18\x08 \x01(\x0c\x12\x17\n\x0fsrc_sha256_hash\x18\t \x01(\x0c\"\xa5\x01\n\x04Type\x12\x0b\n\x07REPLACE\x10\x00\x12\x0e\n\nREPLACE_BZ\x10\x01\x12\x08\n\x04MOVE\x10\x02\x12\n\n\x06\x42SDIFF\x10\x03\x12\x0f\n\x0bSOURCE_COPY\x10\x04\x12\x11\n\rSOURCE_BSDIFF\x10\x05\x12\x08\n\x04ZERO\x10\x06\x12\x0b\n\x07\x44ISCARD\x10\x07\x12\x0e\n\nREPLACE_XZ\x10\x08\x12\x0c\n\x08PUFFDIFF\x10\t\x12\x11\n\rBROTLI_BSDIFF\x10\n\"\xa6\x03\n\x0fPartitionUpdate\x12\x16\n\x0epartition_name\x18\x01 \x02(\t\x12\x17\n\x0frun_postinstall\x18\x02 \x01(\x08\x12\x18\n\x10postinstall_path\x18\x03 \x01(\t\x12\x17\n\x0f\x66ilesystem_type\x18\x04 \x01(\t\x12M\n\x17new_partition_signature\x18\x05 \x03(\x0b\x32,.chromeos_update_engine.Signatures.Signature\x12\x41\n\x12old_partition_info\x18\x06 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12\x41\n\x12new_partition_info\x18\x07 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12<\n\noperations\x18\x08 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12\x1c\n\x14postinstall_optional\x18\t \x01(\x08\"\xc4\x05\n\x14\x44\x65ltaArchiveManifest\x12\x44\n\x12install_operations\x18\x01 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12K\n\x19kernel_install_operations\x18\x02 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12\x18\n\nblock_size\x18\x03 \x01(\r:\x04\x34\x30\x39\x36\x12\x19\n\x11signatures_offset\x18\x04 \x01(\x04\x12\x17\n\x0fsignatures_size\x18\x05 \x01(\x04\x12>\n\x0fold_kernel_info\x18\x06 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12>\n\x0fnew_kernel_info\x18\x07 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12>\n\x0fold_rootfs_info\x18\x08 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12>\n\x0fnew_rootfs_info\x18\t \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12\x39\n\x0eold_image_info\x18\n \x01(\x0b\x32!.chromeos_update_engine.ImageInfo\x12\x39\n\x0enew_image_info\x18\x0b \x01(\x0b\x32!.chromeos_update_engine.ImageInfo\x12\x18\n\rminor_version\x18\x0c \x01(\r:\x01\x30\x12;\n\npartitions\x18\r \x03(\x0b\x32\'.chromeos_update_engine.PartitionUpdateB\x02H\x03')
+
+
+
+_INSTALLOPERATION_TYPE = _descriptor.EnumDescriptor(
+ name='Type',
+ full_name='chromeos_update_engine.InstallOperation.Type',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='REPLACE', index=0, number=0,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='REPLACE_BZ', index=1, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='MOVE', index=2, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='BSDIFF', index=3, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SOURCE_COPY', index=4, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SOURCE_BSDIFF', index=5, number=5,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='ZERO', index=6, number=6,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='DISCARD', index=7, number=7,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='REPLACE_XZ', index=8, number=8,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='PUFFDIFF', index=9, number=9,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='BROTLI_BSDIFF', index=10, number=10,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=712,
+ serialized_end=877,
+)
+
+
+_EXTENT = _descriptor.Descriptor(
+ name='Extent',
+ full_name='chromeos_update_engine.Extent',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='start_block', full_name='chromeos_update_engine.Extent.start_block', index=0,
+ number=1, type=4, cpp_type=4, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='num_blocks', full_name='chromeos_update_engine.Extent.num_blocks', index=1,
+ number=2, type=4, cpp_type=4, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=49,
+ serialized_end=98,
+)
+
+
+_SIGNATURES_SIGNATURE = _descriptor.Descriptor(
+ name='Signature',
+ full_name='chromeos_update_engine.Signatures.Signature',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='version', full_name='chromeos_update_engine.Signatures.Signature.version', index=0,
+ number=1, type=13, cpp_type=3, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='data', full_name='chromeos_update_engine.Signatures.Signature.data', index=1,
+ number=2, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value="",
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=180,
+ serialized_end=222,
+)
+
+_SIGNATURES = _descriptor.Descriptor(
+ name='Signatures',
+ full_name='chromeos_update_engine.Signatures',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='signatures', full_name='chromeos_update_engine.Signatures.signatures', index=0,
+ number=1, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_SIGNATURES_SIGNATURE, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=100,
+ serialized_end=222,
+)
+
+
+_PARTITIONINFO = _descriptor.Descriptor(
+ name='PartitionInfo',
+ full_name='chromeos_update_engine.PartitionInfo',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='size', full_name='chromeos_update_engine.PartitionInfo.size', index=0,
+ number=1, type=4, cpp_type=4, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='hash', full_name='chromeos_update_engine.PartitionInfo.hash', index=1,
+ number=2, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value="",
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=224,
+ serialized_end=267,
+)
+
+
+_IMAGEINFO = _descriptor.Descriptor(
+ name='ImageInfo',
+ full_name='chromeos_update_engine.ImageInfo',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='board', full_name='chromeos_update_engine.ImageInfo.board', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='key', full_name='chromeos_update_engine.ImageInfo.key', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='channel', full_name='chromeos_update_engine.ImageInfo.channel', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='version', full_name='chromeos_update_engine.ImageInfo.version', index=3,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='build_channel', full_name='chromeos_update_engine.ImageInfo.build_channel', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='build_version', full_name='chromeos_update_engine.ImageInfo.build_version', index=5,
+ number=6, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=269,
+ serialized_end=388,
+)
+
+
+_INSTALLOPERATION = _descriptor.Descriptor(
+ name='InstallOperation',
+ full_name='chromeos_update_engine.InstallOperation',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='type', full_name='chromeos_update_engine.InstallOperation.type', index=0,
+ number=1, type=14, cpp_type=8, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='data_offset', full_name='chromeos_update_engine.InstallOperation.data_offset', index=1,
+ number=2, type=13, cpp_type=3, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='data_length', full_name='chromeos_update_engine.InstallOperation.data_length', index=2,
+ number=3, type=13, cpp_type=3, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='src_extents', full_name='chromeos_update_engine.InstallOperation.src_extents', index=3,
+ number=4, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='src_length', full_name='chromeos_update_engine.InstallOperation.src_length', index=4,
+ number=5, type=4, cpp_type=4, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='dst_extents', full_name='chromeos_update_engine.InstallOperation.dst_extents', index=5,
+ number=6, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='dst_length', full_name='chromeos_update_engine.InstallOperation.dst_length', index=6,
+ number=7, type=4, cpp_type=4, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='data_sha256_hash', full_name='chromeos_update_engine.InstallOperation.data_sha256_hash', index=7,
+ number=8, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value="",
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='src_sha256_hash', full_name='chromeos_update_engine.InstallOperation.src_sha256_hash', index=8,
+ number=9, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value="",
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _INSTALLOPERATION_TYPE,
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=391,
+ serialized_end=877,
+)
+
+
+_PARTITIONUPDATE = _descriptor.Descriptor(
+ name='PartitionUpdate',
+ full_name='chromeos_update_engine.PartitionUpdate',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='partition_name', full_name='chromeos_update_engine.PartitionUpdate.partition_name', index=0,
+ number=1, type=9, cpp_type=9, label=2,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='run_postinstall', full_name='chromeos_update_engine.PartitionUpdate.run_postinstall', index=1,
+ number=2, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='postinstall_path', full_name='chromeos_update_engine.PartitionUpdate.postinstall_path', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='filesystem_type', full_name='chromeos_update_engine.PartitionUpdate.filesystem_type', index=3,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=unicode("", "utf-8"),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='new_partition_signature', full_name='chromeos_update_engine.PartitionUpdate.new_partition_signature', index=4,
+ number=5, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='old_partition_info', full_name='chromeos_update_engine.PartitionUpdate.old_partition_info', index=5,
+ number=6, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='new_partition_info', full_name='chromeos_update_engine.PartitionUpdate.new_partition_info', index=6,
+ number=7, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='operations', full_name='chromeos_update_engine.PartitionUpdate.operations', index=7,
+ number=8, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='postinstall_optional', full_name='chromeos_update_engine.PartitionUpdate.postinstall_optional', index=8,
+ number=9, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=880,
+ serialized_end=1302,
+)
+
+
+_DELTAARCHIVEMANIFEST = _descriptor.Descriptor(
+ name='DeltaArchiveManifest',
+ full_name='chromeos_update_engine.DeltaArchiveManifest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='install_operations', full_name='chromeos_update_engine.DeltaArchiveManifest.install_operations', index=0,
+ number=1, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='kernel_install_operations', full_name='chromeos_update_engine.DeltaArchiveManifest.kernel_install_operations', index=1,
+ number=2, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='block_size', full_name='chromeos_update_engine.DeltaArchiveManifest.block_size', index=2,
+ number=3, type=13, cpp_type=3, label=1,
+ has_default_value=True, default_value=4096,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='signatures_offset', full_name='chromeos_update_engine.DeltaArchiveManifest.signatures_offset', index=3,
+ number=4, type=4, cpp_type=4, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='signatures_size', full_name='chromeos_update_engine.DeltaArchiveManifest.signatures_size', index=4,
+ number=5, type=4, cpp_type=4, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='old_kernel_info', full_name='chromeos_update_engine.DeltaArchiveManifest.old_kernel_info', index=5,
+ number=6, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='new_kernel_info', full_name='chromeos_update_engine.DeltaArchiveManifest.new_kernel_info', index=6,
+ number=7, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='old_rootfs_info', full_name='chromeos_update_engine.DeltaArchiveManifest.old_rootfs_info', index=7,
+ number=8, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='new_rootfs_info', full_name='chromeos_update_engine.DeltaArchiveManifest.new_rootfs_info', index=8,
+ number=9, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='old_image_info', full_name='chromeos_update_engine.DeltaArchiveManifest.old_image_info', index=9,
+ number=10, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='new_image_info', full_name='chromeos_update_engine.DeltaArchiveManifest.new_image_info', index=10,
+ number=11, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='minor_version', full_name='chromeos_update_engine.DeltaArchiveManifest.minor_version', index=11,
+ number=12, type=13, cpp_type=3, label=1,
+ has_default_value=True, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='partitions', full_name='chromeos_update_engine.DeltaArchiveManifest.partitions', index=12,
+ number=13, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ serialized_start=1305,
+ serialized_end=2013,
+)
+
+_SIGNATURES_SIGNATURE.containing_type = _SIGNATURES;
+_SIGNATURES.fields_by_name['signatures'].message_type = _SIGNATURES_SIGNATURE
+_INSTALLOPERATION.fields_by_name['type'].enum_type = _INSTALLOPERATION_TYPE
+_INSTALLOPERATION.fields_by_name['src_extents'].message_type = _EXTENT
+_INSTALLOPERATION.fields_by_name['dst_extents'].message_type = _EXTENT
+_INSTALLOPERATION_TYPE.containing_type = _INSTALLOPERATION;
+_PARTITIONUPDATE.fields_by_name['new_partition_signature'].message_type = _SIGNATURES_SIGNATURE
+_PARTITIONUPDATE.fields_by_name['old_partition_info'].message_type = _PARTITIONINFO
+_PARTITIONUPDATE.fields_by_name['new_partition_info'].message_type = _PARTITIONINFO
+_PARTITIONUPDATE.fields_by_name['operations'].message_type = _INSTALLOPERATION
+_DELTAARCHIVEMANIFEST.fields_by_name['install_operations'].message_type = _INSTALLOPERATION
+_DELTAARCHIVEMANIFEST.fields_by_name['kernel_install_operations'].message_type = _INSTALLOPERATION
+_DELTAARCHIVEMANIFEST.fields_by_name['old_kernel_info'].message_type = _PARTITIONINFO
+_DELTAARCHIVEMANIFEST.fields_by_name['new_kernel_info'].message_type = _PARTITIONINFO
+_DELTAARCHIVEMANIFEST.fields_by_name['old_rootfs_info'].message_type = _PARTITIONINFO
+_DELTAARCHIVEMANIFEST.fields_by_name['new_rootfs_info'].message_type = _PARTITIONINFO
+_DELTAARCHIVEMANIFEST.fields_by_name['old_image_info'].message_type = _IMAGEINFO
+_DELTAARCHIVEMANIFEST.fields_by_name['new_image_info'].message_type = _IMAGEINFO
+_DELTAARCHIVEMANIFEST.fields_by_name['partitions'].message_type = _PARTITIONUPDATE
+DESCRIPTOR.message_types_by_name['Extent'] = _EXTENT
+DESCRIPTOR.message_types_by_name['Signatures'] = _SIGNATURES
+DESCRIPTOR.message_types_by_name['PartitionInfo'] = _PARTITIONINFO
+DESCRIPTOR.message_types_by_name['ImageInfo'] = _IMAGEINFO
+DESCRIPTOR.message_types_by_name['InstallOperation'] = _INSTALLOPERATION
+DESCRIPTOR.message_types_by_name['PartitionUpdate'] = _PARTITIONUPDATE
+DESCRIPTOR.message_types_by_name['DeltaArchiveManifest'] = _DELTAARCHIVEMANIFEST
+
+class Extent(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+ DESCRIPTOR = _EXTENT
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.Extent)
+
+class Signatures(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+
+ class Signature(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+ DESCRIPTOR = _SIGNATURES_SIGNATURE
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.Signatures.Signature)
+ DESCRIPTOR = _SIGNATURES
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.Signatures)
+
+class PartitionInfo(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+ DESCRIPTOR = _PARTITIONINFO
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.PartitionInfo)
+
+class ImageInfo(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+ DESCRIPTOR = _IMAGEINFO
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.ImageInfo)
+
+class InstallOperation(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+ DESCRIPTOR = _INSTALLOPERATION
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.InstallOperation)
+
+class PartitionUpdate(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+ DESCRIPTOR = _PARTITIONUPDATE
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.PartitionUpdate)
+
+class DeltaArchiveManifest(_message.Message):
+ __metaclass__ = _reflection.GeneratedProtocolMessageType
+ DESCRIPTOR = _DELTAARCHIVEMANIFEST
+
+ # @@protoc_insertion_point(class_scope:chromeos_update_engine.DeltaArchiveManifest)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), 'H\003')
+# @@protoc_insertion_point(module_scope)