diff options
Diffstat (limited to 'tests/unit')
24 files changed, 6297 insertions, 13 deletions
diff --git a/tests/unit/applypatch_modes_test.cpp b/tests/unit/applypatch_modes_test.cpp new file mode 100644 index 000000000..08414b796 --- /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 000000000..794f2c103 --- /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)); +} + +// 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)); +} + +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/asn1_decoder_test.cpp b/tests/unit/asn1_decoder_test.cpp index b334a655b..d94dd4353 100644 --- a/tests/unit/asn1_decoder_test.cpp +++ b/tests/unit/asn1_decoder_test.cpp @@ -20,7 +20,7 @@ #include <gtest/gtest.h> -#include "asn1_decoder.h" +#include "private/asn1_decoder.h" TEST(Asn1DecoderTest, Empty_Failure) { uint8_t empty[] = {}; diff --git a/tests/unit/bootloader_message_test.cpp b/tests/unit/bootloader_message_test.cpp new file mode 100644 index 000000000..b005d199c --- /dev/null +++ b/tests/unit/bootloader_message_test.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/strings.h> +#include <bootloader_message/bootloader_message.h> +#include <gtest/gtest.h> + +TEST(BootloaderMessageTest, read_and_write_bootloader_message) { + TemporaryFile temp_misc; + + // Write the BCB. + bootloader_message boot = {}; + strlcpy(boot.command, "command", sizeof(boot.command)); + strlcpy(boot.recovery, "message1\nmessage2\n", sizeof(boot.recovery)); + strlcpy(boot.status, "status1", sizeof(boot.status)); + + std::string err; + ASSERT_TRUE(write_bootloader_message_to(boot, temp_misc.path, &err)) + << "Failed to write BCB: " << err; + + // Read and verify. + bootloader_message boot_verify; + ASSERT_TRUE(read_bootloader_message_from(&boot_verify, temp_misc.path, &err)) + << "Failed to read BCB: " << err; + + ASSERT_EQ(std::string(reinterpret_cast<const char*>(&boot), sizeof(boot)), + std::string(reinterpret_cast<const char*>(&boot_verify), sizeof(boot_verify))); +} + +TEST(BootloaderMessageTest, update_bootloader_message_in_struct) { + // Write the options to BCB. + std::vector<std::string> options = { "option1", "option2" }; + + bootloader_message boot = {}; + // Inject some bytes into boot. + strlcpy(boot.recovery, "random message", sizeof(boot.recovery)); + strlcpy(boot.status, "status bytes", sizeof(boot.status)); + strlcpy(boot.stage, "stage bytes", sizeof(boot.stage)); + strlcpy(boot.reserved, "reserved bytes", sizeof(boot.reserved)); + + ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options)); + + // Verify that command and recovery fields should be set. + ASSERT_EQ("boot-recovery", std::string(boot.command)); + std::string expected = "recovery\n" + android::base::Join(options, "\n") + "\n"; + ASSERT_EQ(expected, std::string(boot.recovery)); + + // The rest should be intact. + ASSERT_EQ("status bytes", std::string(boot.status)); + ASSERT_EQ("stage bytes", std::string(boot.stage)); + ASSERT_EQ("reserved bytes", std::string(boot.reserved)); +} + +TEST(BootloaderMessageTest, update_bootloader_message_recovery_options_empty) { + // Write empty vector. + std::vector<std::string> options; + + // Read and verify. + bootloader_message boot = {}; + ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options)); + + // command and recovery fields should be set. + ASSERT_EQ("boot-recovery", std::string(boot.command)); + ASSERT_EQ("recovery\n", std::string(boot.recovery)); + + // The rest should be empty. + ASSERT_EQ(std::string(sizeof(boot.status), '\0'), std::string(boot.status, sizeof(boot.status))); + ASSERT_EQ(std::string(sizeof(boot.stage), '\0'), std::string(boot.stage, sizeof(boot.stage))); + ASSERT_EQ(std::string(sizeof(boot.reserved), '\0'), + std::string(boot.reserved, sizeof(boot.reserved))); +} + +TEST(BootloaderMessageTest, update_bootloader_message_recovery_options_long) { + // Write super long message. + std::vector<std::string> options; + for (int i = 0; i < 100; i++) { + options.push_back("option: " + std::to_string(i)); + } + + // Read and verify. + bootloader_message boot = {}; + ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options)); + + // Make sure it's long enough. + std::string expected = "recovery\n" + android::base::Join(options, "\n") + "\n"; + ASSERT_GE(expected.size(), sizeof(boot.recovery)); + + // command and recovery fields should be set. + ASSERT_EQ("boot-recovery", std::string(boot.command)); + ASSERT_EQ(expected.substr(0, sizeof(boot.recovery) - 1), std::string(boot.recovery)); + ASSERT_EQ('\0', boot.recovery[sizeof(boot.recovery) - 1]); + + // The rest should be empty. + ASSERT_EQ(std::string(sizeof(boot.status), '\0'), std::string(boot.status, sizeof(boot.status))); + ASSERT_EQ(std::string(sizeof(boot.stage), '\0'), std::string(boot.stage, sizeof(boot.stage))); + ASSERT_EQ(std::string(sizeof(boot.reserved), '\0'), + std::string(boot.reserved, sizeof(boot.reserved))); +} + diff --git a/tests/unit/commands_test.cpp b/tests/unit/commands_test.cpp new file mode 100644 index 000000000..8a54df703 --- /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/dirutil_test.cpp b/tests/unit/dirutil_test.cpp index 7f85d13ea..4dd111a70 100644 --- a/tests/unit/dirutil_test.cpp +++ b/tests/unit/dirutil_test.cpp @@ -20,9 +20,10 @@ #include <string> -#include <android-base/test_utils.h> +#include <android-base/file.h> #include <gtest/gtest.h> -#include <otautil/DirUtil.h> + +#include "otautil/dirutil.h" TEST(DirUtilTest, create_invalid) { // Requesting to create an empty dir is invalid. diff --git a/tests/unit/edify_test.cpp b/tests/unit/edify_test.cpp new file mode 100644 index 000000000..8397bd38e --- /dev/null +++ b/tests/unit/edify_test.cpp @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <memory> +#include <string> + +#include <gtest/gtest.h> + +#include "edify/expr.h" + +static void expect(const std::string& expr_str, const char* expected) { + std::unique_ptr<Expr> e; + int error_count = 0; + EXPECT_EQ(0, ParseString(expr_str, &e, &error_count)); + EXPECT_EQ(0, error_count); + + State state(expr_str, nullptr); + + std::string result; + bool status = Evaluate(&state, e, &result); + + if (expected == nullptr) { + EXPECT_FALSE(status); + } else { + EXPECT_STREQ(expected, result.c_str()); + } +} + +class EdifyTest : public ::testing::Test { + protected: + void SetUp() { + RegisterBuiltins(); + } +}; + +TEST_F(EdifyTest, parsing) { + expect("a", "a"); + expect("\"a\"", "a"); + expect("\"\\x61\"", "a"); + expect("# this is a comment\n" + " a\n" + " \n", + "a"); +} + +TEST_F(EdifyTest, sequence) { + // sequence operator + expect("a; b; c", "c"); +} + +TEST_F(EdifyTest, concat) { + // string concat operator + expect("a + b", "ab"); + expect("a + \n \"b\"", "ab"); + expect("a + b +\nc\n", "abc"); + + // string concat function + expect("concat(a, b)", "ab"); + expect("concat(a,\n \"b\")", "ab"); + expect("concat(a + b,\nc,\"d\")", "abcd"); + expect("\"concat\"(a + b,\nc,\"d\")", "abcd"); +} + +TEST_F(EdifyTest, logical) { + // logical and + expect("a && b", "b"); + expect("a && \"\"", ""); + expect("\"\" && b", ""); + expect("\"\" && \"\"", ""); + expect("\"\" && abort()", ""); // test short-circuiting + expect("t && abort()", nullptr); + + // logical or + expect("a || b", "a"); + expect("a || \"\"", "a"); + expect("\"\" || b", "b"); + expect("\"\" || \"\"", ""); + expect("a || abort()", "a"); // test short-circuiting + expect("\"\" || abort()", NULL); + + // logical not + expect("!a", ""); + expect("! \"\"", "t"); + expect("!!a", "t"); +} + +TEST_F(EdifyTest, precedence) { + // precedence + expect("\"\" == \"\" && b", "b"); + expect("a + b == ab", "t"); + expect("ab == a + b", "t"); + expect("a + (b == ab)", "a"); + expect("(ab == a) + b", "b"); +} + +TEST_F(EdifyTest, substring) { + // substring function + expect("is_substring(cad, abracadabra)", "t"); + expect("is_substring(abrac, abracadabra)", "t"); + expect("is_substring(dabra, abracadabra)", "t"); + expect("is_substring(cad, abracxadabra)", ""); + expect("is_substring(abrac, axbracadabra)", ""); + expect("is_substring(dabra, abracadabrxa)", ""); +} + +TEST_F(EdifyTest, ifelse) { + // ifelse function + expect("ifelse(t, yes, no)", "yes"); + expect("ifelse(!t, yes, no)", "no"); + expect("ifelse(t, yes, abort())", "yes"); + expect("ifelse(!t, abort(), no)", "no"); +} + +TEST_F(EdifyTest, if_statement) { + // if "statements" + expect("if t then yes else no endif", "yes"); + expect("if \"\" then yes else no endif", "no"); + expect("if \"\" then yes endif", ""); + expect("if \"\"; t then yes endif", "yes"); +} + +TEST_F(EdifyTest, comparison) { + // numeric comparisons + expect("less_than_int(3, 14)", "t"); + expect("less_than_int(14, 3)", ""); + expect("less_than_int(x, 3)", ""); + expect("less_than_int(3, x)", ""); + expect("greater_than_int(3, 14)", ""); + expect("greater_than_int(14, 3)", "t"); + expect("greater_than_int(x, 3)", ""); + expect("greater_than_int(3, x)", ""); +} + +TEST_F(EdifyTest, big_string) { + expect(std::string(8192, 's'), std::string(8192, 's').c_str()); +} + +TEST_F(EdifyTest, unknown_function) { + const char* script1 = "unknown_function()"; + std::unique_ptr<Expr> expr; + int error_count = 0; + EXPECT_EQ(1, ParseString(script1, &expr, &error_count)); + EXPECT_EQ(1, error_count); + + const char* script2 = "abc; unknown_function()"; + error_count = 0; + EXPECT_EQ(1, ParseString(script2, &expr, &error_count)); + EXPECT_EQ(1, error_count); + + const char* script3 = "unknown_function1() || yes"; + error_count = 0; + EXPECT_EQ(1, ParseString(script3, &expr, &error_count)); + EXPECT_EQ(1, error_count); +} diff --git a/tests/unit/fuse_provider_test.cpp b/tests/unit/fuse_provider_test.cpp new file mode 100644 index 000000000..c5995dd7d --- /dev/null +++ b/tests/unit/fuse_provider_test.cpp @@ -0,0 +1,103 @@ +/* + * 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 <unistd.h> + +#include <functional> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/strings.h> +#include <android-base/unique_fd.h> +#include <gtest/gtest.h> + +#include "fuse_provider.h" +#include "fuse_sideload.h" +#include "install/install.h" + +TEST(FuseBlockMapTest, CreateFromBlockMap_smoke) { + TemporaryFile fake_block_device; + std::vector<std::string> lines = { + fake_block_device.path, "10000 4096", "3", "10 11", "20 21", "22 23", + }; + + TemporaryFile temp_file; + android::base::WriteStringToFile(android::base::Join(lines, '\n'), temp_file.path); + auto block_map_data = FuseBlockDataProvider::CreateFromBlockMap(temp_file.path, 4096); + + ASSERT_TRUE(block_map_data); + ASSERT_EQ(10000, block_map_data->file_size()); + ASSERT_EQ(4096, block_map_data->fuse_block_size()); + ASSERT_EQ(RangeSet({ { 10, 11 }, { 20, 21 }, { 22, 23 } }), block_map_data->ranges()); +} + +TEST(FuseBlockMapTest, ReadBlockAlignedData_smoke) { + std::string content; + content.reserve(40960); + for (char c = 0; c < 10; c++) { + content += std::string(4096, c); + } + TemporaryFile fake_block_device; + ASSERT_TRUE(android::base::WriteStringToFile(content, fake_block_device.path)); + + std::vector<std::string> lines = { + fake_block_device.path, + "20000 4096", + "1", + "0 5", + }; + TemporaryFile temp_file; + android::base::WriteStringToFile(android::base::Join(lines, '\n'), temp_file.path); + auto block_map_data = FuseBlockDataProvider::CreateFromBlockMap(temp_file.path, 4096); + + std::vector<uint8_t> result(2000); + ASSERT_TRUE(block_map_data->ReadBlockAlignedData(result.data(), 2000, 1)); + ASSERT_EQ(std::vector<uint8_t>(content.begin() + 4096, content.begin() + 6096), result); + + result.resize(20000); + ASSERT_TRUE(block_map_data->ReadBlockAlignedData(result.data(), 20000, 0)); + ASSERT_EQ(std::vector<uint8_t>(content.begin(), content.begin() + 20000), result); +} + +TEST(FuseBlockMapTest, ReadBlockAlignedData_large_fuse_block) { + std::string content; + for (char c = 0; c < 10; c++) { + content += std::string(4096, c); + } + + TemporaryFile temp_file; + ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path)); + + std::vector<std::string> lines = { + temp_file.path, "36384 4096", "2", "0 5", "6 10", + }; + TemporaryFile block_map; + ASSERT_TRUE(android::base::WriteStringToFile(android::base::Join(lines, '\n'), block_map.path)); + + auto block_map_data = FuseBlockDataProvider::CreateFromBlockMap(block_map.path, 16384); + ASSERT_TRUE(block_map_data); + + std::vector<uint8_t> result(20000); + // Out of bound read + ASSERT_FALSE(block_map_data->ReadBlockAlignedData(result.data(), 20000, 2)); + ASSERT_TRUE(block_map_data->ReadBlockAlignedData(result.data(), 20000, 1)); + // expected source block contains: 4, 6-9 + std::string expected = content.substr(16384, 4096) + content.substr(24576, 15904); + ASSERT_EQ(std::vector<uint8_t>(expected.begin(), expected.end()), result); +} diff --git a/tests/unit/fuse_sideload_test.cpp b/tests/unit/fuse_sideload_test.cpp new file mode 100644 index 000000000..6add99f41 --- /dev/null +++ b/tests/unit/fuse_sideload_test.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <unistd.h> + +#include <memory> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> + +#include "fuse_provider.h" +#include "fuse_sideload.h" + +TEST(SideloadTest, fuse_device) { + ASSERT_EQ(0, access("/dev/fuse", R_OK | W_OK)); +} + +class FuseTestDataProvider : public FuseDataProvider { + public: + FuseTestDataProvider(uint64_t file_size, uint32_t block_size) + : FuseDataProvider(file_size, block_size) {} + + private: + bool ReadBlockAlignedData(uint8_t*, uint32_t, uint32_t) const override { + return true; + } +}; + +TEST(SideloadTest, run_fuse_sideload_wrong_parameters) { + auto provider_small_block = std::make_unique<FuseTestDataProvider>(4096, 4095); + ASSERT_EQ(-1, run_fuse_sideload(std::move(provider_small_block))); + + auto provider_large_block = std::make_unique<FuseTestDataProvider>(4096, (1 << 22) + 1); + ASSERT_EQ(-1, run_fuse_sideload(std::move(provider_large_block))); + + auto provider_too_many_blocks = + std::make_unique<FuseTestDataProvider>(((1 << 18) + 1) * 4096, 4096); + ASSERT_EQ(-1, run_fuse_sideload(std::move(provider_too_many_blocks))); +} + +TEST(SideloadTest, run_fuse_sideload) { + const std::vector<std::string> blocks = { + std::string(2048, 'a') + std::string(2048, 'b'), + std::string(2048, 'c') + std::string(2048, 'd'), + std::string(2048, 'e') + std::string(2048, 'f'), + std::string(2048, 'g') + std::string(2048, 'h'), + }; + const std::string content = android::base::Join(blocks, ""); + ASSERT_EQ(16384U, content.size()); + + TemporaryFile temp_file; + ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path)); + + auto provider = std::make_unique<FuseFileDataProvider>(temp_file.path, 4096); + ASSERT_TRUE(provider->Valid()); + TemporaryDir mount_point; + pid_t pid = fork(); + if (pid == 0) { + ASSERT_EQ(0, run_fuse_sideload(std::move(provider), mount_point.path)); + _exit(EXIT_SUCCESS); + } + + std::string package = std::string(mount_point.path) + "/" + FUSE_SIDELOAD_HOST_FILENAME; + int status; + static constexpr int kSideloadInstallTimeout = 10; + for (int i = 0; i < kSideloadInstallTimeout; ++i) { + ASSERT_NE(-1, waitpid(pid, &status, WNOHANG)); + + struct stat sb; + if (stat(package.c_str(), &sb) == 0) { + break; + } + + if (errno == ENOENT && i < kSideloadInstallTimeout - 1) { + sleep(1); + continue; + } + FAIL() << "Timed out waiting for the fuse-provided package."; + } + + std::string content_via_fuse; + ASSERT_TRUE(android::base::ReadFileToString(package, &content_via_fuse)); + ASSERT_EQ(content, content_via_fuse); + + std::string exit_flag = std::string(mount_point.path) + "/" + FUSE_SIDELOAD_HOST_EXIT_FLAG; + struct stat sb; + ASSERT_EQ(0, stat(exit_flag.c_str(), &sb)); + + waitpid(pid, &status, 0); + ASSERT_EQ(0, WEXITSTATUS(status)); + ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); +} diff --git a/tests/unit/imgdiff_test.cpp b/tests/unit/imgdiff_test.cpp new file mode 100644 index 000000000..e76ccbdfb --- /dev/null +++ b/tests/unit/imgdiff_test.cpp @@ -0,0 +1,1114 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> + +#include <algorithm> +#include <string> +#include <tuple> +#include <vector> + +#include <android-base/file.h> +#include <android-base/memory.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#include <applypatch/imgdiff.h> +#include <applypatch/imgdiff_image.h> +#include <applypatch/imgpatch.h> +#include <gtest/gtest.h> +#include <ziparchive/zip_writer.h> + +#include "common/test_constants.h" + +using android::base::get_unaligned; + +// Sanity check for the given imgdiff patch header. +static void verify_patch_header(const std::string& patch, size_t* num_normal, size_t* num_raw, + size_t* num_deflate) { + const size_t size = patch.size(); + const char* data = patch.data(); + + ASSERT_GE(size, 12U); + ASSERT_EQ("IMGDIFF2", std::string(data, 8)); + + const int num_chunks = get_unaligned<int32_t>(data + 8); + ASSERT_GE(num_chunks, 0); + + size_t normal = 0; + size_t raw = 0; + size_t deflate = 0; + + size_t pos = 12; + for (int i = 0; i < num_chunks; ++i) { + ASSERT_LE(pos + 4, size); + int type = get_unaligned<int32_t>(data + pos); + pos += 4; + if (type == CHUNK_NORMAL) { + pos += 24; + ASSERT_LE(pos, size); + normal++; + } else if (type == CHUNK_RAW) { + ASSERT_LE(pos + 4, size); + ssize_t data_len = get_unaligned<int32_t>(data + pos); + ASSERT_GT(data_len, 0); + pos += 4 + data_len; + ASSERT_LE(pos, size); + raw++; + } else if (type == CHUNK_DEFLATE) { + pos += 60; + ASSERT_LE(pos, size); + deflate++; + } else { + FAIL() << "Invalid patch type: " << type; + } + } + + if (num_normal != nullptr) *num_normal = normal; + if (num_raw != nullptr) *num_raw = raw; + if (num_deflate != nullptr) *num_deflate = deflate; +} + +static void GenerateTarget(const std::string& src, const std::string& patch, std::string* patched) { + patched->clear(); + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + [&](const unsigned char* data, size_t len) { + patched->append(reinterpret_cast<const char*>(data), len); + return len; + })); +} + +static void verify_patched_image(const std::string& src, const std::string& patch, + const std::string& tgt) { + std::string patched; + GenerateTarget(src, patch, &patched); + ASSERT_EQ(tgt, patched); +} + +TEST(ImgdiffTest, invalid_args) { + // Insufficient inputs. + ASSERT_EQ(2, imgdiff(1, (const char* []){ "imgdiff" })); + ASSERT_EQ(2, imgdiff(2, (const char* []){ "imgdiff", "-z" })); + ASSERT_EQ(2, imgdiff(2, (const char* []){ "imgdiff", "-b" })); + ASSERT_EQ(2, imgdiff(3, (const char* []){ "imgdiff", "-z", "-b" })); + + // Failed to read bonus file. + ASSERT_EQ(1, imgdiff(3, (const char* []){ "imgdiff", "-b", "doesntexist" })); + + // Failed to read input files. + ASSERT_EQ(1, imgdiff(4, (const char* []){ "imgdiff", "doesntexist", "doesntexist", "output" })); + ASSERT_EQ( + 1, imgdiff(5, (const char* []){ "imgdiff", "-z", "doesntexist", "doesntexist", "output" })); +} + +TEST(ImgdiffTest, image_mode_smoke) { + // Random bytes. + const std::string src("abcdefg"); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + const std::string tgt("abcdefgxyz"); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, zip_mode_smoke_store) { + // Construct src and tgt zip files. + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + ASSERT_EQ(0, src_writer.StartEntry("file1.txt", 0)); // Store mode. + const std::string src_content("abcdefg"); + ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); + ASSERT_EQ(0, src_writer.FinishEntry()); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", 0)); // Store mode. + const std::string tgt_content("abcdefgxyz"); + ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + std::string src; + ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src)); + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, zip_mode_smoke_compressed) { + // Generate 1 block of random data. + std::string random_data; + random_data.reserve(4096); + generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; }); + + // Construct src and tgt zip files. + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string src_content = random_data; + ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); + ASSERT_EQ(0, src_writer.FinishEntry()); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string tgt_content = random_data + "extra contents"; + ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + std::string src; + ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src)); + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer). + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(1U, num_deflate); + ASSERT_EQ(2U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, zip_mode_empty_target) { + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string src_content = "abcdefg"; + ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); + ASSERT_EQ(0, src_writer.FinishEntry()); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + // Construct a empty entry in the target zip. + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string tgt_content; + ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + ASSERT_EQ(0, tgt_writer.Finish()); + + // Compute patch. + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + std::string src; + ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src)); + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) { + // Generate 1 block of random data. + std::string random_data; + random_data.reserve(4096); + generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; }); + + // Construct src and tgt zip files. + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string src_content = random_data; + ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); + ASSERT_EQ(0, src_writer.FinishEntry()); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string tgt_content = random_data + "abcdefg"; + ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + ASSERT_EQ(0, tgt_writer.Finish()); + // Add trailing zeros to the target zip file. + std::vector<uint8_t> zeros(10); + ASSERT_EQ(zeros.size(), fwrite(zeros.data(), sizeof(uint8_t), zeros.size(), tgt_file_ptr)); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + std::string src; + ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src)); + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer). + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(1U, num_deflate); + ASSERT_EQ(2U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, image_mode_simple) { + std::string gzipped_source_path = from_testdata_base("gzipped_source"); + std::string gzipped_source; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source)); + + const std::string src = "abcdefg" + gzipped_source; + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + std::string gzipped_target_path = from_testdata_base("gzipped_target"); + std::string gzipped_target; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target)); + const std::string tgt = "abcdefgxyz" + gzipped_target; + + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer). + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(1U, num_deflate); + ASSERT_EQ(2U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, image_mode_bad_gzip) { + // Modify the uncompressed length in the gzip footer. + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', + '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', + '\xff', '\xff', '\xff' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // Modify the uncompressed length in the gzip footer. + const std::vector<char> tgt_data = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z', '\x1f', '\x8b', + '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac', + '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\xff', '\xff', '\xff' + }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, image_mode_different_num_chunks) { + // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd) + gzipped "test". + const std::vector<char> src_data = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', '\x1f', '\x8b', '\x08', + '\x00', '\xc4', '\x1e', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', '\x02', + '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', '\x00', '\x00', '\x00', '\x1f', '\x8b', + '\x08', '\x00', '\xb2', '\x3a', '\x53', '\x58', '\x00', '\x03', '\x2b', '\x49', '\x2d', + '\x2e', '\x01', '\x00', '\x0c', '\x7e', '\x7f', '\xd8', '\x04', '\x00', '\x00', '\x00' + }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz" + gzipped "xxyyzz". + const std::vector<char> tgt_data = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z', '\x1f', '\x8b', + '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac', + '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00' + }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(1, imgdiff(args.size(), args.data())); +} + +TEST(ImgdiffTest, image_mode_merge_chunks) { + // src: "abcdefg" + gzipped_source. + std::string gzipped_source_path = from_testdata_base("gzipped_source"); + std::string gzipped_source; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source)); + + const std::string src = "abcdefg" + gzipped_source; + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: gzipped_target + "abcdefgxyz". + std::string gzipped_target_path = from_testdata_base("gzipped_target"); + std::string gzipped_target; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target)); + + const std::string tgt = gzipped_target + "abcdefgxyz"; + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + // Since a gzipped entry will become CHUNK_RAW (header) + CHUNK_DEFLATE (data) + + // CHUNK_RAW (footer), they both should contain the same chunk types after merging. + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer). + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(1U, num_deflate); + ASSERT_EQ(2U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, image_mode_spurious_magic) { + // src: "abcdefgh" + '0x1f8b0b00' + some bytes. + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', 't', 'e', 's', 't' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz". + const std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW (header) entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, image_mode_short_input1) { + // src: "abcdefgh" + '0x1f8b0b'. + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', '\x1f', '\x8b', '\x08' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz". + const std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW (header) entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, image_mode_short_input2) { + // src: "abcdefgh" + '0x1f8b0b00'. + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', '\x1f', '\x8b', '\x08', '\x00' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz". + const std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW (header) entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgdiffTest, image_mode_single_entry_long) { + // src: "abcdefgh" + '0x1f8b0b00' + some bytes. + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', 't', 'e', 's', 't' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz" + 200 bytes. + std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' }; + tgt_data.resize(tgt_data.size() + 200); + + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_NORMAL entry, since it's exceeding the 160-byte limit for RAW. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(1U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(0U, num_raw); + + verify_patched_image(src, patch, tgt); +} + +TEST(ImgpatchTest, image_mode_patch_corruption) { + // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd). + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', + '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', + '\x00', '\x00', '\x00' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz" + gzipped "xxyyzz". + const std::vector<char> tgt_data = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z', '\x1f', '\x8b', + '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac', + '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00' + }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + verify_patched_image(src, patch, tgt); + + // Corrupt the end of the patch and expect the ApplyImagePatch to fail. + patch.insert(patch.end() - 10, 10, '0'); + ASSERT_EQ(-1, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + [](const unsigned char* /*data*/, size_t len) { return len; })); +} + +static void construct_store_entry(const std::vector<std::tuple<std::string, size_t, char>>& info, + ZipWriter* writer) { + for (auto& t : info) { + // Create t(1) blocks of t(2), and write the data to t(0) + ASSERT_EQ(0, writer->StartEntry(std::get<0>(t).c_str(), 0)); + const std::string content(std::get<1>(t) * 4096, std::get<2>(t)); + ASSERT_EQ(0, writer->WriteBytes(content.data(), content.size())); + ASSERT_EQ(0, writer->FinishEntry()); + } +} + +static void construct_deflate_entry(const std::vector<std::tuple<std::string, size_t, size_t>>& info, + ZipWriter* writer, const std::string& data) { + for (auto& t : info) { + // t(0): entry_name; t(1): block offset; t(2) length in blocks. + ASSERT_EQ(0, writer->StartEntry(std::get<0>(t).c_str(), ZipWriter::kCompress)); + ASSERT_EQ(0, writer->WriteBytes(data.data() + std::get<1>(t) * 4096, std::get<2>(t) * 4096)); + ASSERT_EQ(0, writer->FinishEntry()); + } +} + +// Look for the source and patch pieces in debug_dir. Generate a target piece from each pair. +// Concatenate all the target pieces and match against the orignal one. Used pieces in debug_dir +// will be cleaned up. +static void GenerateAndCheckSplitTarget(const std::string& debug_dir, size_t count, + const std::string& tgt) { + std::string patched; + for (size_t i = 0; i < count; i++) { + std::string split_src_path = android::base::StringPrintf("%s/src-%zu", debug_dir.c_str(), i); + std::string split_src; + ASSERT_TRUE(android::base::ReadFileToString(split_src_path, &split_src)); + ASSERT_EQ(0, unlink(split_src_path.c_str())); + + std::string split_patch_path = + android::base::StringPrintf("%s/patch-%zu", debug_dir.c_str(), i); + std::string split_patch; + ASSERT_TRUE(android::base::ReadFileToString(split_patch_path, &split_patch)); + ASSERT_EQ(0, unlink(split_patch_path.c_str())); + + std::string split_tgt; + GenerateTarget(split_src, split_patch, &split_tgt); + patched += split_tgt; + } + + // Verify we can get back the original target image. + ASSERT_EQ(tgt, patched); +} + +std::vector<ImageChunk> ConstructImageChunks( + const std::vector<uint8_t>& content, const std::vector<std::tuple<std::string, size_t>>& info) { + std::vector<ImageChunk> chunks; + size_t start = 0; + for (const auto& t : info) { + size_t length = std::get<1>(t); + chunks.emplace_back(CHUNK_NORMAL, start, &content, length, std::get<0>(t)); + start += length; + } + + return chunks; +} + +TEST(ImgdiffTest, zip_mode_split_image_smoke) { + std::vector<uint8_t> content; + content.reserve(4096 * 50); + uint8_t n = 0; + generate_n(back_inserter(content), 4096 * 50, [&n]() { return n++ / 4096; }); + + ZipModeImage tgt_image(false, 4096 * 10); + std::vector<ImageChunk> tgt_chunks = ConstructImageChunks(content, { { "a", 100 }, + { "b", 4096 * 2 }, + { "c", 4096 * 3 }, + { "d", 300 }, + { "e-0", 4096 * 10 }, + { "e-1", 4096 * 5 }, + { "CD", 200 } }); + tgt_image.Initialize(std::move(tgt_chunks), + std::vector<uint8_t>(content.begin(), content.begin() + 82520)); + + tgt_image.DumpChunks(); + + ZipModeImage src_image(true, 4096 * 10); + std::vector<ImageChunk> src_chunks = ConstructImageChunks(content, { { "b", 4096 * 3 }, + { "c-0", 4096 * 10 }, + { "c-1", 4096 * 2 }, + { "a", 4096 * 5 }, + { "e-0", 4096 * 10 }, + { "e-1", 10000 }, + { "CD", 5000 } }); + src_image.Initialize(std::move(src_chunks), + std::vector<uint8_t>(content.begin(), content.begin() + 137880)); + + std::vector<ZipModeImage> split_tgt_images; + std::vector<ZipModeImage> split_src_images; + std::vector<SortedRangeSet> split_src_ranges; + + ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images, + &split_src_images, &split_src_ranges); + + // src_piece 1: a 5 blocks, b 3 blocks + // src_piece 2: c-0 10 blocks + // src_piece 3: d 0 block, e-0 10 blocks + // src_piece 4: e-1 2 blocks; CD 2 blocks + ASSERT_EQ(split_tgt_images.size(), split_src_images.size()); + ASSERT_EQ(static_cast<size_t>(4), split_tgt_images.size()); + + ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[0].NumOfChunks()); + ASSERT_EQ(static_cast<size_t>(12288), split_tgt_images[0][0].DataLengthForPatch()); + ASSERT_EQ("4,0,3,15,20", split_src_ranges[0].ToString()); + + ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[1].NumOfChunks()); + ASSERT_EQ(static_cast<size_t>(12288), split_tgt_images[1][0].DataLengthForPatch()); + ASSERT_EQ("2,3,13", split_src_ranges[1].ToString()); + + ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[2].NumOfChunks()); + ASSERT_EQ(static_cast<size_t>(40960), split_tgt_images[2][0].DataLengthForPatch()); + ASSERT_EQ("2,20,30", split_src_ranges[2].ToString()); + + ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[3].NumOfChunks()); + ASSERT_EQ(static_cast<size_t>(16984), split_tgt_images[3][0].DataLengthForPatch()); + ASSERT_EQ("2,30,34", split_src_ranges[3].ToString()); +} + +TEST(ImgdiffTest, zip_mode_store_large_apk) { + // Construct src and tgt zip files with limit = 10 blocks. + // src tgt + // 12 blocks 'd' 3 blocks 'a' + // 8 blocks 'c' 3 blocks 'b' + // 3 blocks 'b' 8 blocks 'c' (exceeds limit) + // 3 blocks 'a' 12 blocks 'd' (exceeds limit) + // 3 blocks 'e' + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + construct_store_entry( + { { "a", 3, 'a' }, { "b", 3, 'b' }, { "c", 8, 'c' }, { "d", 12, 'd' }, { "e", 3, 'e' } }, + &tgt_writer); + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + construct_store_entry({ { "d", 12, 'd' }, { "c", 8, 'c' }, { "b", 3, 'b' }, { "a", 3, 'a' } }, + &src_writer); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + TemporaryFile split_info_file; + TemporaryDir debug_dir; + std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path); + std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path); + std::vector<const char*> args = { + "imgdiff", "-z", "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(), + src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + + // Expect 4 pieces of patch. (Roughly 3'a',3'b'; 8'c'; 10'd'; 2'd'3'e') + GenerateAndCheckSplitTarget(debug_dir.path, 4, tgt); +} + +TEST(ImgdiffTest, zip_mode_deflate_large_apk) { + // Src and tgt zip files are constructed as follows. + // src tgt + // 22 blocks, "d" 4 blocks, "a" + // 5 blocks, "b" 4 blocks, "b" + // 3 blocks, "a" 8 blocks, "c" (exceeds limit) + // 1 block, "g" 20 blocks, "d" (exceeds limit) + // 8 blocks, "c" 2 blocks, "e" + // 1 block, "f" 1 block , "f" + std::string tgt_path = from_testdata_base("deflate_tgt.zip"); + std::string src_path = from_testdata_base("deflate_src.zip"); + + ZipModeImage src_image(true, 10 * 4096); + ZipModeImage tgt_image(false, 10 * 4096); + ASSERT_TRUE(src_image.Initialize(src_path)); + ASSERT_TRUE(tgt_image.Initialize(tgt_path)); + ASSERT_TRUE(ZipModeImage::CheckAndProcessChunks(&tgt_image, &src_image)); + + src_image.DumpChunks(); + tgt_image.DumpChunks(); + + std::vector<ZipModeImage> split_tgt_images; + std::vector<ZipModeImage> split_src_images; + std::vector<SortedRangeSet> split_src_ranges; + ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images, + &split_src_images, &split_src_ranges); + + // Expected split images with limit = 10 blocks. + // src_piece 0: a 3 blocks, b 5 blocks + // src_piece 1: c 8 blocks + // src_piece 2: d-0 10 block + // src_piece 3: d-1 10 blocks + // src_piece 4: e 1 block, CD + ASSERT_EQ(split_tgt_images.size(), split_src_images.size()); + ASSERT_EQ(static_cast<size_t>(5), split_tgt_images.size()); + + ASSERT_EQ(static_cast<size_t>(2), split_src_images[0].NumOfChunks()); + ASSERT_EQ("a", split_src_images[0][0].GetEntryName()); + ASSERT_EQ("b", split_src_images[0][1].GetEntryName()); + + ASSERT_EQ(static_cast<size_t>(1), split_src_images[1].NumOfChunks()); + ASSERT_EQ("c", split_src_images[1][0].GetEntryName()); + + ASSERT_EQ(static_cast<size_t>(0), split_src_images[2].NumOfChunks()); + ASSERT_EQ(static_cast<size_t>(0), split_src_images[3].NumOfChunks()); + ASSERT_EQ(static_cast<size_t>(0), split_src_images[4].NumOfChunks()); + + // Compute patch. + TemporaryFile patch_file; + TemporaryFile split_info_file; + TemporaryDir debug_dir; + ASSERT_TRUE(ZipModeImage::GeneratePatches(split_tgt_images, split_src_images, split_src_ranges, + patch_file.path, split_info_file.path, debug_dir.path)); + + // Verify the content of split info. + // Expect 5 pieces of patch. ["a","b"; "c"; "d-0"; "d-1"; "e"] + std::string split_info_string; + android::base::ReadFileToString(split_info_file.path, &split_info_string); + std::vector<std::string> info_list = + android::base::Split(android::base::Trim(split_info_string), "\n"); + + ASSERT_EQ(static_cast<size_t>(7), info_list.size()); + ASSERT_EQ("2", android::base::Trim(info_list[0])); + ASSERT_EQ("5", android::base::Trim(info_list[1])); + + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_path, &tgt)); + ASSERT_EQ(static_cast<size_t>(160385), tgt.size()); + std::vector<std::string> tgt_file_ranges = { + "36864 2,22,31", "32768 2,31,40", "40960 2,0,11", "40960 2,11,21", "8833 4,21,22,40,41", + }; + + for (size_t i = 0; i < 5; i++) { + struct stat st; + std::string path = android::base::StringPrintf("%s/patch-%zu", debug_dir.path, i); + ASSERT_EQ(0, stat(path.c_str(), &st)); + ASSERT_EQ(std::to_string(st.st_size) + " " + tgt_file_ranges[i], + android::base::Trim(info_list[i + 2])); + } + + GenerateAndCheckSplitTarget(debug_dir.path, 5, tgt); +} + +TEST(ImgdiffTest, zip_mode_no_match_source) { + // Generate 20 blocks of random data. + std::string random_data; + random_data.reserve(4096 * 20); + generate_n(back_inserter(random_data), 4096 * 20, []() { return rand() % 256; }); + + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + + construct_deflate_entry({ { "a", 0, 4 }, { "b", 5, 5 }, { "c", 11, 5 } }, &tgt_writer, + random_data); + + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + // We don't have a matching source entry. + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + construct_store_entry({ { "d", 1, 'd' } }, &src_writer); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + TemporaryFile split_info_file; + TemporaryDir debug_dir; + std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path); + std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path); + std::vector<const char*> args = { + "imgdiff", "-z", "--block-limit=10", debug_dir_arg.c_str(), split_info_arg.c_str(), + src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + + // Expect 1 pieces of patch due to no matching source entry. + GenerateAndCheckSplitTarget(debug_dir.path, 1, tgt); +} + +TEST(ImgdiffTest, zip_mode_large_enough_limit) { + // Generate 20 blocks of random data. + std::string random_data; + random_data.reserve(4096 * 20); + generate_n(back_inserter(random_data), 4096 * 20, []() { return rand() % 256; }); + + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + + construct_deflate_entry({ { "a", 0, 10 }, { "b", 10, 5 } }, &tgt_writer, random_data); + + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + // Construct 10 blocks of source. + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + construct_deflate_entry({ { "a", 1, 10 } }, &src_writer, random_data); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + // Compute patch with a limit of 20 blocks. + TemporaryFile patch_file; + TemporaryFile split_info_file; + TemporaryDir debug_dir; + std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path); + std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path); + std::vector<const char*> args = { + "imgdiff", "-z", "--block-limit=20", split_info_arg.c_str(), debug_dir_arg.c_str(), + src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + + // Expect 1 piece of patch since limit is larger than the zip file size. + GenerateAndCheckSplitTarget(debug_dir.path, 1, tgt); +} + +TEST(ImgdiffTest, zip_mode_large_apk_small_target_chunk) { + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + + // The first entry is less than 4096 bytes, followed immediately by an entry that has a very + // large counterpart in the source file. Therefore the first entry will be patched separately. + std::string small_chunk("a", 2000); + ASSERT_EQ(0, tgt_writer.StartEntry("a", 0)); + ASSERT_EQ(0, tgt_writer.WriteBytes(small_chunk.data(), small_chunk.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + construct_store_entry( + { + { "b", 12, 'b' }, { "c", 3, 'c' }, + }, + &tgt_writer); + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + construct_store_entry({ { "a", 1, 'a' }, { "b", 13, 'b' }, { "c", 1, 'c' } }, &src_writer); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + TemporaryFile split_info_file; + TemporaryDir debug_dir; + std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path); + std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path); + std::vector<const char*> args = { + "imgdiff", "-z", "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(), + src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + + // Expect three split src images: + // src_piece 0: a 1 blocks + // src_piece 1: b-0 10 blocks + // src_piece 2: b-1 3 blocks, c 1 blocks, CD + GenerateAndCheckSplitTarget(debug_dir.path, 3, tgt); +} + +TEST(ImgdiffTest, zip_mode_large_apk_skipped_small_target_chunk) { + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + + construct_store_entry( + { + { "a", 11, 'a' }, + }, + &tgt_writer); + + // Construct a tiny target entry of 1 byte, which will be skipped due to the tail alignment of + // the previous entry. + std::string small_chunk("b", 1); + ASSERT_EQ(0, tgt_writer.StartEntry("b", 0)); + ASSERT_EQ(0, tgt_writer.WriteBytes(small_chunk.data(), small_chunk.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + construct_store_entry( + { + { "a", 11, 'a' }, { "b", 11, 'b' }, + }, + &src_writer); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + TemporaryFile split_info_file; + TemporaryDir debug_dir; + std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path); + std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path); + std::vector<const char*> args = { + "imgdiff", "-z", "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(), + src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + + // Expect two split src images: + // src_piece 0: a-0 10 blocks + // src_piece 1: a-0 1 block, CD + GenerateAndCheckSplitTarget(debug_dir.path, 2, tgt); +} diff --git a/tests/unit/install_test.cpp b/tests/unit/install_test.cpp new file mode 100644 index 000000000..4ec409908 --- /dev/null +++ b/tests/unit/install_test.cpp @@ -0,0 +1,597 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agree to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include <algorithm> +#include <random> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/properties.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> +#include <vintf/VintfObjectRecovery.h> +#include <ziparchive/zip_archive.h> +#include <ziparchive/zip_writer.h> + +#include "install/install.h" +#include "install/wipe_device.h" +#include "otautil/paths.h" +#include "private/setup_commands.h" + +static void BuildZipArchive(const std::map<std::string, std::string>& file_map, int fd, + int compression_type) { + FILE* zip_file = fdopen(fd, "w"); + ZipWriter writer(zip_file); + for (const auto& [name, content] : file_map) { + ASSERT_EQ(0, writer.StartEntry(name.c_str(), compression_type)); + ASSERT_EQ(0, writer.WriteBytes(content.data(), content.size())); + ASSERT_EQ(0, writer.FinishEntry()); + } + ASSERT_EQ(0, writer.Finish()); + ASSERT_EQ(0, fclose(zip_file)); +} + +TEST(InstallTest, verify_package_compatibility_no_entry) { + TemporaryFile temp_file; + // The archive must have something to be opened correctly. + BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored); + + // Doesn't contain compatibility zip entry. + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + ASSERT_TRUE(verify_package_compatibility(zip)); + CloseArchive(zip); +} + +TEST(InstallTest, verify_package_compatibility_invalid_entry) { + TemporaryFile temp_file; + BuildZipArchive({ { "compatibility.zip", "" } }, temp_file.release(), kCompressStored); + + // Empty compatibility zip entry. + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + ASSERT_FALSE(verify_package_compatibility(zip)); + CloseArchive(zip); +} + +TEST(InstallTest, read_metadata_from_package_smoke) { + TemporaryFile temp_file; + const std::string content("abc=defg"); + BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file.release(), + kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + std::map<std::string, std::string> metadata; + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); + ASSERT_EQ("defg", metadata["abc"]); + CloseArchive(zip); + + TemporaryFile temp_file2; + BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file2.release(), + kCompressDeflated); + + ASSERT_EQ(0, OpenArchive(temp_file2.path, &zip)); + metadata.clear(); + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); + ASSERT_EQ("defg", metadata["abc"]); + CloseArchive(zip); +} + +TEST(InstallTest, read_metadata_from_package_no_entry) { + TemporaryFile temp_file; + BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + std::map<std::string, std::string> metadata; + ASSERT_FALSE(ReadMetadataFromPackage(zip, &metadata)); + CloseArchive(zip); +} + +TEST(InstallTest, read_wipe_ab_partition_list) { + std::vector<std::string> partition_list = { + "/dev/block/bootdevice/by-name/system_a", "/dev/block/bootdevice/by-name/system_b", + "/dev/block/bootdevice/by-name/vendor_a", "/dev/block/bootdevice/by-name/vendor_b", + "/dev/block/bootdevice/by-name/userdata", "# Wipe the boot partitions last", + "/dev/block/bootdevice/by-name/boot_a", "/dev/block/bootdevice/by-name/boot_b", + }; + TemporaryFile temp_file; + BuildZipArchive({ { "recovery.wipe", android::base::Join(partition_list, '\n') } }, + temp_file.release(), kCompressDeflated); + std::string wipe_package; + ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &wipe_package)); + + auto package = Package::CreateMemoryPackage( + std::vector<uint8_t>(wipe_package.begin(), wipe_package.end()), nullptr); + + auto read_partition_list = GetWipePartitionList(package.get()); + std::vector<std::string> expected = { + "/dev/block/bootdevice/by-name/system_a", "/dev/block/bootdevice/by-name/system_b", + "/dev/block/bootdevice/by-name/vendor_a", "/dev/block/bootdevice/by-name/vendor_b", + "/dev/block/bootdevice/by-name/userdata", "/dev/block/bootdevice/by-name/boot_a", + "/dev/block/bootdevice/by-name/boot_b", + }; + ASSERT_EQ(expected, read_partition_list); +} + +TEST(InstallTest, verify_package_compatibility_with_libvintf_malformed_xml) { + TemporaryFile compatibility_zip_file; + std::string malformed_xml = "malformed"; + BuildZipArchive({ { "system_manifest.xml", malformed_xml } }, compatibility_zip_file.release(), + kCompressDeflated); + + TemporaryFile temp_file; + std::string compatibility_zip_content; + ASSERT_TRUE( + android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content)); + BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(), + kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + std::vector<std::string> compatibility_info; + compatibility_info.push_back(malformed_xml); + // Malformed compatibility zip is expected to be rejected by libvintf. But we defer that to + // libvintf. + std::string err; + bool result = + android::vintf::VintfObjectRecovery::CheckCompatibility(compatibility_info, &err) == 0; + ASSERT_EQ(result, verify_package_compatibility(zip)); + CloseArchive(zip); +} + +TEST(InstallTest, verify_package_compatibility_with_libvintf_system_manifest_xml) { + static constexpr const char* system_manifest_xml_path = "/system/manifest.xml"; + if (access(system_manifest_xml_path, R_OK) == -1) { + GTEST_LOG_(INFO) << "Test skipped on devices w/o /system/manifest.xml."; + return; + } + std::string system_manifest_xml_content; + ASSERT_TRUE( + android::base::ReadFileToString(system_manifest_xml_path, &system_manifest_xml_content)); + TemporaryFile compatibility_zip_file; + BuildZipArchive({ { "system_manifest.xml", system_manifest_xml_content } }, + compatibility_zip_file.release(), kCompressDeflated); + + TemporaryFile temp_file; + std::string compatibility_zip_content; + ASSERT_TRUE( + android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content)); + BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(), + kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + std::vector<std::string> compatibility_info; + compatibility_info.push_back(system_manifest_xml_content); + std::string err; + bool result = + android::vintf::VintfObjectRecovery::CheckCompatibility(compatibility_info, &err) == 0; + // Make sure the result is consistent with libvintf library. + ASSERT_EQ(result, verify_package_compatibility(zip)); + CloseArchive(zip); +} + +TEST(InstallTest, SetUpNonAbUpdateCommands) { + TemporaryFile temp_file; + static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary"; + BuildZipArchive({ { UPDATE_BINARY_NAME, "" } }, temp_file.release(), kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + int status_fd = 10; + std::string package = "/path/to/update.zip"; + TemporaryDir td; + std::string binary_path = std::string(td.path) + "/update_binary"; + Paths::Get().set_temporary_update_binary(binary_path); + std::vector<std::string> cmd; + ASSERT_TRUE(SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd)); + ASSERT_EQ(4U, cmd.size()); + ASSERT_EQ(binary_path, cmd[0]); + ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION + ASSERT_EQ(std::to_string(status_fd), cmd[2]); + ASSERT_EQ(package, cmd[3]); + struct stat sb; + ASSERT_EQ(0, stat(binary_path.c_str(), &sb)); + ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); + + // With non-zero retry count. update_binary will be removed automatically. + cmd.clear(); + ASSERT_TRUE(SetUpNonAbUpdateCommands(package, zip, 2, status_fd, &cmd)); + ASSERT_EQ(5U, cmd.size()); + ASSERT_EQ(binary_path, cmd[0]); + ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION + ASSERT_EQ(std::to_string(status_fd), cmd[2]); + ASSERT_EQ(package, cmd[3]); + ASSERT_EQ("retry", cmd[4]); + sb = {}; + ASSERT_EQ(0, stat(binary_path.c_str(), &sb)); + ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); + + CloseArchive(zip); +} + +TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) { + TemporaryFile temp_file; + // The archive must have something to be opened correctly. + BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored); + + // Missing update binary. + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + int status_fd = 10; + std::string package = "/path/to/update.zip"; + TemporaryDir td; + Paths::Get().set_temporary_update_binary(std::string(td.path) + "/update_binary"); + std::vector<std::string> cmd; + ASSERT_FALSE(SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd)); + CloseArchive(zip); +} + +static void VerifyAbUpdateCommands(const std::string& serialno, bool success = true) { + TemporaryFile temp_file; + + const std::string properties = "some_properties"; + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + std::string timestamp = android::base::GetProperty("ro.build.date.utc", ""); + ASSERT_NE("", timestamp); + + std::vector<std::string> meta{ "ota-type=AB", "pre-device=" + device, + "post-timestamp=" + timestamp }; + if (!serialno.empty()) { + meta.push_back("serialno=" + serialno); + } + std::string metadata_string = android::base::Join(meta, "\n"); + + BuildZipArchive({ { "payload.bin", "" }, + { "payload_properties.txt", properties }, + { "META-INF/com/android/metadata", metadata_string } }, + temp_file.release(), kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + ZipEntry payload_entry; + ASSERT_EQ(0, FindEntry(zip, "payload.bin", &payload_entry)); + + std::map<std::string, std::string> metadata; + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); + if (success) { + ASSERT_TRUE(CheckPackageMetadata(metadata, OtaType::AB)); + + int status_fd = 10; + std::string package = "/path/to/update.zip"; + std::vector<std::string> cmd; + ASSERT_TRUE(SetUpAbUpdateCommands(package, zip, status_fd, &cmd)); + ASSERT_EQ(5U, cmd.size()); + ASSERT_EQ("/system/bin/update_engine_sideload", cmd[0]); + ASSERT_EQ("--payload=file://" + package, cmd[1]); + ASSERT_EQ("--offset=" + std::to_string(payload_entry.offset), cmd[2]); + ASSERT_EQ("--headers=" + properties, cmd[3]); + ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]); + } else { + ASSERT_FALSE(CheckPackageMetadata(metadata, OtaType::AB)); + } + CloseArchive(zip); +} + +TEST(InstallTest, SetUpAbUpdateCommands) { + // Empty serialno will pass the verification. + VerifyAbUpdateCommands({}); +} + +TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) { + TemporaryFile temp_file; + + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + std::string timestamp = android::base::GetProperty("ro.build.date.utc", ""); + ASSERT_NE("", timestamp); + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", "pre-device=" + device, "post-timestamp=" + timestamp, + }, + "\n"); + + BuildZipArchive( + { + { "payload.bin", "" }, + { "META-INF/com/android/metadata", metadata }, + }, + temp_file.release(), kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + int status_fd = 10; + std::string package = "/path/to/update.zip"; + std::vector<std::string> cmd; + ASSERT_FALSE(SetUpAbUpdateCommands(package, zip, status_fd, &cmd)); + CloseArchive(zip); +} + +TEST(InstallTest, SetUpAbUpdateCommands_MultipleSerialnos) { + std::string serialno = android::base::GetProperty("ro.serialno", ""); + ASSERT_NE("", serialno); + + // Single matching serialno will pass the verification. + VerifyAbUpdateCommands(serialno); + + static constexpr char alphabet[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + auto generator = []() { return alphabet[rand() % (sizeof(alphabet) - 1)]; }; + + // Generate 900 random serial numbers. + std::string random_serialno; + for (size_t i = 0; i < 900; i++) { + generate_n(back_inserter(random_serialno), serialno.size(), generator); + random_serialno.append("|"); + } + // Random serialnos should fail the verification. + VerifyAbUpdateCommands(random_serialno, false); + + std::string long_serialno = random_serialno + serialno + "|"; + for (size_t i = 0; i < 99; i++) { + generate_n(back_inserter(long_serialno), serialno.size(), generator); + long_serialno.append("|"); + } + // String with the matching serialno should pass the verification. + VerifyAbUpdateCommands(long_serialno); +} + +static void TestCheckPackageMetadata(const std::string& metadata_string, OtaType ota_type, + bool exptected_result) { + TemporaryFile temp_file; + BuildZipArchive( + { + { "META-INF/com/android/metadata", metadata_string }, + }, + temp_file.release(), kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + + std::map<std::string, std::string> metadata; + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); + ASSERT_EQ(exptected_result, CheckPackageMetadata(metadata, ota_type)); + CloseArchive(zip); +} + +TEST(InstallTest, CheckPackageMetadata_ota_type) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + // ota-type must be present + std::string metadata = android::base::Join( + std::vector<std::string>{ + "pre-device=" + device, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, false); + + // Checks if ota-type matches + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, true); + + TestCheckPackageMetadata(metadata, OtaType::BRICK, false); +} + +TEST(InstallTest, CheckPackageMetadata_device_type) { + // device type can not be empty + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::BRICK, false); + + // device type mismatches + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=dummy_device_type", + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::BRICK, false); +} + +TEST(InstallTest, CheckPackageMetadata_serial_number_smoke) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + // Serial number doesn't need to exist + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::BRICK, true); + + // Serial number mismatches + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=dummy_serial", + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::BRICK, false); + + std::string serialno = android::base::GetProperty("ro.serialno", ""); + ASSERT_NE("", serialno); + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=" + serialno, + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::BRICK, true); +} + +TEST(InstallTest, CheckPackageMetadata_multiple_serial_number) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + std::string serialno = android::base::GetProperty("ro.serialno", ""); + ASSERT_NE("", serialno); + + std::vector<std::string> serial_numbers; + // Creates a dummy serial number string. + for (char c = 'a'; c <= 'z'; c++) { + serial_numbers.emplace_back(serialno.size(), c); + } + + // No matched serialno found. + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=" + android::base::Join(serial_numbers, '|'), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::BRICK, false); + + serial_numbers.emplace_back(serialno); + std::shuffle(serial_numbers.begin(), serial_numbers.end(), std::default_random_engine()); + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=" + android::base::Join(serial_numbers, '|'), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::BRICK, true); +} + +TEST(InstallTest, CheckPackageMetadata_ab_build_version) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + std::string build_version = android::base::GetProperty("ro.build.version.incremental", ""); + ASSERT_NE("", build_version); + + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build-incremental=" + build_version, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, true); + + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build-incremental=dummy_build", + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, false); +} + +TEST(InstallTest, CheckPackageMetadata_ab_fingerprint) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + std::string finger_print = android::base::GetProperty("ro.build.fingerprint", ""); + ASSERT_NE("", finger_print); + + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build=" + finger_print, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, true); + + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build=dummy_build_fingerprint", + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, false); +} + +TEST(InstallTest, CheckPackageMetadata_ab_post_timestamp) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + // post timestamp is required for upgrade. + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, false); + + // post timestamp should be larger than the timestamp on device. + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=0", + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, false); + + // fingerprint is required for downgrade + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=0", + "ota-downgrade=yes", + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, false); + + std::string finger_print = android::base::GetProperty("ro.build.fingerprint", ""); + ASSERT_NE("", finger_print); + + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=0", + "pre-build=" + finger_print, + "ota-downgrade=yes", + }, + "\n"); + TestCheckPackageMetadata(metadata, OtaType::AB, true); +} diff --git a/tests/unit/locale_test.cpp b/tests/unit/locale_test.cpp index cdaba0e8b..c69434c12 100644 --- a/tests/unit/locale_test.cpp +++ b/tests/unit/locale_test.cpp @@ -27,7 +27,7 @@ TEST(LocaleTest, Misc) { EXPECT_FALSE(matches_locale("en-GB", "en")); EXPECT_FALSE(matches_locale("en-GB", "en-US")); EXPECT_FALSE(matches_locale("en-US", "")); - // Empty locale prefix in the PNG file will match the input locale. - EXPECT_TRUE(matches_locale("", "en-US")); + // Empty locale prefix in the PNG file should not match the input locale. + EXPECT_FALSE(matches_locale("", "en-US")); EXPECT_TRUE(matches_locale("sr-Latn", "sr-Latn-BA")); } diff --git a/tests/unit/minui_test.cpp b/tests/unit/minui_test.cpp new file mode 100644 index 000000000..c7d7f7eef --- /dev/null +++ b/tests/unit/minui_test.cpp @@ -0,0 +1,54 @@ +/* + * 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 <stdint.h> +#include <stdlib.h> + +#include <limits> +#include <vector> + +#include <gtest/gtest.h> + +#include "minui/minui.h" + +TEST(GRSurfaceTest, Create_aligned) { + auto surface = GRSurface::Create(9, 11, 9, 1); + ASSERT_TRUE(surface); + ASSERT_EQ(0, reinterpret_cast<uintptr_t>(surface->data()) % GRSurface::kSurfaceDataAlignment); + // data_size will be rounded up to the next multiple of GRSurface::kSurfaceDataAlignment. + ASSERT_EQ(0, surface->data_size() % GRSurface::kSurfaceDataAlignment); + ASSERT_GE(surface->data_size(), 11 * 9); +} + +TEST(GRSurfaceTest, Create_invalid_inputs) { + ASSERT_FALSE(GRSurface::Create(9, 11, 0, 1)); + ASSERT_FALSE(GRSurface::Create(9, 0, 9, 1)); + ASSERT_FALSE(GRSurface::Create(0, 11, 9, 1)); + ASSERT_FALSE(GRSurface::Create(9, 11, 9, 0)); + ASSERT_FALSE(GRSurface::Create(9, 101, std::numeric_limits<size_t>::max() / 100, 1)); +} + +TEST(GRSurfaceTest, Clone) { + auto image = GRSurface::Create(50, 10, 50, 1); + ASSERT_GE(image->data_size(), 10 * 50); + for (auto i = 0; i < image->data_size(); i++) { + image->data()[i] = rand() % 128; + } + auto image_copy = image->Clone(); + ASSERT_EQ(image->data_size(), image_copy->data_size()); + ASSERT_EQ(std::vector(image->data(), image->data() + image->data_size()), + std::vector(image_copy->data(), image_copy->data() + image->data_size())); +} diff --git a/tests/unit/package_test.cpp b/tests/unit/package_test.cpp new file mode 100644 index 000000000..5e31f7fa5 --- /dev/null +++ b/tests/unit/package_test.cpp @@ -0,0 +1,116 @@ +/* + * 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 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 <functional> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <gtest/gtest.h> +#include <openssl/sha.h> +#include <ziparchive/zip_writer.h> + +#include "common/test_constants.h" +#include "install/package.h" + +class PackageTest : public ::testing::Test { + protected: + void SetUp() override; + + // A list of package classes for test, including MemoryPackage and FilePackage. + std::vector<std::unique_ptr<Package>> packages_; + + TemporaryFile temp_file_; // test package file. + std::string file_content_; // actual bytes of the package file. +}; + +void PackageTest::SetUp() { + std::vector<std::string> entries = { "file1.txt", "file2.txt", "dir1/file3.txt" }; + FILE* file_ptr = fdopen(temp_file_.release(), "wb"); + ZipWriter writer(file_ptr); + for (const auto& entry : entries) { + ASSERT_EQ(0, writer.StartEntry(entry.c_str(), ZipWriter::kCompress)); + ASSERT_EQ(0, writer.WriteBytes(entry.c_str(), entry.size())); + ASSERT_EQ(0, writer.FinishEntry()); + } + writer.Finish(); + ASSERT_EQ(0, fclose(file_ptr)); + + ASSERT_TRUE(android::base::ReadFileToString(temp_file_.path, &file_content_)); + auto memory_package = Package::CreateMemoryPackage(temp_file_.path, nullptr); + ASSERT_TRUE(memory_package); + packages_.emplace_back(std::move(memory_package)); + + auto file_package = Package::CreateFilePackage(temp_file_.path, nullptr); + ASSERT_TRUE(file_package); + packages_.emplace_back(std::move(file_package)); +} + +TEST_F(PackageTest, ReadFullyAtOffset_success) { + for (const auto& package : packages_) { + std::vector<uint8_t> buffer(file_content_.size()); + ASSERT_TRUE(package->ReadFullyAtOffset(buffer.data(), file_content_.size(), 0)); + ASSERT_EQ(file_content_, std::string(buffer.begin(), buffer.end())); + + ASSERT_TRUE(package->ReadFullyAtOffset(buffer.data(), file_content_.size() - 10, 10)); + ASSERT_EQ(file_content_.substr(10), std::string(buffer.begin(), buffer.end() - 10)); + } +} + +TEST_F(PackageTest, ReadFullyAtOffset_failure) { + for (const auto& package : packages_) { + std::vector<uint8_t> buffer(file_content_.size()); + // Out of bound read. + ASSERT_FALSE(package->ReadFullyAtOffset(buffer.data(), file_content_.size(), 10)); + } +} + +TEST_F(PackageTest, UpdateHashAtOffset_sha1_hash) { + // Check that the hash matches for first half of the file. + uint64_t hash_size = file_content_.size() / 2; + std::vector<uint8_t> expected_sha(SHA_DIGEST_LENGTH); + SHA1(reinterpret_cast<uint8_t*>(file_content_.data()), hash_size, expected_sha.data()); + + for (const auto& package : packages_) { + SHA_CTX ctx; + SHA1_Init(&ctx); + std::vector<HasherUpdateCallback> hashers{ std::bind(&SHA1_Update, &ctx, std::placeholders::_1, + std::placeholders::_2) }; + package->UpdateHashAtOffset(hashers, 0, hash_size); + + std::vector<uint8_t> calculated_sha(SHA_DIGEST_LENGTH); + SHA1_Final(calculated_sha.data(), &ctx); + ASSERT_EQ(expected_sha, calculated_sha); + } +} + +TEST_F(PackageTest, GetZipArchiveHandle_extract_entry) { + for (const auto& package : packages_) { + ZipArchiveHandle zip = package->GetZipArchiveHandle(); + ASSERT_TRUE(zip); + + // Check that we can extract one zip entry. + std::string_view entry_name = "dir1/file3.txt"; + ZipEntry entry; + ASSERT_EQ(0, FindEntry(zip, entry_name, &entry)); + + std::vector<uint8_t> extracted(entry_name.size()); + ASSERT_EQ(0, ExtractToMemory(zip, &entry, extracted.data(), extracted.size())); + ASSERT_EQ(entry_name, std::string(extracted.begin(), extracted.end())); + } +} diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp new file mode 100644 index 000000000..72169a0c6 --- /dev/null +++ b/tests/unit/parse_install_logs_test.cpp @@ -0,0 +1,74 @@ +/* + * 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 <map> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> + +#include "otautil/parse_install_logs.h" + +TEST(ParseInstallLogsTest, EmptyFile) { + TemporaryFile last_install; + + auto metrics = ParseLastInstall(last_install.path); + ASSERT_TRUE(metrics.empty()); +} + +TEST(ParseInstallLogsTest, SideloadSmoke) { + TemporaryFile last_install; + ASSERT_TRUE(android::base::WriteStringToFile("/cache/recovery/ota.zip\n0\n", last_install.path)); + auto metrics = ParseLastInstall(last_install.path); + ASSERT_EQ(metrics.end(), metrics.find("ota_sideload")); + + ASSERT_TRUE(android::base::WriteStringToFile("/sideload/package.zip\n0\n", last_install.path)); + metrics = ParseLastInstall(last_install.path); + ASSERT_NE(metrics.end(), metrics.find("ota_sideload")); +} + +TEST(ParseInstallLogsTest, ParseRecoveryUpdateMetrics) { + std::vector<std::string> lines = { + "/sideload/package.zip", + "0", + "time_total: 300", + "uncrypt_time: 40", + "source_build: 4973410", + "bytes_written_system: " + std::to_string(1200 * 1024 * 1024), + "bytes_stashed_system: " + std::to_string(300 * 1024 * 1024), + "bytes_written_vendor: " + std::to_string(40 * 1024 * 1024), + "bytes_stashed_vendor: " + std::to_string(50 * 1024 * 1024), + "temperature_start: 37000", + "temperature_end: 38000", + "temperature_max: 39000", + "error: 22", + "cause: 55", + }; + + auto metrics = ParseRecoveryUpdateMetrics(lines); + + std::map<std::string, int64_t> expected_result = { + { "ota_time_total", 300 }, { "ota_uncrypt_time", 40 }, + { "ota_source_version", 4973410 }, { "ota_written_in_MiBs", 1240 }, + { "ota_stashed_in_MiBs", 350 }, { "ota_temperature_start", 37000 }, + { "ota_temperature_end", 38000 }, { "ota_temperature_max", 39000 }, + { "ota_non_ab_error_code", 22 }, { "ota_non_ab_cause_code", 55 }, + }; + + ASSERT_EQ(expected_result, metrics); +} diff --git a/tests/unit/rangeset_test.cpp b/tests/unit/rangeset_test.cpp index 7ae193e18..699f933a0 100644 --- a/tests/unit/rangeset_test.cpp +++ b/tests/unit/rangeset_test.cpp @@ -18,6 +18,7 @@ #include <sys/types.h> #include <limits> +#include <optional> #include <vector> #include <gtest/gtest.h> @@ -209,6 +210,7 @@ TEST(RangeSetTest, GetBlockNumber) { ASSERT_EQ(static_cast<size_t>(6), rs.GetBlockNumber(5)); ASSERT_EQ(static_cast<size_t>(9), rs.GetBlockNumber(8)); + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; // Out of bound. ASSERT_EXIT(rs.GetBlockNumber(9), ::testing::KilledBySignal(SIGABRT), ""); } @@ -247,6 +249,29 @@ TEST(RangeSetTest, ToString) { ASSERT_EQ("6,1,3,4,6,15,22", RangeSet::Parse("6,1,3,4,6,15,22").ToString()); } +TEST(RangeSetTest, GetSubRanges_invalid) { + RangeSet range0({ { 1, 11 }, { 20, 30 } }); + ASSERT_FALSE(range0.GetSubRanges(0, 21)); // too many blocks + ASSERT_FALSE(range0.GetSubRanges(21, 1)); // start block OOB +} + +TEST(RangeSetTest, GetSubRanges_empty) { + RangeSet range0({ { 1, 11 }, { 20, 30 } }); + ASSERT_EQ(RangeSet{}, range0.GetSubRanges(1, 0)); // empty num_of_blocks +} + +TEST(RangeSetTest, GetSubRanges_smoke) { + RangeSet range0({ { 10, 11 } }); + ASSERT_EQ(RangeSet({ { 10, 11 } }), range0.GetSubRanges(0, 1)); + + RangeSet range1({ { 10, 11 }, { 20, 21 }, { 30, 31 } }); + ASSERT_EQ(range1, range1.GetSubRanges(0, 3)); + ASSERT_EQ(RangeSet({ { 20, 21 } }), range1.GetSubRanges(1, 1)); + + RangeSet range2({ { 1, 11 }, { 20, 25 }, { 30, 35 } }); + ASSERT_EQ(RangeSet({ { 10, 11 }, { 20, 25 }, { 30, 31 } }), range2.GetSubRanges(9, 7)); +} + TEST(SortedRangeSetTest, Insert) { SortedRangeSet rs({ { 2, 3 }, { 4, 6 }, { 8, 14 } }); rs.Insert({ 1, 2 }); @@ -284,6 +309,8 @@ TEST(SortedRangeSetTest, file_range) { ASSERT_EQ(static_cast<size_t>(10), rs.GetOffsetInRangeSet(4106)); ASSERT_EQ(static_cast<size_t>(40970), rs.GetOffsetInRangeSet(4096 * 16 + 10)); + + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; // block#10 not in range. ASSERT_EXIT(rs.GetOffsetInRangeSet(40970), ::testing::KilledBySignal(SIGABRT), ""); } diff --git a/tests/unit/resources_test.cpp b/tests/unit/resources_test.cpp new file mode 100644 index 000000000..302744308 --- /dev/null +++ b/tests/unit/resources_test.cpp @@ -0,0 +1,136 @@ +/* + * 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 <dirent.h> +#include <stdio.h> +#include <stdlib.h> + +#include <memory> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> +#include <png.h> + +#include "common/test_constants.h" +#include "minui/minui.h" +#include "private/resources.h" + +static const std::string kLocale = "zu"; + +static const std::vector<std::string> kResourceImagesDirs{ + "res-mdpi/images/", "res-hdpi/images/", "res-xhdpi/images/", + "res-xxhdpi/images/", "res-xxxhdpi/images/", +}; + +static int png_filter(const dirent* de) { + if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) { + return 0; + } + return 1; +} + +// Finds out all the PNG files to test, which stay under the same dir with the executabl.. +static std::vector<std::string> add_files() { + std::vector<std::string> files; + for (const std::string& images_dir : kResourceImagesDirs) { + static std::string exec_dir = android::base::GetExecutableDirectory(); + std::string dir_path = exec_dir + "/" + images_dir; + dirent** namelist; + int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort); + if (n == -1) { + printf("Failed to scandir %s: %s\n", dir_path.c_str(), strerror(errno)); + continue; + } + if (n == 0) { + printf("No file is added for test in %s\n", dir_path.c_str()); + } + + while (n--) { + std::string file_path = dir_path + namelist[n]->d_name; + files.push_back(file_path); + free(namelist[n]); + } + free(namelist); + } + return files; +} + +TEST(ResourcesTest, res_create_multi_display_surface) { + GRSurface** frames; + int frame_count; + int fps; + ASSERT_EQ(0, res_create_multi_display_surface(from_testdata_base("battery_scale.png").c_str(), + &frame_count, &fps, &frames)); + ASSERT_EQ(6, frame_count); + ASSERT_EQ(20, fps); + + for (auto i = 0; i < frame_count; i++) { + free(frames[i]); + } + free(frames); +} + +class ResourcesTest : public testing::TestWithParam<std::string> { + public: + static std::vector<std::string> png_list; + + protected: + void SetUp() override { + png_ = std::make_unique<PngHandler>(GetParam()); + ASSERT_TRUE(png_); + + ASSERT_EQ(PNG_COLOR_TYPE_GRAY, png_->color_type()) << "Recovery expects grayscale PNG file."; + ASSERT_LT(static_cast<png_uint_32>(5), png_->width()); + ASSERT_LT(static_cast<png_uint_32>(0), png_->height()); + ASSERT_EQ(1, png_->channels()) << "Recovery background text images expects 1-channel PNG file."; + } + + std::unique_ptr<PngHandler> png_{ nullptr }; +}; + +// Parses a png file and tests if it's qualified for the background text image under recovery. +TEST_P(ResourcesTest, ValidateLocale) { + std::vector<unsigned char> row(png_->width()); + for (png_uint_32 y = 0; y < png_->height(); ++y) { + png_read_row(png_->png_ptr(), row.data(), nullptr); + int w = (row[1] << 8) | row[0]; + int h = (row[3] << 8) | row[2]; + int len = row[4]; + EXPECT_LT(0, w); + EXPECT_LT(0, h); + EXPECT_LT(0, len) << "Locale string should be non-empty."; + EXPECT_NE(0, row[5]) << "Locale string is missing."; + + ASSERT_GE(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file."; + char* loc = reinterpret_cast<char*>(&row[5]); + if (matches_locale(loc, kLocale.c_str())) { + EXPECT_TRUE(android::base::StartsWith(loc, kLocale)); + break; + } + for (int i = 0; i < h; ++i, ++y) { + png_read_row(png_->png_ptr(), row.data(), nullptr); + } + } +} + +std::vector<std::string> ResourcesTest::png_list = add_files(); + +INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourcesTest, + ::testing::ValuesIn(ResourcesTest::png_list.cbegin(), + ResourcesTest::png_list.cend())); diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp new file mode 100644 index 000000000..883dfbde2 --- /dev/null +++ b/tests/unit/screen_ui_test.cpp @@ -0,0 +1,562 @@ +/* + * 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 <stddef.h> +#include <stdio.h> + +#include <functional> +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/stringprintf.h> +#include <gtest/gtest.h> +#include <gtest/gtest_prod.h> + +#include "common/test_constants.h" +#include "minui/minui.h" +#include "otautil/paths.h" +#include "private/resources.h" +#include "recovery_ui/device.h" +#include "recovery_ui/screen_ui.h" + +static const std::vector<std::string> HEADERS{ "header" }; +static const std::vector<std::string> ITEMS{ "item1", "item2", "item3", "item4", "1234567890" }; + +// TODO(xunchang) check if some draw functions are called when drawing menus. +class MockDrawFunctions : public DrawInterface { + void SetColor(UIElement /* element */) const override {} + void DrawHighlightBar(int /* x */, int /* y */, int /* width */, + int /* height */) const override {} + int DrawHorizontalRule(int /* y */) const override { + return 0; + } + int DrawTextLine(int /* x */, int /* y */, const std::string& /* line */, + bool /* bold */) const override { + return 0; + } + void DrawSurface(const GRSurface* /* surface */, int /* sx */, int /* sy */, int /* w */, + int /* h */, int /* dx */, int /* dy */) const override {} + void DrawFill(int /* x */, int /* y */, int /* w */, int /* h */) const override {} + void DrawTextIcon(int /* x */, int /* y */, const GRSurface* /* surface */) const override {} + int DrawTextLines(int /* x */, int /* y */, + const std::vector<std::string>& /* lines */) const override { + return 0; + } + int DrawWrappedTextLines(int /* x */, int /* y */, + const std::vector<std::string>& /* lines */) const override { + return 0; + } +}; + +class ScreenUITest : public testing::Test { + protected: + MockDrawFunctions draw_funcs_; +}; + +TEST_F(ScreenUITest, StartPhoneMenuSmoke) { + TextMenu menu(false, 10, 20, HEADERS, ITEMS, 0, 20, draw_funcs_); + ASSERT_FALSE(menu.scrollable()); + ASSERT_EQ(HEADERS[0], menu.text_headers()[0]); + ASSERT_EQ(5u, menu.ItemsCount()); + + std::string message; + ASSERT_FALSE(menu.ItemsOverflow(&message)); + for (size_t i = 0; i < menu.ItemsCount(); i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + + ASSERT_EQ(0, menu.selection()); +} + +TEST_F(ScreenUITest, StartWearMenuSmoke) { + TextMenu menu(true, 10, 8, HEADERS, ITEMS, 1, 20, draw_funcs_); + ASSERT_TRUE(menu.scrollable()); + ASSERT_EQ(HEADERS[0], menu.text_headers()[0]); + ASSERT_EQ(5u, menu.ItemsCount()); + + std::string message; + ASSERT_FALSE(menu.ItemsOverflow(&message)); + for (size_t i = 0; i < menu.ItemsCount() - 1; i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + // Test of the last item is truncated + ASSERT_EQ("12345678", menu.TextItem(4)); + ASSERT_EQ(1, menu.selection()); +} + +TEST_F(ScreenUITest, StartPhoneMenuItemsOverflow) { + TextMenu menu(false, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_); + ASSERT_FALSE(menu.scrollable()); + ASSERT_EQ(1u, menu.ItemsCount()); + + std::string message; + ASSERT_FALSE(menu.ItemsOverflow(&message)); + for (size_t i = 0; i < menu.ItemsCount(); i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(1u, menu.MenuEnd()); +} + +TEST_F(ScreenUITest, StartWearMenuItemsOverflow) { + TextMenu menu(true, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_); + ASSERT_TRUE(menu.scrollable()); + ASSERT_EQ(5u, menu.ItemsCount()); + + std::string message; + ASSERT_TRUE(menu.ItemsOverflow(&message)); + ASSERT_EQ("Current item: 1/5", message); + + for (size_t i = 0; i < menu.ItemsCount(); i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(1u, menu.MenuEnd()); +} + +TEST_F(ScreenUITest, PhoneMenuSelectSmoke) { + int sel = 0; + TextMenu menu(false, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_); + // Mimic down button 10 times (2 * items size) + for (int i = 0; i < 10; i++) { + sel = menu.Select(++sel); + ASSERT_EQ(sel, menu.selection()); + + // Wraps the selection for unscrollable menu when it reaches the boundary. + int expected = (i + 1) % 5; + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } + + // Mimic up button 10 times + for (int i = 0; i < 10; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(sel, menu.selection()); + + int expected = (9 - i) % 5; + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } +} + +TEST_F(ScreenUITest, WearMenuSelectSmoke) { + int sel = 0; + TextMenu menu(true, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_); + // Mimic pressing down button 10 times (2 * items size) + for (int i = 0; i < 10; i++) { + sel = menu.Select(++sel); + ASSERT_EQ(sel, menu.selection()); + + // Stops the selection at the boundary if the menu is scrollable. + int expected = std::min(i + 1, 4); + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } + + // Mimic pressing up button 10 times + for (int i = 0; i < 10; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(sel, menu.selection()); + + int expected = std::max(3 - i, 0); + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } +} + +TEST_F(ScreenUITest, WearMenuSelectItemsOverflow) { + int sel = 1; + TextMenu menu(true, 3, 20, HEADERS, ITEMS, sel, 20, draw_funcs_); + ASSERT_EQ(5u, menu.ItemsCount()); + + // Scroll the menu to the end, and check the start & end of menu. + for (int i = 0; i < 3; i++) { + sel = menu.Select(++sel); + ASSERT_EQ(i + 2, sel); + ASSERT_EQ(static_cast<size_t>(i), menu.MenuStart()); + ASSERT_EQ(static_cast<size_t>(i + 3), menu.MenuEnd()); + } + + // Press down button one more time won't change the MenuStart() and MenuEnd(). + sel = menu.Select(++sel); + ASSERT_EQ(4, sel); + ASSERT_EQ(2u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + + // Scroll the menu to the top. + // The expected menu sel, start & ends are: + // sel 3, start 2, end 5 + // sel 2, start 2, end 5 + // sel 1, start 1, end 4 + // sel 0, start 0, end 3 + for (int i = 0; i < 4; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(3 - i, sel); + ASSERT_EQ(static_cast<size_t>(std::min(3 - i, 2)), menu.MenuStart()); + ASSERT_EQ(static_cast<size_t>(std::min(6 - i, 5)), menu.MenuEnd()); + } + + // Press up button one more time won't change the MenuStart() and MenuEnd(). + sel = menu.Select(--sel); + ASSERT_EQ(0, sel); + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(3u, menu.MenuEnd()); +} + +TEST_F(ScreenUITest, GraphicMenuSelection) { + auto image = GRSurface::Create(50, 50, 50, 1); + auto header = image->Clone(); + std::vector<const GRSurface*> items = { + image.get(), + image.get(), + image.get(), + }; + GraphicMenu menu(header.get(), items, 0, draw_funcs_); + + ASSERT_EQ(0, menu.selection()); + + int sel = 0; + for (int i = 0; i < 3; i++) { + sel = menu.Select(++sel); + ASSERT_EQ((i + 1) % 3, sel); + ASSERT_EQ(sel, menu.selection()); + } + + sel = 0; + for (int i = 0; i < 3; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(2 - i, sel); + ASSERT_EQ(sel, menu.selection()); + } +} + +TEST_F(ScreenUITest, GraphicMenuValidate) { + auto image = GRSurface::Create(50, 50, 50, 1); + auto header = image->Clone(); + std::vector<const GRSurface*> items = { + image.get(), + image.get(), + image.get(), + }; + + ASSERT_TRUE(GraphicMenu::Validate(200, 200, header.get(), items)); + + // Menu exceeds the horizontal boundary. + auto wide_surface = GRSurface::Create(300, 50, 300, 1); + ASSERT_FALSE(GraphicMenu::Validate(299, 200, wide_surface.get(), items)); + + // Menu exceeds the vertical boundary. + items.emplace_back(image.get()); + ASSERT_FALSE(GraphicMenu::Validate(200, 249, header.get(), items)); +} + +static constexpr int kMagicAction = 101; + +enum class KeyCode : int { + TIMEOUT = -1, + NO_OP = 0, + UP = 1, + DOWN = 2, + ENTER = 3, + MAGIC = 1001, + LAST, +}; + +static const std::map<KeyCode, int> kKeyMapping{ + // clang-format off + { KeyCode::NO_OP, Device::kNoAction }, + { KeyCode::UP, Device::kHighlightUp }, + { KeyCode::DOWN, Device::kHighlightDown }, + { KeyCode::ENTER, Device::kInvokeItem }, + { KeyCode::MAGIC, kMagicAction }, + // clang-format on +}; + +class TestableScreenRecoveryUI : public ScreenRecoveryUI { + public: + int WaitKey() override; + + void SetKeyBuffer(const std::vector<KeyCode>& buffer); + + int KeyHandler(int key, bool visible) const; + + private: + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, Init); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocale); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation_MissingAnimation); + + std::vector<KeyCode> key_buffer_; + size_t key_buffer_index_; +}; + +void TestableScreenRecoveryUI::SetKeyBuffer(const std::vector<KeyCode>& buffer) { + key_buffer_ = buffer; + key_buffer_index_ = 0; +} + +int TestableScreenRecoveryUI::KeyHandler(int key, bool) const { + KeyCode key_code = static_cast<KeyCode>(key); + if (kKeyMapping.find(key_code) != kKeyMapping.end()) { + return kKeyMapping.at(key_code); + } + return Device::kNoAction; +} + +int TestableScreenRecoveryUI::WaitKey() { + if (IsKeyInterrupted()) { + return static_cast<int>(RecoveryUI::KeyError::INTERRUPTED); + } + + CHECK_LT(key_buffer_index_, key_buffer_.size()); + return static_cast<int>(key_buffer_[key_buffer_index_++]); +} + +class DISABLED_ScreenRecoveryUITest : public ::testing::Test { + protected: + const std::string kTestLocale = "en-US"; + const std::string kTestRtlLocale = "ar"; + const std::string kTestRtlLocaleWithSuffix = "ar-EG"; + + void SetUp() override { + has_graphics_ = gr_init() == 0; + gr_exit(); + + if (has_graphics_) { + ui_ = std::make_unique<TestableScreenRecoveryUI>(); + } + + testdata_dir_ = from_testdata_base(""); + Paths::Get().set_resource_dir(testdata_dir_); + res_set_resource_dir(testdata_dir_); + } + + bool has_graphics_; + std::unique_ptr<TestableScreenRecoveryUI> ui_; + std::string testdata_dir_; +}; + +#define RETURN_IF_NO_GRAPHICS \ + do { \ + if (!has_graphics_) { \ + GTEST_LOG_(INFO) << "Test skipped due to no available graphics device"; \ + return; \ + } \ + } while (false) + +TEST_F(DISABLED_ScreenRecoveryUITest, Init) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ASSERT_EQ(kTestLocale, ui_->GetLocale()); + ASSERT_FALSE(ui_->rtl_locale_); + ASSERT_FALSE(ui_->IsTextVisible()); + ASSERT_FALSE(ui_->WasTextEverVisible()); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, dtor_NotCallingInit) { + ui_.reset(); + ASSERT_FALSE(ui_); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, ShowText) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ASSERT_FALSE(ui_->IsTextVisible()); + ui_->ShowText(true); + ASSERT_TRUE(ui_->IsTextVisible()); + ASSERT_TRUE(ui_->WasTextEverVisible()); + + ui_->ShowText(false); + ASSERT_FALSE(ui_->IsTextVisible()); + ASSERT_TRUE(ui_->WasTextEverVisible()); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, RtlLocale) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestRtlLocale)); + ASSERT_TRUE(ui_->rtl_locale_); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix)); + ASSERT_TRUE(ui_->rtl_locale_); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::DOWN, + KeyCode::UP, + KeyCode::DOWN, + KeyCode::ENTER, + }); + ASSERT_EQ(3u, ui_->ShowMenu(HEADERS, ITEMS, 3, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); + + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::UP, + KeyCode::NO_OP, + KeyCode::NO_OP, + KeyCode::UP, + KeyCode::ENTER, + }); + ASSERT_EQ(2u, ui_->ShowMenu(HEADERS, ITEMS, 0, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_NotMenuOnly) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::MAGIC, + }); + ASSERT_EQ(static_cast<size_t>(kMagicAction), + ui_->ShowMenu(HEADERS, ITEMS, 3, false, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_TimedOut) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::TIMEOUT, + }); + ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT), + ui_->ShowMenu(HEADERS, ITEMS, 3, true, nullptr)); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->ShowText(true); + ui_->ShowText(false); + ASSERT_TRUE(ui_->WasTextEverVisible()); + + ui_->SetKeyBuffer({ + KeyCode::TIMEOUT, + KeyCode::DOWN, + KeyCode::ENTER, + }); + ASSERT_EQ(4u, ui_->ShowMenu(HEADERS, ITEMS, 3, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenuWithInterrupt) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::DOWN, + KeyCode::UP, + KeyCode::DOWN, + KeyCode::ENTER, + }); + + ui_->InterruptKey(); + ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED), + ui_->ShowMenu(HEADERS, ITEMS, 3, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); + + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::UP, + KeyCode::NO_OP, + KeyCode::NO_OP, + KeyCode::UP, + KeyCode::ENTER, + }); + ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED), + ui_->ShowMenu(HEADERS, ITEMS, 0, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(DISABLED_ScreenRecoveryUITest, LoadAnimation) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + // Make a few copies of loop00000.png from testdata. + std::string image_data; + ASSERT_TRUE(android::base::ReadFileToString(testdata_dir_ + "/loop00000.png", &image_data)); + + std::vector<std::string> tempfiles; + TemporaryDir resource_dir; + for (const auto& name : { "00002", "00100", "00050" }) { + tempfiles.push_back(android::base::StringPrintf("%s/loop%s.png", resource_dir.path, name)); + ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back())); + } + for (const auto& name : { "00", "01" }) { + tempfiles.push_back(android::base::StringPrintf("%s/intro%s.png", resource_dir.path, name)); + ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back())); + } + Paths::Get().set_resource_dir(resource_dir.path); + + ui_->LoadAnimation(); + + ASSERT_EQ(2u, ui_->intro_frames_.size()); + ASSERT_EQ(3u, ui_->loop_frames_.size()); + + for (const auto& name : tempfiles) { + ASSERT_EQ(0, unlink(name.c_str())); + } +} + +TEST_F(DISABLED_ScreenRecoveryUITest, LoadAnimation_MissingAnimation) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + // We need a dir that doesn't contain any animation. However, using TemporaryDir will give + // leftovers since this is a death test where TemporaryDir::~TemporaryDir() won't be called. + Paths::Get().set_resource_dir("/proc/self"); + + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + ASSERT_EXIT(ui_->LoadAnimation(), ::testing::KilledBySignal(SIGABRT), ""); +} + +#undef RETURN_IF_NO_GRAPHICS diff --git a/tests/unit/sysutil_test.cpp b/tests/unit/sysutil_test.cpp index 434ee25bf..64b8956f7 100644 --- a/tests/unit/sysutil_test.cpp +++ b/tests/unit/sysutil_test.cpp @@ -14,14 +14,14 @@ * limitations under the License. */ -#include <gtest/gtest.h> - #include <string> #include <android-base/file.h> -#include <android-base/test_utils.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> -#include "otautil/SysUtil.h" +#include "otautil/rangeset.h" +#include "otautil/sysutil.h" TEST(SysUtilTest, InvalidArgs) { MemMapping mapping; @@ -30,6 +30,65 @@ TEST(SysUtilTest, InvalidArgs) { ASSERT_FALSE(mapping.MapFile("")); } +TEST(SysUtilTest, ParseBlockMapFile_smoke) { + std::vector<std::string> content = { + "/dev/abc", "49652 4096", "3", "1000 1008", "2100 2102", "30 33", + }; + + TemporaryFile temp_file; + ASSERT_TRUE(android::base::WriteStringToFile(android::base::Join(content, '\n'), temp_file.path)); + + auto block_map_data = BlockMapData::ParseBlockMapFile(temp_file.path); + ASSERT_EQ("/dev/abc", block_map_data.path()); + ASSERT_EQ(49652, block_map_data.file_size()); + ASSERT_EQ(4096, block_map_data.block_size()); + ASSERT_EQ(RangeSet(std::vector<Range>{ + { 1000, 1008 }, + { 2100, 2102 }, + { 30, 33 }, + }), + block_map_data.block_ranges()); +} + +TEST(SysUtilTest, ParseBlockMapFile_invalid_line_count) { + std::vector<std::string> content = { + "/dev/abc", "49652 4096", "2", "1000 1008", "2100 2102", "30 33", + }; + + TemporaryFile temp_file; + ASSERT_TRUE(android::base::WriteStringToFile(android::base::Join(content, '\n'), temp_file.path)); + + auto block_map_data1 = BlockMapData::ParseBlockMapFile(temp_file.path); + ASSERT_FALSE(block_map_data1); +} + +TEST(SysUtilTest, ParseBlockMapFile_invalid_size) { + std::vector<std::string> content = { + "/dev/abc", + "42949672950 4294967295", + "1", + "0 10", + }; + + TemporaryFile temp_file; + ASSERT_TRUE(android::base::WriteStringToFile(android::base::Join(content, '\n'), temp_file.path)); + + auto block_map_data = BlockMapData::ParseBlockMapFile(temp_file.path); + ASSERT_EQ("/dev/abc", block_map_data.path()); + ASSERT_EQ(42949672950, block_map_data.file_size()); + ASSERT_EQ(4294967295, block_map_data.block_size()); + + content[1] = "42949672950 4294967296"; + ASSERT_TRUE(android::base::WriteStringToFile(android::base::Join(content, '\n'), temp_file.path)); + auto large_block_size = BlockMapData::ParseBlockMapFile(temp_file.path); + ASSERT_FALSE(large_block_size); + + content[1] = "4294967296 1"; + ASSERT_TRUE(android::base::WriteStringToFile(android::base::Join(content, '\n'), temp_file.path)); + auto too_many_blocks = BlockMapData::ParseBlockMapFile(temp_file.path); + ASSERT_FALSE(too_many_blocks); +} + TEST(SysUtilTest, MapFileRegularFile) { TemporaryFile temp_file1; std::string content = "abc"; @@ -128,3 +187,13 @@ TEST(SysUtilTest, MapFileBlockMapInvalidBlockMap) { ASSERT_TRUE(android::base::WriteStringToFile("/doesntexist\n4096 4096\n1\n0 1\n", temp_file.path)); ASSERT_FALSE(mapping.MapFile(filename)); } + +TEST(SysUtilTest, StringVectorToNullTerminatedArray) { + std::vector<std::string> args{ "foo", "bar", "baz" }; + auto args_with_nullptr = StringVectorToNullTerminatedArray(args); + ASSERT_EQ(4, args_with_nullptr.size()); + ASSERT_STREQ("foo", args_with_nullptr[0]); + ASSERT_STREQ("bar", args_with_nullptr[1]); + ASSERT_STREQ("baz", args_with_nullptr[2]); + ASSERT_EQ(nullptr, args_with_nullptr[3]); +} diff --git a/tests/unit/uncrypt_test.cpp b/tests/unit/uncrypt_test.cpp new file mode 100644 index 000000000..e97d589a6 --- /dev/null +++ b/tests/unit/uncrypt_test.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <arpa/inet.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> + +#include <algorithm> +#include <string> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/properties.h> +#include <android-base/unique_fd.h> +#include <bootloader_message/bootloader_message.h> +#include <gtest/gtest.h> + +using namespace std::string_literals; + +static const std::string UNCRYPT_SOCKET = "/dev/socket/uncrypt"; +static const std::string INIT_SVC_SETUP_BCB = "init.svc.setup-bcb"; +static const std::string INIT_SVC_CLEAR_BCB = "init.svc.clear-bcb"; +static const std::string INIT_SVC_UNCRYPT = "init.svc.uncrypt"; +static constexpr int SOCKET_CONNECTION_MAX_RETRY = 30; + +static void StopService() { + ASSERT_TRUE(android::base::SetProperty("ctl.stop", "setup-bcb")); + ASSERT_TRUE(android::base::SetProperty("ctl.stop", "clear-bcb")); + ASSERT_TRUE(android::base::SetProperty("ctl.stop", "uncrypt")); + + bool success = false; + for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) { + std::string setup_bcb = android::base::GetProperty(INIT_SVC_SETUP_BCB, ""); + std::string clear_bcb = android::base::GetProperty(INIT_SVC_CLEAR_BCB, ""); + std::string uncrypt = android::base::GetProperty(INIT_SVC_UNCRYPT, ""); + GTEST_LOG_(INFO) << "setup-bcb: [" << setup_bcb << "] clear-bcb: [" << clear_bcb + << "] uncrypt: [" << uncrypt << "]"; + if (setup_bcb != "running" && clear_bcb != "running" && uncrypt != "running") { + success = true; + break; + } + sleep(1); + } + + ASSERT_TRUE(success) << "uncrypt service is not available."; +} + +class UncryptTest : public ::testing::Test { + protected: + UncryptTest() : has_misc(true) {} + + void SetUp() override { + std::string err; + has_misc = !get_bootloader_message_blk_device(&err).empty(); + } + + void TearDown() override { + // Clear the BCB. + if (has_misc) { + std::string err; + ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err; + } + } + + void SetupOrClearBcb(bool isSetup, const std::string& message, + const std::string& message_in_bcb) const { + // Restart the setup-bcb service. + StopService(); + ASSERT_TRUE(android::base::SetProperty("ctl.start", isSetup ? "setup-bcb" : "clear-bcb")); + + // Test tends to be flaky if proceeding immediately ("Transport endpoint is not connected"). + sleep(1); + + sockaddr_un un = {}; + un.sun_family = AF_UNIX; + strlcpy(un.sun_path, UNCRYPT_SOCKET.c_str(), sizeof(un.sun_path)); + + int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_NE(-1, sockfd); + + // Connect to the uncrypt socket. + bool success = false; + for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) { + if (connect(sockfd, reinterpret_cast<sockaddr*>(&un), sizeof(sockaddr_un)) != 0) { + success = true; + break; + } + sleep(1); + } + ASSERT_TRUE(success); + + if (isSetup) { + // Send out the BCB message. + int length = static_cast<int>(message.size()); + int length_out = htonl(length); + ASSERT_TRUE(android::base::WriteFully(sockfd, &length_out, sizeof(int))) + << "Failed to write length: " << strerror(errno); + ASSERT_TRUE(android::base::WriteFully(sockfd, message.data(), length)) + << "Failed to write message: " << strerror(errno); + } + + // Check the status code from uncrypt. + int status; + ASSERT_TRUE(android::base::ReadFully(sockfd, &status, sizeof(int))); + ASSERT_EQ(100U, ntohl(status)); + + // Ack having received the status code. + int code = 0; + ASSERT_TRUE(android::base::WriteFully(sockfd, &code, sizeof(int))); + + ASSERT_EQ(0, close(sockfd)); + + ASSERT_TRUE(android::base::SetProperty("ctl.stop", isSetup ? "setup-bcb" : "clear-bcb")); + + // Verify the message by reading from BCB directly. + bootloader_message boot; + std::string err; + ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err; + + if (isSetup) { + ASSERT_EQ("boot-recovery", std::string(boot.command)); + ASSERT_EQ(message_in_bcb, std::string(boot.recovery)); + + // The rest of the boot.recovery message should be zero'd out. + ASSERT_LE(message_in_bcb.size(), sizeof(boot.recovery)); + size_t left = sizeof(boot.recovery) - message_in_bcb.size(); + ASSERT_EQ(std::string(left, '\0'), std::string(&boot.recovery[message_in_bcb.size()], left)); + + // Clear the BCB. + ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err; + } else { + // All the bytes should be cleared. + ASSERT_EQ(std::string(sizeof(boot), '\0'), + std::string(reinterpret_cast<const char*>(&boot), sizeof(boot))); + } + } + + void VerifyBootloaderMessage(const std::string& expected) { + std::string err; + bootloader_message boot; + ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err; + + // Check that we have all the expected bytes. + ASSERT_EQ(expected, std::string(reinterpret_cast<const char*>(&boot), sizeof(boot))); + } + + bool has_misc; +}; + +TEST_F(UncryptTest, setup_bcb) { + if (!has_misc) { + GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device."; + return; + } + + std::string random_data; + random_data.reserve(sizeof(bootloader_message)); + generate_n(back_inserter(random_data), sizeof(bootloader_message), []() { return rand() % 128; }); + + bootloader_message boot; + memcpy(&boot, random_data.c_str(), random_data.size()); + + std::string err; + ASSERT_TRUE(write_bootloader_message(boot, &err)) << "Failed to write BCB: " << err; + VerifyBootloaderMessage(random_data); + + ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err; + VerifyBootloaderMessage(std::string(sizeof(bootloader_message), '\0')); + + std::string message = "--update_message=abc value"; + std::string message_in_bcb = "recovery\n--update_message=abc value\n"; + SetupOrClearBcb(true, message, message_in_bcb); + + SetupOrClearBcb(false, "", ""); + + TemporaryFile wipe_package; + ASSERT_TRUE(android::base::WriteStringToFile(std::string(345, 'a'), wipe_package.path)); + + // It's expected to store a wipe package in /misc, with the package size passed to recovery. + message = "--wipe_ab\n--wipe_package="s + wipe_package.path + "\n--reason=wipePackage"s; + message_in_bcb = "recovery\n--wipe_ab\n--wipe_package_size=345\n--reason=wipePackage\n"; + SetupOrClearBcb(true, message, message_in_bcb); +} diff --git a/tests/unit/update_verifier_test.cpp b/tests/unit/update_verifier_test.cpp new file mode 100644 index 000000000..e27e58c22 --- /dev/null +++ b/tests/unit/update_verifier_test.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <update_verifier/update_verifier.h> + +#include <functional> +#include <string> +#include <unordered_map> +#include <vector> + +#include <android-base/file.h> +#include <android-base/properties.h> +#include <android-base/strings.h> +#include <google/protobuf/repeated_field.h> +#include <gtest/gtest.h> + +#include "care_map.pb.h" + +using namespace std::string_literals; + +class UpdateVerifierTest : public ::testing::Test { + protected: + void SetUp() override { + std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", ""); + verity_supported = android::base::EqualsIgnoreCase(verity_mode, "enforcing"); + + care_map_prefix_ = care_map_dir_.path + "/care_map"s; + care_map_pb_ = care_map_dir_.path + "/care_map.pb"s; + care_map_txt_ = care_map_dir_.path + "/care_map.txt"s; + // Overrides the the care_map_prefix. + verifier_.set_care_map_prefix(care_map_prefix_); + + property_id_ = "ro.build.fingerprint"; + fingerprint_ = android::base::GetProperty(property_id_, ""); + // Overrides the property_reader if we cannot read the given property on the device. + if (fingerprint_.empty()) { + fingerprint_ = "mock_fingerprint"; + verifier_.set_property_reader([](const std::string& /* id */) { return "mock_fingerprint"; }); + } + } + + void TearDown() override { + unlink(care_map_pb_.c_str()); + unlink(care_map_txt_.c_str()); + } + + // Returns a serialized string of the proto3 message according to the given partition info. + std::string ConstructProto( + std::vector<std::unordered_map<std::string, std::string>>& partitions) { + recovery_update_verifier::CareMap result; + for (const auto& partition : partitions) { + recovery_update_verifier::CareMap::PartitionInfo info; + if (partition.find("name") != partition.end()) { + info.set_name(partition.at("name")); + } + if (partition.find("ranges") != partition.end()) { + info.set_ranges(partition.at("ranges")); + } + if (partition.find("id") != partition.end()) { + info.set_id(partition.at("id")); + } + if (partition.find("fingerprint") != partition.end()) { + info.set_fingerprint(partition.at("fingerprint")); + } + + *result.add_partitions() = info; + } + + return result.SerializeAsString(); + } + + bool verity_supported; + UpdateVerifier verifier_; + + TemporaryDir care_map_dir_; + std::string care_map_prefix_; + std::string care_map_pb_; + std::string care_map_txt_; + + std::string property_id_; + std::string fingerprint_; +}; + +TEST_F(UpdateVerifierTest, verify_image_no_care_map) { + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_text_format) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::string content = "system\n2,0,1"; + ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_)); + // CareMap in text format is no longer supported. + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_empty_care_map) { + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_TRUE(verifier_.ParseCareMap()); + ASSERT_TRUE(verifier_.VerifyPartitions()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "3,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_empty_fingerprint) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "2,0,1" }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_fingerprint_mismatch) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", "unsupported_fingerprint" }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} diff --git a/tests/unit/updater_test.cpp b/tests/unit/updater_test.cpp new file mode 100644 index 000000000..4a8d1e6ff --- /dev/null +++ b/tests/unit/updater_test.cpp @@ -0,0 +1,1219 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include <algorithm> +#include <memory> +#include <string> +#include <string_view> +#include <unordered_map> +#include <vector> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/parseint.h> +#include <android-base/properties.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#include <bootloader_message/bootloader_message.h> +#include <brotli/encode.h> +#include <bsdiff/bsdiff.h> +#include <gtest/gtest.h> +#include <verity/hash_tree_builder.h> +#include <ziparchive/zip_archive.h> +#include <ziparchive/zip_writer.h> + +#include "applypatch/applypatch.h" +#include "common/test_constants.h" +#include "edify/expr.h" +#include "otautil/error_code.h" +#include "otautil/paths.h" +#include "otautil/print_sha1.h" +#include "otautil/sysutil.h" +#include "private/commands.h" +#include "updater/blockimg.h" +#include "updater/install.h" +#include "updater/updater.h" + +using namespace std::string_literals; + +using PackageEntries = std::unordered_map<std::string, std::string>; + +static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code, + Updater* updater = nullptr) { + std::unique_ptr<Expr> e; + int error_count = 0; + ASSERT_EQ(0, ParseString(expr_str, &e, &error_count)); + ASSERT_EQ(0, error_count); + + State state(expr_str, 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 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: + 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, nullptr)); + ASSERT_TRUE(updater_.RunUpdate()); + ASSERT_EQ(result, updater_.result()); + + // 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/tests/unit/verifier_test.cpp b/tests/unit/verifier_test.cpp new file mode 100644 index 000000000..ded23c52f --- /dev/null +++ b/tests/unit/verifier_test.cpp @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agree to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#include <android-base/unique_fd.h> +#include <gtest/gtest.h> +#include <openssl/bn.h> +#include <openssl/ec.h> +#include <openssl/nid.h> +#include <ziparchive/zip_writer.h> + +#include "common/test_constants.h" +#include "install/package.h" +#include "install/verifier.h" +#include "otautil/sysutil.h" + +using namespace std::string_literals; + +static void LoadKeyFromFile(const std::string& file_name, Certificate* cert) { + std::string testkey_string; + ASSERT_TRUE(android::base::ReadFileToString(file_name, &testkey_string)); + ASSERT_TRUE(LoadCertificateFromBuffer( + std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), cert)); +} + +static void VerifyFile(const std::string& content, const std::vector<Certificate>& keys, + int expected) { + auto package = + Package::CreateMemoryPackage(std::vector<uint8_t>(content.begin(), content.end()), nullptr); + ASSERT_NE(nullptr, package); + + ASSERT_EQ(expected, verify_file(package.get(), keys)); +} + +static void VerifyPackageWithCertificates(const std::string& name, + const std::vector<Certificate>& certs) { + std::string path = from_testdata_base(name); + auto package = Package::CreateMemoryPackage(path, nullptr); + ASSERT_NE(nullptr, package); + + ASSERT_EQ(VERIFY_SUCCESS, verify_file(package.get(), certs)); +} + +static void VerifyPackageWithSingleCertificate(const std::string& name, Certificate&& cert) { + std::vector<Certificate> certs; + certs.emplace_back(std::move(cert)); + VerifyPackageWithCertificates(name, certs); +} + +static void BuildCertificateArchive(const std::vector<std::string>& file_names, int fd) { + FILE* zip_file_ptr = fdopen(fd, "wb"); + ZipWriter zip_writer(zip_file_ptr); + + for (const auto& name : file_names) { + std::string content; + ASSERT_TRUE(android::base::ReadFileToString(name, &content)); + + // Makes sure the zip entry name has the correct suffix. + std::string entry_name = name; + if (!android::base::EndsWith(entry_name, "x509.pem")) { + entry_name += "x509.pem"; + } + ASSERT_EQ(0, zip_writer.StartEntry(entry_name.c_str(), ZipWriter::kCompress)); + ASSERT_EQ(0, zip_writer.WriteBytes(content.data(), content.size())); + ASSERT_EQ(0, zip_writer.FinishEntry()); + } + + ASSERT_EQ(0, zip_writer.Finish()); + ASSERT_EQ(0, fclose(zip_file_ptr)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_failure) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + std::string testkey_string; + ASSERT_TRUE( + android::base::ReadFileToString(from_testdata_base("testkey_v1.txt"), &testkey_string)); + ASSERT_FALSE(LoadCertificateFromBuffer( + std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), &cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent3) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v1.x509.pem"), &cert); + + ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v1.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent65537) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v2.x509.pem"), &cert); + + ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v2.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent3) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &cert); + + ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v3.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent65537) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v4.x509.pem"), &cert); + + ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v4.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha256_ec256bits) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v5.x509.pem"), &cert); + + ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_EC, cert.key_type); + ASSERT_EQ(nullptr, cert.rsa); + + VerifyPackageWithSingleCertificate("otasigned_v5.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha256_rsa4096_bits) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_4096bits.x509.pem"), &cert); + + ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_4096bits.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_check_rsa_keys) { + std::unique_ptr<RSA, RSADeleter> rsa(RSA_new()); + std::unique_ptr<BIGNUM, decltype(&BN_free)> exponent(BN_new(), BN_free); + BN_set_word(exponent.get(), 3); + RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr); + ASSERT_TRUE(CheckRSAKey(rsa)); + + // Exponent is expected to be 3 or 65537 + BN_set_word(exponent.get(), 17); + RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr); + ASSERT_FALSE(CheckRSAKey(rsa)); + + // Modulus is expected to be 2048. + BN_set_word(exponent.get(), 3); + RSA_generate_key_ex(rsa.get(), 1024, exponent.get(), nullptr); + ASSERT_FALSE(CheckRSAKey(rsa)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_check_ec_keys) { + std::unique_ptr<EC_KEY, ECKEYDeleter> ec(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + ASSERT_EQ(1, EC_KEY_generate_key(ec.get())); + ASSERT_TRUE(CheckECKey(ec)); + + // Expects 256-bit EC key with curve NIST P-256 + ec.reset(EC_KEY_new_by_curve_name(NID_secp224r1)); + ASSERT_EQ(1, EC_KEY_generate_key(ec.get())); + ASSERT_FALSE(CheckECKey(ec)); +} + +TEST(VerifierTest, LoadKeysFromZipfile_empty_archive) { + TemporaryFile otacerts; + BuildCertificateArchive({}, otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_TRUE(certs.empty()); +} + +TEST(VerifierTest, LoadKeysFromZipfile_single_key) { + TemporaryFile otacerts; + BuildCertificateArchive({ from_testdata_base("testkey_v1.x509.pem") }, otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_EQ(1, certs.size()); + + VerifyPackageWithCertificates("otasigned_v1.zip", certs); +} + +TEST(VerifierTest, LoadKeysFromZipfile_corrupted_key) { + TemporaryFile corrupted_key; + std::string content; + ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v1.x509.pem"), &content)); + content = "random-contents" + content; + ASSERT_TRUE(android::base::WriteStringToFd(content, corrupted_key.release())); + + TemporaryFile otacerts; + BuildCertificateArchive({ from_testdata_base("testkey_v2.x509.pem"), corrupted_key.path }, + otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_EQ(0, certs.size()); +} + +TEST(VerifierTest, LoadKeysFromZipfile_multiple_key) { + TemporaryFile otacerts; + BuildCertificateArchive( + { + from_testdata_base("testkey_v3.x509.pem"), + from_testdata_base("testkey_v4.x509.pem"), + from_testdata_base("testkey_v5.x509.pem"), + + }, + otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_EQ(3, certs.size()); + + VerifyPackageWithCertificates("otasigned_v3.zip", certs); + VerifyPackageWithCertificates("otasigned_v4.zip", certs); + VerifyPackageWithCertificates("otasigned_v5.zip", certs); +} + +class VerifierTest : public testing::TestWithParam<std::vector<std::string>> { + protected: + void SetUp() override { + std::vector<std::string> args = GetParam(); + std::string path = from_testdata_base(args[0]); + memory_package_ = Package::CreateMemoryPackage(path, nullptr); + ASSERT_NE(nullptr, memory_package_); + file_package_ = Package::CreateFilePackage(path, nullptr); + ASSERT_NE(nullptr, file_package_); + + for (auto it = ++args.cbegin(); it != args.cend(); ++it) { + std::string public_key_file = from_testdata_base("testkey_" + *it + ".x509.pem"); + certs_.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(public_key_file, &certs_.back()); + } + } + + std::unique_ptr<Package> memory_package_; + std::unique_ptr<Package> file_package_; + std::vector<Certificate> certs_; +}; + +class VerifierSuccessTest : public VerifierTest { +}; + +class VerifierFailureTest : public VerifierTest { +}; + +TEST(VerifierTest, BadPackage_AlteredFooter) { + std::vector<Certificate> certs; + certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back()); + + std::string package; + ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package)); + ASSERT_EQ(std::string("\xc0\x06\xff\xff\xd2\x06", 6), package.substr(package.size() - 6, 6)); + + // Alter the footer. + package[package.size() - 5] = '\x05'; + VerifyFile(package, certs, VERIFY_FAILURE); +} + +TEST(VerifierTest, BadPackage_AlteredContent) { + std::vector<Certificate> certs; + certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back()); + + std::string package; + ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package)); + ASSERT_GT(package.size(), static_cast<size_t>(100)); + + // Alter the content. + std::string altered1(package); + altered1[50] += 1; + VerifyFile(altered1, certs, VERIFY_FAILURE); + + std::string altered2(package); + altered2[10] += 1; + VerifyFile(altered2, certs, VERIFY_FAILURE); +} + +TEST(VerifierTest, BadPackage_SignatureStartOutOfBounds) { + std::vector<Certificate> certs; + certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back()); + + // Signature start is 65535 (0xffff) while comment size is 0 (Bug: 31914369). + std::string package = "\x50\x4b\x05\x06"s + std::string(12, '\0') + "\xff\xff\xff\xff\x00\x00"s; + VerifyFile(package, certs, VERIFY_FAILURE); +} + +TEST_P(VerifierSuccessTest, VerifySucceed) { + ASSERT_EQ(VERIFY_SUCCESS, verify_file(memory_package_.get(), certs_)); + ASSERT_EQ(VERIFY_SUCCESS, verify_file(file_package_.get(), certs_)); +} + +TEST_P(VerifierFailureTest, VerifyFailure) { + ASSERT_EQ(VERIFY_FAILURE, verify_file(memory_package_.get(), certs_)); + ASSERT_EQ(VERIFY_FAILURE, verify_file(file_package_.get(), certs_)); +} + +INSTANTIATE_TEST_CASE_P(SingleKeySuccess, VerifierSuccessTest, + ::testing::Values( + std::vector<std::string>({"otasigned_v1.zip", "v1"}), + std::vector<std::string>({"otasigned_v2.zip", "v2"}), + std::vector<std::string>({"otasigned_v3.zip", "v3"}), + std::vector<std::string>({"otasigned_v4.zip", "v4"}), + std::vector<std::string>({"otasigned_v5.zip", "v5"}))); + +INSTANTIATE_TEST_CASE_P(MultiKeySuccess, VerifierSuccessTest, + ::testing::Values( + std::vector<std::string>({"otasigned_v1.zip", "v1", "v2"}), + std::vector<std::string>({"otasigned_v2.zip", "v5", "v2"}), + std::vector<std::string>({"otasigned_v3.zip", "v5", "v1", "v3"}), + std::vector<std::string>({"otasigned_v4.zip", "v5", "v1", "v4"}), + std::vector<std::string>({"otasigned_v5.zip", "v4", "v1", "v5"}))); + +INSTANTIATE_TEST_CASE_P(WrongKey, VerifierFailureTest, + ::testing::Values( + std::vector<std::string>({"otasigned_v1.zip", "v2"}), + std::vector<std::string>({"otasigned_v2.zip", "v1"}), + std::vector<std::string>({"otasigned_v3.zip", "v5"}), + std::vector<std::string>({"otasigned_v4.zip", "v5"}), + std::vector<std::string>({"otasigned_v5.zip", "v3"}))); + +INSTANTIATE_TEST_CASE_P(WrongHash, VerifierFailureTest, + ::testing::Values( + std::vector<std::string>({"otasigned_v1.zip", "v3"}), + std::vector<std::string>({"otasigned_v2.zip", "v4"}), + std::vector<std::string>({"otasigned_v3.zip", "v1"}), + std::vector<std::string>({"otasigned_v4.zip", "v2"}))); + +INSTANTIATE_TEST_CASE_P(BadPackage, VerifierFailureTest, + ::testing::Values( + std::vector<std::string>({"random.zip", "v1"}), + std::vector<std::string>({"fake-eocd.zip", "v1"}))); diff --git a/tests/unit/zip_test.cpp b/tests/unit/zip_test.cpp index 827668521..0753d64e1 100644 --- a/tests/unit/zip_test.cpp +++ b/tests/unit/zip_test.cpp @@ -21,12 +21,11 @@ #include <vector> #include <android-base/file.h> -#include <android-base/test_utils.h> #include <gtest/gtest.h> -#include <otautil/SysUtil.h> #include <ziparchive/zip_archive.h> #include "common/test_constants.h" +#include "otautil/sysutil.h" TEST(ZipTest, OpenFromMemory) { std::string zip_path = from_testdata_base("ziptest_dummy-update.zip"); @@ -38,10 +37,9 @@ TEST(ZipTest, OpenFromMemory) { ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_path.c_str(), &handle)); static constexpr const char* BINARY_PATH = "META-INF/com/google/android/update-binary"; - ZipString binary_path(BINARY_PATH); ZipEntry binary_entry; // Make sure the package opens correctly and its entry can be read. - ASSERT_EQ(0, FindEntry(handle, binary_path, &binary_entry)); + ASSERT_EQ(0, FindEntry(handle, BINARY_PATH, &binary_entry)); TemporaryFile tmp_binary; ASSERT_NE(-1, tmp_binary.fd); |