paycheck: applier honors sparse holes

Up until now, paycheck's applier ignored sparse (aka pseudo-) extents
completely, except for REPLACE operations (where a sparse extent is an
indicator for a signature blob). This is now fixed.

BUG=chromium:221846
TEST=Passes integration test

Change-Id: I8b7fde6e4bb20912a59ce9a509a20c00c5c0751b
Reviewed-on: https://gerrit.chromium.org/gerrit/50558
Tested-by: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Don Garrett <dgarrett@chromium.org>
Commit-Queue: Gilad Arnold <garnold@chromium.org>
diff --git a/host/lib/update_payload/applier.py b/host/lib/update_payload/applier.py
index 9a1b509..f5ae6b7 100644
--- a/host/lib/update_payload/applier.py
+++ b/host/lib/update_payload/applier.py
@@ -15,6 +15,7 @@
 import array
 import bz2
 import hashlib
+import itertools
 import os
 import shutil
 import subprocess
@@ -86,10 +87,17 @@
   for ex in extents:
     if max_length == 0:
       break
-    file_obj.seek(ex.start_block * block_size)
     read_length = min(max_length, ex.num_blocks * block_size)
-    data.fromfile(file_obj, read_length)
+
+    # 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
 
 
@@ -114,9 +122,13 @@
     if not data_length:
       raise PayloadError('%s: more write extents than data' % ex_name)
     write_length = min(data_length, ex.num_blocks * block_size)
-    file_obj.seek(ex.start_block * block_size)
-    data_view = buffer(data, data_offset, write_length)
-    file_obj.write(data_view)
+
+    # 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
 
@@ -148,12 +160,18 @@
   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)
-    start_byte = ex.start_block * block_size
+
+    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:
-      pad_off = start_byte + data_length
-      pad_len = num_bytes - data_length
+      # 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
 
@@ -251,6 +269,10 @@
   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