diff --git a/tools/releasetools/test_common.py b/tools/releasetools/test_common.py index a8613465c4..36f256dc5c 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 @@ class CommonZipTest(unittest.TestCase): 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 1dd31591bd..8ac3322c63 100755 --- a/tools/releasetools/validate_target_files.py +++ b/tools/releasetools/validate_target_files.py @@ -26,6 +26,7 @@ It performs checks to ensure the integrity of the input zip. import common import logging import os.path +import re import sparse_img import sys @@ -43,13 +44,38 @@ def _GetImage(which, tmpdir): 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 @@ def ValidateFileConsistency(input_zip, input_tmp): # 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 @@ def ValidateFileConsistency(input_zip, input_tmp): # 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 @@ def main(argv): 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.")