Verify the contents in install-recovery.sh

Check the SHA1 of recovery.img and boot.img embedded in the
install-recovery.sh.

Bug: 35411009
Test: validation script detects mismatch for both full recovery and
recovery-from-boot.
Change-Id: I5f07a869d9fa17fad26a22ef9ca3ecb06b1b28e3
diff --git a/tools/releasetools/test_common.py b/tools/releasetools/test_common.py
index a861346..36f256d 100644
--- a/tools/releasetools/test_common.py
+++ b/tools/releasetools/test_common.py
@@ -14,12 +14,14 @@
 # limitations under the License.
 #
 import os
+import shutil
 import tempfile
 import time
 import unittest
 import zipfile
 
 import common
+import validate_target_files
 
 
 def random_string_with_holes(size, block_size, step_size):
@@ -295,3 +297,55 @@
                    expected_mode=0o400)
     finally:
       os.remove(zip_file_name)
+
+class InstallRecoveryScriptFormatTest(unittest.TestCase):
+  """Check the format of install-recovery.sh
+
+  Its format should match between common.py and validate_target_files.py."""
+
+  def setUp(self):
+    self._tempdir = tempfile.mkdtemp()
+    # Create a dummy dict that contains the fstab info for boot&recovery.
+    self._info = {"fstab" : {}}
+    dummy_fstab = \
+        ["/dev/soc.0/by-name/boot /boot emmc defaults defaults",
+         "/dev/soc.0/by-name/recovery /recovery emmc defaults defaults"]
+    self._info["fstab"] = common.LoadRecoveryFSTab(lambda x : "\n".join(x),
+                                                   2, dummy_fstab)
+
+  def _out_tmp_sink(self, name, data, prefix="SYSTEM"):
+    loc = os.path.join(self._tempdir, prefix, name)
+    if not os.path.exists(os.path.dirname(loc)):
+      os.makedirs(os.path.dirname(loc))
+    with open(loc, "w+") as f:
+      f.write(data)
+
+  def test_full_recovery(self):
+    recovery_image = common.File("recovery.img", "recovery");
+    boot_image = common.File("boot.img", "boot");
+    self._info["full_recovery_image"] = "true"
+
+    common.MakeRecoveryPatch(self._tempdir, self._out_tmp_sink,
+                             recovery_image, boot_image, self._info)
+    validate_target_files.ValidateInstallRecoveryScript(self._tempdir,
+                                                        self._info)
+
+  def test_recovery_from_boot(self):
+    recovery_image = common.File("recovery.img", "recovery");
+    self._out_tmp_sink("recovery.img", recovery_image.data, "IMAGES")
+    boot_image = common.File("boot.img", "boot");
+    self._out_tmp_sink("boot.img", boot_image.data, "IMAGES")
+
+    common.MakeRecoveryPatch(self._tempdir, self._out_tmp_sink,
+                             recovery_image, boot_image, self._info)
+    validate_target_files.ValidateInstallRecoveryScript(self._tempdir,
+                                                        self._info)
+    # Validate 'recovery-from-boot' with bonus argument.
+    self._out_tmp_sink("etc/recovery-resource.dat", "bonus", "SYSTEM")
+    common.MakeRecoveryPatch(self._tempdir, self._out_tmp_sink,
+                             recovery_image, boot_image, self._info)
+    validate_target_files.ValidateInstallRecoveryScript(self._tempdir,
+                                                        self._info)
+
+  def tearDown(self):
+    shutil.rmtree(self._tempdir)
diff --git a/tools/releasetools/validate_target_files.py b/tools/releasetools/validate_target_files.py
index 1dd3159..8ac3322 100755
--- a/tools/releasetools/validate_target_files.py
+++ b/tools/releasetools/validate_target_files.py
@@ -26,6 +26,7 @@
 import common
 import logging
 import os.path
+import re
 import sparse_img
 import sys
 
@@ -43,13 +44,38 @@
   return sparse_img.SparseImage(path, mappath, clobbered_blocks)
 
 
-def ValidateFileConsistency(input_zip, input_tmp):
-  """Compare the files from image files and unpacked folders."""
+def _CalculateFileSha1(file_name, unpacked_name, round_up=False):
+  """Calculate the SHA-1 for a given file. Round up its size to 4K if needed."""
 
   def RoundUpTo4K(value):
     rounded_up = value + 4095
     return rounded_up - (rounded_up % 4096)
 
+  assert os.path.exists(unpacked_name)
+  with open(unpacked_name, 'r') as f:
+    file_data = f.read()
+  file_size = len(file_data)
+  if round_up:
+    file_size_rounded_up = RoundUpTo4K(file_size)
+    file_data += '\0' * (file_size_rounded_up - file_size)
+  return common.File(file_name, file_data).sha1
+
+
+def ValidateFileAgainstSha1(input_tmp, file_name, file_path, expected_sha1):
+  """Check if the file has the expected SHA-1."""
+
+  logging.info('Validating the SHA-1 of {}'.format(file_name))
+  unpacked_name = os.path.join(input_tmp, file_path)
+  assert os.path.exists(unpacked_name)
+  actual_sha1 = _CalculateFileSha1(file_name, unpacked_name, False)
+  assert actual_sha1 == expected_sha1, \
+      'SHA-1 mismatches for {}. actual {}, expected {}'.format(
+      file_name, actual_sha1, expected_sha1)
+
+
+def ValidateFileConsistency(input_zip, input_tmp):
+  """Compare the files from image files and unpacked folders."""
+
   def CheckAllFiles(which):
     logging.info('Checking %s image.', which)
     image = _GetImage(which, input_tmp)
@@ -66,12 +92,7 @@
       # The filename under unpacked directory, such as SYSTEM/bin/sh.
       unpacked_name = os.path.join(
           input_tmp, which.upper(), entry[(len(prefix) + 1):])
-      with open(unpacked_name) as f:
-        file_data = f.read()
-      file_size = len(file_data)
-      file_size_rounded_up = RoundUpTo4K(file_size)
-      file_data += '\0' * (file_size_rounded_up - file_size)
-      file_sha1 = common.File(entry, file_data).sha1
+      file_sha1 = _CalculateFileSha1(entry, unpacked_name, True)
 
       assert blocks_sha1 == file_sha1, \
           'file: %s, range: %s, blocks_sha1: %s, file_sha1: %s' % (
@@ -89,6 +110,78 @@
   # Not checking IMAGES/system_other.img since it doesn't have the map file.
 
 
+def ValidateInstallRecoveryScript(input_tmp, info_dict):
+  """Validate the SHA-1 embedded in install-recovery.sh.
+
+  install-recovery.sh is written in common.py and has the following format:
+
+  1. full recovery:
+  ...
+  if ! applypatch -c type:device:size:SHA-1; then
+  applypatch /system/etc/recovery.img type:device sha1 size && ...
+  ...
+
+  2. recovery from boot:
+  ...
+  applypatch [-b bonus_args] boot_info recovery_info recovery_sha1 \
+  recovery_size patch_info && ...
+  ...
+
+  For full recovery, we want to calculate the SHA-1 of /system/etc/recovery.img
+  and compare it against the one embedded in the script. While for recovery
+  from boot, we want to check the SHA-1 for both recovery.img and boot.img
+  under IMAGES/.
+  """
+
+  script_path = 'SYSTEM/bin/install-recovery.sh'
+  if not os.path.exists(os.path.join(input_tmp, script_path)):
+    logging.info('{} does not exist in input_tmp'.format(script_path))
+    return
+
+  logging.info('Checking {}'.format(script_path))
+  with open(os.path.join(input_tmp, script_path), 'r') as script:
+    lines = script.read().strip().split('\n')
+  assert len(lines) >= 6
+  check_cmd = re.search(r'if ! applypatch -c \w+:.+:\w+:(\w+);',
+                        lines[1].strip())
+  expected_recovery_check_sha1 = check_cmd.group(1)
+  patch_cmd = re.search(r'(applypatch.+)&&', lines[2].strip())
+  applypatch_argv = patch_cmd.group(1).strip().split()
+
+  full_recovery_image = info_dict.get("full_recovery_image") == "true"
+  if full_recovery_image:
+    assert len(applypatch_argv) == 5
+    # Check we have the same expected SHA-1 of recovery.img in both check mode
+    # and patch mode.
+    expected_recovery_sha1 = applypatch_argv[3].strip()
+    assert expected_recovery_check_sha1 == expected_recovery_sha1
+    ValidateFileAgainstSha1(input_tmp, 'recovery.img',
+        'SYSTEM/etc/recovery.img', expected_recovery_sha1)
+  else:
+    # We're patching boot.img to get recovery.img where bonus_args is optional
+    if applypatch_argv[1] == "-b":
+      assert len(applypatch_argv) == 8
+      boot_info_index = 3
+    else:
+      assert len(applypatch_argv) == 6
+      boot_info_index = 1
+
+    # boot_info: boot_type:boot_device:boot_size:boot_sha1
+    boot_info = applypatch_argv[boot_info_index].strip().split(':')
+    assert len(boot_info) == 4
+    ValidateFileAgainstSha1(input_tmp, file_name='boot.img',
+        file_path='IMAGES/boot.img', expected_sha1=boot_info[3])
+
+    recovery_sha1_index = boot_info_index + 2
+    expected_recovery_sha1 = applypatch_argv[recovery_sha1_index]
+    assert expected_recovery_check_sha1 == expected_recovery_sha1
+    ValidateFileAgainstSha1(input_tmp, file_name='recovery.img',
+        file_path='IMAGES/recovery.img',
+        expected_sha1=expected_recovery_sha1)
+
+  logging.info('Done checking {}'.format(script_path))
+
+
 def main(argv):
   def option_handler():
     return True
@@ -112,11 +205,12 @@
 
   ValidateFileConsistency(input_zip, input_tmp)
 
+  info_dict = common.LoadInfoDict(input_tmp)
+  ValidateInstallRecoveryScript(input_tmp, info_dict)
+
   # TODO: Check if the OTA keys have been properly updated (the ones on /system,
   # in recovery image).
 
-  # TODO(b/35411009): Verify the contents in /system/bin/install-recovery.sh.
-
   logging.info("Done.")