aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKelvin Zhang <zhangkelvin@google.com>2024-05-14 17:38:37 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2024-05-14 17:38:37 +0000
commit330bd0526fc50d0f0671d3453c2ad5c884b10e84 (patch)
tree7ec44364cc0375be7051bf901d0cce7087212c31
parent26111114826056e3e69e6b4fc92b89a611ac1abd (diff)
parent63ea13852db8adcb6b168f6a7b4f02efb324104a (diff)
downloadrecovery-main.tar.gz
Merge "Revert "Remove non-AB unittests"" into mainHEADmastermain
-rw-r--r--Android.bp7
-rw-r--r--Android.mk2
-rw-r--r--applypatch/Android.bp6
-rw-r--r--edify/Android.bp2
-rw-r--r--recovery_utils/Android.bp1
-rw-r--r--tests/Android.bp19
-rw-r--r--tests/unit/applypatch_modes_test.cpp198
-rw-r--r--tests/unit/applypatch_test.cpp290
-rw-r--r--tests/unit/commands_test.cpp554
-rw-r--r--tests/unit/edify_test.cpp167
-rw-r--r--tests/unit/host/imgdiff_test.cpp1113
-rw-r--r--tests/unit/host/update_simulator_test.cpp403
-rw-r--r--tests/unit/updater_test.cpp1227
-rw-r--r--updater/Android.bp7
-rw-r--r--updater/Android.mk118
15 files changed, 4094 insertions, 20 deletions
diff --git a/Android.bp b/Android.bp
index 6e68a9c8..9b2f80f0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -35,10 +35,7 @@ package {
// See: http://go/android-license-faq
license {
name: "bootable_recovery_license",
- visibility: [
- ":__subpackages__",
- "//bootable/deprecated-ota:__subpackages__",
- ],
+ visibility: [":__subpackages__"],
license_kinds: [
"SPDX-license-identifier-Apache-2.0",
"SPDX-license-identifier-MIT",
@@ -178,9 +175,11 @@ cc_binary {
required: [
"e2fsdroid.recovery",
"init_recovery.rc",
+ "librecovery_ui_ext",
"minadbd",
"mke2fs.conf.recovery",
"mke2fs.recovery",
+ "recovery_deps",
"ueventd.rc.recovery",
],
}
diff --git a/Android.mk b/Android.mk
index 3d1ee39c..8506040f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -84,3 +84,5 @@ endif
include $(BUILD_PHONY_PACKAGE)
+include \
+ $(LOCAL_PATH)/updater/Android.mk \
diff --git a/applypatch/Android.bp b/applypatch/Android.bp
index f36974f6..0d6d23b9 100644
--- a/applypatch/Android.bp
+++ b/applypatch/Android.bp
@@ -12,12 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-soong_namespace {
- imports: [
- "bootable/recovery/edify",
- ],
-}
-
package {
default_applicable_licenses: ["bootable_recovery_applypatch_license"],
}
diff --git a/edify/Android.bp b/edify/Android.bp
index 1017b889..62ff9113 100644
--- a/edify/Android.bp
+++ b/edify/Android.bp
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-soong_namespace {}
-
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
diff --git a/recovery_utils/Android.bp b/recovery_utils/Android.bp
index d3ab73c8..c31c4073 100644
--- a/recovery_utils/Android.bp
+++ b/recovery_utils/Android.bp
@@ -98,6 +98,5 @@ cc_library_static {
"//bootable/recovery/install",
"//bootable/recovery/minadbd",
"//bootable/recovery/tests",
- "//bootable/deprecated-ota:__subpackages__",
],
}
diff --git a/tests/Android.bp b/tests/Android.bp
index df1dc198..156a6d70 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -62,6 +62,9 @@ cc_defaults {
// libapplypatch, libapplypatch_modes
libapplypatch_static_libs = [
+ "libapplypatch_modes",
+ "libapplypatch",
+ "libedify",
"libotautil",
"libbsdiff",
"libbspatch",
@@ -123,10 +126,16 @@ cc_test {
defaults: [
"recovery_test_defaults",
+ "libupdater_defaults",
+ "libupdater_device_defaults",
],
test_suites: ["device-tests"],
+ tidy_timeout_srcs: [
+ "unit/commands_test.cpp",
+ ],
+
srcs: [
"unit/*.cpp",
],
@@ -144,6 +153,8 @@ cc_test {
"libminui",
"librecovery_utils",
"libotautil",
+ "libupdater_device",
+ "libupdater_core",
"libupdate_verifier",
"libprotobuf-cpp-lite",
@@ -180,6 +191,11 @@ cc_test_host {
defaults: [
"recovery_test_defaults",
+ "libupdater_defaults",
+ ],
+
+ tidy_timeout_srcs: [
+ "unit/host/imgdiff_test.cpp",
],
srcs: [
@@ -187,6 +203,9 @@ cc_test_host {
],
static_libs: [
+ "libupdater_host",
+ "libupdater_core",
+ "libimgdiff",
"libbsdiff",
"libdivsufsort64",
"libdivsufsort",
diff --git a/tests/unit/applypatch_modes_test.cpp b/tests/unit/applypatch_modes_test.cpp
new file mode 100644
index 00000000..08414b79
--- /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/applypatch_test.cpp b/tests/unit/applypatch_test.cpp
new file mode 100644
index 00000000..218a224f
--- /dev/null
+++ b/tests/unit/applypatch_test.cpp
@@ -0,0 +1,290 @@
+/*
+ * 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 <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
+#include <gtest/gtest.h>
+
+#include "applypatch/applypatch.h"
+#include "common/test_constants.h"
+#include "edify/expr.h"
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+
+using namespace std::string_literals;
+
+class ApplyPatchTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ source_file = from_testdata_base("boot.img");
+ FileContents boot_fc;
+ ASSERT_TRUE(LoadFileContents(source_file, &boot_fc));
+ source_size = boot_fc.data.size();
+ source_sha1 = print_sha1(boot_fc.sha1);
+
+ target_file = from_testdata_base("recovery.img");
+ FileContents recovery_fc;
+ ASSERT_TRUE(LoadFileContents(target_file, &recovery_fc));
+ target_size = recovery_fc.data.size();
+ target_sha1 = print_sha1(recovery_fc.sha1);
+
+ source_partition = Partition(source_file, source_size, source_sha1);
+ target_partition = Partition(partition_file.path, target_size, target_sha1);
+
+ srand(time(nullptr));
+ bad_sha1_a = android::base::StringPrintf("%040x", rand());
+ bad_sha1_b = android::base::StringPrintf("%040x", rand());
+
+ // Reset the cache backup file.
+ Paths::Get().set_cache_temp_source(cache_temp_source.path);
+ }
+
+ void TearDown() override {
+ ASSERT_TRUE(android::base::RemoveFileIfExists(cache_temp_source.path));
+ }
+
+ std::string source_file;
+ std::string source_sha1;
+ size_t source_size;
+
+ std::string target_file;
+ std::string target_sha1;
+ size_t target_size;
+
+ std::string bad_sha1_a;
+ std::string bad_sha1_b;
+
+ Partition source_partition;
+ Partition target_partition;
+
+ private:
+ TemporaryFile partition_file;
+ TemporaryFile cache_temp_source;
+};
+
+TEST_F(ApplyPatchTest, CheckPartition) {
+ ASSERT_TRUE(CheckPartition(source_partition));
+}
+
+TEST_F(ApplyPatchTest, CheckPartition_Mismatching) {
+ ASSERT_FALSE(CheckPartition(Partition(source_file, target_size, target_sha1)));
+ ASSERT_FALSE(CheckPartition(Partition(source_file, source_size, bad_sha1_a)));
+
+ ASSERT_FALSE(CheckPartition(Partition(source_file, source_size - 1, source_sha1)));
+ ASSERT_FALSE(CheckPartition(Partition(source_file, source_size + 1, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck) {
+ ASSERT_TRUE(PatchPartitionCheck(target_partition, source_partition));
+
+ ASSERT_TRUE(
+ PatchPartitionCheck(Partition(source_file, source_size - 1, source_sha1), source_partition));
+
+ ASSERT_TRUE(
+ PatchPartitionCheck(Partition(source_file, source_size + 1, source_sha1), source_partition));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup) {
+ ASSERT_FALSE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+
+ Paths::Get().set_cache_temp_source(source_file);
+ ASSERT_TRUE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup_BothCorrupted) {
+ ASSERT_FALSE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+
+ Paths::Get().set_cache_temp_source(target_file);
+ ASSERT_FALSE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartition) {
+ FileContents patch_fc;
+ ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot.p"), &patch_fc));
+ Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend()));
+
+ FileContents bonus_fc;
+ ASSERT_TRUE(LoadFileContents(from_testdata_base("bonus.file"), &bonus_fc));
+ Value bonus(Value::Type::BLOB, std::string(bonus_fc.data.cbegin(), bonus_fc.data.cend()));
+
+ ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, &bonus, false));
+}
+
+// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has
+// everything).
+TEST_F(ApplyPatchTest, PatchPartitionWithoutBonusFile) {
+ FileContents patch_fc;
+ ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot-with-bonus.p"), &patch_fc));
+ Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend()));
+
+ ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, nullptr, false));
+}
+
+class FreeCacheTest : public ::testing::Test {
+ protected:
+ static constexpr size_t PARTITION_SIZE = 4096 * 10;
+
+ // Returns a sorted list of files in |dirname|.
+ static std::vector<std::string> FindFilesInDir(const std::string& dirname) {
+ std::vector<std::string> file_list;
+
+ std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirname.c_str()), closedir);
+ struct dirent* de;
+ while ((de = readdir(d.get())) != 0) {
+ std::string path = dirname + "/" + de->d_name;
+
+ struct stat st;
+ if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+ file_list.emplace_back(de->d_name);
+ }
+ }
+
+ std::sort(file_list.begin(), file_list.end());
+ return file_list;
+ }
+
+ void AddFilesToDir(const std::string& dir, const std::vector<std::string>& files) {
+ std::string zeros(4096, 0);
+ for (const auto& file : files) {
+ temporary_files_.push_back(dir + "/" + file);
+ ASSERT_TRUE(android::base::WriteStringToFile(zeros, temporary_files_.back()));
+ }
+ }
+
+ void SetUp() override {
+ Paths::Get().set_cache_log_directory(mock_log_dir.path);
+ temporary_files_.clear();
+ }
+
+ void TearDown() override {
+ for (const auto& file : temporary_files_) {
+ ASSERT_TRUE(android::base::RemoveFileIfExists(file));
+ }
+ }
+
+ // A mock method to calculate the free space. It assumes the partition has a total size of 40960
+ // bytes and all files are 4096 bytes in size.
+ static size_t MockFreeSpaceChecker(const std::string& dirname) {
+ std::vector<std::string> files = FindFilesInDir(dirname);
+ return PARTITION_SIZE - 4096 * files.size();
+ }
+
+ TemporaryDir mock_cache;
+ TemporaryDir mock_log_dir;
+
+ private:
+ std::vector<std::string> temporary_files_;
+};
+
+TEST_F(FreeCacheTest, FreeCacheSmoke) {
+ std::vector<std::string> files = { "file1", "file2", "file3" };
+ AddFilesToDir(mock_cache.path, files);
+ ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path));
+
+ ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_cache.path, MockFreeSpaceChecker));
+
+ ASSERT_EQ(std::vector<std::string>{ "file3" }, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_cache.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheFreeSpaceCheckerError) {
+ std::vector<std::string> files{ "file1", "file2", "file3" };
+ AddFilesToDir(mock_cache.path, files);
+ ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path));
+
+ ASSERT_FALSE(
+ RemoveFilesInDirectory(4096 * 9, mock_cache.path, [](const std::string&) { return -1; }));
+}
+
+TEST_F(FreeCacheTest, FreeCacheOpenFile) {
+ std::vector<std::string> files = { "file1", "file2" };
+ AddFilesToDir(mock_cache.path, files);
+ ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_cache.path));
+
+ std::string file1_path = mock_cache.path + "/file1"s;
+ android::base::unique_fd fd(open(file1_path.c_str(), O_RDONLY));
+
+ // file1 can't be deleted as it's opened by us.
+ ASSERT_FALSE(RemoveFilesInDirectory(4096 * 10, mock_cache.path, MockFreeSpaceChecker));
+
+ ASSERT_EQ(std::vector<std::string>{ "file1" }, FindFilesInDir(mock_cache.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsSmoke) {
+ std::vector<std::string> log_files = { "last_log", "last_log.1", "last_kmsg.2", "last_log.5",
+ "last_log.10" };
+ AddFilesToDir(mock_log_dir.path, log_files);
+ ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path));
+
+ ASSERT_TRUE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker));
+
+ // Logs with a higher index will be deleted first
+ std::vector<std::string> expected = { "last_log", "last_log.1" };
+ ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+ ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_log_dir.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsStringComparison) {
+ std::vector<std::string> log_files = { "last_log.1", "last_kmsg.1", "last_log.not_number",
+ "last_kmsgrandom" };
+ AddFilesToDir(mock_log_dir.path, log_files);
+ ASSERT_EQ(4096 * 6, MockFreeSpaceChecker(mock_log_dir.path));
+
+ ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_log_dir.path, MockFreeSpaceChecker));
+
+ // Logs with incorrect format will be deleted first; and the last_kmsg with the same index is
+ // deleted before last_log.
+ std::vector<std::string> expected = { "last_log.1" };
+ ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+ ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_log_dir.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsOtherFiles) {
+ std::vector<std::string> log_files = { "last_install", "command", "block.map", "last_log",
+ "last_kmsg.1" };
+ AddFilesToDir(mock_log_dir.path, log_files);
+ ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path));
+
+ ASSERT_FALSE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker));
+
+ // Non log files in /cache/recovery won't be deleted.
+ std::vector<std::string> expected = { "block.map", "command", "last_install" };
+ ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+}
diff --git a/tests/unit/commands_test.cpp b/tests/unit/commands_test.cpp
new file mode 100644
index 00000000..8a54df70
--- /dev/null
+++ b/tests/unit/commands_test.cpp
@@ -0,0 +1,554 @@
+/*
+ * Copyright (C) 2018 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 <algorithm>
+#include <string>
+
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <openssl/sha.h>
+
+#include "otautil/print_sha1.h"
+#include "otautil/rangeset.h"
+#include "private/commands.h"
+
+TEST(CommandsTest, ParseType) {
+ ASSERT_EQ(Command::Type::ZERO, Command::ParseType("zero"));
+ ASSERT_EQ(Command::Type::NEW, Command::ParseType("new"));
+ ASSERT_EQ(Command::Type::ERASE, Command::ParseType("erase"));
+ ASSERT_EQ(Command::Type::MOVE, Command::ParseType("move"));
+ ASSERT_EQ(Command::Type::BSDIFF, Command::ParseType("bsdiff"));
+ ASSERT_EQ(Command::Type::IMGDIFF, Command::ParseType("imgdiff"));
+ ASSERT_EQ(Command::Type::STASH, Command::ParseType("stash"));
+ ASSERT_EQ(Command::Type::FREE, Command::ParseType("free"));
+ ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, Command::ParseType("compute_hash_tree"));
+}
+
+TEST(CommandsTest, ParseType_InvalidCommand) {
+ ASSERT_EQ(Command::Type::LAST, Command::ParseType("foo"));
+ ASSERT_EQ(Command::Type::LAST, Command::ParseType("bar"));
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly) {
+ const std::vector<std::string> tokens{
+ "4,569884,569904,591946,592043",
+ "117",
+ "4,566779,566799,591946,592043",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+ tokens, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
+ target);
+ ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), {}, {}),
+ source);
+ ASSERT_EQ(117, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly) {
+ const std::vector<std::string> tokens{
+ "2,350729,350731",
+ "2",
+ "-",
+ "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15:2,0,2",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+ tokens, "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", &target,
+ "1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", &source, &err));
+ ASSERT_EQ(
+ TargetInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 350729, 350731 } })),
+ target);
+ ASSERT_EQ(
+ SourceInfo("1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", {}, {},
+ {
+ StashInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 0, 2 } })),
+ }),
+ source);
+ ASSERT_EQ(2, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes) {
+ const std::vector<std::string> tokens{
+ "4,611641,611643,636981,637075",
+ "96",
+ "4,636981,637075,770665,770666",
+ "4,0,94,95,96",
+ "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+ tokens, "4734d1b241eb3d0f993714aaf7d665fae43772b6", &target,
+ "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", &source, &err));
+ ASSERT_EQ(TargetInfo("4734d1b241eb3d0f993714aaf7d665fae43772b6",
+ RangeSet({ { 611641, 611643 }, { 636981, 637075 } })),
+ target);
+ ASSERT_EQ(SourceInfo(
+ "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+ RangeSet({ { 636981, 637075 }, { 770665, 770666 } }), // source ranges
+ RangeSet({ { 0, 94 }, { 95, 96 } }), // source location
+ {
+ StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23", RangeSet({ { 94, 95 } })),
+ }),
+ source);
+ ASSERT_EQ(96, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput) {
+ const std::vector<std::string> tokens{
+ "4,611641,611643,636981,637075",
+ "96",
+ "4,636981,637075,770665,770666",
+ "4,0,94,95,96",
+ "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+
+ // Mismatching block count.
+ {
+ std::vector<std::string> tokens_copy(tokens);
+ tokens_copy[1] = "97";
+ ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+ tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ }
+
+ // Excess stashes (causing block count mismatch).
+ {
+ std::vector<std::string> tokens_copy(tokens);
+ tokens_copy.push_back("e145a2f83a33334714ac65e34969c1f115e54a6f:2,0,22");
+ ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+ tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ }
+
+ // Invalid args.
+ for (size_t i = 0; i < tokens.size(); i++) {
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+ std::vector<std::string>(tokens.cbegin() + i + 1, tokens.cend()),
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ }
+}
+
+TEST(CommandsTest, Parse_EmptyInput) {
+ std::string err;
+ ASSERT_FALSE(Command::Parse("", 0, &err));
+ ASSERT_EQ("invalid type", err);
+}
+
+TEST(CommandsTest, Parse_ABORT_Allowed) {
+ Command::abort_allowed_ = true;
+
+ const std::string input{ "abort" };
+ std::string err;
+ Command command = Command::Parse(input, 0, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_ABORT_NotAllowed) {
+ const std::string input{ "abort" };
+ std::string err;
+ Command command = Command::Parse(input, 0, &err);
+ ASSERT_FALSE(command);
+}
+
+TEST(CommandsTest, Parse_BSDIFF) {
+ const std::string input{
+ "bsdiff 0 148 "
+ "f201a4e04bd3860da6ad47b957ef424d58a58f8c 9d5d223b4bc5c45dbd25a799c4f1a98466731599 "
+ "4,565704,565752,566779,566799 "
+ "68 4,64525,64545,565704,565752"
+ };
+ std::string err;
+ Command command = Command::Parse(input, 1, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::BSDIFF, command.type());
+ ASSERT_EQ(1, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("9d5d223b4bc5c45dbd25a799c4f1a98466731599",
+ RangeSet({ { 565704, 565752 }, { 566779, 566799 } })),
+ command.target());
+ ASSERT_EQ(SourceInfo("f201a4e04bd3860da6ad47b957ef424d58a58f8c",
+ RangeSet({ { 64525, 64545 }, { 565704, 565752 } }), RangeSet(), {}),
+ command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(0, 148), command.patch());
+}
+
+TEST(CommandsTest, Parse_ERASE) {
+ const std::string input{ "erase 2,5,10" };
+ std::string err;
+ Command command = Command::Parse(input, 2, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::ERASE, command.type());
+ ASSERT_EQ(2, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 5, 10 } })), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_FREE) {
+ const std::string input{ "free hash1" };
+ std::string err;
+ Command command = Command::Parse(input, 3, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::FREE, command.type());
+ ASSERT_EQ(3, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo("hash1", RangeSet()), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_IMGDIFF) {
+ const std::string input{
+ "imgdiff 29629269 185 "
+ "a6b1c49aed1b57a2aab1ec3e1505b945540cd8db 51978f65035f584a8ef7afa941dacb6d5e862164 "
+ "2,90851,90852 "
+ "1 2,90851,90852"
+ };
+ std::string err;
+ Command command = Command::Parse(input, 4, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::IMGDIFF, command.type());
+ ASSERT_EQ(4, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("51978f65035f584a8ef7afa941dacb6d5e862164", RangeSet({ { 90851, 90852 } })),
+ command.target());
+ ASSERT_EQ(SourceInfo("a6b1c49aed1b57a2aab1ec3e1505b945540cd8db", RangeSet({ { 90851, 90852 } }),
+ RangeSet(), {}),
+ command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(29629269, 185), command.patch());
+}
+
+TEST(CommandsTest, Parse_MOVE) {
+ const std::string input{
+ "move 1d74d1a60332fd38cf9405f1bae67917888da6cb "
+ "4,569884,569904,591946,592043 117 4,566779,566799,591946,592043"
+ };
+ std::string err;
+ Command command = Command::Parse(input, 5, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::MOVE, command.type());
+ ASSERT_EQ(5, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
+ command.target());
+ ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), RangeSet(), {}),
+ command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_NEW) {
+ const std::string input{ "new 4,3,5,10,12" };
+ std::string err;
+ Command command = Command::Parse(input, 6, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::NEW, command.type());
+ ASSERT_EQ(6, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 3, 5 }, { 10, 12 } })), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_STASH) {
+ const std::string input{ "stash hash1 2,5,10" };
+ std::string err;
+ Command command = Command::Parse(input, 7, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::STASH, command.type());
+ ASSERT_EQ(7, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo("hash1", RangeSet({ { 5, 10 } })), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_ZERO) {
+ const std::string input{ "zero 2,1,5" };
+ std::string err;
+ Command command = Command::Parse(input, 8, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::ZERO, command.type());
+ ASSERT_EQ(8, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 1, 5 } })), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_COMPUTE_HASH_TREE) {
+ const std::string input{ "compute_hash_tree 2,0,1 2,3,4 sha1 unknown-salt unknown-root-hash" };
+ std::string err;
+ Command command = Command::Parse(input, 9, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, command.type());
+ ASSERT_EQ(9, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ HashTreeInfo expected_info(RangeSet({ { 0, 1 } }), RangeSet({ { 3, 4 } }), "sha1", "unknown-salt",
+ "unknown-root-hash");
+ ASSERT_EQ(expected_info, command.hash_tree_info());
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_InvalidNumberOfArgs) {
+ Command::abort_allowed_ = true;
+
+ // Note that the case of having excess args in BSDIFF, IMGDIFF and MOVE is covered by
+ // ParseTargetInfoAndSourceInfo_InvalidInput.
+ std::vector<std::string> inputs{
+ "abort foo",
+ "bsdiff",
+ "compute_hash_tree, 2,0,1 2,0,1 unknown-algorithm unknown-salt",
+ "erase",
+ "erase 4,3,5,10,12 hash1",
+ "free",
+ "free id1 id2",
+ "imgdiff",
+ "move",
+ "new",
+ "new 4,3,5,10,12 hash1",
+ "stash",
+ "stash id1",
+ "stash id1 4,3,5,10,12 id2",
+ "zero",
+ "zero 4,3,5,10,12 hash2",
+ };
+ for (const auto& input : inputs) {
+ std::string err;
+ ASSERT_FALSE(Command::Parse(input, 0, &err));
+ }
+}
+
+TEST(SourceInfoTest, Overlaps) {
+ ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }))));
+
+ ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 4, 7 }, { 16, 23 } }))));
+
+ ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 9, 16 } }))));
+}
+
+TEST(SourceInfoTest, Overlaps_EmptySourceOrTarget) {
+ ASSERT_FALSE(SourceInfo().Overlaps(TargetInfo()));
+
+ ASSERT_FALSE(SourceInfo().Overlaps(
+ TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", RangeSet({ { 7, 9 }, { 16, 20 } }))));
+
+ ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo()));
+}
+
+TEST(SourceInfoTest, Overlaps_WithStashes) {
+ ASSERT_FALSE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+ RangeSet({ { 81, 175 }, { 265, 266 } }), // source ranges
+ RangeSet({ { 0, 94 }, { 95, 96 } }), // source location
+ { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
+ RangeSet({ { 94, 95 } })) })
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 175, 265 } }))));
+
+ ASSERT_TRUE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+ RangeSet({ { 81, 175 }, { 265, 266 } }), // source ranges
+ RangeSet({ { 0, 94 }, { 95, 96 } }), // source location
+ { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
+ RangeSet({ { 94, 95 } })) })
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 265, 266 } }))));
+}
+
+// The block size should be specified by the caller of ReadAll (i.e. from Command instance during
+// normal run).
+constexpr size_t kBlockSize = 4096;
+
+TEST(SourceInfoTest, ReadAll) {
+ // "2727756cfee3fbfe24bf5650123fd7743d7b3465" is the SHA-1 hex digest of 8192 * 'a'.
+ const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
+ {});
+ auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
+ std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
+ return 0;
+ };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+ ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+ ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());
+
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ SHA1(buffer.data(), buffer.size(), digest);
+ ASSERT_EQ(source.hash(), print_sha1(digest));
+}
+
+TEST(SourceInfoTest, ReadAll_WithStashes) {
+ const SourceInfo source(
+ // SHA-1 hex digest of 8192 * 'a' + 4096 * 'b'.
+ "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
+ { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
+ auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
+ std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
+ return 0;
+ };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>* stash_buffer) -> int {
+ std::fill_n(stash_buffer->begin(), kBlockSize, 'b');
+ return 0;
+ };
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+ ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+ ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());
+
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ SHA1(buffer.data(), buffer.size(), digest);
+ ASSERT_EQ(source.hash(), print_sha1(digest));
+}
+
+TEST(SourceInfoTest, ReadAll_BufferTooSmall) {
+ const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
+ {});
+ auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize - 1);
+ ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+}
+
+TEST(SourceInfoTest, ReadAll_FailingReader) {
+ const SourceInfo source(
+ "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
+ { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+ auto failing_block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return -1; };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+ ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, failing_block_reader, stash_reader));
+
+ auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
+ auto failing_stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return -1; };
+ ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, failing_stash_reader));
+}
+
+TEST(TransferListTest, Parse) {
+ std::vector<std::string> input_lines{
+ "4", // version
+ "2", // total blocks
+ "1", // max stashed entries
+ "1", // max stashed blocks
+ "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
+ "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1 2,0,1",
+ };
+
+ std::string err;
+ TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+ ASSERT_TRUE(static_cast<bool>(transfer_list));
+ ASSERT_EQ(4, transfer_list.version());
+ ASSERT_EQ(2, transfer_list.total_blocks());
+ ASSERT_EQ(1, transfer_list.stash_max_entries());
+ ASSERT_EQ(1, transfer_list.stash_max_blocks());
+ ASSERT_EQ(2U, transfer_list.commands().size());
+ ASSERT_EQ(Command::Type::STASH, transfer_list.commands()[0].type());
+ ASSERT_EQ(Command::Type::MOVE, transfer_list.commands()[1].type());
+}
+
+TEST(TransferListTest, Parse_InvalidCommand) {
+ std::vector<std::string> input_lines{
+ "4", // version
+ "2", // total blocks
+ "1", // max stashed entries
+ "1", // max stashed blocks
+ "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
+ "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1",
+ };
+
+ std::string err;
+ TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+ ASSERT_FALSE(static_cast<bool>(transfer_list));
+}
+
+TEST(TransferListTest, Parse_ZeroTotalBlocks) {
+ std::vector<std::string> input_lines{
+ "4", // version
+ "0", // total blocks
+ "0", // max stashed entries
+ "0", // max stashed blocks
+ };
+
+ std::string err;
+ TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+ ASSERT_TRUE(static_cast<bool>(transfer_list));
+ ASSERT_EQ(4, transfer_list.version());
+ ASSERT_EQ(0, transfer_list.total_blocks());
+ ASSERT_EQ(0, transfer_list.stash_max_entries());
+ ASSERT_EQ(0, transfer_list.stash_max_blocks());
+ ASSERT_TRUE(transfer_list.commands().empty());
+}
diff --git a/tests/unit/edify_test.cpp b/tests/unit/edify_test.cpp
new file mode 100644
index 00000000..8397bd38
--- /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/host/imgdiff_test.cpp b/tests/unit/host/imgdiff_test.cpp
new file mode 100644
index 00000000..978ac7c2
--- /dev/null
+++ b/tests/unit/host/imgdiff_test.cpp
@@ -0,0 +1,1113 @@
+/*
+ * 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;
+
+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/host/update_simulator_test.cpp b/tests/unit/host/update_simulator_test.cpp
new file mode 100644
index 00000000..1603982e
--- /dev/null
+++ b/tests/unit/host/update_simulator_test.cpp
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2019 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 <stdint.h>
+#include <stdio.h>
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <bsdiff/bsdiff.h>
+#include <gtest/gtest.h>
+#include <ziparchive/zip_writer.h>
+
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+#include "updater/blockimg.h"
+#include "updater/build_info.h"
+#include "updater/install.h"
+#include "updater/simulator_runtime.h"
+#include "updater/target_files.h"
+#include "updater/updater.h"
+
+using std::string;
+
+// echo -n "system.img" > system.img && img2simg system.img sparse_system_string_.img 4096 &&
+// hexdump -v -e '" " 12/1 "0x%02x, " "\n"' sparse_system_string_.img
+// The total size of the result sparse image is 4136 bytes; and we can append 0s in the end to get
+// the full image.
+constexpr uint8_t SPARSE_SYSTEM_HEADER[] = {
+ 0x3a, 0xff, 0x26, 0xed, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x10, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, 0xca,
+ 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x10, 0x00, 0x00, 0x73, 0x79, 0x73, 0x74, 0x65,
+ 0x6d, 0x2e, 0x69, 0x6d, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static void AddZipEntries(int fd, const std::map<string, string>& entries) {
+ FILE* zip_file = fdopen(fd, "w");
+ ZipWriter writer(zip_file);
+ for (const auto& pair : entries) {
+ ASSERT_EQ(0, writer.StartEntry(pair.first.c_str(), 0));
+ ASSERT_EQ(0, writer.WriteBytes(pair.second.data(), pair.second.size()));
+ ASSERT_EQ(0, writer.FinishEntry());
+ }
+ ASSERT_EQ(0, writer.Finish());
+ ASSERT_EQ(0, fclose(zip_file));
+}
+
+static string CalculateSha1(const string& data) {
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest);
+ return print_sha1(digest);
+}
+
+static void CreateBsdiffPatch(const string& src, const string& tgt, string* patch) {
+ TemporaryFile patch_file;
+ ASSERT_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src.data()), src.size(),
+ reinterpret_cast<const uint8_t*>(tgt.data()), tgt.size(),
+ patch_file.path, nullptr));
+ ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, patch));
+}
+
+static void RunSimulation(std::string_view src_tf, std::string_view ota_package, bool expected) {
+ TemporaryFile cmd_pipe;
+ TemporaryFile temp_saved_source;
+ TemporaryFile temp_last_command;
+ TemporaryDir temp_stash_base;
+
+ 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);
+
+ // Configure edify's functions.
+ RegisterBuiltins();
+ RegisterInstallFunctions();
+ RegisterBlockImageFunctions();
+
+ // Run the update simulation and check the result.
+ TemporaryDir work_dir;
+ BuildInfo build_info(work_dir.path, false);
+ ASSERT_TRUE(build_info.ParseTargetFile(src_tf, false));
+ Updater updater(std::make_unique<SimulatorRuntime>(&build_info));
+ ASSERT_TRUE(updater.Init(cmd_pipe.release(), ota_package, false));
+ ASSERT_EQ(expected, updater.RunUpdate());
+ // TODO(xunchang) check the recovery&system has the expected contents.
+}
+
+class DISABLED_UpdateSimulatorTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ std::vector<string> props = {
+ "import /oem/oem.prop oem*",
+ "# begin build properties",
+ "# autogenerated by buildinfo.sh",
+ "ro.build.id=OPR1.170510.001",
+ "ro.build.display.id=OPR1.170510.001 dev-keys",
+ "ro.build.version.incremental=3993052",
+ "ro.build.version.release=O",
+ "ro.build.date=Wed May 10 11:10:29 UTC 2017",
+ "ro.build.date.utc=1494414629",
+ "ro.build.type=user",
+ "ro.build.tags=dev-keys",
+ "ro.build.flavor=angler-user",
+ "ro.product.system.brand=google",
+ "ro.product.system.name=angler",
+ "ro.product.system.device=angler",
+ };
+ build_prop_string_ = android::base::Join(props, "\n");
+
+ fstab_content_ = R"(
+#<src> <mnt_point> <type> <mnt_flags and options> <fs_mgr_flags>
+# More comments.....
+
+/dev/block/by-name/system /system ext4 ro,barrier=1 wait
+/dev/block/by-name/vendor /vendor ext4 ro wait,verify=/dev/metadata
+/dev/block/by-name/cache /cache ext4 noatime,errors=panic wait,check
+/dev/block/by-name/modem /firmware vfat ro,uid=1000,gid=1000, wait
+/dev/block/by-name/boot /boot emmc defaults defaults
+/dev/block/by-name/recovery /recovery emmc defaults defaults
+/dev/block/by-name/misc /misc emmc defaults
+/dev/block/by-name/modem /modem emmc defaults defaults)";
+
+ raw_system_string_ = "system.img" + string(4086, '\0'); // raw image is 4096 bytes in total
+ sparse_system_string_ = string(SPARSE_SYSTEM_HEADER, std::end(SPARSE_SYSTEM_HEADER)) +
+ string(4136 - sizeof(SPARSE_SYSTEM_HEADER), '\0');
+ }
+
+ string build_prop_string_;
+ string fstab_content_;
+ string raw_system_string_;
+ string sparse_system_string_;
+};
+
+TEST_F(DISABLED_UpdateSimulatorTest, TargetFile_ExtractImage) {
+ TemporaryFile zip_file;
+ AddZipEntries(zip_file.release(), { { "META/misc_info.txt", "extfs_sparse_flag=-s" },
+ { "IMAGES/system.img", sparse_system_string_ } });
+ TargetFile target_file(zip_file.path, false);
+ ASSERT_TRUE(target_file.Open());
+
+ TemporaryDir temp_dir;
+ TemporaryFile raw_image;
+ ASSERT_TRUE(target_file.ExtractImage(
+ "IMAGES/system.img", FstabInfo("/dev/system", "system", "ext4"), temp_dir.path, &raw_image));
+
+ // Check the raw image has expected contents.
+ string content;
+ ASSERT_TRUE(android::base::ReadFileToString(raw_image.path, &content));
+ string expected_content = "system.img" + string(4086, '\0');
+ ASSERT_EQ(expected_content, content);
+}
+
+TEST_F(DISABLED_UpdateSimulatorTest, TargetFile_ParseFstabInfo) {
+ TemporaryFile zip_file;
+ AddZipEntries(zip_file.release(),
+ { { "META/misc_info.txt", "" },
+ { "RECOVERY/RAMDISK/system/etc/recovery.fstab", fstab_content_ } });
+ TargetFile target_file(zip_file.path, false);
+ ASSERT_TRUE(target_file.Open());
+
+ std::vector<FstabInfo> fstab_info;
+ EXPECT_TRUE(target_file.ParseFstabInfo(&fstab_info));
+
+ std::vector<std::vector<string>> transformed;
+ std::transform(fstab_info.begin(), fstab_info.end(), std::back_inserter(transformed),
+ [](const FstabInfo& info) {
+ return std::vector<string>{ info.blockdev_name, info.mount_point, info.fs_type };
+ });
+
+ std::vector<std::vector<string>> expected = {
+ { "/dev/block/by-name/system", "/system", "ext4" },
+ { "/dev/block/by-name/vendor", "/vendor", "ext4" },
+ { "/dev/block/by-name/cache", "/cache", "ext4" },
+ { "/dev/block/by-name/boot", "/boot", "emmc" },
+ { "/dev/block/by-name/recovery", "/recovery", "emmc" },
+ { "/dev/block/by-name/misc", "/misc", "emmc" },
+ { "/dev/block/by-name/modem", "/modem", "emmc" },
+ };
+ EXPECT_EQ(expected, transformed);
+}
+
+TEST_F(DISABLED_UpdateSimulatorTest, BuildInfo_ParseTargetFile) {
+ std::map<string, string> entries = {
+ { "META/misc_info.txt", "" },
+ { "SYSTEM/build.prop", build_prop_string_ },
+ { "RECOVERY/RAMDISK/system/etc/recovery.fstab", fstab_content_ },
+ { "IMAGES/recovery.img", "" },
+ { "IMAGES/boot.img", "" },
+ { "IMAGES/misc.img", "" },
+ { "IMAGES/system.map", "" },
+ { "IMAGES/system.img", sparse_system_string_ },
+ };
+
+ TemporaryFile zip_file;
+ AddZipEntries(zip_file.release(), entries);
+
+ TemporaryDir temp_dir;
+ BuildInfo build_info(temp_dir.path, false);
+ ASSERT_TRUE(build_info.ParseTargetFile(zip_file.path, false));
+
+ std::map<string, string> expected_result = {
+ { "ro.build.id", "OPR1.170510.001" },
+ { "ro.build.display.id", "OPR1.170510.001 dev-keys" },
+ { "ro.build.version.incremental", "3993052" },
+ { "ro.build.version.release", "O" },
+ { "ro.build.date", "Wed May 10 11:10:29 UTC 2017" },
+ { "ro.build.date.utc", "1494414629" },
+ { "ro.build.type", "user" },
+ { "ro.build.tags", "dev-keys" },
+ { "ro.build.flavor", "angler-user" },
+ { "ro.product.brand", "google" },
+ { "ro.product.name", "angler" },
+ { "ro.product.device", "angler" },
+ };
+
+ for (const auto& [key, value] : expected_result) {
+ ASSERT_EQ(value, build_info.GetProperty(key, ""));
+ }
+
+ // Check that the temp files for each block device are created successfully.
+ for (auto name : { "/dev/block/by-name/system", "/dev/block/by-name/recovery",
+ "/dev/block/by-name/boot", "/dev/block/by-name/misc" }) {
+ ASSERT_EQ(0, access(build_info.FindBlockDeviceName(name).c_str(), R_OK));
+ }
+}
+
+TEST_F(DISABLED_UpdateSimulatorTest, RunUpdateSmoke) {
+ string recovery_img_string = "recovery.img";
+ string boot_img_string = "boot.img";
+
+ std::map<string, string> src_entries{
+ { "META/misc_info.txt", "extfs_sparse_flag=-s" },
+ { "RECOVERY/RAMDISK/etc/recovery.fstab", fstab_content_ },
+ { "SYSTEM/build.prop", build_prop_string_ },
+ { "IMAGES/recovery.img", "" },
+ { "IMAGES/boot.img", boot_img_string },
+ { "IMAGES/system.img", sparse_system_string_ },
+ };
+
+ // Construct the source target-files.
+ TemporaryFile src_tf;
+ AddZipEntries(src_tf.release(), src_entries);
+
+ string recovery_from_boot;
+ CreateBsdiffPatch(boot_img_string, recovery_img_string, &recovery_from_boot);
+
+ // Set up the apply patch commands to patch the recovery image.
+ string recovery_sha1 = CalculateSha1(recovery_img_string);
+ string boot_sha1 = CalculateSha1(boot_img_string);
+ string apply_patch_source_string = android::base::StringPrintf(
+ "EMMC:/dev/block/by-name/boot:%zu:%s", boot_img_string.size(), boot_sha1.c_str());
+ string apply_patch_target_string = android::base::StringPrintf(
+ "EMMC:/dev/block/by-name/recovery:%zu:%s", recovery_img_string.size(), recovery_sha1.c_str());
+ string check_command = android::base::StringPrintf(
+ R"(patch_partition_check("%s", "%s") || abort("check failed");)",
+ apply_patch_target_string.c_str(), apply_patch_source_string.c_str());
+ string patch_command = android::base::StringPrintf(
+ R"(patch_partition("%s", "%s", package_extract_file("patch.p")) || abort("patch failed");)",
+ apply_patch_target_string.c_str(), apply_patch_source_string.c_str());
+
+ // Add the commands to update the system image. Test common commands:
+ // * getprop
+ // * ui_print
+ // * patch_partition
+ // * package_extract_file (single argument)
+ // * block_image_verify, block_image_update
+ string tgt_system_string = string(4096, 'a');
+ string system_patch;
+ CreateBsdiffPatch(raw_system_string_, tgt_system_string, &system_patch);
+
+ string tgt_system_hash = CalculateSha1(tgt_system_string);
+ string src_system_hash = CalculateSha1(raw_system_string_);
+
+ std::vector<std::string> transfer_list = {
+ "4",
+ "1",
+ "0",
+ "0",
+ android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,1 1 2,0,1", system_patch.size(),
+ src_system_hash.c_str(), tgt_system_hash.c_str()),
+ };
+
+ // Construct the updater_script.
+ std::vector<string> updater_commands = {
+ R"(getprop("ro.product.device") == "angler" || abort("This package is for \"angler\"");)",
+ R"(ui_print("Source: angler/OPR1.170510.001");)",
+ check_command,
+ patch_command,
+ R"(block_image_verify("/dev/block/by-name/system", )"
+ R"(package_extract_file("system.transfer.list"), "system.new.dat", "system.patch.dat") || )"
+ R"(abort("Failed to verify system.");)",
+ R"(block_image_update("/dev/block/by-name/system", )"
+ R"(package_extract_file("system.transfer.list"), "system.new.dat", "system.patch.dat") || )"
+ R"(abort("Failed to verify system.");)",
+ };
+ string updater_script = android::base::Join(updater_commands, '\n');
+
+ // Construct the ota update package.
+ std::map<string, string> ota_entries{
+ { "system.new.dat", "" },
+ { "system.patch.dat", system_patch },
+ { "system.transfer.list", android::base::Join(transfer_list, '\n') },
+ { "META-INF/com/google/android/updater-script", updater_script },
+ { "patch.p", recovery_from_boot },
+ };
+
+ TemporaryFile ota_package;
+ AddZipEntries(ota_package.release(), ota_entries);
+
+ RunSimulation(src_tf.path, ota_package.path, true);
+}
+
+TEST_F(DISABLED_UpdateSimulatorTest, RunUpdateUnrecognizedFunction) {
+ std::map<string, string> src_entries{
+ { "META/misc_info.txt", "extfs_sparse_flag=-s" },
+ { "IMAGES/system.img", sparse_system_string_ },
+ { "RECOVERY/RAMDISK/etc/recovery.fstab", fstab_content_ },
+ { "SYSTEM/build.prop", build_prop_string_ },
+ };
+
+ TemporaryFile src_tf;
+ AddZipEntries(src_tf.release(), src_entries);
+
+ std::map<string, string> ota_entries{
+ { "system.new.dat", "" },
+ { "system.patch.dat", "" },
+ { "system.transfer.list", "" },
+ { "META-INF/com/google/android/updater-script", R"(bad_function("");)" },
+ };
+
+ TemporaryFile ota_package;
+ AddZipEntries(ota_package.release(), ota_entries);
+
+ RunSimulation(src_tf.path, ota_package.path, false);
+}
+
+TEST_F(DISABLED_UpdateSimulatorTest, RunUpdateApplyPatchFailed) {
+ string recovery_img_string = "recovery.img";
+ string boot_img_string = "boot.img";
+
+ std::map<string, string> src_entries{
+ { "META/misc_info.txt", "extfs_sparse_flag=-s" },
+ { "IMAGES/recovery.img", "" },
+ { "IMAGES/boot.img", boot_img_string },
+ { "IMAGES/system.img", sparse_system_string_ },
+ { "RECOVERY/RAMDISK/etc/recovery.fstab", fstab_content_ },
+ { "SYSTEM/build.prop", build_prop_string_ },
+ };
+
+ TemporaryFile src_tf;
+ AddZipEntries(src_tf.release(), src_entries);
+
+ string recovery_sha1 = CalculateSha1(recovery_img_string);
+ string boot_sha1 = CalculateSha1(boot_img_string);
+ string apply_patch_source_string = android::base::StringPrintf(
+ "EMMC:/dev/block/by-name/boot:%zu:%s", boot_img_string.size(), boot_sha1.c_str());
+ string apply_patch_target_string = android::base::StringPrintf(
+ "EMMC:/dev/block/by-name/recovery:%zu:%s", recovery_img_string.size(), recovery_sha1.c_str());
+ string check_command = android::base::StringPrintf(
+ R"(patch_partition_check("%s", "%s") || abort("check failed");)",
+ apply_patch_target_string.c_str(), apply_patch_source_string.c_str());
+ string patch_command = android::base::StringPrintf(
+ R"(patch_partition("%s", "%s", package_extract_file("patch.p")) || abort("failed");)",
+ apply_patch_target_string.c_str(), apply_patch_source_string.c_str());
+
+ // Give an invalid recovery patch and expect the apply patch to fail.
+ // TODO(xunchang) check the cause code.
+ std::vector<string> updater_commands = {
+ R"(ui_print("Source: angler/OPR1.170510.001");)",
+ check_command,
+ patch_command,
+ };
+
+ string updater_script = android::base::Join(updater_commands, '\n');
+ std::map<string, string> ota_entries{
+ { "system.new.dat", "" },
+ { "system.patch.dat", "" },
+ { "system.transfer.list", "" },
+ { "META-INF/com/google/android/updater-script", updater_script },
+ { "patch.p", "random string" },
+ };
+
+ TemporaryFile ota_package;
+ AddZipEntries(ota_package.release(), ota_entries);
+
+ RunSimulation(src_tf.path, ota_package.path, false);
+}
diff --git a/tests/unit/updater_test.cpp b/tests/unit/updater_test.cpp
new file mode 100644
index 00000000..8993dd8b
--- /dev/null
+++ b/tests/unit/updater_test.cpp
@@ -0,0 +1,1227 @@
+/*
+ * 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"
+#include "updater/updater_runtime.h"
+
+using namespace std::string_literals;
+
+using PackageEntries = std::unordered_map<std::string, std::string>;
+
+static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code,
+ Updater* updater) {
+ 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, updater);
+
+ 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 expect(const char* expected, const std::string& expr_str, CauseCode cause_code) {
+ Updater updater(std::make_unique<UpdaterRuntime>(nullptr));
+ expect(expected, expr_str, cause_code, &updater);
+}
+
+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 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 UpdaterTestBase {
+ protected:
+ UpdaterTestBase() : updater_(std::make_unique<UpdaterRuntime>(nullptr)) {}
+
+ void SetUp() {
+ RegisterBuiltins();
+ RegisterInstallFunctions();
+ RegisterBlockImageFunctions();
+
+ // 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);
+
+ last_command_file_ = temp_last_command_.path;
+ image_file_ = image_temp_file_.path;
+ }
+
+ void TearDown() {
+ // 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));
+ }
+
+ void RunBlockImageUpdate(bool is_verify, PackageEntries entries, const std::string& image_file,
+ const std::string& result, CauseCode cause_code = kNoCause) {
+ CHECK(entries.find("transfer_list") != entries.end());
+ 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"))";
+ entries.emplace(Updater::SCRIPT_NAME, script);
+
+ // Build the update package.
+ TemporaryFile zip_file;
+ BuildUpdatePackage(entries, zip_file.release());
+
+ // Set up the handler, command_pipe, patch offset & length.
+ TemporaryFile temp_pipe;
+ ASSERT_TRUE(updater_.Init(temp_pipe.release(), zip_file.path, false));
+ ASSERT_TRUE(updater_.RunUpdate());
+ ASSERT_EQ(result, updater_.GetResult());
+
+ // Parse the cause code written to the command pipe.
+ int received_cause_code = kNoCause;
+ std::string pipe_content;
+ ASSERT_TRUE(android::base::ReadFileToString(temp_pipe.path, &pipe_content));
+ auto lines = android::base::Split(pipe_content, "\n");
+ for (std::string_view line : lines) {
+ if (android::base::ConsumePrefix(&line, "log cause: ")) {
+ ASSERT_TRUE(android::base::ParseInt(line.data(), &received_cause_code));
+ }
+ }
+ ASSERT_EQ(cause_code, received_cause_code);
+ }
+
+ TemporaryFile temp_saved_source_;
+ TemporaryDir temp_stash_base_;
+ std::string last_command_file_;
+ std::string image_file_;
+
+ Updater updater_;
+
+ private:
+ TemporaryFile temp_last_command_;
+ TemporaryFile image_temp_file_;
+};
+
+class UpdaterTest : public UpdaterTestBase, public ::testing::Test {
+ protected:
+ void SetUp() override {
+ UpdaterTestBase::SetUp();
+
+ RegisterFunction("blob_to_string", BlobToString);
+ // Enable a special command "abort" to simulate interruption.
+ Command::abort_allowed_ = true;
+ }
+
+ void TearDown() override {
+ UpdaterTestBase::TearDown();
+ }
+
+ void SetUpdaterCmdPipe(int fd) {
+ FILE* cmd_pipe = fdopen(fd, "w");
+ ASSERT_NE(nullptr, cmd_pipe);
+ updater_.cmd_pipe_.reset(cmd_pipe);
+ }
+
+ void SetUpdaterOtaPackageHandle(ZipArchiveHandle handle) {
+ updater_.package_handle_ = handle;
+ }
+
+ void FlushUpdaterCommandPipe() const {
+ fflush(updater_.cmd_pipe_.get());
+ }
+};
+
+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.
+ SetUpdaterOtaPackageHandle(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_);
+
+ // 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_);
+
+ 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_);
+
+ // Extract to /dev/full should fail.
+ script = "package_extract_file(\"a.txt\", \"/dev/full\")";
+ expect("", script, kNoCause, &updater_);
+
+ // 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_);
+
+ script = "blob_to_string(package_extract_file(\"b.txt\")) == \"" + kBTxtContents + "\"";
+ expect("t", script, kNoCause, &updater_);
+
+ // Missing entry. The one-argument version aborts the evaluation.
+ script = "package_extract_file(\"doesntexist\")";
+ expect(nullptr, script, kPackageExtractFileFailure, &updater_);
+}
+
+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;
+ SetUpdaterCmdPipe(tf.release());
+ expect(".52", "set_progress(\".52\")", kNoCause, &updater_);
+ FlushUpdaterCommandPipe();
+
+ 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());
+}
+
+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;
+ SetUpdaterCmdPipe(tf.release());
+ expect(".52", "show_progress(\".52\", \"10\")", kNoCause, &updater_);
+ FlushUpdaterCommandPipe();
+
+ 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());
+}
+
+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 UpdaterTestBase, public testing::TestWithParam<size_t> {
+ protected:
+ void SetUp() override {
+ UpdaterTestBase::SetUp();
+ // Enable a special command "abort" to simulate interruption.
+ Command::abort_allowed_ = true;
+ index_ = GetParam();
+ }
+
+ void TearDown() override {
+ UpdaterTestBase::TearDown();
+ }
+
+ size_t index_;
+};
+
+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/updater/Android.bp b/updater/Android.bp
index e17303bb..4fc3c494 100644
--- a/updater/Android.bp
+++ b/updater/Android.bp
@@ -12,13 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-soong_namespace {
- imports: [
- "bootable/recovery/edify",
- "bootable/recovery/applypatch",
- ],
-}
-
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
diff --git a/updater/Android.mk b/updater/Android.mk
new file mode 100644
index 00000000..2fd56397
--- /dev/null
+++ b/updater/Android.mk
@@ -0,0 +1,118 @@
+# Copyright 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.
+
+LOCAL_PATH := $(call my-dir)
+
+tune2fs_static_libraries := \
+ libext2_com_err \
+ libext2_blkid \
+ libext2_quota \
+ libext2_uuid \
+ libext2_e2p \
+ libext2fs
+
+updater_common_static_libraries := \
+ libapplypatch \
+ libbootloader_message \
+ libbspatch \
+ libedify \
+ libotautil \
+ libext4_utils \
+ libdm \
+ libfec \
+ libfec_rs \
+ libavb \
+ libverity_tree \
+ liblog \
+ liblp \
+ libselinux \
+ libsparse \
+ libsquashfs_utils \
+ libbrotli \
+ libbz \
+ libziparchive \
+ libz_stable \
+ libbase \
+ libcrypto_static \
+ libcrypto_utils \
+ libcutils \
+ libutils
+
+
+# Each library in TARGET_RECOVERY_UPDATER_LIBS should have a function
+# named "Register_<libname>()". Here we emit a little C function that
+# gets #included by updater.cpp. It calls all those registration
+# functions.
+# $(1): the path to the register.inc file
+# $(2): a list of TARGET_RECOVERY_UPDATER_LIBS
+define generate-register-inc
+ $(hide) mkdir -p $(dir $(1))
+ $(hide) echo "" > $(1)
+ $(hide) $(foreach lib,$(2),echo "extern void Register_$(lib)(void);" >> $(1);)
+ $(hide) echo "void RegisterDeviceExtensions() {" >> $(1)
+ $(hide) $(foreach lib,$(2),echo " Register_$(lib)();" >> $(1);)
+ $(hide) echo "}" >> $(1)
+endef
+
+
+# updater (static executable)
+# ===============================
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := updater
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../NOTICE
+
+LOCAL_SRC_FILES := \
+ updater_main.cpp
+
+LOCAL_C_INCLUDES := \
+ $(LOCAL_PATH)/include
+
+LOCAL_CFLAGS := \
+ -Wall \
+ -Werror
+
+LOCAL_STATIC_LIBRARIES := \
+ libupdater_device \
+ libupdater_core \
+ $(TARGET_RECOVERY_UPDATER_LIBS) \
+ $(TARGET_RECOVERY_UPDATER_EXTRA_LIBS) \
+ $(updater_common_static_libraries) \
+ libfs_mgr \
+ libtune2fs \
+ $(tune2fs_static_libraries)
+
+LOCAL_HEADER_LIBRARIES := libgtest_prod_headers
+
+LOCAL_MODULE_CLASS := EXECUTABLES
+inc := $(call local-generated-sources-dir)/register.inc
+
+# Devices can also add libraries to TARGET_RECOVERY_UPDATER_EXTRA_LIBS.
+# These libs are also linked in with updater, but we don't try to call
+# any sort of registration function for these. Use this variable for
+# any subsidiary static libraries required for your registered
+# extension libs.
+$(inc) : libs := $(TARGET_RECOVERY_UPDATER_LIBS)
+$(inc) :
+ $(call generate-register-inc,$@,$(libs))
+
+LOCAL_GENERATED_SOURCES := $(inc)
+
+inc :=
+
+LOCAL_FORCE_STATIC_EXECUTABLE := true
+
+include $(BUILD_EXECUTABLE)