tests: Merge recovery_component_test into recovery_unit_test.

Most of the tests in component/ are in fact unit tests. And it doesn't
look practically beneficial to distinguish between the two:
- They have the same test setup;
- We always run both (recovery_unit_test and recovery_component_test)
  at the same time;
- Breaking any of them would be equally bad.

This CL merges the tests in recovery_component_test into
recovery_unit_test to save the effort to maintain both.

Test: Run recovery_unit_test on marlin (via `adb sync data`).
Test: `atest recovery_unit_test`
Change-Id: I93ff32e7219cd83425a4bcfe5613978a8dd48d75
diff --git a/tests/unit/applypatch_modes_test.cpp b/tests/unit/applypatch_modes_test.cpp
new file mode 100644
index 0000000..08414b7
--- /dev/null
+++ b/tests/unit/applypatch_modes_test.cpp
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2016 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 agree 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/strings.h>
+#include <bsdiff/bsdiff.h>
+#include <gtest/gtest.h>
+#include <openssl/sha.h>
+
+#include "applypatch/applypatch_modes.h"
+#include "common/test_constants.h"
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
+
+using namespace std::string_literals;
+
+// Loads a given partition and returns a string of form "EMMC:name:size:hash".
+static std::string GetEmmcTargetString(const std::string& filename,
+                                       const std::string& display_name = "") {
+  std::string data;
+  if (!android::base::ReadFileToString(filename, &data)) {
+    PLOG(ERROR) << "Failed to read " << filename;
+    return {};
+  }
+
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest);
+
+  return "EMMC:"s + (display_name.empty() ? filename : display_name) + ":" +
+         std::to_string(data.size()) + ":" + print_sha1(digest);
+}
+
+class ApplyPatchModesTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    source = GetEmmcTargetString(from_testdata_base("boot.img"));
+    ASSERT_FALSE(source.empty());
+
+    std::string recovery_file = from_testdata_base("recovery.img");
+    recovery = GetEmmcTargetString(recovery_file);
+    ASSERT_FALSE(recovery.empty());
+
+    ASSERT_TRUE(android::base::WriteStringToFile("", patched_file_.path));
+    target = GetEmmcTargetString(recovery_file, patched_file_.path);
+    ASSERT_FALSE(target.empty());
+
+    Paths::Get().set_cache_temp_source(cache_source_.path);
+  }
+
+  std::string source;
+  std::string target;
+  std::string recovery;
+
+ private:
+  TemporaryFile cache_source_;
+  TemporaryFile patched_file_;
+};
+
+static int InvokeApplyPatchModes(const std::vector<std::string>& args) {
+  auto args_to_call = StringVectorToNullTerminatedArray(args);
+  return applypatch_modes(args_to_call.size() - 1, args_to_call.data());
+}
+
+static void VerifyPatchedTarget(const std::string& target) {
+  std::vector<std::string> pieces = android::base::Split(target, ":");
+  ASSERT_EQ(4, pieces.size());
+  ASSERT_EQ("EMMC", pieces[0]);
+
+  std::string patched_emmc = GetEmmcTargetString(pieces[1]);
+  ASSERT_FALSE(patched_emmc.empty());
+  ASSERT_EQ(target, patched_emmc);
+}
+
+TEST_F(ApplyPatchModesTest, InvalidArgs) {
+  // At least two args (including the filename).
+  ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch" }));
+
+  // Unrecognized args.
+  ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "-x" }));
+}
+
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) {
+  std::vector<std::string> args{
+    "applypatch",
+    "--bonus",
+    from_testdata_base("bonus.file"),
+    "--patch",
+    from_testdata_base("recovery-from-boot.p"),
+    "--target",
+    target,
+    "--source",
+    source,
+  };
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has
+// everything).
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithoutBonusFile) {
+  std::vector<std::string> args{
+    "applypatch", "--patch", from_testdata_base("recovery-from-boot-with-bonus.p"),
+    "--target",   target,    "--source",
+    source,
+  };
+
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+// Ensures that applypatch works with a bsdiff based recovery-from-boot.p.
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithBsdiffPatch) {
+  // Generate the bsdiff patch of recovery-from-boot.p.
+  std::string src_content;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("boot.img"), &src_content));
+
+  std::string tgt_content;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("recovery.img"), &tgt_content));
+
+  TemporaryFile patch_file;
+  ASSERT_EQ(0,
+            bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(),
+                           reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(),
+                           patch_file.path, nullptr));
+
+  std::vector<std::string> args{
+    "applypatch", "--patch", patch_file.path, "--target", target, "--source", source,
+  };
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+TEST_F(ApplyPatchModesTest, PatchModeInvalidArgs) {
+  // Invalid bonus file.
+  std::vector<std::string> args{
+    "applypatch", "--bonus", "/doesntexist", "--patch", from_testdata_base("recovery-from-boot.p"),
+    "--target",   target,    "--source",     source,
+  };
+  ASSERT_NE(0, InvokeApplyPatchModes(args));
+
+  // With bonus file, but missing args.
+  ASSERT_NE(0,
+            InvokeApplyPatchModes({ "applypatch", "--bonus", from_testdata_base("bonus.file") }));
+}
+
+TEST_F(ApplyPatchModesTest, FlashMode) {
+  std::vector<std::string> args{
+    "applypatch", "--flash", from_testdata_base("recovery.img"), "--target", target,
+  };
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+TEST_F(ApplyPatchModesTest, FlashModeInvalidArgs) {
+  std::vector<std::string> args{
+    "applypatch", "--bonus", from_testdata_base("bonus.file"), "--flash", source,
+    "--target",   target,
+  };
+  ASSERT_NE(0, InvokeApplyPatchModes(args));
+}
+
+TEST_F(ApplyPatchModesTest, CheckMode) {
+  ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", recovery }));
+  ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", source }));
+}
+
+TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) {
+  ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "--check" }));
+}
+
+TEST_F(ApplyPatchModesTest, CheckModeNonEmmcTarget) {
+  ASSERT_NE(0, InvokeApplyPatchModes({ "applypatch", "--check", from_testdata_base("boot.img") }));
+}
+
+TEST_F(ApplyPatchModesTest, ShowLicenses) {
+  ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--license" }));
+}
diff --git a/tests/unit/bootloader_message_test.cpp b/tests/unit/bootloader_message_test.cpp
new file mode 100644
index 0000000..b005d19
--- /dev/null
+++ b/tests/unit/bootloader_message_test.cpp
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <bootloader_message/bootloader_message.h>
+#include <gtest/gtest.h>
+
+TEST(BootloaderMessageTest, read_and_write_bootloader_message) {
+  TemporaryFile temp_misc;
+
+  // Write the BCB.
+  bootloader_message boot = {};
+  strlcpy(boot.command, "command", sizeof(boot.command));
+  strlcpy(boot.recovery, "message1\nmessage2\n", sizeof(boot.recovery));
+  strlcpy(boot.status, "status1", sizeof(boot.status));
+
+  std::string err;
+  ASSERT_TRUE(write_bootloader_message_to(boot, temp_misc.path, &err))
+      << "Failed to write BCB: " << err;
+
+  // Read and verify.
+  bootloader_message boot_verify;
+  ASSERT_TRUE(read_bootloader_message_from(&boot_verify, temp_misc.path, &err))
+      << "Failed to read BCB: " << err;
+
+  ASSERT_EQ(std::string(reinterpret_cast<const char*>(&boot), sizeof(boot)),
+            std::string(reinterpret_cast<const char*>(&boot_verify), sizeof(boot_verify)));
+}
+
+TEST(BootloaderMessageTest, update_bootloader_message_in_struct) {
+  // Write the options to BCB.
+  std::vector<std::string> options = { "option1", "option2" };
+
+  bootloader_message boot = {};
+  // Inject some bytes into boot.
+  strlcpy(boot.recovery, "random message", sizeof(boot.recovery));
+  strlcpy(boot.status, "status bytes", sizeof(boot.status));
+  strlcpy(boot.stage, "stage bytes", sizeof(boot.stage));
+  strlcpy(boot.reserved, "reserved bytes", sizeof(boot.reserved));
+
+  ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options));
+
+  // Verify that command and recovery fields should be set.
+  ASSERT_EQ("boot-recovery", std::string(boot.command));
+  std::string expected = "recovery\n" + android::base::Join(options, "\n") + "\n";
+  ASSERT_EQ(expected, std::string(boot.recovery));
+
+  // The rest should be intact.
+  ASSERT_EQ("status bytes", std::string(boot.status));
+  ASSERT_EQ("stage bytes", std::string(boot.stage));
+  ASSERT_EQ("reserved bytes", std::string(boot.reserved));
+}
+
+TEST(BootloaderMessageTest, update_bootloader_message_recovery_options_empty) {
+  // Write empty vector.
+  std::vector<std::string> options;
+
+  // Read and verify.
+  bootloader_message boot = {};
+  ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options));
+
+  // command and recovery fields should be set.
+  ASSERT_EQ("boot-recovery", std::string(boot.command));
+  ASSERT_EQ("recovery\n", std::string(boot.recovery));
+
+  // The rest should be empty.
+  ASSERT_EQ(std::string(sizeof(boot.status), '\0'), std::string(boot.status, sizeof(boot.status)));
+  ASSERT_EQ(std::string(sizeof(boot.stage), '\0'), std::string(boot.stage, sizeof(boot.stage)));
+  ASSERT_EQ(std::string(sizeof(boot.reserved), '\0'),
+            std::string(boot.reserved, sizeof(boot.reserved)));
+}
+
+TEST(BootloaderMessageTest, update_bootloader_message_recovery_options_long) {
+  // Write super long message.
+  std::vector<std::string> options;
+  for (int i = 0; i < 100; i++) {
+    options.push_back("option: " + std::to_string(i));
+  }
+
+  // Read and verify.
+  bootloader_message boot = {};
+  ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options));
+
+  // Make sure it's long enough.
+  std::string expected = "recovery\n" + android::base::Join(options, "\n") + "\n";
+  ASSERT_GE(expected.size(), sizeof(boot.recovery));
+
+  // command and recovery fields should be set.
+  ASSERT_EQ("boot-recovery", std::string(boot.command));
+  ASSERT_EQ(expected.substr(0, sizeof(boot.recovery) - 1), std::string(boot.recovery));
+  ASSERT_EQ('\0', boot.recovery[sizeof(boot.recovery) - 1]);
+
+  // The rest should be empty.
+  ASSERT_EQ(std::string(sizeof(boot.status), '\0'), std::string(boot.status, sizeof(boot.status)));
+  ASSERT_EQ(std::string(sizeof(boot.stage), '\0'), std::string(boot.stage, sizeof(boot.stage)));
+  ASSERT_EQ(std::string(sizeof(boot.reserved), '\0'),
+            std::string(boot.reserved, sizeof(boot.reserved)));
+}
+
diff --git a/tests/unit/edify_test.cpp b/tests/unit/edify_test.cpp
new file mode 100644
index 0000000..8397bd3
--- /dev/null
+++ b/tests/unit/edify_test.cpp
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <memory>
+#include <string>
+
+#include <gtest/gtest.h>
+
+#include "edify/expr.h"
+
+static void expect(const std::string& expr_str, const char* expected) {
+  std::unique_ptr<Expr> e;
+  int error_count = 0;
+  EXPECT_EQ(0, ParseString(expr_str, &e, &error_count));
+  EXPECT_EQ(0, error_count);
+
+  State state(expr_str, nullptr);
+
+  std::string result;
+  bool status = Evaluate(&state, e, &result);
+
+  if (expected == nullptr) {
+    EXPECT_FALSE(status);
+  } else {
+    EXPECT_STREQ(expected, result.c_str());
+  }
+}
+
+class EdifyTest : public ::testing::Test {
+ protected:
+  void SetUp() {
+    RegisterBuiltins();
+  }
+};
+
+TEST_F(EdifyTest, parsing) {
+    expect("a", "a");
+    expect("\"a\"", "a");
+    expect("\"\\x61\"", "a");
+    expect("# this is a comment\n"
+           "  a\n"
+           "   \n",
+           "a");
+}
+
+TEST_F(EdifyTest, sequence) {
+    // sequence operator
+    expect("a; b; c", "c");
+}
+
+TEST_F(EdifyTest, concat) {
+    // string concat operator
+    expect("a + b", "ab");
+    expect("a + \n \"b\"", "ab");
+    expect("a + b +\nc\n", "abc");
+
+    // string concat function
+    expect("concat(a, b)", "ab");
+    expect("concat(a,\n \"b\")", "ab");
+    expect("concat(a + b,\nc,\"d\")", "abcd");
+    expect("\"concat\"(a + b,\nc,\"d\")", "abcd");
+}
+
+TEST_F(EdifyTest, logical) {
+    // logical and
+    expect("a && b", "b");
+    expect("a && \"\"", "");
+    expect("\"\" && b", "");
+    expect("\"\" && \"\"", "");
+    expect("\"\" && abort()", "");   // test short-circuiting
+    expect("t && abort()", nullptr);
+
+    // logical or
+    expect("a || b", "a");
+    expect("a || \"\"", "a");
+    expect("\"\" || b", "b");
+    expect("\"\" || \"\"", "");
+    expect("a || abort()", "a");     // test short-circuiting
+    expect("\"\" || abort()", NULL);
+
+    // logical not
+    expect("!a", "");
+    expect("! \"\"", "t");
+    expect("!!a", "t");
+}
+
+TEST_F(EdifyTest, precedence) {
+    // precedence
+    expect("\"\" == \"\" && b", "b");
+    expect("a + b == ab", "t");
+    expect("ab == a + b", "t");
+    expect("a + (b == ab)", "a");
+    expect("(ab == a) + b", "b");
+}
+
+TEST_F(EdifyTest, substring) {
+    // substring function
+    expect("is_substring(cad, abracadabra)", "t");
+    expect("is_substring(abrac, abracadabra)", "t");
+    expect("is_substring(dabra, abracadabra)", "t");
+    expect("is_substring(cad, abracxadabra)", "");
+    expect("is_substring(abrac, axbracadabra)", "");
+    expect("is_substring(dabra, abracadabrxa)", "");
+}
+
+TEST_F(EdifyTest, ifelse) {
+    // ifelse function
+    expect("ifelse(t, yes, no)", "yes");
+    expect("ifelse(!t, yes, no)", "no");
+    expect("ifelse(t, yes, abort())", "yes");
+    expect("ifelse(!t, abort(), no)", "no");
+}
+
+TEST_F(EdifyTest, if_statement) {
+    // if "statements"
+    expect("if t then yes else no endif", "yes");
+    expect("if \"\" then yes else no endif", "no");
+    expect("if \"\" then yes endif", "");
+    expect("if \"\"; t then yes endif", "yes");
+}
+
+TEST_F(EdifyTest, comparison) {
+    // numeric comparisons
+    expect("less_than_int(3, 14)", "t");
+    expect("less_than_int(14, 3)", "");
+    expect("less_than_int(x, 3)", "");
+    expect("less_than_int(3, x)", "");
+    expect("greater_than_int(3, 14)", "");
+    expect("greater_than_int(14, 3)", "t");
+    expect("greater_than_int(x, 3)", "");
+    expect("greater_than_int(3, x)", "");
+}
+
+TEST_F(EdifyTest, big_string) {
+  expect(std::string(8192, 's'), std::string(8192, 's').c_str());
+}
+
+TEST_F(EdifyTest, unknown_function) {
+  const char* script1 = "unknown_function()";
+  std::unique_ptr<Expr> expr;
+  int error_count = 0;
+  EXPECT_EQ(1, ParseString(script1, &expr, &error_count));
+  EXPECT_EQ(1, error_count);
+
+  const char* script2 = "abc; unknown_function()";
+  error_count = 0;
+  EXPECT_EQ(1, ParseString(script2, &expr, &error_count));
+  EXPECT_EQ(1, error_count);
+
+  const char* script3 = "unknown_function1() || yes";
+  error_count = 0;
+  EXPECT_EQ(1, ParseString(script3, &expr, &error_count));
+  EXPECT_EQ(1, error_count);
+}
diff --git a/tests/unit/fuse_sideload_test.cpp b/tests/unit/fuse_sideload_test.cpp
new file mode 100644
index 0000000..6add99f
--- /dev/null
+++ b/tests/unit/fuse_sideload_test.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include <unistd.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+
+#include "fuse_provider.h"
+#include "fuse_sideload.h"
+
+TEST(SideloadTest, fuse_device) {
+  ASSERT_EQ(0, access("/dev/fuse", R_OK | W_OK));
+}
+
+class FuseTestDataProvider : public FuseDataProvider {
+ public:
+  FuseTestDataProvider(uint64_t file_size, uint32_t block_size)
+      : FuseDataProvider(file_size, block_size) {}
+
+ private:
+  bool ReadBlockAlignedData(uint8_t*, uint32_t, uint32_t) const override {
+    return true;
+  }
+};
+
+TEST(SideloadTest, run_fuse_sideload_wrong_parameters) {
+  auto provider_small_block = std::make_unique<FuseTestDataProvider>(4096, 4095);
+  ASSERT_EQ(-1, run_fuse_sideload(std::move(provider_small_block)));
+
+  auto provider_large_block = std::make_unique<FuseTestDataProvider>(4096, (1 << 22) + 1);
+  ASSERT_EQ(-1, run_fuse_sideload(std::move(provider_large_block)));
+
+  auto provider_too_many_blocks =
+      std::make_unique<FuseTestDataProvider>(((1 << 18) + 1) * 4096, 4096);
+  ASSERT_EQ(-1, run_fuse_sideload(std::move(provider_too_many_blocks)));
+}
+
+TEST(SideloadTest, run_fuse_sideload) {
+  const std::vector<std::string> blocks = {
+    std::string(2048, 'a') + std::string(2048, 'b'),
+    std::string(2048, 'c') + std::string(2048, 'd'),
+    std::string(2048, 'e') + std::string(2048, 'f'),
+    std::string(2048, 'g') + std::string(2048, 'h'),
+  };
+  const std::string content = android::base::Join(blocks, "");
+  ASSERT_EQ(16384U, content.size());
+
+  TemporaryFile temp_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
+
+  auto provider = std::make_unique<FuseFileDataProvider>(temp_file.path, 4096);
+  ASSERT_TRUE(provider->Valid());
+  TemporaryDir mount_point;
+  pid_t pid = fork();
+  if (pid == 0) {
+    ASSERT_EQ(0, run_fuse_sideload(std::move(provider), mount_point.path));
+    _exit(EXIT_SUCCESS);
+  }
+
+  std::string package = std::string(mount_point.path) + "/" + FUSE_SIDELOAD_HOST_FILENAME;
+  int status;
+  static constexpr int kSideloadInstallTimeout = 10;
+  for (int i = 0; i < kSideloadInstallTimeout; ++i) {
+    ASSERT_NE(-1, waitpid(pid, &status, WNOHANG));
+
+    struct stat sb;
+    if (stat(package.c_str(), &sb) == 0) {
+      break;
+    }
+
+    if (errno == ENOENT && i < kSideloadInstallTimeout - 1) {
+      sleep(1);
+      continue;
+    }
+    FAIL() << "Timed out waiting for the fuse-provided package.";
+  }
+
+  std::string content_via_fuse;
+  ASSERT_TRUE(android::base::ReadFileToString(package, &content_via_fuse));
+  ASSERT_EQ(content, content_via_fuse);
+
+  std::string exit_flag = std::string(mount_point.path) + "/" + FUSE_SIDELOAD_HOST_EXIT_FLAG;
+  struct stat sb;
+  ASSERT_EQ(0, stat(exit_flag.c_str(), &sb));
+
+  waitpid(pid, &status, 0);
+  ASSERT_EQ(0, WEXITSTATUS(status));
+  ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
diff --git a/tests/unit/imgdiff_test.cpp b/tests/unit/imgdiff_test.cpp
new file mode 100644
index 0000000..e76ccbd
--- /dev/null
+++ b/tests/unit/imgdiff_test.cpp
@@ -0,0 +1,1114 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include <stdio.h>
+
+#include <algorithm>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/memory.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <applypatch/imgdiff.h>
+#include <applypatch/imgdiff_image.h>
+#include <applypatch/imgpatch.h>
+#include <gtest/gtest.h>
+#include <ziparchive/zip_writer.h>
+
+#include "common/test_constants.h"
+
+using android::base::get_unaligned;
+
+// Sanity check for the given imgdiff patch header.
+static void verify_patch_header(const std::string& patch, size_t* num_normal, size_t* num_raw,
+                                size_t* num_deflate) {
+  const size_t size = patch.size();
+  const char* data = patch.data();
+
+  ASSERT_GE(size, 12U);
+  ASSERT_EQ("IMGDIFF2", std::string(data, 8));
+
+  const int num_chunks = get_unaligned<int32_t>(data + 8);
+  ASSERT_GE(num_chunks, 0);
+
+  size_t normal = 0;
+  size_t raw = 0;
+  size_t deflate = 0;
+
+  size_t pos = 12;
+  for (int i = 0; i < num_chunks; ++i) {
+    ASSERT_LE(pos + 4, size);
+    int type = get_unaligned<int32_t>(data + pos);
+    pos += 4;
+    if (type == CHUNK_NORMAL) {
+      pos += 24;
+      ASSERT_LE(pos, size);
+      normal++;
+    } else if (type == CHUNK_RAW) {
+      ASSERT_LE(pos + 4, size);
+      ssize_t data_len = get_unaligned<int32_t>(data + pos);
+      ASSERT_GT(data_len, 0);
+      pos += 4 + data_len;
+      ASSERT_LE(pos, size);
+      raw++;
+    } else if (type == CHUNK_DEFLATE) {
+      pos += 60;
+      ASSERT_LE(pos, size);
+      deflate++;
+    } else {
+      FAIL() << "Invalid patch type: " << type;
+    }
+  }
+
+  if (num_normal != nullptr) *num_normal = normal;
+  if (num_raw != nullptr) *num_raw = raw;
+  if (num_deflate != nullptr) *num_deflate = deflate;
+}
+
+static void GenerateTarget(const std::string& src, const std::string& patch, std::string* patched) {
+  patched->clear();
+  ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(),
+                               reinterpret_cast<const unsigned char*>(patch.data()), patch.size(),
+                               [&](const unsigned char* data, size_t len) {
+                                 patched->append(reinterpret_cast<const char*>(data), len);
+                                 return len;
+                               }));
+}
+
+static void verify_patched_image(const std::string& src, const std::string& patch,
+                                 const std::string& tgt) {
+  std::string patched;
+  GenerateTarget(src, patch, &patched);
+  ASSERT_EQ(tgt, patched);
+}
+
+TEST(ImgdiffTest, invalid_args) {
+  // Insufficient inputs.
+  ASSERT_EQ(2, imgdiff(1, (const char* []){ "imgdiff" }));
+  ASSERT_EQ(2, imgdiff(2, (const char* []){ "imgdiff", "-z" }));
+  ASSERT_EQ(2, imgdiff(2, (const char* []){ "imgdiff", "-b" }));
+  ASSERT_EQ(2, imgdiff(3, (const char* []){ "imgdiff", "-z", "-b" }));
+
+  // Failed to read bonus file.
+  ASSERT_EQ(1, imgdiff(3, (const char* []){ "imgdiff", "-b", "doesntexist" }));
+
+  // Failed to read input files.
+  ASSERT_EQ(1, imgdiff(4, (const char* []){ "imgdiff", "doesntexist", "doesntexist", "output" }));
+  ASSERT_EQ(
+      1, imgdiff(5, (const char* []){ "imgdiff", "-z", "doesntexist", "doesntexist", "output" }));
+}
+
+TEST(ImgdiffTest, image_mode_smoke) {
+  // Random bytes.
+  const std::string src("abcdefg");
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  const std::string tgt("abcdefgxyz");
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect one CHUNK_RAW entry.
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(0U, num_deflate);
+  ASSERT_EQ(1U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_smoke_store) {
+  // Construct src and tgt zip files.
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  ASSERT_EQ(0, src_writer.StartEntry("file1.txt", 0));  // Store mode.
+  const std::string src_content("abcdefg");
+  ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
+  ASSERT_EQ(0, src_writer.FinishEntry());
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+  ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", 0));  // Store mode.
+  const std::string tgt_content("abcdefgxyz");
+  ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+  std::string src;
+  ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src));
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect one CHUNK_RAW entry.
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(0U, num_deflate);
+  ASSERT_EQ(1U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_smoke_compressed) {
+  // Generate 1 block of random data.
+  std::string random_data;
+  random_data.reserve(4096);
+  generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; });
+
+  // Construct src and tgt zip files.
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+  const std::string src_content = random_data;
+  ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
+  ASSERT_EQ(0, src_writer.FinishEntry());
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+  ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+  const std::string tgt_content = random_data + "extra contents";
+  ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+  std::string src;
+  ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src));
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer).
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(1U, num_deflate);
+  ASSERT_EQ(2U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_empty_target) {
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+  const std::string src_content = "abcdefg";
+  ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
+  ASSERT_EQ(0, src_writer.FinishEntry());
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Construct a empty entry in the target zip.
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+  ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+  const std::string tgt_content;
+  ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+  ASSERT_EQ(0, tgt_writer.Finish());
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+  std::string src;
+  ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src));
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) {
+  // Generate 1 block of random data.
+  std::string random_data;
+  random_data.reserve(4096);
+  generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; });
+
+  // Construct src and tgt zip files.
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+  const std::string src_content = random_data;
+  ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
+  ASSERT_EQ(0, src_writer.FinishEntry());
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+  ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+  const std::string tgt_content = random_data + "abcdefg";
+  ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+  ASSERT_EQ(0, tgt_writer.Finish());
+  // Add trailing zeros to the target zip file.
+  std::vector<uint8_t> zeros(10);
+  ASSERT_EQ(zeros.size(), fwrite(zeros.data(), sizeof(uint8_t), zeros.size(), tgt_file_ptr));
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+  std::string src;
+  ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src));
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer).
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(1U, num_deflate);
+  ASSERT_EQ(2U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, image_mode_simple) {
+  std::string gzipped_source_path = from_testdata_base("gzipped_source");
+  std::string gzipped_source;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source));
+
+  const std::string src = "abcdefg" + gzipped_source;
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  std::string gzipped_target_path = from_testdata_base("gzipped_target");
+  std::string gzipped_target;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target));
+  const std::string tgt = "abcdefgxyz" + gzipped_target;
+
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer).
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(1U, num_deflate);
+  ASSERT_EQ(2U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, image_mode_bad_gzip) {
+  // Modify the uncompressed length in the gzip footer.
+  const std::vector<char> src_data = { 'a',    'b',    'c',    'd',    'e',    'f',    'g',
+                                       'h',    '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
+                                       '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac',
+                                       '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03',
+                                       '\xff', '\xff', '\xff' };
+  const std::string src(src_data.cbegin(), src_data.cend());
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // Modify the uncompressed length in the gzip footer.
+  const std::vector<char> tgt_data = {
+    'a',    'b',    'c',    'd',    'e',    'f',    'g',    'x',    'y',    'z',    '\x1f', '\x8b',
+    '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac',
+    '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\xff', '\xff', '\xff'
+  };
+  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, image_mode_different_num_chunks) {
+  // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd) + gzipped "test".
+  const std::vector<char> src_data = {
+    'a',    'b',    'c',    'd',    'e',    'f',    'g',    'h',    '\x1f', '\x8b', '\x08',
+    '\x00', '\xc4', '\x1e', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', '\x02',
+    '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', '\x00', '\x00', '\x00', '\x1f', '\x8b',
+    '\x08', '\x00', '\xb2', '\x3a', '\x53', '\x58', '\x00', '\x03', '\x2b', '\x49', '\x2d',
+    '\x2e', '\x01', '\x00', '\x0c', '\x7e', '\x7f', '\xd8', '\x04', '\x00', '\x00', '\x00'
+  };
+  const std::string src(src_data.cbegin(), src_data.cend());
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // tgt: "abcdefgxyz" + gzipped "xxyyzz".
+  const std::vector<char> tgt_data = {
+    'a',    'b',    'c',    'd',    'e',    'f',    'g',    'x',    'y',    'z',    '\x1f', '\x8b',
+    '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac',
+    '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00'
+  };
+  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(1, imgdiff(args.size(), args.data()));
+}
+
+TEST(ImgdiffTest, image_mode_merge_chunks) {
+  // src: "abcdefg" + gzipped_source.
+  std::string gzipped_source_path = from_testdata_base("gzipped_source");
+  std::string gzipped_source;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source));
+
+  const std::string src = "abcdefg" + gzipped_source;
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // tgt: gzipped_target + "abcdefgxyz".
+  std::string gzipped_target_path = from_testdata_base("gzipped_target");
+  std::string gzipped_target;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target));
+
+  const std::string tgt = gzipped_target + "abcdefgxyz";
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  // Since a gzipped entry will become CHUNK_RAW (header) + CHUNK_DEFLATE (data) +
+  // CHUNK_RAW (footer), they both should contain the same chunk types after merging.
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer).
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(1U, num_deflate);
+  ASSERT_EQ(2U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, image_mode_spurious_magic) {
+  // src: "abcdefgh" + '0x1f8b0b00' + some bytes.
+  const std::vector<char> src_data = { 'a',    'b',    'c',    'd',    'e',    'f',    'g',
+                                       'h',    '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
+                                       '\x53', '\x58', 't',    'e',    's',    't' };
+  const std::string src(src_data.cbegin(), src_data.cend());
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // tgt: "abcdefgxyz".
+  const std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' };
+  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect one CHUNK_RAW (header) entry.
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(0U, num_deflate);
+  ASSERT_EQ(1U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, image_mode_short_input1) {
+  // src: "abcdefgh" + '0x1f8b0b'.
+  const std::vector<char> src_data = { 'a', 'b', 'c',    'd',    'e',   'f',
+                                       'g', 'h', '\x1f', '\x8b', '\x08' };
+  const std::string src(src_data.cbegin(), src_data.cend());
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // tgt: "abcdefgxyz".
+  const std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' };
+  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect one CHUNK_RAW (header) entry.
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(0U, num_deflate);
+  ASSERT_EQ(1U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, image_mode_short_input2) {
+  // src: "abcdefgh" + '0x1f8b0b00'.
+  const std::vector<char> src_data = { 'a', 'b', 'c',    'd',    'e',    'f',
+                                       'g', 'h', '\x1f', '\x8b', '\x08', '\x00' };
+  const std::string src(src_data.cbegin(), src_data.cend());
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // tgt: "abcdefgxyz".
+  const std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' };
+  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect one CHUNK_RAW (header) entry.
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(0U, num_normal);
+  ASSERT_EQ(0U, num_deflate);
+  ASSERT_EQ(1U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgdiffTest, image_mode_single_entry_long) {
+  // src: "abcdefgh" + '0x1f8b0b00' + some bytes.
+  const std::vector<char> src_data = { 'a',    'b',    'c',    'd',    'e',    'f',    'g',
+                                       'h',    '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
+                                       '\x53', '\x58', 't',    'e',    's',    't' };
+  const std::string src(src_data.cbegin(), src_data.cend());
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // tgt: "abcdefgxyz" + 200 bytes.
+  std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' };
+  tgt_data.resize(tgt_data.size() + 200);
+
+  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  // Expect one CHUNK_NORMAL entry, since it's exceeding the 160-byte limit for RAW.
+  size_t num_normal;
+  size_t num_raw;
+  size_t num_deflate;
+  verify_patch_header(patch, &num_normal, &num_raw, &num_deflate);
+  ASSERT_EQ(1U, num_normal);
+  ASSERT_EQ(0U, num_deflate);
+  ASSERT_EQ(0U, num_raw);
+
+  verify_patched_image(src, patch, tgt);
+}
+
+TEST(ImgpatchTest, image_mode_patch_corruption) {
+  // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd).
+  const std::vector<char> src_data = { 'a',    'b',    'c',    'd',    'e',    'f',    'g',
+                                       'h',    '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
+                                       '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac',
+                                       '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03',
+                                       '\x00', '\x00', '\x00' };
+  const std::string src(src_data.cbegin(), src_data.cend());
+  TemporaryFile src_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
+
+  // tgt: "abcdefgxyz" + gzipped "xxyyzz".
+  const std::vector<char> tgt_data = {
+    'a',    'b',    'c',    'd',    'e',    'f',    'g',    'x',    'y',    'z',    '\x1f', '\x8b',
+    '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac',
+    '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00'
+  };
+  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  TemporaryFile tgt_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
+
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+  verify_patched_image(src, patch, tgt);
+
+  // Corrupt the end of the patch and expect the ApplyImagePatch to fail.
+  patch.insert(patch.end() - 10, 10, '0');
+  ASSERT_EQ(-1, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(),
+                                reinterpret_cast<const unsigned char*>(patch.data()), patch.size(),
+                                [](const unsigned char* /*data*/, size_t len) { return len; }));
+}
+
+static void construct_store_entry(const std::vector<std::tuple<std::string, size_t, char>>& info,
+                                  ZipWriter* writer) {
+  for (auto& t : info) {
+    // Create t(1) blocks of t(2), and write the data to t(0)
+    ASSERT_EQ(0, writer->StartEntry(std::get<0>(t).c_str(), 0));
+    const std::string content(std::get<1>(t) * 4096, std::get<2>(t));
+    ASSERT_EQ(0, writer->WriteBytes(content.data(), content.size()));
+    ASSERT_EQ(0, writer->FinishEntry());
+  }
+}
+
+static void construct_deflate_entry(const std::vector<std::tuple<std::string, size_t, size_t>>& info,
+                                    ZipWriter* writer, const std::string& data) {
+  for (auto& t : info) {
+    // t(0): entry_name; t(1): block offset; t(2) length in blocks.
+    ASSERT_EQ(0, writer->StartEntry(std::get<0>(t).c_str(), ZipWriter::kCompress));
+    ASSERT_EQ(0, writer->WriteBytes(data.data() + std::get<1>(t) * 4096, std::get<2>(t) * 4096));
+    ASSERT_EQ(0, writer->FinishEntry());
+  }
+}
+
+// Look for the source and patch pieces in debug_dir. Generate a target piece from each pair.
+// Concatenate all the target pieces and match against the orignal one. Used pieces in debug_dir
+// will be cleaned up.
+static void GenerateAndCheckSplitTarget(const std::string& debug_dir, size_t count,
+                                        const std::string& tgt) {
+  std::string patched;
+  for (size_t i = 0; i < count; i++) {
+    std::string split_src_path = android::base::StringPrintf("%s/src-%zu", debug_dir.c_str(), i);
+    std::string split_src;
+    ASSERT_TRUE(android::base::ReadFileToString(split_src_path, &split_src));
+    ASSERT_EQ(0, unlink(split_src_path.c_str()));
+
+    std::string split_patch_path =
+        android::base::StringPrintf("%s/patch-%zu", debug_dir.c_str(), i);
+    std::string split_patch;
+    ASSERT_TRUE(android::base::ReadFileToString(split_patch_path, &split_patch));
+    ASSERT_EQ(0, unlink(split_patch_path.c_str()));
+
+    std::string split_tgt;
+    GenerateTarget(split_src, split_patch, &split_tgt);
+    patched += split_tgt;
+  }
+
+  // Verify we can get back the original target image.
+  ASSERT_EQ(tgt, patched);
+}
+
+std::vector<ImageChunk> ConstructImageChunks(
+    const std::vector<uint8_t>& content, const std::vector<std::tuple<std::string, size_t>>& info) {
+  std::vector<ImageChunk> chunks;
+  size_t start = 0;
+  for (const auto& t : info) {
+    size_t length = std::get<1>(t);
+    chunks.emplace_back(CHUNK_NORMAL, start, &content, length, std::get<0>(t));
+    start += length;
+  }
+
+  return chunks;
+}
+
+TEST(ImgdiffTest, zip_mode_split_image_smoke) {
+  std::vector<uint8_t> content;
+  content.reserve(4096 * 50);
+  uint8_t n = 0;
+  generate_n(back_inserter(content), 4096 * 50, [&n]() { return n++ / 4096; });
+
+  ZipModeImage tgt_image(false, 4096 * 10);
+  std::vector<ImageChunk> tgt_chunks = ConstructImageChunks(content, { { "a", 100 },
+                                                                       { "b", 4096 * 2 },
+                                                                       { "c", 4096 * 3 },
+                                                                       { "d", 300 },
+                                                                       { "e-0", 4096 * 10 },
+                                                                       { "e-1", 4096 * 5 },
+                                                                       { "CD", 200 } });
+  tgt_image.Initialize(std::move(tgt_chunks),
+                       std::vector<uint8_t>(content.begin(), content.begin() + 82520));
+
+  tgt_image.DumpChunks();
+
+  ZipModeImage src_image(true, 4096 * 10);
+  std::vector<ImageChunk> src_chunks = ConstructImageChunks(content, { { "b", 4096 * 3 },
+                                                                       { "c-0", 4096 * 10 },
+                                                                       { "c-1", 4096 * 2 },
+                                                                       { "a", 4096 * 5 },
+                                                                       { "e-0", 4096 * 10 },
+                                                                       { "e-1", 10000 },
+                                                                       { "CD", 5000 } });
+  src_image.Initialize(std::move(src_chunks),
+                       std::vector<uint8_t>(content.begin(), content.begin() + 137880));
+
+  std::vector<ZipModeImage> split_tgt_images;
+  std::vector<ZipModeImage> split_src_images;
+  std::vector<SortedRangeSet> split_src_ranges;
+
+  ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images,
+                                           &split_src_images, &split_src_ranges);
+
+  // src_piece 1: a 5 blocks, b 3 blocks
+  // src_piece 2: c-0 10 blocks
+  // src_piece 3: d 0 block, e-0 10 blocks
+  // src_piece 4: e-1 2 blocks; CD 2 blocks
+  ASSERT_EQ(split_tgt_images.size(), split_src_images.size());
+  ASSERT_EQ(static_cast<size_t>(4), split_tgt_images.size());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[0].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(12288), split_tgt_images[0][0].DataLengthForPatch());
+  ASSERT_EQ("4,0,3,15,20", split_src_ranges[0].ToString());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[1].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(12288), split_tgt_images[1][0].DataLengthForPatch());
+  ASSERT_EQ("2,3,13", split_src_ranges[1].ToString());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[2].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(40960), split_tgt_images[2][0].DataLengthForPatch());
+  ASSERT_EQ("2,20,30", split_src_ranges[2].ToString());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[3].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(16984), split_tgt_images[3][0].DataLengthForPatch());
+  ASSERT_EQ("2,30,34", split_src_ranges[3].ToString());
+}
+
+TEST(ImgdiffTest, zip_mode_store_large_apk) {
+  // Construct src and tgt zip files with limit = 10 blocks.
+  //     src              tgt
+  //  12 blocks 'd'    3 blocks  'a'
+  //  8 blocks  'c'    3 blocks  'b'
+  //  3 blocks  'b'    8 blocks  'c' (exceeds limit)
+  //  3 blocks  'a'    12 blocks 'd' (exceeds limit)
+  //                   3 blocks  'e'
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+  construct_store_entry(
+      { { "a", 3, 'a' }, { "b", 3, 'b' }, { "c", 8, 'c' }, { "d", 12, 'd' }, { "e", 3, 'e' } },
+      &tgt_writer);
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry({ { "d", 12, 'd' }, { "c", 8, 'c' }, { "b", 3, 'b' }, { "a", 3, 'a' } },
+                        &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff", "-z", "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect 4 pieces of patch. (Roughly 3'a',3'b'; 8'c'; 10'd'; 2'd'3'e')
+  GenerateAndCheckSplitTarget(debug_dir.path, 4, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_deflate_large_apk) {
+  // Src and tgt zip files are constructed as follows.
+  //     src               tgt
+  //  22 blocks, "d"    4  blocks,  "a"
+  //  5 blocks,  "b"    4  blocks,  "b"
+  //  3 blocks,  "a"    8  blocks,  "c" (exceeds limit)
+  //  1 block,   "g"    20 blocks,  "d" (exceeds limit)
+  //  8 blocks,  "c"    2  blocks,  "e"
+  //  1 block,   "f"    1  block ,  "f"
+  std::string tgt_path = from_testdata_base("deflate_tgt.zip");
+  std::string src_path = from_testdata_base("deflate_src.zip");
+
+  ZipModeImage src_image(true, 10 * 4096);
+  ZipModeImage tgt_image(false, 10 * 4096);
+  ASSERT_TRUE(src_image.Initialize(src_path));
+  ASSERT_TRUE(tgt_image.Initialize(tgt_path));
+  ASSERT_TRUE(ZipModeImage::CheckAndProcessChunks(&tgt_image, &src_image));
+
+  src_image.DumpChunks();
+  tgt_image.DumpChunks();
+
+  std::vector<ZipModeImage> split_tgt_images;
+  std::vector<ZipModeImage> split_src_images;
+  std::vector<SortedRangeSet> split_src_ranges;
+  ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images,
+                                           &split_src_images, &split_src_ranges);
+
+  // Expected split images with limit = 10 blocks.
+  // src_piece 0: a 3 blocks, b 5 blocks
+  // src_piece 1: c 8 blocks
+  // src_piece 2: d-0 10 block
+  // src_piece 3: d-1 10 blocks
+  // src_piece 4: e 1 block, CD
+  ASSERT_EQ(split_tgt_images.size(), split_src_images.size());
+  ASSERT_EQ(static_cast<size_t>(5), split_tgt_images.size());
+
+  ASSERT_EQ(static_cast<size_t>(2), split_src_images[0].NumOfChunks());
+  ASSERT_EQ("a", split_src_images[0][0].GetEntryName());
+  ASSERT_EQ("b", split_src_images[0][1].GetEntryName());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_src_images[1].NumOfChunks());
+  ASSERT_EQ("c", split_src_images[1][0].GetEntryName());
+
+  ASSERT_EQ(static_cast<size_t>(0), split_src_images[2].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(0), split_src_images[3].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(0), split_src_images[4].NumOfChunks());
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  ASSERT_TRUE(ZipModeImage::GeneratePatches(split_tgt_images, split_src_images, split_src_ranges,
+                                            patch_file.path, split_info_file.path, debug_dir.path));
+
+  // Verify the content of split info.
+  // Expect 5 pieces of patch. ["a","b"; "c"; "d-0"; "d-1"; "e"]
+  std::string split_info_string;
+  android::base::ReadFileToString(split_info_file.path, &split_info_string);
+  std::vector<std::string> info_list =
+      android::base::Split(android::base::Trim(split_info_string), "\n");
+
+  ASSERT_EQ(static_cast<size_t>(7), info_list.size());
+  ASSERT_EQ("2", android::base::Trim(info_list[0]));
+  ASSERT_EQ("5", android::base::Trim(info_list[1]));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_path, &tgt));
+  ASSERT_EQ(static_cast<size_t>(160385), tgt.size());
+  std::vector<std::string> tgt_file_ranges = {
+    "36864 2,22,31", "32768 2,31,40", "40960 2,0,11", "40960 2,11,21", "8833 4,21,22,40,41",
+  };
+
+  for (size_t i = 0; i < 5; i++) {
+    struct stat st;
+    std::string path = android::base::StringPrintf("%s/patch-%zu", debug_dir.path, i);
+    ASSERT_EQ(0, stat(path.c_str(), &st));
+    ASSERT_EQ(std::to_string(st.st_size) + " " + tgt_file_ranges[i],
+              android::base::Trim(info_list[i + 2]));
+  }
+
+  GenerateAndCheckSplitTarget(debug_dir.path, 5, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_no_match_source) {
+  // Generate 20 blocks of random data.
+  std::string random_data;
+  random_data.reserve(4096 * 20);
+  generate_n(back_inserter(random_data), 4096 * 20, []() { return rand() % 256; });
+
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  construct_deflate_entry({ { "a", 0, 4 }, { "b", 5, 5 }, { "c", 11, 5 } }, &tgt_writer,
+                          random_data);
+
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  // We don't have a matching source entry.
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry({ { "d", 1, 'd' } }, &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff", "-z", "--block-limit=10", debug_dir_arg.c_str(), split_info_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect 1 pieces of patch due to no matching source entry.
+  GenerateAndCheckSplitTarget(debug_dir.path, 1, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_large_enough_limit) {
+  // Generate 20 blocks of random data.
+  std::string random_data;
+  random_data.reserve(4096 * 20);
+  generate_n(back_inserter(random_data), 4096 * 20, []() { return rand() % 256; });
+
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  construct_deflate_entry({ { "a", 0, 10 }, { "b", 10, 5 } }, &tgt_writer, random_data);
+
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  // Construct 10 blocks of source.
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_deflate_entry({ { "a", 1, 10 } }, &src_writer, random_data);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch with a limit of 20 blocks.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff", "-z", "--block-limit=20", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect 1 piece of patch since limit is larger than the zip file size.
+  GenerateAndCheckSplitTarget(debug_dir.path, 1, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_large_apk_small_target_chunk) {
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  // The first entry is less than 4096 bytes, followed immediately by an entry that has a very
+  // large counterpart in the source file. Therefore the first entry will be patched separately.
+  std::string small_chunk("a", 2000);
+  ASSERT_EQ(0, tgt_writer.StartEntry("a", 0));
+  ASSERT_EQ(0, tgt_writer.WriteBytes(small_chunk.data(), small_chunk.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+  construct_store_entry(
+      {
+          { "b", 12, 'b' }, { "c", 3, 'c' },
+      },
+      &tgt_writer);
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry({ { "a", 1, 'a' }, { "b", 13, 'b' }, { "c", 1, 'c' } }, &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff",     "-z",          "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect three split src images:
+  // src_piece 0: a 1 blocks
+  // src_piece 1: b-0 10 blocks
+  // src_piece 2: b-1 3 blocks, c 1 blocks, CD
+  GenerateAndCheckSplitTarget(debug_dir.path, 3, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_large_apk_skipped_small_target_chunk) {
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  construct_store_entry(
+      {
+          { "a", 11, 'a' },
+      },
+      &tgt_writer);
+
+  // Construct a tiny target entry of 1 byte, which will be skipped due to the tail alignment of
+  // the previous entry.
+  std::string small_chunk("b", 1);
+  ASSERT_EQ(0, tgt_writer.StartEntry("b", 0));
+  ASSERT_EQ(0, tgt_writer.WriteBytes(small_chunk.data(), small_chunk.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry(
+      {
+          { "a", 11, 'a' }, { "b", 11, 'b' },
+      },
+      &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff",     "-z",          "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect two split src images:
+  // src_piece 0: a-0 10 blocks
+  // src_piece 1: a-0 1 block, CD
+  GenerateAndCheckSplitTarget(debug_dir.path, 2, tgt);
+}
diff --git a/tests/unit/install_test.cpp b/tests/unit/install_test.cpp
new file mode 100644
index 0000000..36820f8
--- /dev/null
+++ b/tests/unit/install_test.cpp
@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2017 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 agree 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.
+ */
+
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <random>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <vintf/VintfObjectRecovery.h>
+#include <ziparchive/zip_archive.h>
+#include <ziparchive/zip_writer.h>
+
+#include "install/install.h"
+#include "install/wipe_device.h"
+#include "otautil/paths.h"
+#include "private/setup_commands.h"
+
+static void BuildZipArchive(const std::map<std::string, std::string>& file_map, int fd,
+                            int compression_type) {
+  FILE* zip_file = fdopen(fd, "w");
+  ZipWriter writer(zip_file);
+  for (const auto& [name, content] : file_map) {
+    ASSERT_EQ(0, writer.StartEntry(name.c_str(), compression_type));
+    ASSERT_EQ(0, writer.WriteBytes(content.data(), content.size()));
+    ASSERT_EQ(0, writer.FinishEntry());
+  }
+  ASSERT_EQ(0, writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file));
+}
+
+TEST(InstallTest, verify_package_compatibility_no_entry) {
+  TemporaryFile temp_file;
+  // The archive must have something to be opened correctly.
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
+
+  // Doesn't contain compatibility zip entry.
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  ASSERT_TRUE(verify_package_compatibility(zip));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, verify_package_compatibility_invalid_entry) {
+  TemporaryFile temp_file;
+  BuildZipArchive({ { "compatibility.zip", "" } }, temp_file.release(), kCompressStored);
+
+  // Empty compatibility zip entry.
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  ASSERT_FALSE(verify_package_compatibility(zip));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, read_metadata_from_package_smoke) {
+  TemporaryFile temp_file;
+  const std::string content("abc=defg");
+  BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file.release(),
+                  kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  ASSERT_EQ("defg", metadata["abc"]);
+  CloseArchive(zip);
+
+  TemporaryFile temp_file2;
+  BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file2.release(),
+                  kCompressDeflated);
+
+  ASSERT_EQ(0, OpenArchive(temp_file2.path, &zip));
+  metadata.clear();
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  ASSERT_EQ("defg", metadata["abc"]);
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, read_metadata_from_package_no_entry) {
+  TemporaryFile temp_file;
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  std::map<std::string, std::string> metadata;
+  ASSERT_FALSE(ReadMetadataFromPackage(zip, &metadata));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, read_wipe_ab_partition_list) {
+  std::vector<std::string> partition_list = {
+    "/dev/block/bootdevice/by-name/system_a", "/dev/block/bootdevice/by-name/system_b",
+    "/dev/block/bootdevice/by-name/vendor_a", "/dev/block/bootdevice/by-name/vendor_b",
+    "/dev/block/bootdevice/by-name/userdata", "# Wipe the boot partitions last",
+    "/dev/block/bootdevice/by-name/boot_a",   "/dev/block/bootdevice/by-name/boot_b",
+  };
+  TemporaryFile temp_file;
+  BuildZipArchive({ { "recovery.wipe", android::base::Join(partition_list, '\n') } },
+                  temp_file.release(), kCompressDeflated);
+  std::string wipe_package;
+  ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &wipe_package));
+
+  auto package = Package::CreateMemoryPackage(
+      std::vector<uint8_t>(wipe_package.begin(), wipe_package.end()), nullptr);
+
+  auto read_partition_list = GetWipePartitionList(package.get());
+  std::vector<std::string> expected = {
+    "/dev/block/bootdevice/by-name/system_a", "/dev/block/bootdevice/by-name/system_b",
+    "/dev/block/bootdevice/by-name/vendor_a", "/dev/block/bootdevice/by-name/vendor_b",
+    "/dev/block/bootdevice/by-name/userdata", "/dev/block/bootdevice/by-name/boot_a",
+    "/dev/block/bootdevice/by-name/boot_b",
+  };
+  ASSERT_EQ(expected, read_partition_list);
+}
+
+TEST(InstallTest, verify_package_compatibility_with_libvintf_malformed_xml) {
+  TemporaryFile compatibility_zip_file;
+  std::string malformed_xml = "malformed";
+  BuildZipArchive({ { "system_manifest.xml", malformed_xml } }, compatibility_zip_file.release(),
+                  kCompressDeflated);
+
+  TemporaryFile temp_file;
+  std::string compatibility_zip_content;
+  ASSERT_TRUE(
+      android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content));
+  BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(),
+                  kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  std::vector<std::string> compatibility_info;
+  compatibility_info.push_back(malformed_xml);
+  // Malformed compatibility zip is expected to be rejected by libvintf. But we defer that to
+  // libvintf.
+  std::string err;
+  bool result =
+      android::vintf::VintfObjectRecovery::CheckCompatibility(compatibility_info, &err) == 0;
+  ASSERT_EQ(result, verify_package_compatibility(zip));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, verify_package_compatibility_with_libvintf_system_manifest_xml) {
+  static constexpr const char* system_manifest_xml_path = "/system/manifest.xml";
+  if (access(system_manifest_xml_path, R_OK) == -1) {
+    GTEST_LOG_(INFO) << "Test skipped on devices w/o /system/manifest.xml.";
+    return;
+  }
+  std::string system_manifest_xml_content;
+  ASSERT_TRUE(
+      android::base::ReadFileToString(system_manifest_xml_path, &system_manifest_xml_content));
+  TemporaryFile compatibility_zip_file;
+  BuildZipArchive({ { "system_manifest.xml", system_manifest_xml_content } },
+                  compatibility_zip_file.release(), kCompressDeflated);
+
+  TemporaryFile temp_file;
+  std::string compatibility_zip_content;
+  ASSERT_TRUE(
+      android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content));
+  BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(),
+                  kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  std::vector<std::string> compatibility_info;
+  compatibility_info.push_back(system_manifest_xml_content);
+  std::string err;
+  bool result =
+      android::vintf::VintfObjectRecovery::CheckCompatibility(compatibility_info, &err) == 0;
+  // Make sure the result is consistent with libvintf library.
+  ASSERT_EQ(result, verify_package_compatibility(zip));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, SetUpNonAbUpdateCommands) {
+  TemporaryFile temp_file;
+  static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary";
+  BuildZipArchive({ { UPDATE_BINARY_NAME, "" } }, temp_file.release(), kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  int status_fd = 10;
+  std::string package = "/path/to/update.zip";
+  TemporaryDir td;
+  std::string binary_path = std::string(td.path) + "/update_binary";
+  Paths::Get().set_temporary_update_binary(binary_path);
+  std::vector<std::string> cmd;
+  ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd));
+  ASSERT_EQ(4U, cmd.size());
+  ASSERT_EQ(binary_path, cmd[0]);
+  ASSERT_EQ("3", cmd[1]);  // RECOVERY_API_VERSION
+  ASSERT_EQ(std::to_string(status_fd), cmd[2]);
+  ASSERT_EQ(package, cmd[3]);
+  struct stat sb;
+  ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
+  ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+
+  // With non-zero retry count. update_binary will be removed automatically.
+  cmd.clear();
+  ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 2, status_fd, &cmd));
+  ASSERT_EQ(5U, cmd.size());
+  ASSERT_EQ(binary_path, cmd[0]);
+  ASSERT_EQ("3", cmd[1]);  // RECOVERY_API_VERSION
+  ASSERT_EQ(std::to_string(status_fd), cmd[2]);
+  ASSERT_EQ(package, cmd[3]);
+  ASSERT_EQ("retry", cmd[4]);
+  sb = {};
+  ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
+  ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) {
+  TemporaryFile temp_file;
+  // The archive must have something to be opened correctly.
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
+
+  // Missing update binary.
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  int status_fd = 10;
+  std::string package = "/path/to/update.zip";
+  TemporaryDir td;
+  Paths::Get().set_temporary_update_binary(std::string(td.path) + "/update_binary");
+  std::vector<std::string> cmd;
+  ASSERT_EQ(INSTALL_CORRUPT, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd));
+  CloseArchive(zip);
+}
+
+static void VerifyAbUpdateCommands(const std::string& serialno, bool success = true) {
+  TemporaryFile temp_file;
+
+  const std::string properties = "some_properties";
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+  std::string timestamp = android::base::GetProperty("ro.build.date.utc", "");
+  ASSERT_NE("", timestamp);
+
+  std::vector<std::string> meta{ "ota-type=AB", "pre-device=" + device,
+                                 "post-timestamp=" + timestamp };
+  if (!serialno.empty()) {
+    meta.push_back("serialno=" + serialno);
+  }
+  std::string metadata_string = android::base::Join(meta, "\n");
+
+  BuildZipArchive({ { "payload.bin", "" },
+                    { "payload_properties.txt", properties },
+                    { "META-INF/com/android/metadata", metadata_string } },
+                  temp_file.release(), kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  ZipString payload_name("payload.bin");
+  ZipEntry payload_entry;
+  ASSERT_EQ(0, FindEntry(zip, payload_name, &payload_entry));
+
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  if (success) {
+    ASSERT_EQ(0, CheckPackageMetadata(metadata, OtaType::AB));
+
+    int status_fd = 10;
+    std::string package = "/path/to/update.zip";
+    std::vector<std::string> cmd;
+    ASSERT_EQ(0, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
+    ASSERT_EQ(5U, cmd.size());
+    ASSERT_EQ("/system/bin/update_engine_sideload", cmd[0]);
+    ASSERT_EQ("--payload=file://" + package, cmd[1]);
+    ASSERT_EQ("--offset=" + std::to_string(payload_entry.offset), cmd[2]);
+    ASSERT_EQ("--headers=" + properties, cmd[3]);
+    ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]);
+  } else {
+    ASSERT_EQ(INSTALL_ERROR, CheckPackageMetadata(metadata, OtaType::AB));
+  }
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, SetUpAbUpdateCommands) {
+  // Empty serialno will pass the verification.
+  VerifyAbUpdateCommands({});
+}
+
+TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) {
+  TemporaryFile temp_file;
+
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+  std::string timestamp = android::base::GetProperty("ro.build.date.utc", "");
+  ASSERT_NE("", timestamp);
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB", "pre-device=" + device, "post-timestamp=" + timestamp,
+      },
+      "\n");
+
+  BuildZipArchive(
+      {
+          { "payload.bin", "" },
+          { "META-INF/com/android/metadata", metadata },
+      },
+      temp_file.release(), kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  int status_fd = 10;
+  std::string package = "/path/to/update.zip";
+  std::vector<std::string> cmd;
+  ASSERT_EQ(INSTALL_CORRUPT, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, SetUpAbUpdateCommands_MultipleSerialnos) {
+  std::string serialno = android::base::GetProperty("ro.serialno", "");
+  ASSERT_NE("", serialno);
+
+  // Single matching serialno will pass the verification.
+  VerifyAbUpdateCommands(serialno);
+
+  static constexpr char alphabet[] =
+      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+  auto generator = []() { return alphabet[rand() % (sizeof(alphabet) - 1)]; };
+
+  // Generate 900 random serial numbers.
+  std::string random_serialno;
+  for (size_t i = 0; i < 900; i++) {
+    generate_n(back_inserter(random_serialno), serialno.size(), generator);
+    random_serialno.append("|");
+  }
+  // Random serialnos should fail the verification.
+  VerifyAbUpdateCommands(random_serialno, false);
+
+  std::string long_serialno = random_serialno + serialno + "|";
+  for (size_t i = 0; i < 99; i++) {
+    generate_n(back_inserter(long_serialno), serialno.size(), generator);
+    long_serialno.append("|");
+  }
+  // String with the matching serialno should pass the verification.
+  VerifyAbUpdateCommands(long_serialno);
+}
+
+static void test_check_package_metadata(const std::string& metadata_string, OtaType ota_type,
+                                        int exptected_result) {
+  TemporaryFile temp_file;
+  BuildZipArchive(
+      {
+          { "META-INF/com/android/metadata", metadata_string },
+      },
+      temp_file.release(), kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  ASSERT_EQ(exptected_result, CheckPackageMetadata(metadata, ota_type));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ota_type) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  // ota-type must be present
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "pre-device=" + device,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  // Checks if ota-type matches
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_device_type) {
+  // device type can not be empty
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+
+  // device type mismatches
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=dummy_device_type",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_serial_number_smoke) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  // Serial number doesn't need to exist
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, 0);
+
+  // Serial number mismatches
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=dummy_serial",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+
+  std::string serialno = android::base::GetProperty("ro.serialno", "");
+  ASSERT_NE("", serialno);
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=" + serialno,
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, 0);
+}
+
+TEST(InstallTest, CheckPackageMetadata_multiple_serial_number) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  std::string serialno = android::base::GetProperty("ro.serialno", "");
+  ASSERT_NE("", serialno);
+
+  std::vector<std::string> serial_numbers;
+  // Creates a dummy serial number string.
+  for (char c = 'a'; c <= 'z'; c++) {
+    serial_numbers.emplace_back(serialno.size(), c);
+  }
+
+  // No matched serialno found.
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=" + android::base::Join(serial_numbers, '|'),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+
+  serial_numbers.emplace_back(serialno);
+  std::shuffle(serial_numbers.begin(), serial_numbers.end(), std::default_random_engine());
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=" + android::base::Join(serial_numbers, '|'),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, 0);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ab_build_version) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  std::string build_version = android::base::GetProperty("ro.build.version.incremental", "");
+  ASSERT_NE("", build_version);
+
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build-incremental=" + build_version,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build-incremental=dummy_build",
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ab_fingerprint) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  std::string finger_print = android::base::GetProperty("ro.build.fingerprint", "");
+  ASSERT_NE("", finger_print);
+
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build=" + finger_print,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build=dummy_build_fingerprint",
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ab_post_timestamp) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  // post timestamp is required for upgrade.
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  // post timestamp should be larger than the timestamp on device.
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=0",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  // fingerprint is required for downgrade
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=0",
+          "ota-downgrade=yes",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  std::string finger_print = android::base::GetProperty("ro.build.fingerprint", "");
+  ASSERT_NE("", finger_print);
+
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=0",
+          "pre-build=" + finger_print,
+          "ota-downgrade=yes",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+}
diff --git a/tests/unit/resources_test.cpp b/tests/unit/resources_test.cpp
index c3f7271..3027443 100644
--- a/tests/unit/resources_test.cpp
+++ b/tests/unit/resources_test.cpp
@@ -14,12 +14,62 @@
  * limitations under the License.
  */
 
-#include <string>
+#include <dirent.h>
+#include <stdio.h>
+#include <stdlib.h>
 
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
 #include <gtest/gtest.h>
+#include <png.h>
 
 #include "common/test_constants.h"
 #include "minui/minui.h"
+#include "private/resources.h"
+
+static const std::string kLocale = "zu";
+
+static const std::vector<std::string> kResourceImagesDirs{
+  "res-mdpi/images/",   "res-hdpi/images/",    "res-xhdpi/images/",
+  "res-xxhdpi/images/", "res-xxxhdpi/images/",
+};
+
+static int png_filter(const dirent* de) {
+  if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) {
+    return 0;
+  }
+  return 1;
+}
+
+// Finds out all the PNG files to test, which stay under the same dir with the executabl..
+static std::vector<std::string> add_files() {
+  std::vector<std::string> files;
+  for (const std::string& images_dir : kResourceImagesDirs) {
+    static std::string exec_dir = android::base::GetExecutableDirectory();
+    std::string dir_path = exec_dir + "/" + images_dir;
+    dirent** namelist;
+    int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort);
+    if (n == -1) {
+      printf("Failed to scandir %s: %s\n", dir_path.c_str(), strerror(errno));
+      continue;
+    }
+    if (n == 0) {
+      printf("No file is added for test in %s\n", dir_path.c_str());
+    }
+
+    while (n--) {
+      std::string file_path = dir_path + namelist[n]->d_name;
+      files.push_back(file_path);
+      free(namelist[n]);
+    }
+    free(namelist);
+  }
+  return files;
+}
 
 TEST(ResourcesTest, res_create_multi_display_surface) {
   GRSurface** frames;
@@ -35,3 +85,52 @@
   }
   free(frames);
 }
+
+class ResourcesTest : public testing::TestWithParam<std::string> {
+ public:
+  static std::vector<std::string> png_list;
+
+ protected:
+  void SetUp() override {
+    png_ = std::make_unique<PngHandler>(GetParam());
+    ASSERT_TRUE(png_);
+
+    ASSERT_EQ(PNG_COLOR_TYPE_GRAY, png_->color_type()) << "Recovery expects grayscale PNG file.";
+    ASSERT_LT(static_cast<png_uint_32>(5), png_->width());
+    ASSERT_LT(static_cast<png_uint_32>(0), png_->height());
+    ASSERT_EQ(1, png_->channels()) << "Recovery background text images expects 1-channel PNG file.";
+  }
+
+  std::unique_ptr<PngHandler> png_{ nullptr };
+};
+
+// Parses a png file and tests if it's qualified for the background text image under recovery.
+TEST_P(ResourcesTest, ValidateLocale) {
+  std::vector<unsigned char> row(png_->width());
+  for (png_uint_32 y = 0; y < png_->height(); ++y) {
+    png_read_row(png_->png_ptr(), row.data(), nullptr);
+    int w = (row[1] << 8) | row[0];
+    int h = (row[3] << 8) | row[2];
+    int len = row[4];
+    EXPECT_LT(0, w);
+    EXPECT_LT(0, h);
+    EXPECT_LT(0, len) << "Locale string should be non-empty.";
+    EXPECT_NE(0, row[5]) << "Locale string is missing.";
+
+    ASSERT_GE(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file.";
+    char* loc = reinterpret_cast<char*>(&row[5]);
+    if (matches_locale(loc, kLocale.c_str())) {
+      EXPECT_TRUE(android::base::StartsWith(loc, kLocale));
+      break;
+    }
+    for (int i = 0; i < h; ++i, ++y) {
+      png_read_row(png_->png_ptr(), row.data(), nullptr);
+    }
+  }
+}
+
+std::vector<std::string> ResourcesTest::png_list = add_files();
+
+INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourcesTest,
+                        ::testing::ValuesIn(ResourcesTest::png_list.cbegin(),
+                                            ResourcesTest::png_list.cend()));
diff --git a/tests/unit/uncrypt_test.cpp b/tests/unit/uncrypt_test.cpp
new file mode 100644
index 0000000..e97d589
--- /dev/null
+++ b/tests/unit/uncrypt_test.cpp
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/unique_fd.h>
+#include <bootloader_message/bootloader_message.h>
+#include <gtest/gtest.h>
+
+using namespace std::string_literals;
+
+static const std::string UNCRYPT_SOCKET = "/dev/socket/uncrypt";
+static const std::string INIT_SVC_SETUP_BCB = "init.svc.setup-bcb";
+static const std::string INIT_SVC_CLEAR_BCB = "init.svc.clear-bcb";
+static const std::string INIT_SVC_UNCRYPT = "init.svc.uncrypt";
+static constexpr int SOCKET_CONNECTION_MAX_RETRY = 30;
+
+static void StopService() {
+  ASSERT_TRUE(android::base::SetProperty("ctl.stop", "setup-bcb"));
+  ASSERT_TRUE(android::base::SetProperty("ctl.stop", "clear-bcb"));
+  ASSERT_TRUE(android::base::SetProperty("ctl.stop", "uncrypt"));
+
+  bool success = false;
+  for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
+    std::string setup_bcb = android::base::GetProperty(INIT_SVC_SETUP_BCB, "");
+    std::string clear_bcb = android::base::GetProperty(INIT_SVC_CLEAR_BCB, "");
+    std::string uncrypt = android::base::GetProperty(INIT_SVC_UNCRYPT, "");
+    GTEST_LOG_(INFO) << "setup-bcb: [" << setup_bcb << "] clear-bcb: [" << clear_bcb
+                     << "] uncrypt: [" << uncrypt << "]";
+    if (setup_bcb != "running" && clear_bcb != "running" && uncrypt != "running") {
+      success = true;
+      break;
+    }
+    sleep(1);
+  }
+
+  ASSERT_TRUE(success) << "uncrypt service is not available.";
+}
+
+class UncryptTest : public ::testing::Test {
+ protected:
+  UncryptTest() : has_misc(true) {}
+
+  void SetUp() override {
+    std::string err;
+    has_misc = !get_bootloader_message_blk_device(&err).empty();
+  }
+
+  void TearDown() override {
+    // Clear the BCB.
+    if (has_misc) {
+      std::string err;
+      ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err;
+    }
+  }
+
+  void SetupOrClearBcb(bool isSetup, const std::string& message,
+                       const std::string& message_in_bcb) const {
+    // Restart the setup-bcb service.
+    StopService();
+    ASSERT_TRUE(android::base::SetProperty("ctl.start", isSetup ? "setup-bcb" : "clear-bcb"));
+
+    // Test tends to be flaky if proceeding immediately ("Transport endpoint is not connected").
+    sleep(1);
+
+    sockaddr_un un = {};
+    un.sun_family = AF_UNIX;
+    strlcpy(un.sun_path, UNCRYPT_SOCKET.c_str(), sizeof(un.sun_path));
+
+    int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+    ASSERT_NE(-1, sockfd);
+
+    // Connect to the uncrypt socket.
+    bool success = false;
+    for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
+      if (connect(sockfd, reinterpret_cast<sockaddr*>(&un), sizeof(sockaddr_un)) != 0) {
+        success = true;
+        break;
+      }
+      sleep(1);
+    }
+    ASSERT_TRUE(success);
+
+    if (isSetup) {
+      // Send out the BCB message.
+      int length = static_cast<int>(message.size());
+      int length_out = htonl(length);
+      ASSERT_TRUE(android::base::WriteFully(sockfd, &length_out, sizeof(int)))
+          << "Failed to write length: " << strerror(errno);
+      ASSERT_TRUE(android::base::WriteFully(sockfd, message.data(), length))
+          << "Failed to write message: " << strerror(errno);
+    }
+
+    // Check the status code from uncrypt.
+    int status;
+    ASSERT_TRUE(android::base::ReadFully(sockfd, &status, sizeof(int)));
+    ASSERT_EQ(100U, ntohl(status));
+
+    // Ack having received the status code.
+    int code = 0;
+    ASSERT_TRUE(android::base::WriteFully(sockfd, &code, sizeof(int)));
+
+    ASSERT_EQ(0, close(sockfd));
+
+    ASSERT_TRUE(android::base::SetProperty("ctl.stop", isSetup ? "setup-bcb" : "clear-bcb"));
+
+    // Verify the message by reading from BCB directly.
+    bootloader_message boot;
+    std::string err;
+    ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err;
+
+    if (isSetup) {
+      ASSERT_EQ("boot-recovery", std::string(boot.command));
+      ASSERT_EQ(message_in_bcb, std::string(boot.recovery));
+
+      // The rest of the boot.recovery message should be zero'd out.
+      ASSERT_LE(message_in_bcb.size(), sizeof(boot.recovery));
+      size_t left = sizeof(boot.recovery) - message_in_bcb.size();
+      ASSERT_EQ(std::string(left, '\0'), std::string(&boot.recovery[message_in_bcb.size()], left));
+
+      // Clear the BCB.
+      ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err;
+    } else {
+      // All the bytes should be cleared.
+      ASSERT_EQ(std::string(sizeof(boot), '\0'),
+                std::string(reinterpret_cast<const char*>(&boot), sizeof(boot)));
+    }
+  }
+
+  void VerifyBootloaderMessage(const std::string& expected) {
+    std::string err;
+    bootloader_message boot;
+    ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err;
+
+    // Check that we have all the expected bytes.
+    ASSERT_EQ(expected, std::string(reinterpret_cast<const char*>(&boot), sizeof(boot)));
+  }
+
+  bool has_misc;
+};
+
+TEST_F(UncryptTest, setup_bcb) {
+  if (!has_misc) {
+    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
+    return;
+  }
+
+  std::string random_data;
+  random_data.reserve(sizeof(bootloader_message));
+  generate_n(back_inserter(random_data), sizeof(bootloader_message), []() { return rand() % 128; });
+
+  bootloader_message boot;
+  memcpy(&boot, random_data.c_str(), random_data.size());
+
+  std::string err;
+  ASSERT_TRUE(write_bootloader_message(boot, &err)) << "Failed to write BCB: " << err;
+  VerifyBootloaderMessage(random_data);
+
+  ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err;
+  VerifyBootloaderMessage(std::string(sizeof(bootloader_message), '\0'));
+
+  std::string message = "--update_message=abc value";
+  std::string message_in_bcb = "recovery\n--update_message=abc value\n";
+  SetupOrClearBcb(true, message, message_in_bcb);
+
+  SetupOrClearBcb(false, "", "");
+
+  TemporaryFile wipe_package;
+  ASSERT_TRUE(android::base::WriteStringToFile(std::string(345, 'a'), wipe_package.path));
+
+  // It's expected to store a wipe package in /misc, with the package size passed to recovery.
+  message = "--wipe_ab\n--wipe_package="s + wipe_package.path + "\n--reason=wipePackage"s;
+  message_in_bcb = "recovery\n--wipe_ab\n--wipe_package_size=345\n--reason=wipePackage\n";
+  SetupOrClearBcb(true, message, message_in_bcb);
+}
diff --git a/tests/unit/update_verifier_test.cpp b/tests/unit/update_verifier_test.cpp
new file mode 100644
index 0000000..e27e58c
--- /dev/null
+++ b/tests/unit/update_verifier_test.cpp
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include <update_verifier/update_verifier.h>
+
+#include <functional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+#include <google/protobuf/repeated_field.h>
+#include <gtest/gtest.h>
+
+#include "care_map.pb.h"
+
+using namespace std::string_literals;
+
+class UpdateVerifierTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", "");
+    verity_supported = android::base::EqualsIgnoreCase(verity_mode, "enforcing");
+
+    care_map_prefix_ = care_map_dir_.path + "/care_map"s;
+    care_map_pb_ = care_map_dir_.path + "/care_map.pb"s;
+    care_map_txt_ = care_map_dir_.path + "/care_map.txt"s;
+    // Overrides the the care_map_prefix.
+    verifier_.set_care_map_prefix(care_map_prefix_);
+
+    property_id_ = "ro.build.fingerprint";
+    fingerprint_ = android::base::GetProperty(property_id_, "");
+    // Overrides the property_reader if we cannot read the given property on the device.
+    if (fingerprint_.empty()) {
+      fingerprint_ = "mock_fingerprint";
+      verifier_.set_property_reader([](const std::string& /* id */) { return "mock_fingerprint"; });
+    }
+  }
+
+  void TearDown() override {
+    unlink(care_map_pb_.c_str());
+    unlink(care_map_txt_.c_str());
+  }
+
+  // Returns a serialized string of the proto3 message according to the given partition info.
+  std::string ConstructProto(
+      std::vector<std::unordered_map<std::string, std::string>>& partitions) {
+    recovery_update_verifier::CareMap result;
+    for (const auto& partition : partitions) {
+      recovery_update_verifier::CareMap::PartitionInfo info;
+      if (partition.find("name") != partition.end()) {
+        info.set_name(partition.at("name"));
+      }
+      if (partition.find("ranges") != partition.end()) {
+        info.set_ranges(partition.at("ranges"));
+      }
+      if (partition.find("id") != partition.end()) {
+        info.set_id(partition.at("id"));
+      }
+      if (partition.find("fingerprint") != partition.end()) {
+        info.set_fingerprint(partition.at("fingerprint"));
+      }
+
+      *result.add_partitions() = info;
+    }
+
+    return result.SerializeAsString();
+  }
+
+  bool verity_supported;
+  UpdateVerifier verifier_;
+
+  TemporaryDir care_map_dir_;
+  std::string care_map_prefix_;
+  std::string care_map_pb_;
+  std::string care_map_txt_;
+
+  std::string property_id_;
+  std::string fingerprint_;
+};
+
+TEST_F(UpdateVerifierTest, verify_image_no_care_map) {
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_text_format) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::string content = "system\n2,0,1";
+  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+  // CareMap in text format is no longer supported.
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_empty_care_map) {
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_TRUE(verifier_.ParseCareMap());
+  ASSERT_TRUE(verifier_.VerifyPartitions());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "3,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_empty_fingerprint) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_fingerprint_mismatch) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", "unsupported_fingerprint" },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
diff --git a/tests/unit/updater_test.cpp b/tests/unit/updater_test.cpp
new file mode 100644
index 0000000..a0a7b66
--- /dev/null
+++ b/tests/unit/updater_test.cpp
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/properties.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <bootloader_message/bootloader_message.h>
+#include <brotli/encode.h>
+#include <bsdiff/bsdiff.h>
+#include <gtest/gtest.h>
+#include <verity/hash_tree_builder.h>
+#include <ziparchive/zip_archive.h>
+#include <ziparchive/zip_writer.h>
+
+#include "applypatch/applypatch.h"
+#include "common/test_constants.h"
+#include "edify/expr.h"
+#include "otautil/error_code.h"
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
+#include "private/commands.h"
+#include "updater/blockimg.h"
+#include "updater/install.h"
+#include "updater/updater.h"
+
+using namespace std::string_literals;
+
+using PackageEntries = std::unordered_map<std::string, std::string>;
+
+struct selabel_handle* sehandle = nullptr;
+
+static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code,
+                   UpdaterInfo* info = nullptr) {
+  std::unique_ptr<Expr> e;
+  int error_count = 0;
+  ASSERT_EQ(0, ParseString(expr_str, &e, &error_count));
+  ASSERT_EQ(0, error_count);
+
+  State state(expr_str, info);
+
+  std::string result;
+  bool status = Evaluate(&state, e, &result);
+
+  if (expected == nullptr) {
+    ASSERT_FALSE(status);
+  } else {
+    ASSERT_TRUE(status) << "Evaluate() finished with error message: " << state.errmsg;
+    ASSERT_STREQ(expected, result.c_str());
+  }
+
+  // Error code is set in updater/updater.cpp only, by parsing State.errmsg.
+  ASSERT_EQ(kNoError, state.error_code);
+
+  // Cause code should always be available.
+  ASSERT_EQ(cause_code, state.cause_code);
+}
+
+static void BuildUpdatePackage(const PackageEntries& entries, int fd) {
+  FILE* zip_file_ptr = fdopen(fd, "wb");
+  ZipWriter zip_writer(zip_file_ptr);
+
+  for (const auto& entry : entries) {
+    // All the entries are written as STORED.
+    ASSERT_EQ(0, zip_writer.StartEntry(entry.first.c_str(), 0));
+    if (!entry.second.empty()) {
+      ASSERT_EQ(0, zip_writer.WriteBytes(entry.second.data(), entry.second.size()));
+    }
+    ASSERT_EQ(0, zip_writer.FinishEntry());
+  }
+
+  ASSERT_EQ(0, zip_writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file_ptr));
+}
+
+static void RunBlockImageUpdate(bool is_verify, const PackageEntries& entries,
+                                const std::string& image_file, const std::string& result,
+                                CauseCode cause_code = kNoCause) {
+  CHECK(entries.find("transfer_list") != entries.end());
+
+  // Build the update package.
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
+
+  MemMapping map;
+  ASSERT_TRUE(map.MapFile(zip_file.path));
+  ZipArchiveHandle handle;
+  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+
+  // Set up the handler, command_pipe, patch offset & length.
+  UpdaterInfo updater_info;
+  updater_info.package_zip = handle;
+  TemporaryFile temp_pipe;
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
+  updater_info.package_zip_addr = map.addr;
+  updater_info.package_zip_len = map.length;
+
+  std::string new_data = entries.find("new_data.br") != entries.end() ? "new_data.br" : "new_data";
+  std::string script = is_verify ? "block_image_verify" : "block_image_update";
+  script += R"((")" + image_file + R"(", package_extract_file("transfer_list"), ")" + new_data +
+            R"(", "patch_data"))";
+  expect(result.c_str(), script, cause_code, &updater_info);
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+  CloseArchive(handle);
+}
+
+static std::string GetSha1(std::string_view content) {
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  SHA1(reinterpret_cast<const uint8_t*>(content.data()), content.size(), digest);
+  return print_sha1(digest);
+}
+
+static Value* BlobToString(const char* name, State* state,
+                           const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() != 1) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size());
+  }
+
+  std::vector<std::unique_ptr<Value>> args;
+  if (!ReadValueArgs(state, argv, &args)) {
+    return nullptr;
+  }
+
+  if (args[0]->type != Value::Type::BLOB) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() expects a BLOB argument", name);
+  }
+
+  args[0]->type = Value::Type::STRING;
+  return args[0].release();
+}
+
+class UpdaterTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    RegisterBuiltins();
+    RegisterInstallFunctions();
+    RegisterBlockImageFunctions();
+
+    RegisterFunction("blob_to_string", BlobToString);
+
+    // Each test is run in a separate process (isolated mode). Shared temporary files won't cause
+    // conflicts.
+    Paths::Get().set_cache_temp_source(temp_saved_source_.path);
+    Paths::Get().set_last_command_file(temp_last_command_.path);
+    Paths::Get().set_stash_directory_base(temp_stash_base_.path);
+
+    // Enable a special command "abort" to simulate interruption.
+    Command::abort_allowed_ = true;
+
+    last_command_file_ = temp_last_command_.path;
+    image_file_ = image_temp_file_.path;
+  }
+
+  void TearDown() override {
+    // Clean up the last_command_file if any.
+    ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_));
+
+    // Clear partition updated marker if any.
+    std::string updated_marker{ temp_stash_base_.path };
+    updated_marker += "/" + GetSha1(image_temp_file_.path) + ".UPDATED";
+    ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
+  }
+
+  TemporaryFile temp_saved_source_;
+  TemporaryDir temp_stash_base_;
+  std::string last_command_file_;
+  std::string image_file_;
+
+ private:
+  TemporaryFile temp_last_command_;
+  TemporaryFile image_temp_file_;
+};
+
+TEST_F(UpdaterTest, getprop) {
+    expect(android::base::GetProperty("ro.product.device", "").c_str(),
+           "getprop(\"ro.product.device\")",
+           kNoCause);
+
+    expect(android::base::GetProperty("ro.build.fingerprint", "").c_str(),
+           "getprop(\"ro.build.fingerprint\")",
+           kNoCause);
+
+    // getprop() accepts only one parameter.
+    expect(nullptr, "getprop()", kArgsParsingFailure);
+    expect(nullptr, "getprop(\"arg1\", \"arg2\")", kArgsParsingFailure);
+}
+
+TEST_F(UpdaterTest, patch_partition_check) {
+  // Zero argument is not valid.
+  expect(nullptr, "patch_partition_check()", kArgsParsingFailure);
+
+  std::string source_file = from_testdata_base("boot.img");
+  std::string source_content;
+  ASSERT_TRUE(android::base::ReadFileToString(source_file, &source_content));
+  size_t source_size = source_content.size();
+  std::string source_hash = GetSha1(source_content);
+  Partition source(source_file, source_size, source_hash);
+
+  std::string target_file = from_testdata_base("recovery.img");
+  std::string target_content;
+  ASSERT_TRUE(android::base::ReadFileToString(target_file, &target_content));
+  size_t target_size = target_content.size();
+  std::string target_hash = GetSha1(target_content);
+  Partition target(target_file, target_size, target_hash);
+
+  // One argument is not valid.
+  expect(nullptr, "patch_partition_check(\"" + source.ToString() + "\")", kArgsParsingFailure);
+  expect(nullptr, "patch_partition_check(\"" + target.ToString() + "\")", kArgsParsingFailure);
+
+  // Both of the source and target have the desired checksum.
+  std::string cmd =
+      "patch_partition_check(\"" + source.ToString() + "\", \"" + target.ToString() + "\")";
+  expect("t", cmd, kNoCause);
+
+  // Only source partition has the desired checksum.
+  Partition bad_target(target_file, target_size - 1, target_hash);
+  cmd = "patch_partition_check(\"" + source.ToString() + "\", \"" + bad_target.ToString() + "\")";
+  expect("t", cmd, kNoCause);
+
+  // Only target partition has the desired checksum.
+  Partition bad_source(source_file, source_size + 1, source_hash);
+  cmd = "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + target.ToString() + "\")";
+  expect("t", cmd, kNoCause);
+
+  // Neither of the source or target has the desired checksum.
+  cmd =
+      "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + bad_target.ToString() + "\")";
+  expect("", cmd, kNoCause);
+}
+
+TEST_F(UpdaterTest, file_getprop) {
+    // file_getprop() expects two arguments.
+    expect(nullptr, "file_getprop()", kArgsParsingFailure);
+    expect(nullptr, "file_getprop(\"arg1\")", kArgsParsingFailure);
+    expect(nullptr, "file_getprop(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
+
+    // File doesn't exist.
+    expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFreadFailure);
+
+    // Reject too large files (current limit = 65536).
+    TemporaryFile temp_file1;
+    std::string buffer(65540, '\0');
+    ASSERT_TRUE(android::base::WriteStringToFile(buffer, temp_file1.path));
+
+    // Read some keys.
+    TemporaryFile temp_file2;
+    std::string content("ro.product.name=tardis\n"
+                        "# comment\n\n\n"
+                        "ro.product.model\n"
+                        "ro.product.board =  magic \n");
+    ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file2.path));
+
+    std::string script1("file_getprop(\"" + std::string(temp_file2.path) +
+                       "\", \"ro.product.name\")");
+    expect("tardis", script1, kNoCause);
+
+    std::string script2("file_getprop(\"" + std::string(temp_file2.path) +
+                       "\", \"ro.product.board\")");
+    expect("magic", script2, kNoCause);
+
+    // No match.
+    std::string script3("file_getprop(\"" + std::string(temp_file2.path) +
+                       "\", \"ro.product.wrong\")");
+    expect("", script3, kNoCause);
+
+    std::string script4("file_getprop(\"" + std::string(temp_file2.path) +
+                       "\", \"ro.product.name=\")");
+    expect("", script4, kNoCause);
+
+    std::string script5("file_getprop(\"" + std::string(temp_file2.path) +
+                       "\", \"ro.product.nam\")");
+    expect("", script5, kNoCause);
+
+    std::string script6("file_getprop(\"" + std::string(temp_file2.path) +
+                       "\", \"ro.product.model\")");
+    expect("", script6, kNoCause);
+}
+
+// TODO: Test extracting to block device.
+TEST_F(UpdaterTest, package_extract_file) {
+  // package_extract_file expects 1 or 2 arguments.
+  expect(nullptr, "package_extract_file()", kArgsParsingFailure);
+  expect(nullptr, "package_extract_file(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
+
+  std::string zip_path = from_testdata_base("ziptest_valid.zip");
+  ZipArchiveHandle handle;
+  ASSERT_EQ(0, OpenArchive(zip_path.c_str(), &handle));
+
+  // Need to set up the ziphandle.
+  UpdaterInfo updater_info;
+  updater_info.package_zip = handle;
+
+  // Two-argument version.
+  TemporaryFile temp_file1;
+  std::string script("package_extract_file(\"a.txt\", \"" + std::string(temp_file1.path) + "\")");
+  expect("t", script, kNoCause, &updater_info);
+
+  // Verify the extracted entry.
+  std::string data;
+  ASSERT_TRUE(android::base::ReadFileToString(temp_file1.path, &data));
+  ASSERT_EQ(kATxtContents, data);
+
+  // Now extract another entry to the same location, which should overwrite.
+  script = "package_extract_file(\"b.txt\", \"" + std::string(temp_file1.path) + "\")";
+  expect("t", script, kNoCause, &updater_info);
+
+  ASSERT_TRUE(android::base::ReadFileToString(temp_file1.path, &data));
+  ASSERT_EQ(kBTxtContents, data);
+
+  // Missing zip entry. The two-argument version doesn't abort.
+  script = "package_extract_file(\"doesntexist\", \"" + std::string(temp_file1.path) + "\")";
+  expect("", script, kNoCause, &updater_info);
+
+  // Extract to /dev/full should fail.
+  script = "package_extract_file(\"a.txt\", \"/dev/full\")";
+  expect("", script, kNoCause, &updater_info);
+
+  // One-argument version. package_extract_file() gives a VAL_BLOB, which needs to be converted to
+  // VAL_STRING for equality test.
+  script = "blob_to_string(package_extract_file(\"a.txt\")) == \"" + kATxtContents + "\"";
+  expect("t", script, kNoCause, &updater_info);
+
+  script = "blob_to_string(package_extract_file(\"b.txt\")) == \"" + kBTxtContents + "\"";
+  expect("t", script, kNoCause, &updater_info);
+
+  // Missing entry. The one-argument version aborts the evaluation.
+  script = "package_extract_file(\"doesntexist\")";
+  expect(nullptr, script, kPackageExtractFileFailure, &updater_info);
+
+  CloseArchive(handle);
+}
+
+TEST_F(UpdaterTest, read_file) {
+  // read_file() expects one argument.
+  expect(nullptr, "read_file()", kArgsParsingFailure);
+  expect(nullptr, "read_file(\"arg1\", \"arg2\")", kArgsParsingFailure);
+
+  // Write some value to file and read back.
+  TemporaryFile temp_file;
+  std::string script("write_value(\"foo\", \""s + temp_file.path + "\");");
+  expect("t", script, kNoCause);
+
+  script = "read_file(\""s + temp_file.path + "\") == \"foo\"";
+  expect("t", script, kNoCause);
+
+  script = "read_file(\""s + temp_file.path + "\") == \"bar\"";
+  expect("", script, kNoCause);
+
+  // It should fail gracefully when read fails.
+  script = "read_file(\"/doesntexist\")";
+  expect("", script, kNoCause);
+}
+
+TEST_F(UpdaterTest, compute_hash_tree_smoke) {
+  std::string data;
+  for (unsigned char i = 0; i < 128; i++) {
+    data += std::string(4096, i);
+  }
+  // Appends an additional block for verity data.
+  data += std::string(4096, 0);
+  ASSERT_EQ(129 * 4096, data.size());
+  ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
+
+  std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
+  std::string expected_root_hash =
+      "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
+  // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
+  std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
+                                   expected_root_hash };
+  std::string hash_tree_command = android::base::Join(tokens, " ");
+
+  std::vector<std::string> transfer_list{
+    "4", "2", "0", "2", hash_tree_command,
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, "\n") },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+
+  std::string updated;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated));
+  ASSERT_EQ(129 * 4096, updated.size());
+  ASSERT_EQ(data.substr(0, 128 * 4096), updated.substr(0, 128 * 4096));
+
+  // Computes the SHA256 of the salt + hash_tree_data and expects the result to match with the
+  // root_hash.
+  std::vector<unsigned char> salt_bytes;
+  ASSERT_TRUE(HashTreeBuilder::ParseBytesArrayFromString(salt, &salt_bytes));
+  std::vector<unsigned char> hash_tree = std::move(salt_bytes);
+  hash_tree.insert(hash_tree.end(), updated.begin() + 128 * 4096, updated.end());
+
+  std::vector<unsigned char> digest(SHA256_DIGEST_LENGTH);
+  SHA256(hash_tree.data(), hash_tree.size(), digest.data());
+  ASSERT_EQ(expected_root_hash, HashTreeBuilder::BytesArrayToString(digest));
+}
+
+TEST_F(UpdaterTest, compute_hash_tree_root_mismatch) {
+  std::string data;
+  for (size_t i = 0; i < 128; i++) {
+    data += std::string(4096, i);
+  }
+  // Appends an additional block for verity data.
+  data += std::string(4096, 0);
+  ASSERT_EQ(129 * 4096, data.size());
+  // Corrupts one bit
+  data[4096] = 'A';
+  ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
+
+  std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
+  std::string expected_root_hash =
+      "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
+  // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
+  std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
+                                   expected_root_hash };
+  std::string hash_tree_command = android::base::Join(tokens, " ");
+
+  std::vector<std::string> transfer_list{
+    "4", "2", "0", "2", hash_tree_command,
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, "\n") },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "", kHashTreeComputationFailure);
+}
+
+TEST_F(UpdaterTest, write_value) {
+  // write_value() expects two arguments.
+  expect(nullptr, "write_value()", kArgsParsingFailure);
+  expect(nullptr, "write_value(\"arg1\")", kArgsParsingFailure);
+  expect(nullptr, "write_value(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
+
+  // filename cannot be empty.
+  expect(nullptr, "write_value(\"value\", \"\")", kArgsParsingFailure);
+
+  // Write some value to file.
+  TemporaryFile temp_file;
+  std::string value = "magicvalue";
+  std::string script("write_value(\"" + value + "\", \"" + std::string(temp_file.path) + "\")");
+  expect("t", script, kNoCause);
+
+  // Verify the content.
+  std::string content;
+  ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &content));
+  ASSERT_EQ(value, content);
+
+  // Allow writing empty string.
+  script = "write_value(\"\", \"" + std::string(temp_file.path) + "\")";
+  expect("t", script, kNoCause);
+
+  // Verify the content.
+  ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &content));
+  ASSERT_EQ("", content);
+
+  // It should fail gracefully when write fails.
+  script = "write_value(\"value\", \"/proc/0/file1\")";
+  expect("", script, kNoCause);
+}
+
+TEST_F(UpdaterTest, get_stage) {
+  // get_stage() expects one argument.
+  expect(nullptr, "get_stage()", kArgsParsingFailure);
+  expect(nullptr, "get_stage(\"arg1\", \"arg2\")", kArgsParsingFailure);
+  expect(nullptr, "get_stage(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
+
+  // Set up a local file as BCB.
+  TemporaryFile tf;
+  std::string temp_file(tf.path);
+  bootloader_message boot;
+  strlcpy(boot.stage, "2/3", sizeof(boot.stage));
+  std::string err;
+  ASSERT_TRUE(write_bootloader_message_to(boot, temp_file, &err));
+
+  // Can read the stage value.
+  std::string script("get_stage(\"" + temp_file + "\")");
+  expect("2/3", script, kNoCause);
+
+  // Bad BCB path.
+  script = "get_stage(\"doesntexist\")";
+  expect("", script, kNoCause);
+}
+
+TEST_F(UpdaterTest, set_stage) {
+  // set_stage() expects two arguments.
+  expect(nullptr, "set_stage()", kArgsParsingFailure);
+  expect(nullptr, "set_stage(\"arg1\")", kArgsParsingFailure);
+  expect(nullptr, "set_stage(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
+
+  // Set up a local file as BCB.
+  TemporaryFile tf;
+  std::string temp_file(tf.path);
+  bootloader_message boot;
+  strlcpy(boot.command, "command", sizeof(boot.command));
+  strlcpy(boot.stage, "2/3", sizeof(boot.stage));
+  std::string err;
+  ASSERT_TRUE(write_bootloader_message_to(boot, temp_file, &err));
+
+  // Write with set_stage().
+  std::string script("set_stage(\"" + temp_file + "\", \"1/3\")");
+  expect(tf.path, script, kNoCause);
+
+  // Verify.
+  bootloader_message boot_verify;
+  ASSERT_TRUE(read_bootloader_message_from(&boot_verify, temp_file, &err));
+
+  // Stage should be updated, with command part untouched.
+  ASSERT_STREQ("1/3", boot_verify.stage);
+  ASSERT_STREQ(boot.command, boot_verify.command);
+
+  // Bad BCB path.
+  script = "set_stage(\"doesntexist\", \"1/3\")";
+  expect("", script, kNoCause);
+
+  script = "set_stage(\"/dev/full\", \"1/3\")";
+  expect("", script, kNoCause);
+}
+
+TEST_F(UpdaterTest, set_progress) {
+  // set_progress() expects one argument.
+  expect(nullptr, "set_progress()", kArgsParsingFailure);
+  expect(nullptr, "set_progress(\"arg1\", \"arg2\")", kArgsParsingFailure);
+
+  // Invalid progress argument.
+  expect(nullptr, "set_progress(\"arg1\")", kArgsParsingFailure);
+  expect(nullptr, "set_progress(\"3x+5\")", kArgsParsingFailure);
+  expect(nullptr, "set_progress(\".3.5\")", kArgsParsingFailure);
+
+  TemporaryFile tf;
+  UpdaterInfo updater_info;
+  updater_info.cmd_pipe = fdopen(tf.release(), "w");
+  expect(".52", "set_progress(\".52\")", kNoCause, &updater_info);
+  fflush(updater_info.cmd_pipe);
+
+  std::string cmd;
+  ASSERT_TRUE(android::base::ReadFileToString(tf.path, &cmd));
+  ASSERT_EQ(android::base::StringPrintf("set_progress %f\n", .52), cmd);
+  // recovery-updater protocol expects 2 tokens ("set_progress <frac>").
+  ASSERT_EQ(2U, android::base::Split(cmd, " ").size());
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+}
+
+TEST_F(UpdaterTest, show_progress) {
+  // show_progress() expects two arguments.
+  expect(nullptr, "show_progress()", kArgsParsingFailure);
+  expect(nullptr, "show_progress(\"arg1\")", kArgsParsingFailure);
+  expect(nullptr, "show_progress(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
+
+  // Invalid progress arguments.
+  expect(nullptr, "show_progress(\"arg1\", \"arg2\")", kArgsParsingFailure);
+  expect(nullptr, "show_progress(\"3x+5\", \"10\")", kArgsParsingFailure);
+  expect(nullptr, "show_progress(\".3\", \"5a\")", kArgsParsingFailure);
+
+  TemporaryFile tf;
+  UpdaterInfo updater_info;
+  updater_info.cmd_pipe = fdopen(tf.release(), "w");
+  expect(".52", "show_progress(\".52\", \"10\")", kNoCause, &updater_info);
+  fflush(updater_info.cmd_pipe);
+
+  std::string cmd;
+  ASSERT_TRUE(android::base::ReadFileToString(tf.path, &cmd));
+  ASSERT_EQ(android::base::StringPrintf("progress %f %d\n", .52, 10), cmd);
+  // recovery-updater protocol expects 3 tokens ("progress <frac> <secs>").
+  ASSERT_EQ(3U, android::base::Split(cmd, " ").size());
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+}
+
+TEST_F(UpdaterTest, block_image_update_parsing_error) {
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    // clang-format on
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "", kArgsParsingFailure);
+}
+
+// Generates the bsdiff of the given source and target images, and writes the result entries.
+// target_blocks specifies the block count to be written into the `bsdiff` command, which may be
+// different from the given target size in order to trigger overrun / underrun paths.
+static void GetEntriesForBsdiff(std::string_view source, std::string_view target,
+                                size_t target_blocks, PackageEntries* entries) {
+  // Generate the patch data.
+  TemporaryFile patch_file;
+  ASSERT_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(source.data()), source.size(),
+                              reinterpret_cast<const uint8_t*>(target.data()), target.size(),
+                              patch_file.path, nullptr));
+  std::string patch_content;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch_content));
+
+  // Create the transfer list that contains a bsdiff.
+  std::string src_hash = GetSha1(source);
+  std::string tgt_hash = GetSha1(target);
+  size_t source_blocks = source.size() / 4096;
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    std::to_string(target_blocks),
+    "0",
+    "0",
+    // bsdiff patch_offset patch_length source_hash target_hash target_range source_block_count
+    // source_range
+    android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,%zu %zu 2,0,%zu", patch_content.size(),
+                                src_hash.c_str(), tgt_hash.c_str(), target_blocks, source_blocks,
+                                source_blocks),
+    // clang-format on
+  };
+
+  *entries = {
+    { "new_data", "" },
+    { "patch_data", patch_content },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+}
+
+TEST_F(UpdaterTest, block_image_update_patch_data) {
+  // Both source and target images have 10 blocks.
+  std::string source =
+      std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0');
+  std::string target =
+      std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0');
+  ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_));
+
+  PackageEntries entries;
+  GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2),
+                      std::string_view(target).substr(0, 4096 * 2), 2, &entries);
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+
+  // The update_file should be patched correctly.
+  std::string updated;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated));
+  ASSERT_EQ(target, updated);
+}
+
+TEST_F(UpdaterTest, block_image_update_patch_overrun) {
+  // Both source and target images have 10 blocks.
+  std::string source =
+      std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0');
+  std::string target =
+      std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0');
+  ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_));
+
+  // Provide one less block to trigger the overrun path.
+  PackageEntries entries;
+  GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2),
+                      std::string_view(target).substr(0, 4096 * 2), 1, &entries);
+
+  // The update should fail due to overrun.
+  RunBlockImageUpdate(false, entries, image_file_, "", kPatchApplicationFailure);
+}
+
+TEST_F(UpdaterTest, block_image_update_patch_underrun) {
+  // Both source and target images have 10 blocks.
+  std::string source =
+      std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0');
+  std::string target =
+      std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0');
+  ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_));
+
+  // Provide one more block to trigger the overrun path.
+  PackageEntries entries;
+  GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2),
+                      std::string_view(target).substr(0, 4096 * 2), 3, &entries);
+
+  // The update should fail due to underrun.
+  RunBlockImageUpdate(false, entries, image_file_, "", kPatchApplicationFailure);
+}
+
+TEST_F(UpdaterTest, block_image_update_fail) {
+  std::string src_content(4096 * 2, 'e');
+  std::string src_hash = GetSha1(src_content);
+  // Stash and free some blocks, then fail the update intentionally.
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + src_hash + " 2,0,2",
+    "free " + src_hash,
+    "abort",
+    // clang-format on
+  };
+
+  // Add a new data of 10 bytes to test the deadlock.
+  PackageEntries entries{
+    { "new_data", std::string(10, 0) },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_));
+
+  RunBlockImageUpdate(false, entries, image_file_, "");
+
+  // Updater generates the stash name based on the input file name.
+  std::string name_digest = GetSha1(image_file_);
+  std::string stash_base = std::string(temp_stash_base_.path) + "/" + name_digest;
+  ASSERT_EQ(0, access(stash_base.c_str(), F_OK));
+  // Expect the stashed blocks to be freed.
+  ASSERT_EQ(-1, access((stash_base + src_hash).c_str(), F_OK));
+  ASSERT_EQ(0, rmdir(stash_base.c_str()));
+}
+
+TEST_F(UpdaterTest, new_data_over_write) {
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "1",
+    "0",
+    "0",
+    "new 2,0,1",
+    // clang-format on
+  };
+
+  // Write 4096 + 100 bytes of new data.
+  PackageEntries entries{
+    { "new_data", std::string(4196, 0) },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+}
+
+TEST_F(UpdaterTest, new_data_short_write) {
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "1",
+    "0",
+    "0",
+    "new 2,0,1",
+    // clang-format on
+  };
+
+  PackageEntries entries{
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  // Updater should report the failure gracefully rather than stuck in deadlock.
+  entries["new_data"] = "";
+  RunBlockImageUpdate(false, entries, image_file_, "");
+
+  entries["new_data"] = std::string(10, 'a');
+  RunBlockImageUpdate(false, entries, image_file_, "");
+
+  // Expect to write 1 block of new data successfully.
+  entries["new_data"] = std::string(4096, 'a');
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+}
+
+TEST_F(UpdaterTest, brotli_new_data) {
+  auto generator = []() { return rand() % 128; };
+  // Generate 100 blocks of random data.
+  std::string brotli_new_data;
+  brotli_new_data.reserve(4096 * 100);
+  generate_n(back_inserter(brotli_new_data), 4096 * 100, generator);
+
+  size_t encoded_size = BrotliEncoderMaxCompressedSize(brotli_new_data.size());
+  std::string encoded_data(encoded_size, 0);
+  ASSERT_TRUE(BrotliEncoderCompress(
+      BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, brotli_new_data.size(),
+      reinterpret_cast<const uint8_t*>(brotli_new_data.data()), &encoded_size,
+      reinterpret_cast<uint8_t*>(const_cast<char*>(encoded_data.data()))));
+  encoded_data.resize(encoded_size);
+
+  // Write a few small chunks of new data, then a large chunk, and finally a few small chunks.
+  // This helps us to catch potential short writes.
+  std::vector<std::string> transfer_list = {
+    "4",
+    "100",
+    "0",
+    "0",
+    "new 2,0,1",
+    "new 2,1,2",
+    "new 4,2,50,50,97",
+    "new 2,97,98",
+    "new 2,98,99",
+    "new 2,99,100",
+  };
+
+  PackageEntries entries{
+    { "new_data.br", std::move(encoded_data) },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+
+  std::string updated_content;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content));
+  ASSERT_EQ(brotli_new_data, updated_content);
+}
+
+TEST_F(UpdaterTest, last_command_update) {
+  std::string block1(4096, '1');
+  std::string block2(4096, '2');
+  std::string block3(4096, '3');
+  std::string block1_hash = GetSha1(block1);
+  std::string block2_hash = GetSha1(block2);
+  std::string block3_hash = GetSha1(block3);
+
+  // Compose the transfer list to fail the first update.
+  std::vector<std::string> transfer_list_fail{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block1_hash + " 2,1,2 1 2,0,1",
+    "stash " + block3_hash + " 2,2,3",
+    "abort",
+    // clang-format on
+  };
+
+  // Mimic a resumed update with the same transfer commands.
+  std::vector<std::string> transfer_list_continue{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block1_hash + " 2,1,2 1 2,0,1",
+    "stash " + block3_hash + " 2,2,3",
+    "move " + block1_hash + " 2,2,3 1 2,0,1",
+    // clang-format on
+  };
+
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list_fail, '\n') },
+  };
+
+  // "2\nstash " + block3_hash + " 2,2,3"
+  std::string last_command_content =
+      "2\n" + transfer_list_fail[TransferList::kTransferListHeaderLines + 2];
+
+  RunBlockImageUpdate(false, entries, image_file_, "");
+
+  // Expect last_command to contain the last stash command.
+  std::string last_command_actual;
+  ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+  EXPECT_EQ(last_command_content, last_command_actual);
+
+  std::string updated_contents;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_contents));
+  ASSERT_EQ(block1 + block1 + block3, updated_contents);
+
+  // "Resume" the update. Expect the first 'move' to be skipped but the second 'move' to be
+  // executed. Note that we intentionally reset the image file.
+  entries["transfer_list"] = android::base::Join(transfer_list_continue, '\n');
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_contents));
+  ASSERT_EQ(block1 + block2 + block1, updated_contents);
+}
+
+TEST_F(UpdaterTest, last_command_update_unresumable) {
+  std::string block1(4096, '1');
+  std::string block2(4096, '2');
+  std::string block1_hash = GetSha1(block1);
+  std::string block2_hash = GetSha1(block2);
+
+  // Construct an unresumable update with source blocks mismatch.
+  std::vector<std::string> transfer_list_unresumable{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block2_hash + " 2,1,2 1 2,0,1",
+    // clang-format on
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list_unresumable, '\n') },
+  };
+
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1, image_file_));
+
+  std::string last_command_content =
+      "0\n" + transfer_list_unresumable[TransferList::kTransferListHeaderLines];
+  ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_));
+
+  RunBlockImageUpdate(false, entries, image_file_, "");
+
+  // The last_command_file will be deleted if the update encounters an unresumable failure later.
+  ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+}
+
+TEST_F(UpdaterTest, last_command_verify) {
+  std::string block1(4096, '1');
+  std::string block2(4096, '2');
+  std::string block3(4096, '3');
+  std::string block1_hash = GetSha1(block1);
+  std::string block2_hash = GetSha1(block2);
+  std::string block3_hash = GetSha1(block3);
+
+  std::vector<std::string> transfer_list_verify{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block1_hash + " 2,0,1 1 2,0,1",
+    "move " + block1_hash + " 2,1,2 1 2,0,1",
+    "stash " + block3_hash + " 2,2,3",
+    // clang-format on
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list_verify, '\n') },
+  };
+
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1 + block3, image_file_));
+
+  // Last command: "move " + block1_hash + " 2,1,2 1 2,0,1"
+  std::string last_command_content =
+      "2\n" + transfer_list_verify[TransferList::kTransferListHeaderLines + 2];
+
+  // First run: expect the verification to succeed and the last_command_file is intact.
+  ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_));
+
+  RunBlockImageUpdate(true, entries, image_file_, "t");
+
+  std::string last_command_actual;
+  ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+  EXPECT_EQ(last_command_content, last_command_actual);
+
+  // Second run with a mismatching block image: expect the verification to succeed but
+  // last_command_file to be deleted; because the target blocks in the last command don't have the
+  // expected contents for the second move command.
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+  RunBlockImageUpdate(true, entries, image_file_, "t");
+  ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+}
+
+class ResumableUpdaterTest : public testing::TestWithParam<size_t> {
+ protected:
+  void SetUp() override {
+    RegisterBuiltins();
+    RegisterInstallFunctions();
+    RegisterBlockImageFunctions();
+
+    Paths::Get().set_cache_temp_source(temp_saved_source_.path);
+    Paths::Get().set_last_command_file(temp_last_command_.path);
+    Paths::Get().set_stash_directory_base(temp_stash_base_.path);
+
+    // Enable a special command "abort" to simulate interruption.
+    Command::abort_allowed_ = true;
+
+    index_ = GetParam();
+    image_file_ = image_temp_file_.path;
+    last_command_file_ = temp_last_command_.path;
+  }
+
+  void TearDown() override {
+    // Clean up the last_command_file if any.
+    ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_));
+
+    // Clear partition updated marker if any.
+    std::string updated_marker{ temp_stash_base_.path };
+    updated_marker += "/" + GetSha1(image_temp_file_.path) + ".UPDATED";
+    ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
+  }
+
+  TemporaryFile temp_saved_source_;
+  TemporaryDir temp_stash_base_;
+  std::string last_command_file_;
+  std::string image_file_;
+  size_t index_;
+
+ private:
+  TemporaryFile temp_last_command_;
+  TemporaryFile image_temp_file_;
+};
+
+static std::string g_source_image;
+static std::string g_target_image;
+static PackageEntries g_entries;
+
+static std::vector<std::string> GenerateTransferList() {
+  std::string a(4096, 'a');
+  std::string b(4096, 'b');
+  std::string c(4096, 'c');
+  std::string d(4096, 'd');
+  std::string e(4096, 'e');
+  std::string f(4096, 'f');
+  std::string g(4096, 'g');
+  std::string h(4096, 'h');
+  std::string i(4096, 'i');
+  std::string zero(4096, '\0');
+
+  std::string a_hash = GetSha1(a);
+  std::string b_hash = GetSha1(b);
+  std::string c_hash = GetSha1(c);
+  std::string e_hash = GetSha1(e);
+
+  auto loc = [](const std::string& range_text) {
+    std::vector<std::string> pieces = android::base::Split(range_text, "-");
+    size_t left;
+    size_t right;
+    if (pieces.size() == 1) {
+      CHECK(android::base::ParseUint(pieces[0], &left));
+      right = left + 1;
+    } else {
+      CHECK_EQ(2u, pieces.size());
+      CHECK(android::base::ParseUint(pieces[0], &left));
+      CHECK(android::base::ParseUint(pieces[1], &right));
+      right++;
+    }
+    return android::base::StringPrintf("2,%zu,%zu", left, right);
+  };
+
+  // patch 1: "b d c" -> "g"
+  TemporaryFile patch_file_bdc_g;
+  std::string bdc = b + d + c;
+  std::string bdc_hash = GetSha1(bdc);
+  std::string g_hash = GetSha1(g);
+  CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(bdc.data()), bdc.size(),
+                             reinterpret_cast<const uint8_t*>(g.data()), g.size(),
+                             patch_file_bdc_g.path, nullptr));
+  std::string patch_bdc_g;
+  CHECK(android::base::ReadFileToString(patch_file_bdc_g.path, &patch_bdc_g));
+
+  // patch 2: "a b c d" -> "d c b"
+  TemporaryFile patch_file_abcd_dcb;
+  std::string abcd = a + b + c + d;
+  std::string abcd_hash = GetSha1(abcd);
+  std::string dcb = d + c + b;
+  std::string dcb_hash = GetSha1(dcb);
+  CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(abcd.data()), abcd.size(),
+                             reinterpret_cast<const uint8_t*>(dcb.data()), dcb.size(),
+                             patch_file_abcd_dcb.path, nullptr));
+  std::string patch_abcd_dcb;
+  CHECK(android::base::ReadFileToString(patch_file_abcd_dcb.path, &patch_abcd_dcb));
+
+  std::vector<std::string> transfer_list{
+    "4",
+    "10",  // total blocks written
+    "2",   // maximum stash entries
+    "2",   // maximum number of stashed blocks
+
+    // a b c d e a b c d e
+    "stash " + b_hash + " " + loc("1"),
+    // a b c d e a b c d e    [b(1)]
+    "stash " + c_hash + " " + loc("2"),
+    // a b c d e a b c d e    [b(1)][c(2)]
+    "new " + loc("1-2"),
+    // a i h d e a b c d e    [b(1)][c(2)]
+    "zero " + loc("0"),
+    // 0 i h d e a b c d e    [b(1)][c(2)]
+
+    // bsdiff "b d c" (from stash, 3, stash) to get g(3)
+    android::base::StringPrintf(
+        "bsdiff 0 %zu %s %s %s 3 %s %s %s:%s %s:%s",
+        patch_bdc_g.size(),                  // patch start (0), patch length
+        bdc_hash.c_str(),                    // source hash
+        g_hash.c_str(),                      // target hash
+        loc("3").c_str(),                    // target range
+        loc("3").c_str(), loc("1").c_str(),  // load "d" from block 3, into buffer at offset 1
+        b_hash.c_str(), loc("0").c_str(),    // load "b" from stash, into buffer at offset 0
+        c_hash.c_str(), loc("2").c_str()),   // load "c" from stash, into buffer at offset 2
+
+    // 0 i h g e a b c d e    [b(1)][c(2)]
+    "free " + b_hash,
+    // 0 i h g e a b c d e    [c(2)]
+    "free " + a_hash,
+    // 0 i h g e a b c d e
+    "stash " + a_hash + " " + loc("5"),
+    // 0 i h g e a b c d e    [a(5)]
+    "move " + e_hash + " " + loc("5") + " 1 " + loc("4"),
+    // 0 i h g e e b c d e    [a(5)]
+
+    // bsdiff "a b c d" (from stash, 6-8) to "d c b" (6-8)
+    android::base::StringPrintf(  //
+        "bsdiff %zu %zu %s %s %s 4 %s %s %s:%s",
+        patch_bdc_g.size(),                          // patch start
+        patch_bdc_g.size() + patch_abcd_dcb.size(),  // patch length
+        abcd_hash.c_str(),                           // source hash
+        dcb_hash.c_str(),                            // target hash
+        loc("6-8").c_str(),                          // target range
+        loc("6-8").c_str(),                          // load "b c d" from blocks 6-8
+        loc("1-3").c_str(),                          //   into buffer at offset 1-3
+        a_hash.c_str(),                              // load "a" from stash
+        loc("0").c_str()),                           //   into buffer at offset 0
+
+    // 0 i h g e e d c b e    [a(5)]
+    "new " + loc("4"),
+    // 0 i h g f e d c b e    [a(5)]
+    "move " + a_hash + " " + loc("9") + " 1 - " + a_hash + ":" + loc("0"),
+    // 0 i h g f e d c b a    [a(5)]
+    "free " + a_hash,
+    // 0 i h g f e d c b a
+  };
+
+  std::string new_data = i + h + f;
+  std::string patch_data = patch_bdc_g + patch_abcd_dcb;
+
+  g_entries = {
+    { "new_data", new_data },
+    { "patch_data", patch_data },
+  };
+  g_source_image = a + b + c + d + e + a + b + c + d + e;
+  g_target_image = zero + i + h + g + f + e + d + c + b + a;
+
+  return transfer_list;
+}
+
+static const std::vector<std::string> g_transfer_list = GenerateTransferList();
+
+INSTANTIATE_TEST_CASE_P(InterruptAfterEachCommand, ResumableUpdaterTest,
+                        ::testing::Range(static_cast<size_t>(0),
+                                         g_transfer_list.size() -
+                                             TransferList::kTransferListHeaderLines));
+
+TEST_P(ResumableUpdaterTest, InterruptVerifyResume) {
+  ASSERT_TRUE(android::base::WriteStringToFile(g_source_image, image_file_));
+
+  LOG(INFO) << "Interrupting at line " << index_ << " ("
+            << g_transfer_list[TransferList::kTransferListHeaderLines + index_] << ")";
+
+  std::vector<std::string> transfer_list_copy{ g_transfer_list };
+  transfer_list_copy[TransferList::kTransferListHeaderLines + index_] = "abort";
+
+  g_entries["transfer_list"] = android::base::Join(transfer_list_copy, '\n');
+
+  // Run update that's expected to fail.
+  RunBlockImageUpdate(false, g_entries, image_file_, "");
+
+  std::string last_command_expected;
+
+  // Assert the last_command_file.
+  if (index_ == 0) {
+    ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+  } else {
+    last_command_expected = std::to_string(index_ - 1) + "\n" +
+                            g_transfer_list[TransferList::kTransferListHeaderLines + index_ - 1];
+    std::string last_command_actual;
+    ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+    ASSERT_EQ(last_command_expected, last_command_actual);
+  }
+
+  g_entries["transfer_list"] = android::base::Join(g_transfer_list, '\n');
+
+  // Resume the interrupted update, by doing verification first.
+  RunBlockImageUpdate(true, g_entries, image_file_, "t");
+
+  // last_command_file should remain intact.
+  if (index_ == 0) {
+    ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+  } else {
+    std::string last_command_actual;
+    ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+    ASSERT_EQ(last_command_expected, last_command_actual);
+  }
+
+  // Resume the update.
+  RunBlockImageUpdate(false, g_entries, image_file_, "t");
+
+  // last_command_file should be gone after successful update.
+  ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+
+  std::string updated_image_actual;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_image_actual));
+  ASSERT_EQ(g_target_image, updated_image_actual);
+}
diff --git a/tests/unit/verifier_test.cpp b/tests/unit/verifier_test.cpp
new file mode 100644
index 0000000..ded23c5
--- /dev/null
+++ b/tests/unit/verifier_test.cpp
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2009 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 agree 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <gtest/gtest.h>
+#include <openssl/bn.h>
+#include <openssl/ec.h>
+#include <openssl/nid.h>
+#include <ziparchive/zip_writer.h>
+
+#include "common/test_constants.h"
+#include "install/package.h"
+#include "install/verifier.h"
+#include "otautil/sysutil.h"
+
+using namespace std::string_literals;
+
+static void LoadKeyFromFile(const std::string& file_name, Certificate* cert) {
+  std::string testkey_string;
+  ASSERT_TRUE(android::base::ReadFileToString(file_name, &testkey_string));
+  ASSERT_TRUE(LoadCertificateFromBuffer(
+      std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), cert));
+}
+
+static void VerifyFile(const std::string& content, const std::vector<Certificate>& keys,
+                       int expected) {
+  auto package =
+      Package::CreateMemoryPackage(std::vector<uint8_t>(content.begin(), content.end()), nullptr);
+  ASSERT_NE(nullptr, package);
+
+  ASSERT_EQ(expected, verify_file(package.get(), keys));
+}
+
+static void VerifyPackageWithCertificates(const std::string& name,
+                                          const std::vector<Certificate>& certs) {
+  std::string path = from_testdata_base(name);
+  auto package = Package::CreateMemoryPackage(path, nullptr);
+  ASSERT_NE(nullptr, package);
+
+  ASSERT_EQ(VERIFY_SUCCESS, verify_file(package.get(), certs));
+}
+
+static void VerifyPackageWithSingleCertificate(const std::string& name, Certificate&& cert) {
+  std::vector<Certificate> certs;
+  certs.emplace_back(std::move(cert));
+  VerifyPackageWithCertificates(name, certs);
+}
+
+static void BuildCertificateArchive(const std::vector<std::string>& file_names, int fd) {
+  FILE* zip_file_ptr = fdopen(fd, "wb");
+  ZipWriter zip_writer(zip_file_ptr);
+
+  for (const auto& name : file_names) {
+    std::string content;
+    ASSERT_TRUE(android::base::ReadFileToString(name, &content));
+
+    // Makes sure the zip entry name has the correct suffix.
+    std::string entry_name = name;
+    if (!android::base::EndsWith(entry_name, "x509.pem")) {
+      entry_name += "x509.pem";
+    }
+    ASSERT_EQ(0, zip_writer.StartEntry(entry_name.c_str(), ZipWriter::kCompress));
+    ASSERT_EQ(0, zip_writer.WriteBytes(content.data(), content.size()));
+    ASSERT_EQ(0, zip_writer.FinishEntry());
+  }
+
+  ASSERT_EQ(0, zip_writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file_ptr));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_failure) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  std::string testkey_string;
+  ASSERT_TRUE(
+      android::base::ReadFileToString(from_testdata_base("testkey_v1.txt"), &testkey_string));
+  ASSERT_FALSE(LoadCertificateFromBuffer(
+      std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), &cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent3) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v1.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v1.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent65537) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v2.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v2.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent3) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v3.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent65537) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v4.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v4.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_ec256bits) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v5.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_EC, cert.key_type);
+  ASSERT_EQ(nullptr, cert.rsa);
+
+  VerifyPackageWithSingleCertificate("otasigned_v5.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_rsa4096_bits) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_4096bits.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_4096bits.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_check_rsa_keys) {
+  std::unique_ptr<RSA, RSADeleter> rsa(RSA_new());
+  std::unique_ptr<BIGNUM, decltype(&BN_free)> exponent(BN_new(), BN_free);
+  BN_set_word(exponent.get(), 3);
+  RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr);
+  ASSERT_TRUE(CheckRSAKey(rsa));
+
+  // Exponent is expected to be 3 or 65537
+  BN_set_word(exponent.get(), 17);
+  RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr);
+  ASSERT_FALSE(CheckRSAKey(rsa));
+
+  // Modulus is expected to be 2048.
+  BN_set_word(exponent.get(), 3);
+  RSA_generate_key_ex(rsa.get(), 1024, exponent.get(), nullptr);
+  ASSERT_FALSE(CheckRSAKey(rsa));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_check_ec_keys) {
+  std::unique_ptr<EC_KEY, ECKEYDeleter> ec(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
+  ASSERT_EQ(1, EC_KEY_generate_key(ec.get()));
+  ASSERT_TRUE(CheckECKey(ec));
+
+  // Expects 256-bit EC key with curve NIST P-256
+  ec.reset(EC_KEY_new_by_curve_name(NID_secp224r1));
+  ASSERT_EQ(1, EC_KEY_generate_key(ec.get()));
+  ASSERT_FALSE(CheckECKey(ec));
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_empty_archive) {
+  TemporaryFile otacerts;
+  BuildCertificateArchive({}, otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_TRUE(certs.empty());
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_single_key) {
+  TemporaryFile otacerts;
+  BuildCertificateArchive({ from_testdata_base("testkey_v1.x509.pem") }, otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_EQ(1, certs.size());
+
+  VerifyPackageWithCertificates("otasigned_v1.zip", certs);
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_corrupted_key) {
+  TemporaryFile corrupted_key;
+  std::string content;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v1.x509.pem"), &content));
+  content = "random-contents" + content;
+  ASSERT_TRUE(android::base::WriteStringToFd(content, corrupted_key.release()));
+
+  TemporaryFile otacerts;
+  BuildCertificateArchive({ from_testdata_base("testkey_v2.x509.pem"), corrupted_key.path },
+                          otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_EQ(0, certs.size());
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_multiple_key) {
+  TemporaryFile otacerts;
+  BuildCertificateArchive(
+      {
+          from_testdata_base("testkey_v3.x509.pem"),
+          from_testdata_base("testkey_v4.x509.pem"),
+          from_testdata_base("testkey_v5.x509.pem"),
+
+      },
+      otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_EQ(3, certs.size());
+
+  VerifyPackageWithCertificates("otasigned_v3.zip", certs);
+  VerifyPackageWithCertificates("otasigned_v4.zip", certs);
+  VerifyPackageWithCertificates("otasigned_v5.zip", certs);
+}
+
+class VerifierTest : public testing::TestWithParam<std::vector<std::string>> {
+ protected:
+  void SetUp() override {
+    std::vector<std::string> args = GetParam();
+    std::string path = from_testdata_base(args[0]);
+    memory_package_ = Package::CreateMemoryPackage(path, nullptr);
+    ASSERT_NE(nullptr, memory_package_);
+    file_package_ = Package::CreateFilePackage(path, nullptr);
+    ASSERT_NE(nullptr, file_package_);
+
+    for (auto it = ++args.cbegin(); it != args.cend(); ++it) {
+      std::string public_key_file = from_testdata_base("testkey_" + *it + ".x509.pem");
+      certs_.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+      LoadKeyFromFile(public_key_file, &certs_.back());
+    }
+  }
+
+  std::unique_ptr<Package> memory_package_;
+  std::unique_ptr<Package> file_package_;
+  std::vector<Certificate> certs_;
+};
+
+class VerifierSuccessTest : public VerifierTest {
+};
+
+class VerifierFailureTest : public VerifierTest {
+};
+
+TEST(VerifierTest, BadPackage_AlteredFooter) {
+  std::vector<Certificate> certs;
+  certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back());
+
+  std::string package;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package));
+  ASSERT_EQ(std::string("\xc0\x06\xff\xff\xd2\x06", 6), package.substr(package.size() - 6, 6));
+
+  // Alter the footer.
+  package[package.size() - 5] = '\x05';
+  VerifyFile(package, certs, VERIFY_FAILURE);
+}
+
+TEST(VerifierTest, BadPackage_AlteredContent) {
+  std::vector<Certificate> certs;
+  certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back());
+
+  std::string package;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package));
+  ASSERT_GT(package.size(), static_cast<size_t>(100));
+
+  // Alter the content.
+  std::string altered1(package);
+  altered1[50] += 1;
+  VerifyFile(altered1, certs, VERIFY_FAILURE);
+
+  std::string altered2(package);
+  altered2[10] += 1;
+  VerifyFile(altered2, certs, VERIFY_FAILURE);
+}
+
+TEST(VerifierTest, BadPackage_SignatureStartOutOfBounds) {
+  std::vector<Certificate> certs;
+  certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back());
+
+  // Signature start is 65535 (0xffff) while comment size is 0 (Bug: 31914369).
+  std::string package = "\x50\x4b\x05\x06"s + std::string(12, '\0') + "\xff\xff\xff\xff\x00\x00"s;
+  VerifyFile(package, certs, VERIFY_FAILURE);
+}
+
+TEST_P(VerifierSuccessTest, VerifySucceed) {
+  ASSERT_EQ(VERIFY_SUCCESS, verify_file(memory_package_.get(), certs_));
+  ASSERT_EQ(VERIFY_SUCCESS, verify_file(file_package_.get(), certs_));
+}
+
+TEST_P(VerifierFailureTest, VerifyFailure) {
+  ASSERT_EQ(VERIFY_FAILURE, verify_file(memory_package_.get(), certs_));
+  ASSERT_EQ(VERIFY_FAILURE, verify_file(file_package_.get(), certs_));
+}
+
+INSTANTIATE_TEST_CASE_P(SingleKeySuccess, VerifierSuccessTest,
+    ::testing::Values(
+      std::vector<std::string>({"otasigned_v1.zip", "v1"}),
+      std::vector<std::string>({"otasigned_v2.zip", "v2"}),
+      std::vector<std::string>({"otasigned_v3.zip", "v3"}),
+      std::vector<std::string>({"otasigned_v4.zip", "v4"}),
+      std::vector<std::string>({"otasigned_v5.zip", "v5"})));
+
+INSTANTIATE_TEST_CASE_P(MultiKeySuccess, VerifierSuccessTest,
+    ::testing::Values(
+      std::vector<std::string>({"otasigned_v1.zip", "v1", "v2"}),
+      std::vector<std::string>({"otasigned_v2.zip", "v5", "v2"}),
+      std::vector<std::string>({"otasigned_v3.zip", "v5", "v1", "v3"}),
+      std::vector<std::string>({"otasigned_v4.zip", "v5", "v1", "v4"}),
+      std::vector<std::string>({"otasigned_v5.zip", "v4", "v1", "v5"})));
+
+INSTANTIATE_TEST_CASE_P(WrongKey, VerifierFailureTest,
+    ::testing::Values(
+      std::vector<std::string>({"otasigned_v1.zip", "v2"}),
+      std::vector<std::string>({"otasigned_v2.zip", "v1"}),
+      std::vector<std::string>({"otasigned_v3.zip", "v5"}),
+      std::vector<std::string>({"otasigned_v4.zip", "v5"}),
+      std::vector<std::string>({"otasigned_v5.zip", "v3"})));
+
+INSTANTIATE_TEST_CASE_P(WrongHash, VerifierFailureTest,
+    ::testing::Values(
+      std::vector<std::string>({"otasigned_v1.zip", "v3"}),
+      std::vector<std::string>({"otasigned_v2.zip", "v4"}),
+      std::vector<std::string>({"otasigned_v3.zip", "v1"}),
+      std::vector<std::string>({"otasigned_v4.zip", "v2"})));
+
+INSTANTIATE_TEST_CASE_P(BadPackage, VerifierFailureTest,
+    ::testing::Values(
+      std::vector<std::string>({"random.zip", "v1"}),
+      std::vector<std::string>({"fake-eocd.zip", "v1"})));