update_payload: De-duplicate paycheck partition handling for major version 2

This commit makes handling of major version 2 rootfs/kernel partitions more
generic, with the goal of reusing the same code paths for major version 2
(which has an arbitrary number of partitions per payload).

BUG=b:794404
TEST=no errors during run_unittests and test_paycheck.sh

Change-Id: Ic6e8cdaae557c10dfbd302dba0e52ff8b2870c9f
Reviewed-on: https://chromium-review.googlesource.com/1101500
Commit-Ready: Tudor Brindus <tbrindus@chromium.org>
Tested-by: Tudor Brindus <tbrindus@chromium.org>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
diff --git a/scripts/update_payload/checker.py b/scripts/update_payload/checker.py
index 68c70d4..49c556f 100644
--- a/scripts/update_payload/checker.py
+++ b/scripts/update_payload/checker.py
@@ -28,6 +28,7 @@
 
 import array
 import base64
+import collections
 import hashlib
 import itertools
 import os
@@ -330,10 +331,10 @@
     # 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.old_part_info = {}
+    self.new_part_info = {}
+    self.new_fs_sizes = collections.defaultdict(int)
+    self.old_fs_sizes = collections.defaultdict(int)
     self.minor_version = None
     # TODO(*): When fixing crbug.com/794404, the major version should be
     # correctly handled in update_payload scripts. So stop forcing
@@ -368,22 +369,24 @@
     Raises:
       error.PayloadError if a mandatory element is missing.
     """
+    element_result = collections.namedtuple('element_result', ['msg', 'report'])
+
     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
+      return element_result(None, None)
 
     value = getattr(msg, name)
     if is_submsg:
-      return value, report and report.AddSubReport(name)
+      return element_result(value, report and report.AddSubReport(name))
     else:
       if report:
         report.AddField(name, convert(value), linebreak=linebreak,
                         indent=indent)
-      return value, None
+      return element_result(value, None)
 
   @staticmethod
   def _CheckMandatoryField(msg, field_name, report, msg_name, convert=str,
@@ -544,13 +547,12 @@
       raise error.PayloadError('Unsupported minor version: %d' %
                                self.minor_version)
 
-  def _CheckManifest(self, report, rootfs_part_size=0, kernel_part_size=0):
+  def _CheckManifest(self, report, part_sizes=None):
     """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.
+      part_sizes: Map of partition label to partition size in bytes.
 
     Returns:
       A tuple consisting of the partition block size used during the update
@@ -559,6 +561,9 @@
     Raises:
       error.PayloadError if any of the checks fail.
     """
+    if part_sizes is None:
+      part_sizes = collections.defaultdict(int)
+
     manifest = self.payload.manifest
     report.AddSection('manifest')
 
@@ -577,39 +582,38 @@
     self._CheckPresentIff(self.sigs_offset, self.sigs_size,
                           'signatures_offset', 'signatures_size', 'manifest')
 
+    for part in common.CROS_PARTITIONS:
+      self.old_part_info[part] = self._CheckOptionalSubMsg(
+          manifest, 'old_%s_info' % part, report)
+      self.new_part_info[part] = self._CheckMandatorySubMsg(
+          manifest, 'new_%s_info' % part, report, '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
+    self._CheckPresentIff(self.old_part_info[common.KERNEL].msg,
+                          self.old_part_info[common.ROOTFS].msg,
+                          'old_kernel_info', 'old_rootfs_info', 'manifest')
+
+    if self.old_part_info[common.KERNEL].msg:  # equivalently, rootfs 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)
+      for part in common.CROS_PARTITIONS:
+        # Check: {size, hash} present in old_{kernel,rootfs}_info.
+        field = 'old_%s_info' % part
+        msg, report = self.old_part_info[part]
+        self.old_fs_sizes[part] = self._CheckMandatoryField(msg, 'size',
+                                                            report, field)
+        self._CheckMandatoryField(msg, 'hash', report, field,
+                                  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))
+        # Check: old_{kernel,rootfs} size must fit in respective partition.
+        if self.old_fs_sizes[part] > part_sizes[part] > 0:
+          raise error.PayloadError(
+              'Old %s content (%d) exceed partition size (%d).' %
+              (part, self.old_fs_sizes[part], part_sizes[part]))
     else:
       # Assert/mark full payload.
       if self.payload_type == _TYPE_DELTA:
@@ -617,31 +621,21 @@
             '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_{kernel,rootfs}_info present; contains {size, hash}.
+    for part in common.CROS_PARTITIONS:
+      field = 'new_%s_info' % part
+      msg, report = self.new_part_info[part]
 
-    # 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)
+      self.new_fs_sizes[part] = self._CheckMandatoryField(msg, 'size', report,
+                                                          field)
+      self._CheckMandatoryField(msg, 'hash', report, field,
+                                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: new_{kernel,rootfs} size must fit in respective partition.
+      if self.new_fs_sizes[part] > part_sizes[part] > 0:
+        raise error.PayloadError(
+            'New %s content (%d) exceed partition size (%d).' %
+            (part, self.new_fs_sizes[part], part_sizes[part]))
 
     # Check: minor_version makes sense for the payload type. This check should
     # run after the payload type has been set.
@@ -1282,7 +1276,10 @@
       report.AddField('manifest len', self.payload.header.manifest_len)
 
       # Part 2: Check the manifest.
-      self._CheckManifest(report, rootfs_part_size, kernel_part_size)
+      self._CheckManifest(report, {
+          common.ROOTFS: rootfs_part_size,
+          common.KERNEL: kernel_part_size
+      })
       assert self.payload_type, 'payload type should be known by now'
 
       # Infer the usable partition size when validating rootfs operations:
@@ -1291,8 +1288,8 @@
       #   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
+      new_rootfs_usable_size = self.new_fs_sizes[common.ROOTFS]
+      old_rootfs_usable_size = self.old_fs_sizes[common.ROOTFS]
       if rootfs_part_size:
         new_rootfs_usable_size = rootfs_part_size
         old_rootfs_usable_size = rootfs_part_size
@@ -1307,19 +1304,20 @@
       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,
+          'install_operations', self.old_fs_sizes[common.ROOTFS],
+          self.new_fs_sizes[common.ROOTFS], 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')
+      old_kernel_fs_size = self.old_fs_sizes[common.KERNEL]
+      new_kernel_fs_size = self.new_fs_sizes[common.KERNEL]
       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,
+          'kernel_install_operations', old_kernel_fs_size, new_kernel_fs_size,
+          kernel_part_size if kernel_part_size else old_kernel_fs_size,
+          kernel_part_size if kernel_part_size else new_kernel_fs_size,
           total_blob_size, True)
 
       # Check: Operations data reach the end of the payload file.