diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, 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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..cc6bcd7 --- /dev/null +++ b/OWNERS @@ -0,0 +1,3 @@ +zhangkelvin@google.com +akailash@google.com + diff --git a/applypatch/Android.bp b/applypatch/Android.bp new file mode 100644 index 0000000..0d6d23b --- /dev/null +++ b/applypatch/Android.bp @@ -0,0 +1,201 @@ +// 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. + +package { + default_applicable_licenses: ["bootable_recovery_applypatch_license"], +} + +// Added automatically by a large-scale-change +// See: http://go/android-license-faq +license { + name: "bootable_recovery_applypatch_license", + visibility: [":__subpackages__"], + license_kinds: [ + "SPDX-license-identifier-Apache-2.0", + ], + license_text: [ + "NOTICE", + ], +} + +cc_defaults { + name: "applypatch_defaults", + + cflags: [ + "-D_FILE_OFFSET_BITS=64", + "-DZLIB_CONST", + "-Wall", + "-Werror", + ], + + local_include_dirs: [ + "include", + ], +} + +cc_library_static { + name: "libapplypatch", + + host_supported: true, + vendor_available: true, + + defaults: [ + "applypatch_defaults", + ], + + srcs: [ + "applypatch.cpp", + "bspatch.cpp", + "freecache.cpp", + "imgpatch.cpp", + ], + + export_include_dirs: [ + "include", + ], + + static_libs: [ + "libbase", + "libbspatch", + "libbz", + "libedify", + "libotautil", + "libz_stable", + ], + + shared_libs: [ + "libcrypto", + ], + + target: { + darwin: { + enabled: false, + }, + }, +} + +cc_library_static { + name: "libapplypatch_modes", + vendor_available: true, + + defaults: [ + "applypatch_defaults", + ], + + srcs: [ + "applypatch_modes.cpp", + ], + + static_libs: [ + "libapplypatch", + "libbase", + "libedify", + "libotautil", + ], + + shared_libs: [ + "libcrypto", + ], +} + +cc_binary { + name: "applypatch", + vendor: true, + + defaults: [ + "applypatch_defaults", + ], + + srcs: [ + "applypatch_main.cpp", + ], + + static_libs: [ + "libapplypatch_modes", + "libapplypatch", + "libedify", + "libotautil", + + // External dependencies. + "libbspatch", + "libbrotli", + "libbz", + ], + + shared_libs: [ + "libbase", + "libcrypto", + "liblog", + "libz_stable", + "libziparchive", + ], + + init_rc: [ + "vendor_flash_recovery.rc", + ], +} + +cc_library_static { + name: "libimgdiff", + host_supported: true, + defaults: [ + "applypatch_defaults", + ], + + srcs: [ + "imgdiff.cpp", + ], + + export_include_dirs: [ + "include", + ], + + static_libs: [ + "libbase", + "libbsdiff", + "libdivsufsort", + "libdivsufsort64", + "liblog", + "libotautil", + "libutils", + "libz_stable", + "libziparchive", + ], +} + +cc_binary_host { + name: "imgdiff", + srcs: [ + "imgdiff_main.cpp", + ], + + defaults: [ + "applypatch_defaults", + ], + + static_libs: [ + "libimgdiff", + "libotautil", + "libbsdiff", + "libdivsufsort", + "libdivsufsort64", + "libziparchive", + "libbase", + "libutils", + "liblog", + "libbrotli", + "libbz", + "libz_stable", + ], +} diff --git a/applypatch/NOTICE b/applypatch/NOTICE new file mode 100644 index 0000000..6156a0c --- /dev/null +++ b/applypatch/NOTICE @@ -0,0 +1,41 @@ +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. + + +bsdiff.c +bspatch.c + +Copyright 2003-2005 Colin Percival +All rights reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted providing that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/applypatch/applypatch.cpp b/applypatch/applypatch.cpp new file mode 100644 index 0000000..adda697 --- /dev/null +++ b/applypatch/applypatch.cpp @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2008 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 "applypatch/applypatch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "edify/expr.h" +#include "otautil/paths.h" +#include "otautil/print_sha1.h" + +using namespace std::string_literals; + +static bool GenerateTarget(const Partition& target, const FileContents& source_file, + const Value& patch, const Value* bonus_data, bool backup_source); + +bool LoadFileContents(const std::string& filename, FileContents* file) { + // No longer allow loading contents from eMMC partitions. + if (android::base::StartsWith(filename, "EMMC:")) { + return false; + } + + std::string data; + if (!android::base::ReadFileToString(filename, &data)) { + PLOG(ERROR) << "Failed to read \"" << filename << "\""; + return false; + } + + file->data = std::vector(data.begin(), data.end()); + SHA1(file->data.data(), file->data.size(), file->sha1); + return true; +} + +// Reads the contents of a Partition to the given FileContents buffer. +static bool ReadPartitionToBuffer(const Partition& partition, FileContents* out, + bool check_backup) { + uint8_t expected_sha1[SHA_DIGEST_LENGTH]; + if (ParseSha1(partition.hash, expected_sha1) != 0) { + LOG(ERROR) << "Failed to parse target hash \"" << partition.hash << "\""; + return false; + } + + android::base::unique_fd dev(open(partition.name.c_str(), O_RDONLY)); + if (dev == -1) { + PLOG(ERROR) << "Failed to open eMMC partition \"" << partition << "\""; + } else { + std::vector buffer(partition.size); + if (!android::base::ReadFully(dev, buffer.data(), buffer.size())) { + PLOG(ERROR) << "Failed to read " << buffer.size() << " bytes of data for partition " + << partition; + } else { + SHA1(buffer.data(), buffer.size(), out->sha1); + if (memcmp(out->sha1, expected_sha1, SHA_DIGEST_LENGTH) == 0) { + out->data = std::move(buffer); + return true; + } + } + } + + if (!check_backup) { + LOG(ERROR) << "Partition contents don't have the expected checksum"; + return false; + } + + if (LoadFileContents(Paths::Get().cache_temp_source(), out) && + memcmp(out->sha1, expected_sha1, SHA_DIGEST_LENGTH) == 0) { + return true; + } + + LOG(ERROR) << "Both of partition contents and backup don't have the expected checksum"; + return false; +} + +bool SaveFileContents(const std::string& filename, const FileContents* file) { + android::base::unique_fd fd( + open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR)); + if (fd == -1) { + PLOG(ERROR) << "Failed to open \"" << filename << "\" for write"; + return false; + } + + if (!android::base::WriteFully(fd, file->data.data(), file->data.size())) { + PLOG(ERROR) << "Failed to write " << file->data.size() << " bytes of data to " << filename; + return false; + } + + if (fsync(fd) != 0) { + PLOG(ERROR) << "Failed to fsync \"" << filename << "\""; + return false; + } + + if (close(fd.release()) != 0) { + PLOG(ERROR) << "Failed to close \"" << filename << "\""; + return false; + } + + return true; +} + +// Writes a memory buffer to 'target' Partition. +static bool WriteBufferToPartition(const FileContents& file_contents, const Partition& partition) { + const unsigned char* data = file_contents.data.data(); + size_t len = file_contents.data.size(); + size_t start = 0; + bool success = false; + for (size_t attempt = 0; attempt < 2; ++attempt) { + android::base::unique_fd fd(open(partition.name.c_str(), O_RDWR)); + if (fd == -1) { + PLOG(ERROR) << "Failed to open \"" << partition << "\""; + return false; + } + + if (TEMP_FAILURE_RETRY(lseek(fd, start, SEEK_SET)) == -1) { + PLOG(ERROR) << "Failed to seek to " << start << " on \"" << partition << "\""; + return false; + } + + if (!android::base::WriteFully(fd, data + start, len - start)) { + PLOG(ERROR) << "Failed to write " << len - start << " bytes to \"" << partition << "\""; + return false; + } + + if (fsync(fd) != 0) { + PLOG(ERROR) << "Failed to sync \"" << partition << "\""; + return false; + } + if (close(fd.release()) != 0) { + PLOG(ERROR) << "Failed to close \"" << partition << "\""; + return false; + } + + fd.reset(open(partition.name.c_str(), O_RDONLY)); + if (fd == -1) { + PLOG(ERROR) << "Failed to reopen \"" << partition << "\" for verification"; + return false; + } + + // Drop caches so our subsequent verification read won't just be reading the cache. + sync(); + std::string drop_cache = "/proc/sys/vm/drop_caches"; + if (!android::base::WriteStringToFile("3\n", drop_cache)) { + PLOG(ERROR) << "Failed to write to " << drop_cache; + } else { + LOG(INFO) << " caches dropped"; + } + sleep(1); + + // Verify. + if (TEMP_FAILURE_RETRY(lseek(fd, 0, SEEK_SET)) == -1) { + PLOG(ERROR) << "Failed to seek to 0 on " << partition; + return false; + } + + unsigned char buffer[4096]; + start = len; + for (size_t p = 0; p < len; p += sizeof(buffer)) { + size_t to_read = len - p; + if (to_read > sizeof(buffer)) { + to_read = sizeof(buffer); + } + + if (!android::base::ReadFully(fd, buffer, to_read)) { + PLOG(ERROR) << "Failed to verify-read " << partition << " at " << p; + return false; + } + + if (memcmp(buffer, data + p, to_read) != 0) { + LOG(ERROR) << "Verification failed starting at " << p; + start = p; + break; + } + } + + if (start == len) { + LOG(INFO) << "Verification read succeeded (attempt " << attempt + 1 << ")"; + success = true; + break; + } + + if (close(fd.release()) != 0) { + PLOG(ERROR) << "Failed to close " << partition; + return false; + } + } + + if (!success) { + LOG(ERROR) << "Failed to verify after all attempts"; + return false; + } + + sync(); + + return true; +} + +int ParseSha1(const std::string& str, uint8_t* digest) { + const char* ps = str.c_str(); + uint8_t* pd = digest; + for (int i = 0; i < SHA_DIGEST_LENGTH * 2; ++i, ++ps) { + int digit; + if (*ps >= '0' && *ps <= '9') { + digit = *ps - '0'; + } else if (*ps >= 'a' && *ps <= 'f') { + digit = *ps - 'a' + 10; + } else if (*ps >= 'A' && *ps <= 'F') { + digit = *ps - 'A' + 10; + } else { + return -1; + } + if (i % 2 == 0) { + *pd = digit << 4; + } else { + *pd |= digit; + ++pd; + } + } + if (*ps != '\0') return -1; + return 0; +} + +bool PatchPartitionCheck(const Partition& target, const Partition& source) { + FileContents target_file; + FileContents source_file; + return (ReadPartitionToBuffer(target, &target_file, false) || + ReadPartitionToBuffer(source, &source_file, true)); +} + +int ShowLicenses() { + ShowBSDiffLicense(); + return 0; +} + +bool PatchPartition(const Partition& target, const Partition& source, const Value& patch, + const Value* bonus, bool backup_source) { + LOG(INFO) << "Patching " << target.name; + + // We try to load and check against the target hash first. + FileContents target_file; + if (ReadPartitionToBuffer(target, &target_file, false)) { + // The early-exit case: the patch was already applied, this file has the desired hash, nothing + // for us to do. + LOG(INFO) << " already " << target.hash.substr(0, 8); + return true; + } + + FileContents source_file; + if (ReadPartitionToBuffer(source, &source_file, backup_source)) { + return GenerateTarget(target, source_file, patch, bonus, backup_source); + } + + LOG(ERROR) << "Failed to find any match"; + return false; +} + +bool FlashPartition(const Partition& partition, const std::string& source_filename) { + LOG(INFO) << "Flashing " << partition; + + // We try to load and check against the target hash first. + FileContents target_file; + if (ReadPartitionToBuffer(partition, &target_file, false)) { + // The early-exit case: the patch was already applied, this file has the desired hash, nothing + // for us to do. + LOG(INFO) << " already " << partition.hash.substr(0, 8); + return true; + } + + FileContents source_file; + if (!LoadFileContents(source_filename, &source_file)) { + LOG(ERROR) << "Failed to load source file"; + return false; + } + + uint8_t expected_sha1[SHA_DIGEST_LENGTH]; + if (ParseSha1(partition.hash, expected_sha1) != 0) { + LOG(ERROR) << "Failed to parse source hash \"" << partition.hash << "\""; + return false; + } + + if (memcmp(source_file.sha1, expected_sha1, SHA_DIGEST_LENGTH) != 0) { + // The source doesn't have desired checksum. + LOG(ERROR) << "source \"" << source_filename << "\" doesn't have expected SHA-1 sum"; + LOG(ERROR) << "expected: " << partition.hash.substr(0, 8) + << ", found: " << short_sha1(source_file.sha1); + return false; + } + if (!WriteBufferToPartition(source_file, partition)) { + LOG(ERROR) << "Failed to write to " << partition; + return false; + } + return true; +} + +static bool GenerateTarget(const Partition& target, const FileContents& source_file, + const Value& patch, const Value* bonus_data, bool backup_source) { + uint8_t expected_sha1[SHA_DIGEST_LENGTH]; + if (ParseSha1(target.hash, expected_sha1) != 0) { + LOG(ERROR) << "Failed to parse target hash \"" << target.hash << "\""; + return false; + } + + if (patch.type != Value::Type::BLOB) { + LOG(ERROR) << "patch is not a blob"; + return false; + } + + const char* header = patch.data.data(); + size_t header_bytes_read = patch.data.size(); + bool use_bsdiff = false; + if (header_bytes_read >= 8 && memcmp(header, "BSDIFF40", 8) == 0) { + use_bsdiff = true; + } else if (header_bytes_read >= 8 && memcmp(header, "IMGDIFF2", 8) == 0) { + use_bsdiff = false; + } else { + LOG(ERROR) << "Unknown patch file format"; + return false; + } + + // We write the original source to cache, in case the partition write is interrupted. + if (backup_source && !CheckAndFreeSpaceOnCache(source_file.data.size())) { + LOG(ERROR) << "Not enough free space on /cache"; + return false; + } + if (backup_source && !SaveFileContents(Paths::Get().cache_temp_source(), &source_file)) { + LOG(ERROR) << "Failed to back up source file"; + return false; + } + + // We store the decoded output in memory. + FileContents patched; + SHA_CTX ctx; + SHA1_Init(&ctx); + SinkFn sink = [&patched, &ctx](const unsigned char* data, size_t len) { + SHA1_Update(&ctx, data, len); + patched.data.insert(patched.data.end(), data, data + len); + return len; + }; + + int result; + if (use_bsdiff) { + result = ApplyBSDiffPatch(source_file.data.data(), source_file.data.size(), patch, 0, sink); + } else { + result = + ApplyImagePatch(source_file.data.data(), source_file.data.size(), patch, sink, bonus_data); + } + + if (result != 0) { + LOG(ERROR) << "Failed to apply the patch: " << result; + return false; + } + + SHA1_Final(patched.sha1, &ctx); + if (memcmp(patched.sha1, expected_sha1, SHA_DIGEST_LENGTH) != 0) { + LOG(ERROR) << "Patching did not produce the expected SHA-1 of " << short_sha1(expected_sha1); + + LOG(ERROR) << "target size " << patched.data.size() << " SHA-1 " << short_sha1(patched.sha1); + LOG(ERROR) << "source size " << source_file.data.size() << " SHA-1 " + << short_sha1(source_file.sha1); + + uint8_t patch_digest[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast(patch.data.data()), patch.data.size(), patch_digest); + LOG(ERROR) << "patch size " << patch.data.size() << " SHA-1 " << short_sha1(patch_digest); + + if (bonus_data != nullptr) { + uint8_t bonus_digest[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast(bonus_data->data.data()), bonus_data->data.size(), + bonus_digest); + LOG(ERROR) << "bonus size " << bonus_data->data.size() << " SHA-1 " + << short_sha1(bonus_digest); + } + + return false; + } + + LOG(INFO) << " now " << short_sha1(expected_sha1); + + // Write back the temp file to the partition. + if (!WriteBufferToPartition(patched, target)) { + LOG(ERROR) << "Failed to write patched data to " << target.name; + return false; + } + + // Delete the backup copy of the source. + if (backup_source) { + unlink(Paths::Get().cache_temp_source().c_str()); + } + + // Success! + return true; +} + +bool CheckPartition(const Partition& partition) { + FileContents target_file; + return ReadPartitionToBuffer(partition, &target_file, false); +} + +Partition Partition::Parse(const std::string& input_str, std::string* err) { + std::vector pieces = android::base::Split(input_str, ":"); + if (pieces.size() != 4 || pieces[0] != "EMMC") { + *err = "Invalid number of tokens or non-eMMC target"; + return {}; + } + + size_t size; + if (!android::base::ParseUint(pieces[2], &size) || size == 0) { + *err = "Failed to parse \"" + pieces[2] + "\" as byte count"; + return {}; + } + + return Partition(pieces[1], size, pieces[3]); +} + +std::string Partition::ToString() const { + if (*this) { + return "EMMC:"s + name + ":" + std::to_string(size) + ":" + hash; + } + return ""; +} + +std::ostream& operator<<(std::ostream& os, const Partition& partition) { + os << partition.ToString(); + return os; +} diff --git a/applypatch/applypatch_main.cpp b/applypatch/applypatch_main.cpp new file mode 100644 index 0000000..92d2b3f --- /dev/null +++ b/applypatch/applypatch_main.cpp @@ -0,0 +1,25 @@ +/* + * 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 "applypatch_modes.h" + +#include + +// See the comments for applypatch() function. +int main(int argc, char** argv) { + android::base::InitLogging(argv); + return applypatch_modes(argc, argv); +} diff --git a/applypatch/applypatch_modes.cpp b/applypatch/applypatch_modes.cpp new file mode 100644 index 0000000..bb5eeae --- /dev/null +++ b/applypatch/applypatch_modes.cpp @@ -0,0 +1,187 @@ +/* + * 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 "applypatch_modes.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "applypatch/applypatch.h" +#include "edify/expr.h" + +static int CheckMode(const std::string& target_emmc) { + std::string err; + auto target = Partition::Parse(target_emmc, &err); + if (!target) { + LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err; + return 2; + } + return CheckPartition(target) ? 0 : 1; +} + +static int FlashMode(const std::string& target_emmc, const std::string& source_file) { + std::string err; + auto target = Partition::Parse(target_emmc, &err); + if (!target) { + LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err; + return 2; + } + return FlashPartition(target, source_file) ? 0 : 1; +} + +static int PatchMode(const std::string& target_emmc, const std::string& source_emmc, + const std::string& patch_file, const std::string& bonus_file) { + std::string err; + auto target = Partition::Parse(target_emmc, &err); + if (!target) { + LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err; + return 2; + } + + auto source = Partition::Parse(source_emmc, &err); + if (!source) { + LOG(ERROR) << "Failed to parse source \"" << source_emmc << "\": " << err; + return 2; + } + + std::string patch_contents; + if (!android::base::ReadFileToString(patch_file, &patch_contents)) { + PLOG(ERROR) << "Failed to read patch file \"" << patch_file << "\""; + return 1; + } + + Value patch(Value::Type::BLOB, std::move(patch_contents)); + std::unique_ptr bonus; + if (!bonus_file.empty()) { + std::string bonus_contents; + if (!android::base::ReadFileToString(bonus_file, &bonus_contents)) { + PLOG(ERROR) << "Failed to read bonus file \"" << bonus_file << "\""; + return 1; + } + bonus = std::make_unique(Value::Type::BLOB, std::move(bonus_contents)); + } + + return PatchPartition(target, source, patch, bonus.get(), false) ? 0 : 1; +} + +static void Usage() { + printf( + "Usage: \n" + "check mode\n" + " applypatch --check EMMC:::\n\n" + "flash mode\n" + " applypatch --flash \n" + " --target EMMC:::\n\n" + "patch mode\n" + " applypatch [--bonus ]\n" + " --patch \n" + " --target EMMC:::\n" + " --source EMMC:::\n\n" + "show license\n" + " applypatch --license\n" + "\n\n"); +} + +int applypatch_modes(int argc, char* argv[]) { + static constexpr struct option OPTIONS[]{ + // clang-format off + { "bonus", required_argument, nullptr, 0 }, + { "check", required_argument, nullptr, 0 }, + { "flash", required_argument, nullptr, 0 }, + { "license", no_argument, nullptr, 0 }, + { "patch", required_argument, nullptr, 0 }, + { "source", required_argument, nullptr, 0 }, + { "target", required_argument, nullptr, 0 }, + { nullptr, 0, nullptr, 0 }, + // clang-format on + }; + + std::string check_target; + std::string source; + std::string target; + std::string patch; + std::string bonus; + + bool check_mode = false; + bool flash_mode = false; + bool patch_mode = false; + + optind = 1; + + int arg; + int option_index; + while ((arg = getopt_long(argc, argv, "", OPTIONS, &option_index)) != -1) { + switch (arg) { + case 0: { + std::string option = OPTIONS[option_index].name; + if (option == "bonus") { + bonus = optarg; + } else if (option == "check") { + check_target = optarg; + check_mode = true; + } else if (option == "flash") { + source = optarg; + flash_mode = true; + } else if (option == "license") { + return ShowLicenses(); + } else if (option == "patch") { + patch = optarg; + patch_mode = true; + } else if (option == "source") { + source = optarg; + } else if (option == "target") { + target = optarg; + } + break; + } + case '?': + default: + LOG(ERROR) << "Invalid argument"; + Usage(); + return 2; + } + } + + if (check_mode) { + return CheckMode(check_target); + } + if (flash_mode) { + if (!bonus.empty()) { + LOG(ERROR) << "bonus file not supported in flash mode"; + return 1; + } + return FlashMode(target, source); + } + if (patch_mode) { + return PatchMode(target, source, patch, bonus); + } + + Usage(); + return 2; +} diff --git a/applypatch/applypatch_modes.h b/applypatch/applypatch_modes.h new file mode 100644 index 0000000..aa60a43 --- /dev/null +++ b/applypatch/applypatch_modes.h @@ -0,0 +1,22 @@ +/* + * 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. + */ + +#ifndef _APPLYPATCH_MODES_H +#define _APPLYPATCH_MODES_H + +int applypatch_modes(int argc, char* argv[]); + +#endif // _APPLYPATCH_MODES_H diff --git a/applypatch/bspatch.cpp b/applypatch/bspatch.cpp new file mode 100644 index 0000000..ba33c3a --- /dev/null +++ b/applypatch/bspatch.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2008 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. + */ + +// This file is a nearly line-for-line copy of bspatch.c from the +// bsdiff-4.3 distribution; the primary differences being how the +// input and output data are read and the error handling. Running +// applypatch with the -l option will display the bsdiff license +// notice. + +#include +#include + +#include + +#include +#include +#include + +#include "applypatch/applypatch.h" +#include "edify/expr.h" +#include "otautil/print_sha1.h" + +void ShowBSDiffLicense() { + puts("The bsdiff library used herein is:\n" + "\n" + "Copyright 2003-2005 Colin Percival\n" + "All rights reserved\n" + "\n" + "Redistribution and use in source and binary forms, with or without\n" + "modification, are permitted providing that the following conditions\n" + "are met:\n" + "1. Redistributions of source code must retain the above copyright\n" + " notice, this list of conditions and the following disclaimer.\n" + "2. Redistributions in binary form must reproduce the above copyright\n" + " notice, this list of conditions and the following disclaimer in the\n" + " documentation and/or other materials provided with the distribution.\n" + "\n" + "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n" + "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n" + "WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n" + "ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\n" + "DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n" + "DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n" + "OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n" + "HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n" + "STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\n" + "IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n" + "POSSIBILITY OF SUCH DAMAGE.\n" + "\n------------------\n\n" + "This program uses Julian R Seward's \"libbzip2\" library, available\n" + "from http://www.bzip.org/.\n" + ); +} + +int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value& patch, + size_t patch_offset, SinkFn sink) { + CHECK_LE(patch_offset, patch.data.size()); + + int result = bsdiff::bspatch(old_data, old_size, + reinterpret_cast(&patch.data[patch_offset]), + patch.data.size() - patch_offset, sink); + if (result != 0) { + LOG(ERROR) << "bspatch failed, result: " << result; + // print SHA1 of the patch in the case of a data error. + if (result == 2) { + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast(patch.data.data() + patch_offset), + patch.data.size() - patch_offset, digest); + std::string patch_sha1 = print_sha1(digest); + LOG(ERROR) << "Patch may be corrupted, offset: " << patch_offset << ", SHA1: " << patch_sha1; + } + } + return result; +} diff --git a/applypatch/freecache.cpp b/applypatch/freecache.cpp new file mode 100644 index 0000000..3868ef2 --- /dev/null +++ b/applypatch/freecache.cpp @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2010 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "applypatch/applypatch.h" +#include "otautil/paths.h" + +static int EliminateOpenFiles(const std::string& dirname, std::set* files) { + std::unique_ptr d(opendir("/proc"), closedir); + if (!d) { + PLOG(ERROR) << "Failed to open /proc"; + return -1; + } + struct dirent* de; + while ((de = readdir(d.get())) != 0) { + unsigned int pid; + if (!android::base::ParseUint(de->d_name, &pid)) { + continue; + } + std::string path = android::base::StringPrintf("/proc/%s/fd/", de->d_name); + + struct dirent* fdde; + std::unique_ptr fdd(opendir(path.c_str()), closedir); + if (!fdd) { + PLOG(ERROR) << "Failed to open " << path; + continue; + } + while ((fdde = readdir(fdd.get())) != 0) { + std::string fd_path = path + fdde->d_name; + char link[FILENAME_MAX]; + + int count = readlink(fd_path.c_str(), link, sizeof(link)-1); + if (count >= 0) { + link[count] = '\0'; + if (android::base::StartsWith(link, dirname)) { + if (files->erase(link) > 0) { + LOG(INFO) << link << " is open by " << de->d_name; + } + } + } + } + } + return 0; +} + +static std::vector FindExpendableFiles( + const std::string& dirname, const std::function& name_filter) { + std::unique_ptr d(opendir(dirname.c_str()), closedir); + if (!d) { + PLOG(ERROR) << "Failed to open " << dirname; + return {}; + } + + // Look for regular files in the directory (not in any subdirectories). + std::set files; + struct dirent* de; + while ((de = readdir(d.get())) != 0) { + std::string path = dirname + "/" + de->d_name; + + // We can't delete cache_temp_source; if it's there we might have restarted during + // installation and could be depending on it to be there. + if (path == Paths::Get().cache_temp_source()) { + continue; + } + + // Do not delete the file if it doesn't have the expected format. + if (name_filter != nullptr && !name_filter(de->d_name)) { + continue; + } + + struct stat st; + if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) { + files.insert(path); + } + } + + LOG(INFO) << files.size() << " regular files in deletable directory"; + if (EliminateOpenFiles(dirname, &files) < 0) { + return {}; + } + + return std::vector(files.begin(), files.end()); +} + +// Parses the index of given log file, e.g. 3 for last_log.3; returns max number if the log name +// doesn't have the expected format so that we'll delete these ones first. +static unsigned int GetLogIndex(const std::string& log_name) { + if (log_name == "last_log" || log_name == "last_kmsg") { + return 0; + } + + unsigned int index; + if (sscanf(log_name.c_str(), "last_log.%u", &index) == 1 || + sscanf(log_name.c_str(), "last_kmsg.%u", &index) == 1) { + return index; + } + + return std::numeric_limits::max(); +} + +// Returns the amount of free space (in bytes) on the filesystem containing filename, or -1 on +// error. +static int64_t FreeSpaceForFile(const std::string& filename) { + struct statfs sf; + if (statfs(filename.c_str(), &sf) == -1) { + PLOG(ERROR) << "Failed to statfs " << filename; + return -1; + } + + auto f_bsize = static_cast(sf.f_bsize); + auto free_space = sf.f_bsize * sf.f_bavail; + if (f_bsize == 0 || free_space / f_bsize != static_cast(sf.f_bavail)) { + LOG(ERROR) << "Invalid block size or overflow (sf.f_bsize " << sf.f_bsize << ", sf.f_bavail " + << sf.f_bavail << ")"; + return -1; + } + return free_space; +} + +bool CheckAndFreeSpaceOnCache(size_t bytes) { +#ifndef __ANDROID__ + // TODO(xunchang): Implement a heuristic cache size check during host simulation. + LOG(WARNING) << "Skipped making (" << bytes + << ") bytes free space on /cache; program is running on host"; + return true; +#endif + + std::vector dirs{ "/cache", Paths::Get().cache_log_directory() }; + for (const auto& dirname : dirs) { + if (RemoveFilesInDirectory(bytes, dirname, FreeSpaceForFile)) { + return true; + } + } + + return false; +} + +bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname, + const std::function& space_checker) { + // The requested size cannot exceed max int64_t. + if (static_cast(bytes_needed) > + static_cast(std::numeric_limits::max())) { + LOG(ERROR) << "Invalid arg of bytes_needed: " << bytes_needed; + return false; + } + + struct stat st; + if (stat(dirname.c_str(), &st) == -1) { + PLOG(ERROR) << "Failed to stat " << dirname; + return false; + } + if (!S_ISDIR(st.st_mode)) { + LOG(ERROR) << dirname << " is not a directory"; + return false; + } + + int64_t free_now = space_checker(dirname); + if (free_now == -1) { + LOG(ERROR) << "Failed to check free space for " << dirname; + return false; + } + LOG(INFO) << free_now << " bytes free on " << dirname << " (" << bytes_needed << " needed)"; + + if (free_now >= static_cast(bytes_needed)) { + return true; + } + + std::vector files; + if (dirname == Paths::Get().cache_log_directory()) { + // Deletes the log files only. + auto log_filter = [](const std::string& file_name) { + return android::base::StartsWith(file_name, "last_log") || + android::base::StartsWith(file_name, "last_kmsg"); + }; + + files = FindExpendableFiles(dirname, log_filter); + + // Older logs will come to the top of the queue. + auto comparator = [](const std::string& name1, const std::string& name2) -> bool { + unsigned int index1 = GetLogIndex(android::base::Basename(name1)); + unsigned int index2 = GetLogIndex(android::base::Basename(name2)); + if (index1 == index2) { + return name1 < name2; + } + + return index1 > index2; + }; + + std::sort(files.begin(), files.end(), comparator); + } else { + // We're allowed to delete unopened regular files in the directory. + files = FindExpendableFiles(dirname, nullptr); + } + + for (const auto& file : files) { + if (unlink(file.c_str()) == -1) { + PLOG(ERROR) << "Failed to delete " << file; + continue; + } + + free_now = space_checker(dirname); + if (free_now == -1) { + LOG(ERROR) << "Failed to check free space for " << dirname; + return false; + } + LOG(INFO) << "Deleted " << file << "; now " << free_now << " bytes free"; + if (free_now >= static_cast(bytes_needed)) { + return true; + } + } + + return false; +} diff --git a/applypatch/imgdiff.cpp b/applypatch/imgdiff.cpp new file mode 100644 index 0000000..33ed330 --- /dev/null +++ b/applypatch/imgdiff.cpp @@ -0,0 +1,1641 @@ +/* + * 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. + */ + +/* + * This program constructs binary patches for images -- such as boot.img and recovery.img -- that + * consist primarily of large chunks of gzipped data interspersed with uncompressed data. Doing a + * naive bsdiff of these files is not useful because small changes in the data lead to large + * changes in the compressed bitstream; bsdiff patches of gzipped data are typically as large as + * the data itself. + * + * To patch these usefully, we break the source and target images up into chunks of two types: + * "normal" and "gzip". Normal chunks are simply patched using a plain bsdiff. Gzip chunks are + * first expanded, then a bsdiff is applied to the uncompressed data, then the patched data is + * gzipped using the same encoder parameters. Patched chunks are concatenated together to create + * the output file; the output image should be *exactly* the same series of bytes as the target + * image used originally to generate the patch. + * + * To work well with this tool, the gzipped sections of the target image must have been generated + * using the same deflate encoder that is available in applypatch, namely, the one in the zlib + * library. In practice this means that images should be compressed using the toybox "gzip" toy, + * not the GNU gzip program. + * + * An "imgdiff" patch consists of a header describing the chunk structure of the file and any + * encoding parameters needed for the gzipped chunks, followed by N bsdiff patches, one per chunk. + * + * For a diff to be generated, the source and target must be in well-formed zip archive format; + * or they are image files with the same "chunk" structure: that is, the same number of gzipped and + * normal chunks in the same order. Android boot and recovery images currently consist of five + * chunks: a small normal header, a gzipped kernel, a small normal section, a gzipped ramdisk, and + * finally a small normal footer. + * + * Caveats: we locate gzipped sections within the source and target images by searching for the + * byte sequence 1f8b0800: 1f8b is the gzip magic number; 08 specifies the "deflate" encoding + * [the only encoding supported by the gzip standard]; and 00 is the flags byte. We do not + * currently support any extra header fields (which would be indicated by a nonzero flags byte). + * We also don't handle the case when that byte sequence appears spuriously in the file. (Note + * that it would have to occur spuriously within a normal chunk to be a problem.) + * + * + * The imgdiff patch header looks like this: + * + * "IMGDIFF2" (8) [magic number and version] + * chunk count (4) + * for each chunk: + * chunk type (4) [CHUNK_{NORMAL, GZIP, DEFLATE, RAW}] + * if chunk type == CHUNK_NORMAL: + * source start (8) + * source len (8) + * bsdiff patch offset (8) [from start of patch file] + * if chunk type == CHUNK_GZIP: (version 1 only) + * source start (8) + * source len (8) + * bsdiff patch offset (8) [from start of patch file] + * source expanded len (8) [size of uncompressed source] + * target expected len (8) [size of uncompressed target] + * gzip level (4) + * method (4) + * windowBits (4) + * memLevel (4) + * strategy (4) + * gzip header len (4) + * gzip header (gzip header len) + * gzip footer (8) + * if chunk type == CHUNK_DEFLATE: (version 2 only) + * source start (8) + * source len (8) + * bsdiff patch offset (8) [from start of patch file] + * source expanded len (8) [size of uncompressed source] + * target expected len (8) [size of uncompressed target] + * gzip level (4) + * method (4) + * windowBits (4) + * memLevel (4) + * strategy (4) + * if chunk type == RAW: (version 2 only) + * target len (4) + * data (target len) + * + * All integers are little-endian. "source start" and "source len" specify the section of the + * input image that comprises this chunk, including the gzip header and footer for gzip chunks. + * "source expanded len" is the size of the uncompressed source data. "target expected len" is the + * size of the uncompressed data after applying the bsdiff patch. The next five parameters + * specify the zlib parameters to be used when compressing the patched data, and the next three + * specify the header and footer to be wrapped around the compressed data to create the output + * chunk (so that header contents like the timestamp are recreated exactly). + * + * After the header there are 'chunk count' bsdiff patches; the offset of each from the beginning + * of the file is specified in the header. + * + * This tool can take an optional file of "bonus data". This is an extra file of data that is + * appended to chunk #1 after it is compressed (it must be a CHUNK_DEFLATE chunk). The same file + * must be available (and passed to applypatch with -b) when applying the patch. This is used to + * reduce the size of recovery-from-boot patches by combining the boot image with recovery ramdisk + * information that is stored on the system partition. + * + * When generating the patch between two zip files, this tool has an option "--block-limit" to + * split the large source/target files into several pair of pieces, with each piece has at most + * *limit* blocks. When this option is used, we also need to output the split info into the file + * path specified by "--split-info". + * + * Format of split info file: + * 2 [version of imgdiff] + * n [count of split pieces] + * , , [size and ranges for split piece#1] + * ... + * , , [size and ranges for split piece#n] + * + * To split a pair of large zip files, we walk through the chunks in target zip and search by its + * entry_name in the source zip. If the entry_name is non-empty and a matching entry in source + * is found, we'll add the source entry to the current split source image; otherwise we'll skip + * this chunk and later do bsdiff between all the skipped trunks and the whole split source image. + * We move on to the next pair of pieces if the size of the split source image reaches the block + * limit. + * + * After the split, the target pieces are continuous and block aligned, while the source pieces + * are mutually exclusive. Some of the source blocks may not be used if there's no matching + * entry_name in the target; as a result, they won't be included in any of these split source + * images. Then we will generate patches accordingly between each split image pairs; in particular, + * the unmatched trunks in the split target will diff against the entire split source image. + * + * For example: + * Input: [src_image, tgt_image] + * Split: [src-0, tgt-0; src-1, tgt-1, src-2, tgt-2] + * Diff: [ patch-0; patch-1; patch-2] + * + * Patch: [(src-0, patch-0) = tgt-0; (src-1, patch-1) = tgt-1; (src-2, patch-2) = tgt-2] + * Concatenate: [tgt-0 + tgt-1 + tgt-2 = tgt_image] + */ + +#include "applypatch/imgdiff.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "applypatch/imgdiff_image.h" +#include "otautil/rangeset.h" + +using android::base::get_unaligned; + +static constexpr size_t VERSION = 2; + +// We assume the header "IMGDIFF#" is 8 bytes. +static_assert(VERSION <= 9, "VERSION occupies more than one byte"); + +static constexpr size_t BLOCK_SIZE = 4096; +static constexpr size_t BUFFER_SIZE = 0x8000; + +// If we use this function to write the offset and length (type size_t), their values should not +// exceed 2^63; because the signed bit will be casted away. +static inline bool Write8(int fd, int64_t value) { + return android::base::WriteFully(fd, &value, sizeof(int64_t)); +} + +// Similarly, the value should not exceed 2^31 if we are casting from size_t (e.g. target chunk +// size). +static inline bool Write4(int fd, int32_t value) { + return android::base::WriteFully(fd, &value, sizeof(int32_t)); +} + +// Trim the head or tail to align with the block size. Return false if the chunk has nothing left +// after alignment. +static bool AlignHead(size_t* start, size_t* length) { + size_t residual = (*start % BLOCK_SIZE == 0) ? 0 : BLOCK_SIZE - *start % BLOCK_SIZE; + + if (*length <= residual) { + *length = 0; + return false; + } + + // Trim the data in the beginning. + *start += residual; + *length -= residual; + return true; +} + +static bool AlignTail(size_t* start, size_t* length) { + size_t residual = (*start + *length) % BLOCK_SIZE; + if (*length <= residual) { + *length = 0; + return false; + } + + // Trim the data in the end. + *length -= residual; + return true; +} + +// Remove the used blocks from the source chunk to make sure the source ranges are mutually +// exclusive after split. Return false if we fail to get the non-overlapped ranges. In such +// a case, we'll skip the entire source chunk. +static bool RemoveUsedBlocks(size_t* start, size_t* length, const SortedRangeSet& used_ranges) { + if (!used_ranges.Overlaps(*start, *length)) { + return true; + } + + // TODO find the largest non-overlap chunk. + LOG(INFO) << "Removing block " << used_ranges.ToString() << " from " << *start << " - " + << *start + *length - 1; + + // If there's no duplicate entry name, we should only overlap in the head or tail block. Try to + // trim both blocks. Skip this source chunk in case it still overlaps with the used ranges. + if (AlignHead(start, length) && !used_ranges.Overlaps(*start, *length)) { + return true; + } + if (AlignTail(start, length) && !used_ranges.Overlaps(*start, *length)) { + return true; + } + + LOG(WARNING) << "Failed to remove the overlapped block ranges; skip the source"; + return false; +} + +static const struct option OPTIONS[] = { + { "zip-mode", no_argument, nullptr, 'z' }, + { "bonus-file", required_argument, nullptr, 'b' }, + { "block-limit", required_argument, nullptr, 0 }, + { "debug-dir", required_argument, nullptr, 0 }, + { "split-info", required_argument, nullptr, 0 }, + { "verbose", no_argument, nullptr, 'v' }, + { nullptr, 0, nullptr, 0 }, +}; + +ImageChunk::ImageChunk(int type, size_t start, const std::vector* file_content, + size_t raw_data_len, std::string entry_name) + : type_(type), + start_(start), + input_file_ptr_(file_content), + raw_data_len_(raw_data_len), + compress_level_(6), + entry_name_(std::move(entry_name)) { + CHECK(file_content != nullptr) << "input file container can't be nullptr"; +} + +const uint8_t* ImageChunk::GetRawData() const { + CHECK_LE(start_ + raw_data_len_, input_file_ptr_->size()); + return input_file_ptr_->data() + start_; +} + +const uint8_t * ImageChunk::DataForPatch() const { + if (type_ == CHUNK_DEFLATE) { + return uncompressed_data_.data(); + } + return GetRawData(); +} + +size_t ImageChunk::DataLengthForPatch() const { + if (type_ == CHUNK_DEFLATE) { + return uncompressed_data_.size(); + } + return raw_data_len_; +} + +void ImageChunk::Dump(size_t index) const { + LOG(INFO) << "chunk: " << index << ", type: " << type_ << ", start: " << start_ + << ", len: " << DataLengthForPatch() << ", name: " << entry_name_; +} + +bool ImageChunk::operator==(const ImageChunk& other) const { + if (type_ != other.type_) { + return false; + } + return (raw_data_len_ == other.raw_data_len_ && + memcmp(GetRawData(), other.GetRawData(), raw_data_len_) == 0); +} + +void ImageChunk::SetUncompressedData(std::vector data) { + uncompressed_data_ = std::move(data); +} + +bool ImageChunk::SetBonusData(const std::vector& bonus_data) { + if (type_ != CHUNK_DEFLATE) { + return false; + } + uncompressed_data_.insert(uncompressed_data_.end(), bonus_data.begin(), bonus_data.end()); + return true; +} + +void ImageChunk::ChangeDeflateChunkToNormal() { + if (type_ != CHUNK_DEFLATE) return; + type_ = CHUNK_NORMAL; + // No need to clear the entry name. + uncompressed_data_.clear(); +} + +bool ImageChunk::IsAdjacentNormal(const ImageChunk& other) const { + if (type_ != CHUNK_NORMAL || other.type_ != CHUNK_NORMAL) { + return false; + } + return (other.start_ == start_ + raw_data_len_); +} + +void ImageChunk::MergeAdjacentNormal(const ImageChunk& other) { + CHECK(IsAdjacentNormal(other)); + raw_data_len_ = raw_data_len_ + other.raw_data_len_; +} + +bool ImageChunk::MakePatch(const ImageChunk& tgt, const ImageChunk& src, + std::vector* patch_data, + bsdiff::SuffixArrayIndexInterface** bsdiff_cache) { +#if defined(__ANDROID__) + char ptemp[] = "/data/local/tmp/imgdiff-patch-XXXXXX"; +#else + char ptemp[] = "/tmp/imgdiff-patch-XXXXXX"; +#endif + + int fd = mkstemp(ptemp); + if (fd == -1) { + PLOG(ERROR) << "MakePatch failed to create a temporary file"; + return false; + } + close(fd); + + int r = bsdiff::bsdiff(src.DataForPatch(), src.DataLengthForPatch(), tgt.DataForPatch(), + tgt.DataLengthForPatch(), ptemp, bsdiff_cache); + if (r != 0) { + LOG(ERROR) << "bsdiff() failed: " << r; + return false; + } + + android::base::unique_fd patch_fd(open(ptemp, O_RDONLY)); + if (patch_fd == -1) { + PLOG(ERROR) << "Failed to open " << ptemp; + return false; + } + struct stat st; + if (fstat(patch_fd, &st) != 0) { + PLOG(ERROR) << "Failed to stat patch file " << ptemp; + return false; + } + + size_t sz = static_cast(st.st_size); + + patch_data->resize(sz); + if (!android::base::ReadFully(patch_fd, patch_data->data(), sz)) { + PLOG(ERROR) << "Failed to read " << ptemp; + unlink(ptemp); + return false; + } + + unlink(ptemp); + + return true; +} + +bool ImageChunk::ReconstructDeflateChunk() { + if (type_ != CHUNK_DEFLATE) { + LOG(ERROR) << "Attempted to reconstruct non-deflate chunk"; + return false; + } + + // We only check two combinations of encoder parameters: level 6 (the default) and level 9 + // (the maximum). + for (int level = 6; level <= 9; level += 3) { + if (TryReconstruction(level)) { + compress_level_ = level; + return true; + } + } + + return false; +} + +/* + * Takes the uncompressed data stored in the chunk, compresses it using the zlib parameters stored + * in the chunk, and checks that it matches exactly the compressed data we started with (also + * stored in the chunk). + */ +bool ImageChunk::TryReconstruction(int level) { + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = uncompressed_data_.size(); + strm.next_in = uncompressed_data_.data(); + int ret = deflateInit2(&strm, level, METHOD, WINDOWBITS, MEMLEVEL, STRATEGY); + if (ret < 0) { + LOG(ERROR) << "Failed to initialize deflate: " << ret; + return false; + } + + std::vector buffer(BUFFER_SIZE); + size_t offset = 0; + do { + strm.avail_out = buffer.size(); + strm.next_out = buffer.data(); + ret = deflate(&strm, Z_FINISH); + if (ret < 0) { + LOG(ERROR) << "Failed to deflate: " << ret; + return false; + } + + size_t compressed_size = buffer.size() - strm.avail_out; + if (memcmp(buffer.data(), input_file_ptr_->data() + start_ + offset, compressed_size) != 0) { + // mismatch; data isn't the same. + deflateEnd(&strm); + return false; + } + offset += compressed_size; + } while (ret != Z_STREAM_END); + deflateEnd(&strm); + + if (offset != raw_data_len_) { + // mismatch; ran out of data before we should have. + return false; + } + return true; +} + +PatchChunk::PatchChunk(const ImageChunk& tgt, const ImageChunk& src, std::vector data) + : type_(tgt.GetType()), + source_start_(src.GetStartOffset()), + source_len_(src.GetRawDataLength()), + source_uncompressed_len_(src.DataLengthForPatch()), + target_start_(tgt.GetStartOffset()), + target_len_(tgt.GetRawDataLength()), + target_uncompressed_len_(tgt.DataLengthForPatch()), + target_compress_level_(tgt.GetCompressLevel()), + data_(std::move(data)) {} + +// Construct a CHUNK_RAW patch from the target data directly. +PatchChunk::PatchChunk(const ImageChunk& tgt) + : type_(CHUNK_RAW), + source_start_(0), + source_len_(0), + source_uncompressed_len_(0), + target_start_(tgt.GetStartOffset()), + target_len_(tgt.GetRawDataLength()), + target_uncompressed_len_(tgt.DataLengthForPatch()), + target_compress_level_(tgt.GetCompressLevel()), + data_(tgt.GetRawData(), tgt.GetRawData() + tgt.GetRawDataLength()) {} + +// Return true if raw data is smaller than the patch size. +bool PatchChunk::RawDataIsSmaller(const ImageChunk& tgt, size_t patch_size) { + size_t target_len = tgt.GetRawDataLength(); + return target_len < patch_size || (tgt.GetType() == CHUNK_NORMAL && target_len <= 160); +} + +void PatchChunk::UpdateSourceOffset(const SortedRangeSet& src_range) { + if (type_ == CHUNK_DEFLATE) { + source_start_ = src_range.GetOffsetInRangeSet(source_start_); + } +} + +// Header size: +// header_type 4 bytes +// CHUNK_NORMAL 8*3 = 24 bytes +// CHUNK_DEFLATE 8*5 + 4*5 = 60 bytes +// CHUNK_RAW 4 bytes + patch_size +size_t PatchChunk::GetHeaderSize() const { + switch (type_) { + case CHUNK_NORMAL: + return 4 + 8 * 3; + case CHUNK_DEFLATE: + return 4 + 8 * 5 + 4 * 5; + case CHUNK_RAW: + return 4 + 4 + data_.size(); + default: + CHECK(false) << "unexpected chunk type: " << type_; // Should not reach here. + return 0; + } +} + +// Return the offset of the next patch into the patch data. +size_t PatchChunk::WriteHeaderToFd(int fd, size_t offset, size_t index) const { + Write4(fd, type_); + switch (type_) { + case CHUNK_NORMAL: + LOG(INFO) << android::base::StringPrintf("chunk %zu: normal (%10zu, %10zu) %10zu", index, + target_start_, target_len_, data_.size()); + Write8(fd, static_cast(source_start_)); + Write8(fd, static_cast(source_len_)); + Write8(fd, static_cast(offset)); + return offset + data_.size(); + case CHUNK_DEFLATE: + LOG(INFO) << android::base::StringPrintf("chunk %zu: deflate (%10zu, %10zu) %10zu", index, + target_start_, target_len_, data_.size()); + Write8(fd, static_cast(source_start_)); + Write8(fd, static_cast(source_len_)); + Write8(fd, static_cast(offset)); + Write8(fd, static_cast(source_uncompressed_len_)); + Write8(fd, static_cast(target_uncompressed_len_)); + Write4(fd, target_compress_level_); + Write4(fd, ImageChunk::METHOD); + Write4(fd, ImageChunk::WINDOWBITS); + Write4(fd, ImageChunk::MEMLEVEL); + Write4(fd, ImageChunk::STRATEGY); + return offset + data_.size(); + case CHUNK_RAW: + LOG(INFO) << android::base::StringPrintf("chunk %zu: raw (%10zu, %10zu)", index, + target_start_, target_len_); + Write4(fd, static_cast(data_.size())); + if (!android::base::WriteFully(fd, data_.data(), data_.size())) { + CHECK(false) << "Failed to write " << data_.size() << " bytes patch"; + } + return offset; + default: + CHECK(false) << "unexpected chunk type: " << type_; + return offset; + } +} + +size_t PatchChunk::PatchSize() const { + if (type_ == CHUNK_RAW) { + return GetHeaderSize(); + } + return GetHeaderSize() + data_.size(); +} + +// Write the contents of |patch_chunks| to |patch_fd|. +bool PatchChunk::WritePatchDataToFd(const std::vector& patch_chunks, int patch_fd) { + // Figure out how big the imgdiff file header is going to be, so that we can correctly compute + // the offset of each bsdiff patch within the file. + size_t total_header_size = 12; + for (const auto& patch : patch_chunks) { + total_header_size += patch.GetHeaderSize(); + } + + size_t offset = total_header_size; + + // Write out the headers. + if (!android::base::WriteStringToFd("IMGDIFF" + std::to_string(VERSION), patch_fd)) { + PLOG(ERROR) << "Failed to write \"IMGDIFF" << VERSION << "\""; + return false; + } + + Write4(patch_fd, static_cast(patch_chunks.size())); + LOG(INFO) << "Writing " << patch_chunks.size() << " patch headers..."; + for (size_t i = 0; i < patch_chunks.size(); ++i) { + offset = patch_chunks[i].WriteHeaderToFd(patch_fd, offset, i); + } + + // Append each chunk's bsdiff patch, in order. + for (const auto& patch : patch_chunks) { + if (patch.type_ == CHUNK_RAW) { + continue; + } + if (!android::base::WriteFully(patch_fd, patch.data_.data(), patch.data_.size())) { + PLOG(ERROR) << "Failed to write " << patch.data_.size() << " bytes patch to patch_fd"; + return false; + } + } + + return true; +} + +ImageChunk& Image::operator[](size_t i) { + CHECK_LT(i, chunks_.size()); + return chunks_[i]; +} + +const ImageChunk& Image::operator[](size_t i) const { + CHECK_LT(i, chunks_.size()); + return chunks_[i]; +} + +void Image::MergeAdjacentNormalChunks() { + size_t merged_last = 0, cur = 0; + while (cur < chunks_.size()) { + // Look for normal chunks adjacent to the current one. If such chunk exists, extend the + // length of the current normal chunk. + size_t to_check = cur + 1; + while (to_check < chunks_.size() && chunks_[cur].IsAdjacentNormal(chunks_[to_check])) { + chunks_[cur].MergeAdjacentNormal(chunks_[to_check]); + to_check++; + } + + if (merged_last != cur) { + chunks_[merged_last] = std::move(chunks_[cur]); + } + merged_last++; + cur = to_check; + } + if (merged_last < chunks_.size()) { + chunks_.erase(chunks_.begin() + merged_last, chunks_.end()); + } +} + +void Image::DumpChunks() const { + std::string type = is_source_ ? "source" : "target"; + LOG(INFO) << "Dumping chunks for " << type; + for (size_t i = 0; i < chunks_.size(); ++i) { + chunks_[i].Dump(i); + } +} + +bool Image::ReadFile(const std::string& filename, std::vector* file_content) { + CHECK(file_content != nullptr); + + android::base::unique_fd fd(open(filename.c_str(), O_RDONLY)); + if (fd == -1) { + PLOG(ERROR) << "Failed to open " << filename; + return false; + } + struct stat st; + if (fstat(fd, &st) != 0) { + PLOG(ERROR) << "Failed to stat " << filename; + return false; + } + + size_t sz = static_cast(st.st_size); + file_content->resize(sz); + if (!android::base::ReadFully(fd, file_content->data(), sz)) { + PLOG(ERROR) << "Failed to read " << filename; + return false; + } + fd.reset(); + + return true; +} + +bool ZipModeImage::Initialize(const std::string& filename) { + if (!ReadFile(filename, &file_content_)) { + return false; + } + + // Omit the trailing zeros before we pass the file to ziparchive handler. + size_t zipfile_size; + if (!GetZipFileSize(&zipfile_size)) { + LOG(ERROR) << "Failed to parse the actual size of " << filename; + return false; + } + ZipArchiveHandle handle; + int err = OpenArchiveFromMemory(const_cast(file_content_.data()), zipfile_size, + filename.c_str(), &handle); + if (err != 0) { + LOG(ERROR) << "Failed to open zip file " << filename << ": " << ErrorCodeString(err); + CloseArchive(handle); + return false; + } + + if (!InitializeChunks(filename, handle)) { + CloseArchive(handle); + return false; + } + + CloseArchive(handle); + return true; +} + +// Iterate the zip entries and compose the image chunks accordingly. +bool ZipModeImage::InitializeChunks(const std::string& filename, ZipArchiveHandle handle) { + void* cookie; + int ret = StartIteration(handle, &cookie); + if (ret != 0) { + LOG(ERROR) << "Failed to iterate over entries in " << filename << ": " << ErrorCodeString(ret); + return false; + } + + // Create a list of deflated zip entries, sorted by offset. + std::vector> temp_entries; + std::string name; + ZipEntry64 entry; + while ((ret = Next(cookie, &entry, &name)) == 0) { + if (entry.method == kCompressDeflated || limit_ > 0) { + temp_entries.emplace_back(name, entry); + } + } + + if (ret != -1) { + LOG(ERROR) << "Error while iterating over zip entries: " << ErrorCodeString(ret); + return false; + } + std::sort(temp_entries.begin(), temp_entries.end(), + [](auto& entry1, auto& entry2) { return entry1.second.offset < entry2.second.offset; }); + + EndIteration(cookie); + + // For source chunks, we don't need to compose chunks for the metadata. + if (is_source_) { + for (auto& entry : temp_entries) { + if (!AddZipEntryToChunks(handle, entry.first, &entry.second)) { + LOG(ERROR) << "Failed to add " << entry.first << " to source chunks"; + return false; + } + } + + // Add the end of zip file (mainly central directory) as a normal chunk. + size_t entries_end = 0; + if (!temp_entries.empty()) { + CHECK_GE(temp_entries.back().second.offset, 0); + if (__builtin_add_overflow(temp_entries.back().second.offset, + temp_entries.back().second.compressed_length, &entries_end)) { + LOG(ERROR) << "`entries_end` overflows on entry with offset " + << temp_entries.back().second.offset << " and compressed_length " + << temp_entries.back().second.compressed_length; + return false; + } + } + CHECK_LT(entries_end, file_content_.size()); + chunks_.emplace_back(CHUNK_NORMAL, entries_end, &file_content_, + file_content_.size() - entries_end); + + return true; + } + + // For target chunks, add the deflate entries as CHUNK_DEFLATE and the contents between two + // deflate entries as CHUNK_NORMAL. + size_t pos = 0; + size_t nextentry = 0; + while (pos < file_content_.size()) { + if (nextentry < temp_entries.size() && + static_cast(pos) == temp_entries[nextentry].second.offset) { + // Add the next zip entry. + std::string entry_name = temp_entries[nextentry].first; + if (!AddZipEntryToChunks(handle, entry_name, &temp_entries[nextentry].second)) { + LOG(ERROR) << "Failed to add " << entry_name << " to target chunks"; + return false; + } + if (temp_entries[nextentry].second.compressed_length > std::numeric_limits::max()) { + LOG(ERROR) << "Entry " << name << " compressed size exceeds size of address space. " + << entry.compressed_length; + return false; + } + if (__builtin_add_overflow(pos, temp_entries[nextentry].second.compressed_length, &pos)) { + LOG(ERROR) << "`pos` overflows after adding " + << temp_entries[nextentry].second.compressed_length; + return false; + } + ++nextentry; + continue; + } + + // Use a normal chunk to take all the data up to the start of the next entry. + size_t raw_data_len; + if (nextentry < temp_entries.size()) { + raw_data_len = temp_entries[nextentry].second.offset - pos; + } else { + raw_data_len = file_content_.size() - pos; + } + chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, raw_data_len); + + pos += raw_data_len; + } + + return true; +} + +bool ZipModeImage::AddZipEntryToChunks(ZipArchiveHandle handle, const std::string& entry_name, + ZipEntry64* entry) { + if (entry->compressed_length > std::numeric_limits::max()) { + LOG(ERROR) << "Failed to add " << entry_name + << " because's compressed size exceeds size of address space. " + << entry->compressed_length; + return false; + } + size_t compressed_len = entry->compressed_length; + if (compressed_len == 0) return true; + + // Split the entry into several normal chunks if it's too large. + if (limit_ > 0 && compressed_len > limit_) { + int count = 0; + while (compressed_len > 0) { + size_t length = std::min(limit_, compressed_len); + std::string name = entry_name + "-" + std::to_string(count); + chunks_.emplace_back(CHUNK_NORMAL, entry->offset + limit_ * count, &file_content_, length, + name); + + count++; + compressed_len -= length; + } + } else if (entry->method == kCompressDeflated) { + size_t uncompressed_len = entry->uncompressed_length; + if (uncompressed_len > std::numeric_limits::max()) { + LOG(ERROR) << "Failed to add " << entry_name + << " because's compressed size exceeds size of address space. " + << uncompressed_len; + return false; + } + std::vector uncompressed_data(uncompressed_len); + int ret = ExtractToMemory(handle, entry, uncompressed_data.data(), uncompressed_len); + if (ret != 0) { + LOG(ERROR) << "Failed to extract " << entry_name << " with size " << uncompressed_len << ": " + << ErrorCodeString(ret); + return false; + } + ImageChunk curr(CHUNK_DEFLATE, entry->offset, &file_content_, compressed_len, entry_name); + curr.SetUncompressedData(std::move(uncompressed_data)); + chunks_.push_back(std::move(curr)); + } else { + chunks_.emplace_back(CHUNK_NORMAL, entry->offset, &file_content_, compressed_len, entry_name); + } + + return true; +} + +// EOCD record +// offset 0: signature 0x06054b50, 4 bytes +// offset 4: number of this disk, 2 bytes +// ... +// offset 20: comment length, 2 bytes +// offset 22: comment, n bytes +bool ZipModeImage::GetZipFileSize(size_t* input_file_size) { + if (file_content_.size() < 22) { + LOG(ERROR) << "File is too small to be a zip file"; + return false; + } + + // Look for End of central directory record of the zip file, and calculate the actual + // zip_file size. + for (int i = file_content_.size() - 22; i >= 0; i--) { + if (file_content_[i] == 0x50) { + if (get_unaligned(&file_content_[i]) == 0x06054b50) { + // double-check: this archive consists of a single "disk". + CHECK_EQ(get_unaligned(&file_content_[i + 4]), 0); + + uint16_t comment_length = get_unaligned(&file_content_[i + 20]); + size_t file_size = i + 22 + comment_length; + CHECK_LE(file_size, file_content_.size()); + *input_file_size = file_size; + return true; + } + } + } + + // EOCD not found, this file is likely not a valid zip file. + return false; +} + +ImageChunk ZipModeImage::PseudoSource() const { + CHECK(is_source_); + return ImageChunk(CHUNK_NORMAL, 0, &file_content_, file_content_.size()); +} + +const ImageChunk* ZipModeImage::FindChunkByName(const std::string& name, bool find_normal) const { + if (name.empty()) { + return nullptr; + } + for (auto& chunk : chunks_) { + if (chunk.GetType() != CHUNK_DEFLATE && !find_normal) { + continue; + } + + if (chunk.GetEntryName() == name) { + return &chunk; + } + + // Edge case when target chunk is split due to size limit but source chunk isn't. + if (name == (chunk.GetEntryName() + "-0") || chunk.GetEntryName() == (name + "-0")) { + return &chunk; + } + + // TODO handle the .so files with incremental version number. + // (e.g. lib/arm64-v8a/libcronet.59.0.3050.4.so) + } + + return nullptr; +} + +ImageChunk* ZipModeImage::FindChunkByName(const std::string& name, bool find_normal) { + return const_cast( + static_cast(this)->FindChunkByName(name, find_normal)); +} + +bool ZipModeImage::CheckAndProcessChunks(ZipModeImage* tgt_image, ZipModeImage* src_image) { + for (auto& tgt_chunk : *tgt_image) { + if (tgt_chunk.GetType() != CHUNK_DEFLATE) { + continue; + } + + ImageChunk* src_chunk = src_image->FindChunkByName(tgt_chunk.GetEntryName()); + if (src_chunk == nullptr) { + tgt_chunk.ChangeDeflateChunkToNormal(); + } else if (tgt_chunk == *src_chunk) { + // If two deflate chunks are identical (eg, the kernel has not changed between two builds), + // treat them as normal chunks. This makes applypatch much faster -- it can apply a trivial + // patch to the compressed data, rather than uncompressing and recompressing to apply the + // trivial patch to the uncompressed data. + tgt_chunk.ChangeDeflateChunkToNormal(); + src_chunk->ChangeDeflateChunkToNormal(); + } else if (!tgt_chunk.ReconstructDeflateChunk()) { + // We cannot recompress the data and get exactly the same bits as are in the input target + // image. Treat the chunk as a normal non-deflated chunk. + LOG(WARNING) << "Failed to reconstruct target deflate chunk [" << tgt_chunk.GetEntryName() + << "]; treating as normal"; + + tgt_chunk.ChangeDeflateChunkToNormal(); + src_chunk->ChangeDeflateChunkToNormal(); + } + } + + // For zips, we only need merge normal chunks for the target: deflated chunks are matched via + // filename, and normal chunks are patched using the entire source file as the source. + if (tgt_image->limit_ == 0) { + tgt_image->MergeAdjacentNormalChunks(); + tgt_image->DumpChunks(); + } + + return true; +} + +// For each target chunk, look for the corresponding source chunk by the zip_entry name. If +// found, add the range of this chunk in the original source file to the block aligned source +// ranges. Construct the split src & tgt image once the size of source range reaches limit. +bool ZipModeImage::SplitZipModeImageWithLimit(const ZipModeImage& tgt_image, + const ZipModeImage& src_image, + std::vector* split_tgt_images, + std::vector* split_src_images, + std::vector* split_src_ranges) { + CHECK_EQ(tgt_image.limit_, src_image.limit_); + size_t limit = tgt_image.limit_; + + src_image.DumpChunks(); + LOG(INFO) << "Splitting " << tgt_image.NumOfChunks() << " tgt chunks..."; + + SortedRangeSet used_src_ranges; // ranges used for previous split source images. + + // Reserve the central directory in advance for the last split image. + const auto& central_directory = src_image.cend() - 1; + CHECK_EQ(CHUNK_NORMAL, central_directory->GetType()); + used_src_ranges.Insert(central_directory->GetStartOffset(), + central_directory->DataLengthForPatch()); + + SortedRangeSet src_ranges; + std::vector split_src_chunks; + std::vector split_tgt_chunks; + for (auto tgt = tgt_image.cbegin(); tgt != tgt_image.cend(); tgt++) { + const ImageChunk* src = src_image.FindChunkByName(tgt->GetEntryName(), true); + if (src == nullptr) { + split_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt->GetStartOffset(), &tgt_image.file_content_, + tgt->GetRawDataLength()); + continue; + } + + size_t src_offset = src->GetStartOffset(); + size_t src_length = src->GetRawDataLength(); + + CHECK(src_length > 0); + CHECK_LE(src_length, limit); + + // Make sure this source range hasn't been used before so that the src_range pieces don't + // overlap with each other. + if (!RemoveUsedBlocks(&src_offset, &src_length, used_src_ranges)) { + split_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt->GetStartOffset(), &tgt_image.file_content_, + tgt->GetRawDataLength()); + } else if (src_ranges.blocks() * BLOCK_SIZE + src_length <= limit) { + src_ranges.Insert(src_offset, src_length); + + // Add the deflate source chunk if it hasn't been aligned. + if (src->GetType() == CHUNK_DEFLATE && src_length == src->GetRawDataLength()) { + split_src_chunks.push_back(*src); + split_tgt_chunks.push_back(*tgt); + } else { + // TODO split smarter to avoid alignment of large deflate chunks + split_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt->GetStartOffset(), &tgt_image.file_content_, + tgt->GetRawDataLength()); + } + } else { + bool added_image = ZipModeImage::AddSplitImageFromChunkList( + tgt_image, src_image, src_ranges, split_tgt_chunks, split_src_chunks, split_tgt_images, + split_src_images); + + split_tgt_chunks.clear(); + split_src_chunks.clear(); + // No need to update the split_src_ranges if we don't update the split source images. + if (added_image) { + used_src_ranges.Insert(src_ranges); + split_src_ranges->push_back(std::move(src_ranges)); + } + src_ranges = {}; + + // We don't have enough space for the current chunk; start a new split image and handle + // this chunk there. + tgt--; + } + } + + // TODO Trim it in case the CD exceeds limit too much. + src_ranges.Insert(central_directory->GetStartOffset(), central_directory->DataLengthForPatch()); + bool added_image = ZipModeImage::AddSplitImageFromChunkList(tgt_image, src_image, src_ranges, + split_tgt_chunks, split_src_chunks, + split_tgt_images, split_src_images); + if (added_image) { + split_src_ranges->push_back(std::move(src_ranges)); + } + + ValidateSplitImages(*split_tgt_images, *split_src_images, *split_src_ranges, + tgt_image.file_content_.size()); + + return true; +} + +bool ZipModeImage::AddSplitImageFromChunkList(const ZipModeImage& tgt_image, + const ZipModeImage& src_image, + const SortedRangeSet& split_src_ranges, + const std::vector& split_tgt_chunks, + const std::vector& split_src_chunks, + std::vector* split_tgt_images, + std::vector* split_src_images) { + CHECK(!split_tgt_chunks.empty()); + + std::vector aligned_tgt_chunks; + + // Align the target chunks in the beginning with BLOCK_SIZE. + size_t i = 0; + while (i < split_tgt_chunks.size()) { + size_t tgt_start = split_tgt_chunks[i].GetStartOffset(); + size_t tgt_length = split_tgt_chunks[i].GetRawDataLength(); + + // Current ImageChunk is long enough to align. + if (AlignHead(&tgt_start, &tgt_length)) { + aligned_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt_start, &tgt_image.file_content_, + tgt_length); + break; + } + + i++; + } + + // Nothing left after alignment in the current split tgt chunks; skip adding the split_tgt_image. + if (i == split_tgt_chunks.size()) { + return false; + } + + aligned_tgt_chunks.insert(aligned_tgt_chunks.end(), split_tgt_chunks.begin() + i + 1, + split_tgt_chunks.end()); + CHECK(!aligned_tgt_chunks.empty()); + + // Add a normal chunk to align the contents in the end. + size_t end_offset = + aligned_tgt_chunks.back().GetStartOffset() + aligned_tgt_chunks.back().GetRawDataLength(); + if (end_offset % BLOCK_SIZE != 0 && end_offset < tgt_image.file_content_.size()) { + size_t tail_block_length = std::min(tgt_image.file_content_.size() - end_offset, + BLOCK_SIZE - (end_offset % BLOCK_SIZE)); + aligned_tgt_chunks.emplace_back(CHUNK_NORMAL, end_offset, &tgt_image.file_content_, + tail_block_length); + } + + ZipModeImage split_tgt_image(false); + split_tgt_image.Initialize(aligned_tgt_chunks, {}); + split_tgt_image.MergeAdjacentNormalChunks(); + + // Construct the split source file based on the split src ranges. + std::vector split_src_content; + for (const auto& r : split_src_ranges) { + size_t end = std::min(src_image.file_content_.size(), r.second * BLOCK_SIZE); + split_src_content.insert(split_src_content.end(), + src_image.file_content_.begin() + r.first * BLOCK_SIZE, + src_image.file_content_.begin() + end); + } + + // We should not have an empty src in our design; otherwise we will encounter an error in + // bsdiff since split_src_content.data() == nullptr. + CHECK(!split_src_content.empty()); + + ZipModeImage split_src_image(true); + split_src_image.Initialize(split_src_chunks, split_src_content); + + split_tgt_images->push_back(std::move(split_tgt_image)); + split_src_images->push_back(std::move(split_src_image)); + + return true; +} + +void ZipModeImage::ValidateSplitImages(const std::vector& split_tgt_images, + const std::vector& split_src_images, + std::vector& split_src_ranges, + size_t total_tgt_size) { + CHECK_EQ(split_tgt_images.size(), split_src_images.size()); + + LOG(INFO) << "Validating " << split_tgt_images.size() << " images"; + + // Verify that the target image pieces is continuous and can add up to the total size. + size_t last_offset = 0; + for (const auto& tgt_image : split_tgt_images) { + CHECK(!tgt_image.chunks_.empty()); + + CHECK_EQ(last_offset, tgt_image.chunks_.front().GetStartOffset()); + CHECK(last_offset % BLOCK_SIZE == 0); + + // Check the target chunks within the split image are continuous. + for (const auto& chunk : tgt_image.chunks_) { + CHECK_EQ(last_offset, chunk.GetStartOffset()); + last_offset += chunk.GetRawDataLength(); + } + } + CHECK_EQ(total_tgt_size, last_offset); + + // Verify that the source ranges are mutually exclusive. + CHECK_EQ(split_src_images.size(), split_src_ranges.size()); + SortedRangeSet used_src_ranges; + for (size_t i = 0; i < split_src_ranges.size(); i++) { + CHECK(!used_src_ranges.Overlaps(split_src_ranges[i])) + << "src range " << split_src_ranges[i].ToString() << " overlaps " + << used_src_ranges.ToString(); + used_src_ranges.Insert(split_src_ranges[i]); + } +} + +bool ZipModeImage::GeneratePatchesInternal(const ZipModeImage& tgt_image, + const ZipModeImage& src_image, + std::vector* patch_chunks) { + LOG(INFO) << "Constructing patches for " << tgt_image.NumOfChunks() << " chunks..."; + patch_chunks->clear(); + + bsdiff::SuffixArrayIndexInterface* bsdiff_cache = nullptr; + for (size_t i = 0; i < tgt_image.NumOfChunks(); i++) { + const auto& tgt_chunk = tgt_image[i]; + + if (PatchChunk::RawDataIsSmaller(tgt_chunk, 0)) { + patch_chunks->emplace_back(tgt_chunk); + continue; + } + + const ImageChunk* src_chunk = (tgt_chunk.GetType() != CHUNK_DEFLATE) + ? nullptr + : src_image.FindChunkByName(tgt_chunk.GetEntryName()); + + const auto& src_ref = (src_chunk == nullptr) ? src_image.PseudoSource() : *src_chunk; + bsdiff::SuffixArrayIndexInterface** bsdiff_cache_ptr = + (src_chunk == nullptr) ? &bsdiff_cache : nullptr; + + std::vector patch_data; + if (!ImageChunk::MakePatch(tgt_chunk, src_ref, &patch_data, bsdiff_cache_ptr)) { + LOG(ERROR) << "Failed to generate patch, name: " << tgt_chunk.GetEntryName(); + return false; + } + + LOG(INFO) << "patch " << i << " is " << patch_data.size() << " bytes (of " + << tgt_chunk.GetRawDataLength() << ")"; + + if (PatchChunk::RawDataIsSmaller(tgt_chunk, patch_data.size())) { + patch_chunks->emplace_back(tgt_chunk); + } else { + patch_chunks->emplace_back(tgt_chunk, src_ref, std::move(patch_data)); + } + } + delete bsdiff_cache; + + CHECK_EQ(patch_chunks->size(), tgt_image.NumOfChunks()); + return true; +} + +bool ZipModeImage::GeneratePatches(const ZipModeImage& tgt_image, const ZipModeImage& src_image, + const std::string& patch_name) { + std::vector patch_chunks; + + ZipModeImage::GeneratePatchesInternal(tgt_image, src_image, &patch_chunks); + + CHECK_EQ(tgt_image.NumOfChunks(), patch_chunks.size()); + + android::base::unique_fd patch_fd( + open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR)); + if (patch_fd == -1) { + PLOG(ERROR) << "Failed to open " << patch_name; + return false; + } + + return PatchChunk::WritePatchDataToFd(patch_chunks, patch_fd); +} + +bool ZipModeImage::GeneratePatches(const std::vector& split_tgt_images, + const std::vector& split_src_images, + const std::vector& split_src_ranges, + const std::string& patch_name, + const std::string& split_info_file, + const std::string& debug_dir) { + LOG(INFO) << "Constructing patches for " << split_tgt_images.size() << " split images..."; + + android::base::unique_fd patch_fd( + open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR)); + if (patch_fd == -1) { + PLOG(ERROR) << "Failed to open " << patch_name; + return false; + } + + std::vector split_info_list; + for (size_t i = 0; i < split_tgt_images.size(); i++) { + std::vector patch_chunks; + if (!ZipModeImage::GeneratePatchesInternal(split_tgt_images[i], split_src_images[i], + &patch_chunks)) { + LOG(ERROR) << "Failed to generate split patch"; + return false; + } + + size_t total_patch_size = 12; + for (auto& p : patch_chunks) { + p.UpdateSourceOffset(split_src_ranges[i]); + total_patch_size += p.PatchSize(); + } + + if (!PatchChunk::WritePatchDataToFd(patch_chunks, patch_fd)) { + return false; + } + + size_t split_tgt_size = split_tgt_images[i].chunks_.back().GetStartOffset() + + split_tgt_images[i].chunks_.back().GetRawDataLength() - + split_tgt_images[i].chunks_.front().GetStartOffset(); + std::string split_info = android::base::StringPrintf( + "%zu %zu %s", total_patch_size, split_tgt_size, split_src_ranges[i].ToString().c_str()); + split_info_list.push_back(split_info); + + // Write the split source & patch into the debug directory. + if (!debug_dir.empty()) { + std::string src_name = android::base::StringPrintf("%s/src-%zu", debug_dir.c_str(), i); + android::base::unique_fd fd( + open(src_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR)); + + if (fd == -1) { + PLOG(ERROR) << "Failed to open " << src_name; + return false; + } + if (!android::base::WriteFully(fd, split_src_images[i].PseudoSource().DataForPatch(), + split_src_images[i].PseudoSource().DataLengthForPatch())) { + PLOG(ERROR) << "Failed to write split source data into " << src_name; + return false; + } + + std::string patch_name = android::base::StringPrintf("%s/patch-%zu", debug_dir.c_str(), i); + fd.reset(open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR)); + + if (fd == -1) { + PLOG(ERROR) << "Failed to open " << patch_name; + return false; + } + if (!PatchChunk::WritePatchDataToFd(patch_chunks, fd)) { + return false; + } + } + } + + // Store the split in the following format: + // Line 0: imgdiff version# + // Line 1: number of pieces + // Line 2: patch_size_1 tgt_size_1 src_range_1 + // ... + // Line n+1: patch_size_n tgt_size_n src_range_n + std::string split_info_string = android::base::StringPrintf( + "%zu\n%zu\n", VERSION, split_info_list.size()) + android::base::Join(split_info_list, '\n'); + if (!android::base::WriteStringToFile(split_info_string, split_info_file)) { + PLOG(ERROR) << "Failed to write split info to " << split_info_file; + return false; + } + + return true; +} + +bool ImageModeImage::Initialize(const std::string& filename) { + if (!ReadFile(filename, &file_content_)) { + return false; + } + + size_t sz = file_content_.size(); + size_t pos = 0; + while (pos < sz) { + // 0x00 no header flags, 0x08 deflate compression, 0x1f8b gzip magic number + if (sz - pos >= 4 && get_unaligned(file_content_.data() + pos) == 0x00088b1f) { + // 'pos' is the offset of the start of a gzip chunk. + size_t chunk_offset = pos; + + // The remaining data is too small to be a gzip chunk; treat them as a normal chunk. + if (sz - pos < GZIP_HEADER_LEN + GZIP_FOOTER_LEN) { + chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, sz - pos); + break; + } + + // We need three chunks for the deflated image in total, one normal chunk for the header, + // one deflated chunk for the body, and another normal chunk for the footer. + chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, GZIP_HEADER_LEN); + pos += GZIP_HEADER_LEN; + + // We must decompress this chunk in order to discover where it ends, and so we can update + // the uncompressed_data of the image body and its length. + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = sz - pos; + strm.next_in = file_content_.data() + pos; + + // -15 means we are decoding a 'raw' deflate stream; zlib will + // not expect zlib headers. + int ret = inflateInit2(&strm, -15); + if (ret < 0) { + LOG(ERROR) << "Failed to initialize inflate: " << ret; + return false; + } + + size_t allocated = BUFFER_SIZE; + std::vector uncompressed_data(allocated); + size_t uncompressed_len = 0, raw_data_len = 0; + do { + strm.avail_out = allocated - uncompressed_len; + strm.next_out = uncompressed_data.data() + uncompressed_len; + ret = inflate(&strm, Z_NO_FLUSH); + if (ret < 0) { + LOG(WARNING) << "Inflate failed [" << strm.msg << "] at offset [" << chunk_offset + << "]; treating as a normal chunk"; + break; + } + uncompressed_len = allocated - strm.avail_out; + if (strm.avail_out == 0) { + allocated *= 2; + uncompressed_data.resize(allocated); + } + } while (ret != Z_STREAM_END); + + raw_data_len = sz - strm.avail_in - pos; + inflateEnd(&strm); + + if (ret < 0) { + continue; + } + + // The footer contains the size of the uncompressed data. Double-check to make sure that it + // matches the size of the data we got when we actually did the decompression. + size_t footer_index = pos + raw_data_len + GZIP_FOOTER_LEN - 4; + if (sz - footer_index < 4) { + LOG(WARNING) << "invalid footer position; treating as a normal chunk"; + continue; + } + size_t footer_size = get_unaligned(file_content_.data() + footer_index); + if (footer_size != uncompressed_len) { + LOG(WARNING) << "footer size " << footer_size << " != " << uncompressed_len + << "; treating as a normal chunk"; + continue; + } + + ImageChunk body(CHUNK_DEFLATE, pos, &file_content_, raw_data_len); + uncompressed_data.resize(uncompressed_len); + body.SetUncompressedData(std::move(uncompressed_data)); + chunks_.push_back(std::move(body)); + + pos += raw_data_len; + + // create a normal chunk for the footer + chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, GZIP_FOOTER_LEN); + + pos += GZIP_FOOTER_LEN; + } else { + // Use a normal chunk to take all the contents until the next gzip chunk (or EOF); we expect + // the number of chunks to be small (5 for typical boot and recovery images). + + // Scan forward until we find a gzip header. + size_t data_len = 0; + while (data_len + pos < sz) { + if (data_len + pos + 4 <= sz && + get_unaligned(file_content_.data() + pos + data_len) == 0x00088b1f) { + break; + } + data_len++; + } + chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, data_len); + + pos += data_len; + } + } + + return true; +} + +bool ImageModeImage::SetBonusData(const std::vector& bonus_data) { + CHECK(is_source_); + if (chunks_.size() < 2 || !chunks_[1].SetBonusData(bonus_data)) { + LOG(ERROR) << "Failed to set bonus data"; + DumpChunks(); + return false; + } + + LOG(INFO) << " using " << bonus_data.size() << " bytes of bonus data"; + return true; +} + +// In Image Mode, verify that the source and target images have the same chunk structure (ie, the +// same sequence of deflate and normal chunks). +bool ImageModeImage::CheckAndProcessChunks(ImageModeImage* tgt_image, ImageModeImage* src_image) { + // In image mode, merge the gzip header and footer in with any adjacent normal chunks. + tgt_image->MergeAdjacentNormalChunks(); + src_image->MergeAdjacentNormalChunks(); + + if (tgt_image->NumOfChunks() != src_image->NumOfChunks()) { + LOG(ERROR) << "Source and target don't have same number of chunks!"; + tgt_image->DumpChunks(); + src_image->DumpChunks(); + return false; + } + for (size_t i = 0; i < tgt_image->NumOfChunks(); ++i) { + if ((*tgt_image)[i].GetType() != (*src_image)[i].GetType()) { + LOG(ERROR) << "Source and target don't have same chunk structure! (chunk " << i << ")"; + tgt_image->DumpChunks(); + src_image->DumpChunks(); + return false; + } + } + + for (size_t i = 0; i < tgt_image->NumOfChunks(); ++i) { + auto& tgt_chunk = (*tgt_image)[i]; + auto& src_chunk = (*src_image)[i]; + if (tgt_chunk.GetType() != CHUNK_DEFLATE) { + continue; + } + + // If two deflate chunks are identical treat them as normal chunks. + if (tgt_chunk == src_chunk) { + tgt_chunk.ChangeDeflateChunkToNormal(); + src_chunk.ChangeDeflateChunkToNormal(); + } else if (!tgt_chunk.ReconstructDeflateChunk()) { + // We cannot recompress the data and get exactly the same bits as are in the input target + // image, fall back to normal + LOG(WARNING) << "Failed to reconstruct target deflate chunk " << i << " [" + << tgt_chunk.GetEntryName() << "]; treating as normal"; + tgt_chunk.ChangeDeflateChunkToNormal(); + src_chunk.ChangeDeflateChunkToNormal(); + } + } + + // For images, we need to maintain the parallel structure of the chunk lists, so do the merging + // in both the source and target lists. + tgt_image->MergeAdjacentNormalChunks(); + src_image->MergeAdjacentNormalChunks(); + if (tgt_image->NumOfChunks() != src_image->NumOfChunks()) { + // This shouldn't happen. + LOG(ERROR) << "Merging normal chunks went awry"; + return false; + } + + return true; +} + +// In image mode, generate patches against the given source chunks and bonus_data; write the +// result to |patch_name|. +bool ImageModeImage::GeneratePatches(const ImageModeImage& tgt_image, + const ImageModeImage& src_image, + const std::string& patch_name) { + LOG(INFO) << "Constructing patches for " << tgt_image.NumOfChunks() << " chunks..."; + std::vector patch_chunks; + patch_chunks.reserve(tgt_image.NumOfChunks()); + + for (size_t i = 0; i < tgt_image.NumOfChunks(); i++) { + const auto& tgt_chunk = tgt_image[i]; + const auto& src_chunk = src_image[i]; + + if (PatchChunk::RawDataIsSmaller(tgt_chunk, 0)) { + patch_chunks.emplace_back(tgt_chunk); + continue; + } + + std::vector patch_data; + if (!ImageChunk::MakePatch(tgt_chunk, src_chunk, &patch_data, nullptr)) { + LOG(ERROR) << "Failed to generate patch for target chunk " << i; + return false; + } + LOG(INFO) << "patch " << i << " is " << patch_data.size() << " bytes (of " + << tgt_chunk.GetRawDataLength() << ")"; + + if (PatchChunk::RawDataIsSmaller(tgt_chunk, patch_data.size())) { + patch_chunks.emplace_back(tgt_chunk); + } else { + patch_chunks.emplace_back(tgt_chunk, src_chunk, std::move(patch_data)); + } + } + + CHECK_EQ(tgt_image.NumOfChunks(), patch_chunks.size()); + + android::base::unique_fd patch_fd( + open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR)); + if (patch_fd == -1) { + PLOG(ERROR) << "Failed to open " << patch_name; + return false; + } + + return PatchChunk::WritePatchDataToFd(patch_chunks, patch_fd); +} + +int imgdiff(int argc, const char** argv) { + bool verbose = false; + bool zip_mode = false; + std::vector bonus_data; + size_t blocks_limit = 0; + std::string split_info_file; + std::string debug_dir; + + int opt; + int option_index; + optind = 0; // Reset the getopt state so that we can call it multiple times for test. + + while ((opt = getopt_long(argc, const_cast(argv), "zb:v", OPTIONS, &option_index)) != + -1) { + switch (opt) { + case 'z': + zip_mode = true; + break; + case 'b': { + android::base::unique_fd fd(open(optarg, O_RDONLY)); + if (fd == -1) { + PLOG(ERROR) << "Failed to open bonus file " << optarg; + return 1; + } + struct stat st; + if (fstat(fd, &st) != 0) { + PLOG(ERROR) << "Failed to stat bonus file " << optarg; + return 1; + } + + size_t bonus_size = st.st_size; + bonus_data.resize(bonus_size); + if (!android::base::ReadFully(fd, bonus_data.data(), bonus_size)) { + PLOG(ERROR) << "Failed to read bonus file " << optarg; + return 1; + } + break; + } + case 'v': + verbose = true; + break; + case 0: { + std::string name = OPTIONS[option_index].name; + if (name == "block-limit" && !android::base::ParseUint(optarg, &blocks_limit)) { + LOG(ERROR) << "Failed to parse size blocks_limit: " << optarg; + return 1; + } else if (name == "split-info") { + split_info_file = optarg; + } else if (name == "debug-dir") { + debug_dir = optarg; + } + break; + } + default: + LOG(ERROR) << "unexpected opt: " << static_cast(opt); + return 2; + } + } + + if (!verbose) { + android::base::SetMinimumLogSeverity(android::base::WARNING); + } + + if (argc - optind != 3) { + LOG(ERROR) << "usage: " << argv[0] << " [options] "; + LOG(ERROR) + << " -z , Generate patches in zip mode, src and tgt should be zip files.\n" + " -b , Bonus file in addition to src, image mode only.\n" + " --block-limit, For large zips, split the src and tgt based on the block limit;\n" + " and generate patches between each pair of pieces. Concatenate " + "these\n" + " patches together and output them into .\n" + " --split-info, Output the split information (patch_size, tgt_size, src_ranges);\n" + " zip mode with block-limit only.\n" + " --debug-dir, Debug directory to put the split srcs and patches, zip mode only.\n" + " -v, --verbose, Enable verbose logging."; + return 2; + } + + if (zip_mode) { + ZipModeImage src_image(true, blocks_limit * BLOCK_SIZE); + ZipModeImage tgt_image(false, blocks_limit * BLOCK_SIZE); + + if (!src_image.Initialize(argv[optind])) { + return 1; + } + if (!tgt_image.Initialize(argv[optind + 1])) { + return 1; + } + + if (!ZipModeImage::CheckAndProcessChunks(&tgt_image, &src_image)) { + return 1; + } + + // Compute bsdiff patches for each chunk's data (the uncompressed data, in the case of + // deflate chunks). + if (blocks_limit > 0) { + if (split_info_file.empty()) { + LOG(ERROR) << "split-info path cannot be empty when generating patches with a block-limit"; + return 1; + } + + std::vector split_tgt_images; + std::vector split_src_images; + std::vector split_src_ranges; + ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images, + &split_src_images, &split_src_ranges); + + if (!ZipModeImage::GeneratePatches(split_tgt_images, split_src_images, split_src_ranges, + argv[optind + 2], split_info_file, debug_dir)) { + return 1; + } + + } else if (!ZipModeImage::GeneratePatches(tgt_image, src_image, argv[optind + 2])) { + return 1; + } + } else { + ImageModeImage src_image(true); + ImageModeImage tgt_image(false); + + if (!src_image.Initialize(argv[optind])) { + return 1; + } + if (!tgt_image.Initialize(argv[optind + 1])) { + return 1; + } + + if (!ImageModeImage::CheckAndProcessChunks(&tgt_image, &src_image)) { + return 1; + } + + if (!bonus_data.empty() && !src_image.SetBonusData(bonus_data)) { + return 1; + } + + if (!ImageModeImage::GeneratePatches(tgt_image, src_image, argv[optind + 2])) { + return 1; + } + } + + return 0; +} diff --git a/applypatch/imgdiff_main.cpp b/applypatch/imgdiff_main.cpp new file mode 100644 index 0000000..7d5bdf9 --- /dev/null +++ b/applypatch/imgdiff_main.cpp @@ -0,0 +1,21 @@ +/* + * 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 "applypatch/imgdiff.h" + +int main(int argc, char** argv) { + return imgdiff(argc, const_cast(argv)); +} diff --git a/applypatch/imgpatch.cpp b/applypatch/imgpatch.cpp new file mode 100644 index 0000000..f4c33e5 --- /dev/null +++ b/applypatch/imgpatch.cpp @@ -0,0 +1,293 @@ +/* + * 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. + */ + +// See imgdiff.cpp in this directory for a description of the patch file +// format. + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "edify/expr.h" +#include "otautil/print_sha1.h" + +static inline int64_t Read8(const void *address) { + return android::base::get_unaligned(address); +} + +static inline int32_t Read4(const void *address) { + return android::base::get_unaligned(address); +} + +// This function is a wrapper of ApplyBSDiffPatch(). It has a custom sink function to deflate the +// patched data and stream the deflated data to output. +static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_len, + const Value& patch, size_t patch_offset, + const char* deflate_header, SinkFn sink) { + size_t expected_target_length = static_cast(Read8(deflate_header + 32)); + CHECK_GT(expected_target_length, static_cast(0)); + int level = Read4(deflate_header + 40); + int method = Read4(deflate_header + 44); + int window_bits = Read4(deflate_header + 48); + int mem_level = Read4(deflate_header + 52); + int strategy = Read4(deflate_header + 56); + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.next_in = nullptr; + int ret = deflateInit2(&strm, level, method, window_bits, mem_level, strategy); + if (ret != Z_OK) { + LOG(ERROR) << "Failed to init uncompressed data deflation: " << ret; + return false; + } + + // Define a custom sink wrapper that feeds to bspatch. It deflates the available patch data on + // the fly and outputs the compressed data to the given sink. + size_t actual_target_length = 0; + size_t total_written = 0; + static constexpr size_t buffer_size = 32768; + auto compression_sink = [&strm, &actual_target_length, &expected_target_length, &total_written, + &ret, &sink](const uint8_t* data, size_t len) -> size_t { + // The input patch length for an update never exceeds INT_MAX. + strm.avail_in = len; + strm.next_in = data; + do { + std::vector buffer(buffer_size); + strm.avail_out = buffer_size; + strm.next_out = buffer.data(); + if (actual_target_length + len < expected_target_length) { + ret = deflate(&strm, Z_NO_FLUSH); + } else { + ret = deflate(&strm, Z_FINISH); + } + if (ret != Z_OK && ret != Z_STREAM_END) { + LOG(ERROR) << "Failed to deflate stream: " << ret; + // zero length indicates an error in the sink function of bspatch(). + return 0; + } + + size_t have = buffer_size - strm.avail_out; + total_written += have; + if (sink(buffer.data(), have) != have) { + LOG(ERROR) << "Failed to write " << have << " compressed bytes to output."; + return 0; + } + } while ((strm.avail_in != 0 || strm.avail_out == 0) && ret != Z_STREAM_END); + + actual_target_length += len; + return len; + }; + + int bspatch_result = ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink); + deflateEnd(&strm); + + if (bspatch_result != 0) { + return false; + } + + if (ret != Z_STREAM_END) { + LOG(ERROR) << "ret is expected to be Z_STREAM_END, but it's " << ret; + return false; + } + + if (expected_target_length != actual_target_length) { + LOG(ERROR) << "target length is expected to be " << expected_target_length << ", but it's " + << actual_target_length; + return false; + } + LOG(DEBUG) << "bspatch wrote " << total_written << " bytes in total to streaming output."; + + return true; +} + +int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const unsigned char* patch_data, + size_t patch_size, SinkFn sink) { + Value patch(Value::Type::BLOB, + std::string(reinterpret_cast(patch_data), patch_size)); + return ApplyImagePatch(old_data, old_size, patch, sink, nullptr); +} + +int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value& patch, SinkFn sink, + const Value* bonus_data) { + if (patch.data.size() < 12) { + printf("patch too short to contain header\n"); + return -1; + } + + // IMGDIFF2 uses CHUNK_NORMAL, CHUNK_DEFLATE, and CHUNK_RAW. (IMGDIFF1, which is no longer + // supported, used CHUNK_NORMAL and CHUNK_GZIP.) + const char* const patch_header = patch.data.data(); + if (memcmp(patch_header, "IMGDIFF2", 8) != 0) { + printf("corrupt patch file header (magic number)\n"); + return -1; + } + + int num_chunks = Read4(patch_header + 8); + size_t pos = 12; + for (int i = 0; i < num_chunks; ++i) { + // each chunk's header record starts with 4 bytes. + if (pos + 4 > patch.data.size()) { + printf("failed to read chunk %d record\n", i); + return -1; + } + int type = Read4(patch_header + pos); + pos += 4; + + if (type == CHUNK_NORMAL) { + const char* normal_header = patch_header + pos; + pos += 24; + if (pos > patch.data.size()) { + printf("failed to read chunk %d normal header data\n", i); + return -1; + } + + size_t src_start = static_cast(Read8(normal_header)); + size_t src_len = static_cast(Read8(normal_header + 8)); + size_t patch_offset = static_cast(Read8(normal_header + 16)); + + if (src_start + src_len > old_size) { + printf("source data too short\n"); + return -1; + } + if (ApplyBSDiffPatch(old_data + src_start, src_len, patch, patch_offset, sink) != 0) { + printf("Failed to apply bsdiff patch.\n"); + return -1; + } + + LOG(DEBUG) << "Processed chunk type normal"; + } else if (type == CHUNK_RAW) { + const char* raw_header = patch_header + pos; + pos += 4; + if (pos > patch.data.size()) { + printf("failed to read chunk %d raw header data\n", i); + return -1; + } + + size_t data_len = static_cast(Read4(raw_header)); + + if (pos + data_len > patch.data.size()) { + printf("failed to read chunk %d raw data\n", i); + return -1; + } + if (sink(reinterpret_cast(patch_header + pos), data_len) != data_len) { + printf("failed to write chunk %d raw data\n", i); + return -1; + } + pos += data_len; + + LOG(DEBUG) << "Processed chunk type raw"; + } else if (type == CHUNK_DEFLATE) { + // deflate chunks have an additional 60 bytes in their chunk header. + const char* deflate_header = patch_header + pos; + pos += 60; + if (pos > patch.data.size()) { + printf("failed to read chunk %d deflate header data\n", i); + return -1; + } + + size_t src_start = static_cast(Read8(deflate_header)); + size_t src_len = static_cast(Read8(deflate_header + 8)); + size_t patch_offset = static_cast(Read8(deflate_header + 16)); + size_t expanded_len = static_cast(Read8(deflate_header + 24)); + + if (src_start + src_len > old_size) { + printf("source data too short\n"); + return -1; + } + + // Decompress the source data; the chunk header tells us exactly + // how big we expect it to be when decompressed. + + // Note: expanded_len will include the bonus data size if the patch was constructed with + // bonus data. The deflation will come up 'bonus_size' bytes short; these must be appended + // from the bonus_data value. + size_t bonus_size = (i == 1 && bonus_data != nullptr) ? bonus_data->data.size() : 0; + + std::vector expanded_source(expanded_len); + + // inflate() doesn't like strm.next_out being a nullptr even with + // avail_out being zero (Z_STREAM_ERROR). + if (expanded_len != 0) { + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = src_len; + strm.next_in = old_data + src_start; + strm.avail_out = expanded_len; + strm.next_out = expanded_source.data(); + + int ret = inflateInit2(&strm, -15); + if (ret != Z_OK) { + printf("failed to init source inflation: %d\n", ret); + return -1; + } + + // Because we've provided enough room to accommodate the output + // data, we expect one call to inflate() to suffice. + ret = inflate(&strm, Z_SYNC_FLUSH); + if (ret != Z_STREAM_END) { + printf("source inflation returned %d\n", ret); + return -1; + } + // We should have filled the output buffer exactly, except + // for the bonus_size. + if (strm.avail_out != bonus_size) { + printf("source inflation short by %zu bytes\n", strm.avail_out - bonus_size); + return -1; + } + inflateEnd(&strm); + + if (bonus_size) { + memcpy(expanded_source.data() + (expanded_len - bonus_size), bonus_data->data.data(), + bonus_size); + } + } + + if (!ApplyBSDiffPatchAndStreamOutput(expanded_source.data(), expanded_len, patch, + patch_offset, deflate_header, sink)) { + LOG(ERROR) << "Fail to apply streaming bspatch."; + return -1; + } + + LOG(DEBUG) << "Processed chunk type deflate"; + } else { + printf("patch chunk %d is unknown type %d\n", i, type); + return -1; + } + } + + return 0; +} diff --git a/applypatch/include/applypatch/applypatch.h b/applypatch/include/applypatch/applypatch.h new file mode 100644 index 0000000..799f4b2 --- /dev/null +++ b/applypatch/include/applypatch/applypatch.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2008 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. + */ + +#ifndef _APPLYPATCH_H +#define _APPLYPATCH_H + +#include + +#include +#include +#include +#include +#include + +#include + +// Forward declaration to avoid including "edify/expr.h" in the header. +struct Value; + +struct FileContents { + uint8_t sha1[SHA_DIGEST_LENGTH]; + std::vector data; +}; + +using SinkFn = std::function; + +// applypatch.cpp + +int ShowLicenses(); + +// Parses a given string of 40 hex digits into 20-byte array 'digest'. 'str' may contain only the +// digest or be of the form ":". Returns 0 on success, or -1 on any error. +int ParseSha1(const std::string& str, uint8_t* digest); + +struct Partition { + Partition() = default; + + Partition(const std::string& name, size_t size, const std::string& hash) + : name(name), size(size), hash(hash) {} + + // Parses and returns the given string into a Partition object. The input string is of the form + // "EMMC:::". Returns the parsed Partition, or an empty object on error. + static Partition Parse(const std::string& partition, std::string* err); + + std::string ToString() const; + + // Returns whether the current Partition object is valid. + explicit operator bool() const { + return !name.empty(); + } + + std::string name; + size_t size; + std::string hash; +}; + +std::ostream& operator<<(std::ostream& os, const Partition& partition); + +// Applies the given 'patch' to the 'source' Partition, verifies then writes the patching result to +// the 'target' Partition. While patching, it will backup the data on the source partition to +// /cache, so that the patching could be resumed on interruption even if both of the source and +// target partitions refer to the same device. The function is idempotent if called multiple times. +// 'bonus' can be provided if the patch was generated with a bonus output, or nullptr. +// 'backup_source' indicates whether the source partition should be backed up prior to the update +// (e.g. when doing in-place update). Returns the patching result. +bool PatchPartition(const Partition& target, const Partition& source, const Value& patch, + const Value* bonus, bool backup_source); + +// Returns whether the contents of the eMMC target or the cached file match the embedded hash. +// It will look for the backup on /cache if the given partition doesn't match the checksum. +bool PatchPartitionCheck(const Partition& target, const Partition& source); + +// Checks whether the contents of the given partition has the desired hash. It will NOT look for +// the backup on /cache if the given partition doesn't have the expected checksum. +bool CheckPartition(const Partition& target); + +// Flashes a given image in 'source_filename' to the eMMC target partition. It verifies the target +// checksum first, and will return if target already has the desired hash. Otherwise it checks the +// checksum of the given source image, flashes, and verifies the target partition afterwards. The +// function is idempotent. Returns the flashing result. +bool FlashPartition(const Partition& target, const std::string& source_filename); + +// Reads a file into memory; stores the file contents and associated metadata in *file. +bool LoadFileContents(const std::string& filename, FileContents* file); + +// Saves the given FileContents object to the given filename. +bool SaveFileContents(const std::string& filename, const FileContents* file); + +// bspatch.cpp + +void ShowBSDiffLicense(); + +// Applies the bsdiff-patch given in 'patch' (from offset 'patch_offset' to the end) to the source +// data given by (old_data, old_size). Writes the patched output through the given 'sink'. Returns +// 0 on success. +int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value& patch, + size_t patch_offset, SinkFn sink); + +// imgpatch.cpp + +// Applies the imgdiff-patch given in 'patch' to the source data given by (old_data, old_size), with +// the optional bonus data. Writes the patched output through the given 'sink'. Returns 0 on +// success. +int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value& patch, SinkFn sink, + const Value* bonus_data); + +// freecache.cpp + +// Checks whether /cache partition has at least 'bytes'-byte free space. Returns true immediately +// if so. Otherwise, it will try to free some space by removing older logs, checks again and +// returns the checking result. +bool CheckAndFreeSpaceOnCache(size_t bytes); + +// Removes the files in |dirname| until we have at least |bytes_needed| bytes of free space on the +// partition. |space_checker| should return the size of the free space, or -1 on error. +bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname, + const std::function& space_checker); +#endif diff --git a/applypatch/include/applypatch/imgdiff.h b/applypatch/include/applypatch/imgdiff.h new file mode 100644 index 0000000..22cbd4f --- /dev/null +++ b/applypatch/include/applypatch/imgdiff.h @@ -0,0 +1,39 @@ +/* + * 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. + */ + +#ifndef _APPLYPATCH_IMGDIFF_H +#define _APPLYPATCH_IMGDIFF_H + +#include + +// Image patch chunk types +#define CHUNK_NORMAL 0 +#define CHUNK_GZIP 1 // version 1 only +#define CHUNK_DEFLATE 2 // version 2 only +#define CHUNK_RAW 3 // version 2 only + +// The gzip header size is actually variable, but we currently don't +// support gzipped data with any of the optional fields, so for now it +// will always be ten bytes. See RFC 1952 for the definition of the +// gzip format. +static constexpr size_t GZIP_HEADER_LEN = 10; + +// The gzip footer size really is fixed. +static constexpr size_t GZIP_FOOTER_LEN = 8; + +int imgdiff(int argc, const char** argv); + +#endif // _APPLYPATCH_IMGDIFF_H diff --git a/applypatch/include/applypatch/imgdiff_image.h b/applypatch/include/applypatch/imgdiff_image.h new file mode 100644 index 0000000..b579e56 --- /dev/null +++ b/applypatch/include/applypatch/imgdiff_image.h @@ -0,0 +1,308 @@ +/* + * 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. + */ + +#ifndef _APPLYPATCH_IMGDIFF_IMAGE_H +#define _APPLYPATCH_IMGDIFF_IMAGE_H + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "imgdiff.h" +#include "otautil/rangeset.h" + +class ImageChunk { + public: + static constexpr auto WINDOWBITS = -15; // 32kb window; negative to indicate a raw stream. + static constexpr auto MEMLEVEL = 8; // the default value. + static constexpr auto METHOD = Z_DEFLATED; + static constexpr auto STRATEGY = Z_DEFAULT_STRATEGY; + + ImageChunk(int type, size_t start, const std::vector* file_content, size_t raw_data_len, + std::string entry_name = {}); + + int GetType() const { + return type_; + } + + const uint8_t* GetRawData() const; + size_t GetRawDataLength() const { + return raw_data_len_; + } + const std::string& GetEntryName() const { + return entry_name_; + } + size_t GetStartOffset() const { + return start_; + } + int GetCompressLevel() const { + return compress_level_; + } + + // CHUNK_DEFLATE will return the uncompressed data for diff, while other types will simply return + // the raw data. + const uint8_t* DataForPatch() const; + size_t DataLengthForPatch() const; + + void Dump(size_t index) const; + + void SetUncompressedData(std::vector data); + bool SetBonusData(const std::vector& bonus_data); + + bool operator==(const ImageChunk& other) const; + bool operator!=(const ImageChunk& other) const { + return !(*this == other); + } + + /* + * Cause a gzip chunk to be treated as a normal chunk (ie, as a blob of uninterpreted data). + * The resulting patch will likely be about as big as the target file, but it lets us handle + * the case of images where some gzip chunks are reconstructible but others aren't (by treating + * the ones that aren't as normal chunks). + */ + void ChangeDeflateChunkToNormal(); + + /* + * Verify that we can reproduce exactly the same compressed data that we started with. Sets the + * level, method, windowBits, memLevel, and strategy fields in the chunk to the encoding + * parameters needed to produce the right output. + */ + bool ReconstructDeflateChunk(); + bool IsAdjacentNormal(const ImageChunk& other) const; + void MergeAdjacentNormal(const ImageChunk& other); + + /* + * Compute a bsdiff patch between |src| and |tgt|; Store the result in the patch_data. + * |bsdiff_cache| can be used to cache the suffix array if the same |src| chunk is used + * repeatedly, pass nullptr if not needed. + */ + static bool MakePatch(const ImageChunk& tgt, const ImageChunk& src, + std::vector* patch_data, + bsdiff::SuffixArrayIndexInterface** bsdiff_cache); + + private: + bool TryReconstruction(int level); + + int type_; // CHUNK_NORMAL, CHUNK_DEFLATE, CHUNK_RAW + size_t start_; // offset of chunk in the original input file + const std::vector* input_file_ptr_; // ptr to the full content of original input file + size_t raw_data_len_; + + // deflate encoder parameters + int compress_level_; + + // --- for CHUNK_DEFLATE chunks only: --- + std::vector uncompressed_data_; + std::string entry_name_; // used for zip entries +}; + +// PatchChunk stores the patch data between a source chunk and a target chunk. It also keeps track +// of the metadata of src&tgt chunks (e.g. offset, raw data length, uncompressed data length). +class PatchChunk { + public: + PatchChunk(const ImageChunk& tgt, const ImageChunk& src, std::vector data); + + // Construct a CHUNK_RAW patch from the target data directly. + explicit PatchChunk(const ImageChunk& tgt); + + // Return true if raw data size is smaller than the patch size. + static bool RawDataIsSmaller(const ImageChunk& tgt, size_t patch_size); + + // Update the source start with the new offset within the source range. + void UpdateSourceOffset(const SortedRangeSet& src_range); + + // Return the total size (header + data) of the patch. + size_t PatchSize() const; + + static bool WritePatchDataToFd(const std::vector& patch_chunks, int patch_fd); + + private: + size_t GetHeaderSize() const; + size_t WriteHeaderToFd(int fd, size_t offset, size_t index) const; + + // The patch chunk type is the same as the target chunk type. The only exception is we change + // the |type_| to CHUNK_RAW if target length is smaller than the patch size. + int type_; + + size_t source_start_; + size_t source_len_; + size_t source_uncompressed_len_; + + size_t target_start_; // offset of the target chunk within the target file + size_t target_len_; + size_t target_uncompressed_len_; + size_t target_compress_level_; // the deflate compression level of the target chunk. + + std::vector data_; // storage for the patch data +}; + +// Interface for zip_mode and image_mode images. We initialize the image from an input file and +// split the file content into a list of image chunks. +class Image { + public: + explicit Image(bool is_source) : is_source_(is_source) {} + + virtual ~Image() {} + + // Create a list of image chunks from input file. + virtual bool Initialize(const std::string& filename) = 0; + + // Look for runs of adjacent normal chunks and compress them down into a single chunk. (Such + // runs can be produced when deflate chunks are changed to normal chunks.) + void MergeAdjacentNormalChunks(); + + void DumpChunks() const; + + // Non const iterators to access the stored ImageChunks. + std::vector::iterator begin() { + return chunks_.begin(); + } + + std::vector::iterator end() { + return chunks_.end(); + } + + std::vector::const_iterator cbegin() const { + return chunks_.cbegin(); + } + + std::vector::const_iterator cend() const { + return chunks_.cend(); + } + + ImageChunk& operator[](size_t i); + const ImageChunk& operator[](size_t i) const; + + size_t NumOfChunks() const { + return chunks_.size(); + } + + protected: + bool ReadFile(const std::string& filename, std::vector* file_content); + + bool is_source_; // True if it's for source chunks. + std::vector chunks_; // Internal storage of ImageChunk. + std::vector file_content_; // Store the whole input file in memory. +}; + +class ZipModeImage : public Image { + public: + explicit ZipModeImage(bool is_source, size_t limit = 0) : Image(is_source), limit_(limit) {} + + bool Initialize(const std::string& filename) override; + + // Initialize a fake ZipModeImage from an existing ImageChunk vector. For src img pieces, we + // reconstruct a new file_content based on the source ranges; but it's not needed for the tgt img + // pieces; because for each chunk both the data and their offset within the file are unchanged. + void Initialize(const std::vector& chunks, const std::vector& file_content) { + chunks_ = chunks; + file_content_ = file_content; + } + + // The pesudo source chunk for bsdiff if there's no match for the given target chunk. It's in + // fact the whole source file. + ImageChunk PseudoSource() const; + + // Find the matching deflate source chunk by entry name. Search for normal chunks also if + // |find_normal| is true. + ImageChunk* FindChunkByName(const std::string& name, bool find_normal = false); + + const ImageChunk* FindChunkByName(const std::string& name, bool find_normal = false) const; + + // Verify that we can reconstruct the deflate chunks; also change the type to CHUNK_NORMAL if + // src and tgt are identical. + static bool CheckAndProcessChunks(ZipModeImage* tgt_image, ZipModeImage* src_image); + + // Compute the patch between tgt & src images, and write the data into |patch_name|. + static bool GeneratePatches(const ZipModeImage& tgt_image, const ZipModeImage& src_image, + const std::string& patch_name); + + // Compute the patch based on the lists of split src and tgt images. Generate patches for each + // pair of split pieces and write the data to |patch_name|. If |debug_dir| is specified, write + // each split src data and patch data into that directory. + static bool GeneratePatches(const std::vector& split_tgt_images, + const std::vector& split_src_images, + const std::vector& split_src_ranges, + const std::string& patch_name, const std::string& split_info_file, + const std::string& debug_dir); + + // Split the tgt chunks and src chunks based on the size limit. + static bool SplitZipModeImageWithLimit(const ZipModeImage& tgt_image, + const ZipModeImage& src_image, + std::vector* split_tgt_images, + std::vector* split_src_images, + std::vector* split_src_ranges); + + private: + // Initialize image chunks based on the zip entries. + bool InitializeChunks(const std::string& filename, ZipArchiveHandle handle); + // Add the a zip entry to the list. + bool AddZipEntryToChunks(ZipArchiveHandle handle, const std::string& entry_name, + ZipEntry64* entry); + // Return the real size of the zip file. (omit the trailing zeros that used for alignment) + bool GetZipFileSize(size_t* input_file_size); + + static void ValidateSplitImages(const std::vector& split_tgt_images, + const std::vector& split_src_images, + std::vector& split_src_ranges, + size_t total_tgt_size); + // Construct the fake split images based on the chunks info and source ranges; and move them into + // the given vectors. Return true if we add a new split image into |split_tgt_images|, and + // false otherwise. + static bool AddSplitImageFromChunkList(const ZipModeImage& tgt_image, + const ZipModeImage& src_image, + const SortedRangeSet& split_src_ranges, + const std::vector& split_tgt_chunks, + const std::vector& split_src_chunks, + std::vector* split_tgt_images, + std::vector* split_src_images); + + // Function that actually iterates the tgt_chunks and makes patches. + static bool GeneratePatchesInternal(const ZipModeImage& tgt_image, const ZipModeImage& src_image, + std::vector* patch_chunks); + + // size limit in bytes of each chunk. Also, if the length of one zip_entry exceeds the limit, + // we'll split that entry into several smaller chunks in advance. + size_t limit_; +}; + +class ImageModeImage : public Image { + public: + explicit ImageModeImage(bool is_source) : Image(is_source) {} + + // Initialize the image chunks list by searching the magic numbers in an image file. + bool Initialize(const std::string& filename) override; + + bool SetBonusData(const std::vector& bonus_data); + + // In Image Mode, verify that the source and target images have the same chunk structure (ie, the + // same sequence of deflate and normal chunks). + static bool CheckAndProcessChunks(ImageModeImage* tgt_image, ImageModeImage* src_image); + + // In image mode, generate patches against the given source chunks and bonus_data; write the + // result to |patch_name|. + static bool GeneratePatches(const ImageModeImage& tgt_image, const ImageModeImage& src_image, + const std::string& patch_name); +}; + +#endif // _APPLYPATCH_IMGDIFF_IMAGE_H diff --git a/applypatch/include/applypatch/imgpatch.h b/applypatch/include/applypatch/imgpatch.h new file mode 100644 index 0000000..07c6609 --- /dev/null +++ b/applypatch/include/applypatch/imgpatch.h @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#ifndef _APPLYPATCH_IMGPATCH_H +#define _APPLYPATCH_IMGPATCH_H + +#include + +#include + +using SinkFn = std::function; + +int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const unsigned char* patch_data, + size_t patch_size, SinkFn sink); + +#endif // _APPLYPATCH_IMGPATCH_H diff --git a/applypatch/vendor_flash_recovery.rc b/applypatch/vendor_flash_recovery.rc new file mode 100644 index 0000000..a6003be --- /dev/null +++ b/applypatch/vendor_flash_recovery.rc @@ -0,0 +1,4 @@ +service vendor_flash_recovery /vendor/bin/install-recovery.sh + class main + oneshot + user root diff --git a/edify/Android.bp b/edify/Android.bp new file mode 100644 index 0000000..62ff911 --- /dev/null +++ b/edify/Android.bp @@ -0,0 +1,56 @@ +// 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. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "bootable_recovery_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["bootable_recovery_license"], +} + +cc_library_static { + name: "libedify", + + host_supported: true, + vendor_available: true, + recovery_available: true, + + srcs: [ + "expr.cpp", + "lexer.ll", + "parser.yy", + ], + + cflags: [ + "-Wall", + "-Werror", + "-Wno-deprecated-register", + "-Wno-unused-parameter", + ], + + export_include_dirs: [ + "include", + ], + + local_include_dirs: [ + "include", + ], + + static_libs: [ + "libbase", + "libotautil", + ], +} diff --git a/edify/README.md b/edify/README.md new file mode 100644 index 0000000..b3330e2 --- /dev/null +++ b/edify/README.md @@ -0,0 +1,111 @@ +edify +===== + +Update scripts (from donut onwards) are written in a new little +scripting language ("edify") that is superficially somewhat similar to +the old one ("amend"). This is a brief overview of the new language. + +- The entire script is a single expression. + +- All expressions are string-valued. + +- String literals appear in double quotes. \n, \t, \", and \\ are + understood, as are hexadecimal escapes like \x4a. + +- String literals consisting of only letters, numbers, colons, + underscores, slashes, and periods don't need to be in double quotes. + +- The following words are reserved: + + if then else endif + + They have special meaning when unquoted. (In quotes, they are just + string literals.) + +- When used as a boolean, the empty string is "false" and all other + strings are "true". + +- All functions are actually macros (in the Lisp sense); the body of + the function can control which (if any) of the arguments are + evaluated. This means that functions can act as control + structures. + +- Operators (like "&&" and "||") are just syntactic sugar for builtin + functions, so they can act as control structures as well. + +- ";" is a binary operator; evaluating it just means to first evaluate + the left side, then the right. It can also appear after any + expression. + +- Comments start with "#" and run to the end of the line. + + + +Some examples: + +- There's no distinction between quoted and unquoted strings; the + quotes are only needed if you want characters like whitespace to + appear in the string. The following expressions all evaluate to the + same string. + + "a b" + a + " " + b + "a" + " " + "b" + "a\x20b" + a + "\x20b" + concat(a, " ", "b") + "concat"(a, " ", "b") + + As shown in the last example, function names are just strings, + too. They must be string *literals*, however. This is not legal: + + ("con" + "cat")(a, " ", b) # syntax error! + + +- The ifelse() builtin takes three arguments: it evaluates exactly + one of the second and third, depending on whether the first one is + true. There is also some syntactic sugar to make expressions that + look like if/else statements: + + # these are all equivalent + ifelse(something(), "yes", "no") + if something() then yes else no endif + if something() then "yes" else "no" endif + + The else part is optional. + + if something() then "yes" endif # if something() is false, + # evaluates to false + + ifelse(condition(), "", abort()) # abort() only called if + # condition() is false + + The last example is equivalent to: + + assert(condition()) + + +- The && and || operators can be used similarly; they evaluate their + second argument only if it's needed to determine the truth of the + expression. Their value is the value of the last-evaluated + argument: + + file_exists("/data/system/bad") && delete("/data/system/bad") + + file_exists("/data/system/missing") || create("/data/system/missing") + + get_it() || "xxx" # returns value of get_it() if that value is + # true, otherwise returns "xxx" + + +- The purpose of ";" is to simulate imperative statements, of course, + but the operator can be used anywhere. Its value is the value of + its right side: + + concat(a;b;c, d, e;f) # evaluates to "cdf" + + A more useful example might be something like: + + ifelse(condition(), + (first_step(); second_step();), # second ; is optional + alternative_procedure()) diff --git a/edify/expr.cpp b/edify/expr.cpp new file mode 100644 index 0000000..e5e0e24 --- /dev/null +++ b/edify/expr.cpp @@ -0,0 +1,425 @@ +/* + * 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 "edify/expr.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "otautil/error_code.h" + +// Functions should: +// +// - return a malloc()'d string +// - if Evaluate() on any argument returns nullptr, return nullptr. + +static bool BooleanString(const std::string& s) { + return !s.empty(); +} + +bool Evaluate(State* state, const std::unique_ptr& expr, std::string* result) { + if (result == nullptr) { + return false; + } + + std::unique_ptr v(expr->fn(expr->name.c_str(), state, expr->argv)); + if (!v) { + return false; + } + if (v->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "expecting string, got value type %d", v->type); + return false; + } + + *result = v->data; + return true; +} + +Value* EvaluateValue(State* state, const std::unique_ptr& expr) { + return expr->fn(expr->name.c_str(), state, expr->argv); +} + +Value* StringValue(const char* str) { + if (str == nullptr) { + return nullptr; + } + return new Value(Value::Type::STRING, str); +} + +Value* StringValue(const std::string& str) { + return StringValue(str.c_str()); +} + +Value* ConcatFn(const char* name, State* state, const std::vector>& argv) { + if (argv.empty()) { + return StringValue(""); + } + std::string result; + for (size_t i = 0; i < argv.size(); ++i) { + std::string str; + if (!Evaluate(state, argv[i], &str)) { + return nullptr; + } + result += str; + } + + return StringValue(result); +} + +Value* IfElseFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 2 && argv.size() != 3) { + state->errmsg = "ifelse expects 2 or 3 arguments"; + return nullptr; + } + + std::string cond; + if (!Evaluate(state, argv[0], &cond)) { + return nullptr; + } + + if (!cond.empty()) { + return EvaluateValue(state, argv[1]); + } else if (argv.size() == 3) { + return EvaluateValue(state, argv[2]); + } + + return StringValue(""); +} + +Value* AbortFn(const char* name, State* state, const std::vector>& argv) { + std::string msg; + if (!argv.empty() && Evaluate(state, argv[0], &msg)) { + state->errmsg += msg; + } else { + state->errmsg += "called abort()"; + } + return nullptr; +} + +Value* AssertFn(const char* name, State* state, const std::vector>& argv) { + for (size_t i = 0; i < argv.size(); ++i) { + std::string result; + if (!Evaluate(state, argv[i], &result)) { + return nullptr; + } + if (result.empty()) { + int len = argv[i]->end - argv[i]->start; + state->errmsg = "assert failed: " + state->script.substr(argv[i]->start, len); + return nullptr; + } + } + return StringValue(""); +} + +Value* SleepFn(const char* name, State* state, const std::vector>& argv) { + std::string val; + if (!Evaluate(state, argv[0], &val)) { + return nullptr; + } + + int v; + if (!android::base::ParseInt(val.c_str(), &v, 0)) { + return nullptr; + } + sleep(v); + + return StringValue(val); +} + +Value* StdoutFn(const char* name, State* state, const std::vector>& argv) { + for (size_t i = 0; i < argv.size(); ++i) { + std::string v; + if (!Evaluate(state, argv[i], &v)) { + return nullptr; + } + fputs(v.c_str(), stdout); + } + return StringValue(""); +} + +Value* LogicalAndFn(const char* name, State* state, + const std::vector>& argv) { + std::string left; + if (!Evaluate(state, argv[0], &left)) { + return nullptr; + } + if (BooleanString(left)) { + return EvaluateValue(state, argv[1]); + } else { + return StringValue(""); + } +} + +Value* LogicalOrFn(const char* name, State* state, + const std::vector>& argv) { + std::string left; + if (!Evaluate(state, argv[0], &left)) { + return nullptr; + } + if (!BooleanString(left)) { + return EvaluateValue(state, argv[1]); + } else { + return StringValue(left); + } +} + +Value* LogicalNotFn(const char* name, State* state, + const std::vector>& argv) { + std::string val; + if (!Evaluate(state, argv[0], &val)) { + return nullptr; + } + + return StringValue(BooleanString(val) ? "" : "t"); +} + +Value* SubstringFn(const char* name, State* state, + const std::vector>& argv) { + std::string needle; + if (!Evaluate(state, argv[0], &needle)) { + return nullptr; + } + + std::string haystack; + if (!Evaluate(state, argv[1], &haystack)) { + return nullptr; + } + + std::string result = (haystack.find(needle) != std::string::npos) ? "t" : ""; + return StringValue(result); +} + +Value* EqualityFn(const char* name, State* state, const std::vector>& argv) { + std::string left; + if (!Evaluate(state, argv[0], &left)) { + return nullptr; + } + std::string right; + if (!Evaluate(state, argv[1], &right)) { + return nullptr; + } + + const char* result = (left == right) ? "t" : ""; + return StringValue(result); +} + +Value* InequalityFn(const char* name, State* state, + const std::vector>& argv) { + std::string left; + if (!Evaluate(state, argv[0], &left)) { + return nullptr; + } + std::string right; + if (!Evaluate(state, argv[1], &right)) { + return nullptr; + } + + const char* result = (left != right) ? "t" : ""; + return StringValue(result); +} + +Value* SequenceFn(const char* name, State* state, const std::vector>& argv) { + std::unique_ptr left(EvaluateValue(state, argv[0])); + if (!left) { + return nullptr; + } + return EvaluateValue(state, argv[1]); +} + +Value* LessThanIntFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 2) { + state->errmsg = "less_than_int expects 2 arguments"; + return nullptr; + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return nullptr; + } + + // Parse up to at least long long or 64-bit integers. + int64_t l_int; + if (!android::base::ParseInt(args[0].c_str(), &l_int)) { + state->errmsg = "failed to parse int in " + args[0]; + return nullptr; + } + + int64_t r_int; + if (!android::base::ParseInt(args[1].c_str(), &r_int)) { + state->errmsg = "failed to parse int in " + args[1]; + return nullptr; + } + + return StringValue(l_int < r_int ? "t" : ""); +} + +Value* GreaterThanIntFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 2) { + state->errmsg = "greater_than_int expects 2 arguments"; + return nullptr; + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return nullptr; + } + + // Parse up to at least long long or 64-bit integers. + int64_t l_int; + if (!android::base::ParseInt(args[0].c_str(), &l_int)) { + state->errmsg = "failed to parse int in " + args[0]; + return nullptr; + } + + int64_t r_int; + if (!android::base::ParseInt(args[1].c_str(), &r_int)) { + state->errmsg = "failed to parse int in " + args[1]; + return nullptr; + } + + return StringValue(l_int > r_int ? "t" : ""); +} + +Value* Literal(const char* name, State* state, const std::vector>& argv) { + return StringValue(name); +} + +// ----------------------------------------------------------------- +// the function table +// ----------------------------------------------------------------- + +static std::unordered_map fn_table; + +void RegisterFunction(const std::string& name, Function fn) { + fn_table[name] = fn; +} + +Function FindFunction(const std::string& name) { + if (fn_table.find(name) == fn_table.end()) { + return nullptr; + } else { + return fn_table[name]; + } +} + +void RegisterBuiltins() { + RegisterFunction("ifelse", IfElseFn); + RegisterFunction("abort", AbortFn); + RegisterFunction("assert", AssertFn); + RegisterFunction("concat", ConcatFn); + RegisterFunction("is_substring", SubstringFn); + RegisterFunction("stdout", StdoutFn); + RegisterFunction("sleep", SleepFn); + + RegisterFunction("less_than_int", LessThanIntFn); + RegisterFunction("greater_than_int", GreaterThanIntFn); +} + + +// ----------------------------------------------------------------- +// convenience methods for functions +// ----------------------------------------------------------------- + +// Evaluate the expressions in argv, and put the results of strings in args. If any expression +// evaluates to nullptr, return false. Return true on success. +bool ReadArgs(State* state, const std::vector>& argv, + std::vector* args) { + return ReadArgs(state, argv, args, 0, argv.size()); +} + +bool ReadArgs(State* state, const std::vector>& argv, + std::vector* args, size_t start, size_t len) { + if (args == nullptr) { + return false; + } + if (start + len > argv.size()) { + return false; + } + for (size_t i = start; i < start + len; ++i) { + std::string var; + if (!Evaluate(state, argv[i], &var)) { + args->clear(); + return false; + } + args->push_back(var); + } + return true; +} + +// Evaluate the expressions in argv, and put the results of Value* in args. If any expression +// evaluate to nullptr, return false. Return true on success. +bool ReadValueArgs(State* state, const std::vector>& argv, + std::vector>* args) { + return ReadValueArgs(state, argv, args, 0, argv.size()); +} + +bool ReadValueArgs(State* state, const std::vector>& argv, + std::vector>* args, size_t start, size_t len) { + if (args == nullptr) { + return false; + } + if (len == 0 || start + len > argv.size()) { + return false; + } + for (size_t i = start; i < start + len; ++i) { + std::unique_ptr v(EvaluateValue(state, argv[i])); + if (!v) { + args->clear(); + return false; + } + args->push_back(std::move(v)); + } + return true; +} + +// Use printf-style arguments to compose an error message to put into +// *state. Returns nullptr. +Value* ErrorAbort(State* state, const char* format, ...) { + va_list ap; + va_start(ap, format); + android::base::StringAppendV(&state->errmsg, format, ap); + va_end(ap); + return nullptr; +} + +Value* ErrorAbort(State* state, CauseCode cause_code, const char* format, ...) { + std::string err_message; + va_list ap; + va_start(ap, format); + android::base::StringAppendV(&err_message, format, ap); + va_end(ap); + // Ensure that there's exactly one line break at the end of the error message. + state->errmsg = android::base::Trim(err_message) + "\n"; + state->cause_code = cause_code; + return nullptr; +} + +State::State(const std::string& script, UpdaterInterface* interface) + : script(script), updater(interface), error_code(kNoError), cause_code(kNoCause) {} diff --git a/edify/include/edify/expr.h b/edify/include/edify/expr.h new file mode 100644 index 0000000..3ddf7f5 --- /dev/null +++ b/edify/include/edify/expr.h @@ -0,0 +1,159 @@ +/* + * 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. + */ + +#ifndef _EXPRESSION_H +#define _EXPRESSION_H + +#include + +#include +#include +#include + +#include "edify/updater_interface.h" + +// Forward declaration to avoid including "otautil/error_code.h". +enum ErrorCode : int; +enum CauseCode : int; + +struct State { + State(const std::string& script, UpdaterInterface* cookie); + + // The source of the original script. + const std::string& script; + + // A pointer to app-specific data; the libedify doesn't use this value. + UpdaterInterface* updater; + + // The error message (if any) returned if the evaluation aborts. + // Should be empty initially, will be either empty or a string that + // Evaluate() returns. + std::string errmsg; + + // error code indicates the type of failure (e.g. failure to update system image) + // during the OTA process. + ErrorCode error_code; + + // cause code provides more detailed reason of an OTA failure (e.g. fsync error) + // in addition to the error code. + CauseCode cause_code; + + bool is_retry = false; +}; + +struct Value { + enum class Type { + STRING = 1, + BLOB = 2, + }; + + Value(Type type, std::string str) : type(type), data(std::move(str)) {} + + Type type; + std::string data; +}; + +struct Expr; + +using Function = Value* (*)(const char* name, State* state, + const std::vector>& argv); + +struct Expr { + Function fn; + std::string name; + std::vector> argv; + int start, end; + + Expr(Function fn, const std::string& name, int start, int end) : + fn(fn), + name(name), + start(start), + end(end) {} +}; + +// Evaluate the input expr, return the resulting Value. +Value* EvaluateValue(State* state, const std::unique_ptr& expr); + +// Evaluate the input expr, assert that it is a string, and update the result parameter. This +// function returns true if the evaluation succeeds. This is a convenience function for older +// functions that want to deal only with strings. +bool Evaluate(State* state, const std::unique_ptr& expr, std::string* result); + +// Glue to make an Expr out of a literal. +Value* Literal(const char* name, State* state, const std::vector>& argv); + +// Functions corresponding to various syntactic sugar operators. +// ("concat" is also available as a builtin function, to concatenate +// more than two strings.) +Value* ConcatFn(const char* name, State* state, const std::vector>& argv); +Value* LogicalAndFn(const char* name, State* state, const std::vector>& argv); +Value* LogicalOrFn(const char* name, State* state, const std::vector>& argv); +Value* LogicalNotFn(const char* name, State* state, const std::vector>& argv); +Value* SubstringFn(const char* name, State* state, const std::vector>& argv); +Value* EqualityFn(const char* name, State* state, const std::vector>& argv); +Value* InequalityFn(const char* name, State* state, const std::vector>& argv); +Value* SequenceFn(const char* name, State* state, const std::vector>& argv); + +// Global builtins, registered by RegisterBuiltins(). +Value* IfElseFn(const char* name, State* state, const std::vector>& argv); +Value* AssertFn(const char* name, State* state, const std::vector>& argv); +Value* AbortFn(const char* name, State* state, const std::vector>& argv); + +// Register a new function. The same Function may be registered under +// multiple names, but a given name should only be used once. +void RegisterFunction(const std::string& name, Function fn); + +// Register all the builtins. +void RegisterBuiltins(); + +// Find the Function for a given name; return NULL if no such function +// exists. +Function FindFunction(const std::string& name); + +// --- convenience functions for use in functions --- + +// Evaluate the expressions in argv, and put the results of strings in args. If any expression +// evaluates to nullptr, return false. Return true on success. +bool ReadArgs(State* state, const std::vector>& argv, + std::vector* args); +bool ReadArgs(State* state, const std::vector>& argv, + std::vector* args, size_t start, size_t len); + +// Evaluate the expressions in argv, and put the results of Value* in args. If any +// expression evaluate to nullptr, return false. Return true on success. +bool ReadValueArgs(State* state, const std::vector>& argv, + std::vector>* args); +bool ReadValueArgs(State* state, const std::vector>& argv, + std::vector>* args, size_t start, size_t len); + +// Use printf-style arguments to compose an error message to put into +// *state. Returns NULL. +Value* ErrorAbort(State* state, const char* format, ...) + __attribute__((format(printf, 2, 3), deprecated)); + +// ErrorAbort has an optional (but recommended) argument 'cause_code'. If the cause code +// is set, it will be logged into last_install and provides reason of OTA failures. +Value* ErrorAbort(State* state, CauseCode cause_code, const char* format, ...) + __attribute__((format(printf, 3, 4))); + +// Copying the string into a Value. +Value* StringValue(const char* str); + +Value* StringValue(const std::string& str); + +int ParseString(const std::string& str, std::unique_ptr* root, int* error_count); + +#endif // _EXPRESSION_H diff --git a/edify/include/edify/updater_interface.h b/edify/include/edify/updater_interface.h new file mode 100644 index 0000000..aa977e3 --- /dev/null +++ b/edify/include/edify/updater_interface.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include + +struct ZipArchive; +typedef ZipArchive* ZipArchiveHandle; + +class UpdaterRuntimeInterface; + +class UpdaterInterface { + public: + virtual ~UpdaterInterface() = default; + + // Writes the message to command pipe, adds a new line in the end. + virtual void WriteToCommandPipe(const std::string_view message, bool flush = false) const = 0; + + // Sends over the message to recovery to print it on the screen. + virtual void UiPrint(const std::string_view message) const = 0; + + // Given the name of the block device, returns |name| for updates on the device; or the file path + // to the fake block device for simulations. + virtual std::string FindBlockDeviceName(const std::string_view name) const = 0; + + virtual UpdaterRuntimeInterface* GetRuntime() const = 0; + virtual ZipArchiveHandle GetPackageHandle() const = 0; + virtual std::string GetResult() const = 0; + virtual uint8_t* GetMappedPackageAddress() const = 0; + virtual size_t GetMappedPackageLength() const = 0; +}; diff --git a/edify/include/edify/updater_runtime_interface.h b/edify/include/edify/updater_runtime_interface.h new file mode 100644 index 0000000..bdd6aec --- /dev/null +++ b/edify/include/edify/updater_runtime_interface.h @@ -0,0 +1,77 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +// This class serves as the base to updater runtime. It wraps the runtime dependent functions; and +// updates on device and host simulations can have different implementations. e.g. block devices +// during host simulation merely a temporary file. With this class, the caller side in registered +// updater's functions will stay the same for both update and simulation. +class UpdaterRuntimeInterface { + public: + virtual ~UpdaterRuntimeInterface() = default; + + // Returns true if it's a runtime instance for simulation. + virtual bool IsSimulator() const = 0; + + // Returns the value of system property |key|. If the property doesn't exist, returns + // |default_value|. + virtual std::string GetProperty(const std::string_view key, + const std::string_view default_value) const = 0; + + // Given the name of the block device, returns |name| for updates on the device; or the file path + // to the fake block device for simulations. + virtual std::string FindBlockDeviceName(const std::string_view name) const = 0; + + // Mounts the |location| on |mount_point|. Returns 0 on success. + virtual int Mount(const std::string_view location, const std::string_view mount_point, + const std::string_view fs_type, const std::string_view mount_options) = 0; + + // Returns true if |mount_point| is mounted. + virtual bool IsMounted(const std::string_view mount_point) const = 0; + + // Unmounts the |mount_point|. Returns a pair of results with the first value indicating + // if the |mount_point| is mounted, and the second value indicating the result of umount(2). + virtual std::pair Unmount(const std::string_view mount_point) = 0; + + // Reads |filename| and puts its value to |content|. + virtual bool ReadFileToString(const std::string_view filename, std::string* content) const = 0; + + // Updates the content of |filename| with |content|. + virtual bool WriteStringToFile(const std::string_view content, + const std::string_view filename) const = 0; + + // Wipes the first |len| bytes of block device in |filename|. + virtual int WipeBlockDevice(const std::string_view filename, size_t len) const = 0; + + // Starts a child process and runs the program with |args|. Uses vfork(2) if |is_vfork| is true. + virtual int RunProgram(const std::vector& args, bool is_vfork) const = 0; + + // Runs tune2fs with arguments |args|. + virtual int Tune2Fs(const std::vector& args) const = 0; + + // Dynamic partition related functions. + virtual bool MapPartitionOnDeviceMapper(const std::string& partition_name, std::string* path) = 0; + virtual bool UnmapPartitionOnDeviceMapper(const std::string& partition_name) = 0; + virtual bool UpdateDynamicPartitions(const std::string_view op_list_value) = 0; + + // On devices supports A/B, add current slot suffix to arg. Otherwise, return |arg| as is. + virtual std::string AddSlotSuffix(const std::string_view arg) const = 0; +}; diff --git a/edify/lexer.ll b/edify/lexer.ll new file mode 100644 index 0000000..4e04003 --- /dev/null +++ b/edify/lexer.ll @@ -0,0 +1,112 @@ +%{ +/* + * 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 +#include + +#include "edify/expr.h" +#include "yydefs.h" +#include "parser.h" + +int gLine = 1; +int gColumn = 1; +int gPos = 0; + +std::string string_buffer; + +#define ADVANCE do {yylloc.start=gPos; yylloc.end=gPos+yyleng; \ + gColumn+=yyleng; gPos+=yyleng;} while(0) + +%} + +%x STR + +%option noinput +%option nounput +%option noyywrap + +%% + + +\" { + BEGIN(STR); + string_buffer.clear(); + yylloc.start = gPos; + ++gColumn; + ++gPos; +} + +{ + \" { + ++gColumn; + ++gPos; + BEGIN(INITIAL); + yylval.str = strdup(string_buffer.c_str()); + yylloc.end = gPos; + return STRING; + } + + \\n { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\n'); } + \\t { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\t'); } + \\\" { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\"'); } + \\\\ { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\\'); } + + \\x[0-9a-fA-F]{2} { + gColumn += yyleng; + gPos += yyleng; + int val; + sscanf(yytext+2, "%x", &val); + string_buffer.push_back(static_cast(val)); + } + + \n { + ++gLine; + ++gPos; + gColumn = 1; + string_buffer.push_back(yytext[0]); + } + + . { + ++gColumn; + ++gPos; + string_buffer.push_back(yytext[0]); + } +} + +if ADVANCE; return IF; +then ADVANCE; return THEN; +else ADVANCE; return ELSE; +endif ADVANCE; return ENDIF; + +[a-zA-Z0-9_:/.]+ { + ADVANCE; + yylval.str = strdup(yytext); + return STRING; +} + +\&\& ADVANCE; return AND; +\|\| ADVANCE; return OR; +== ADVANCE; return EQ; +!= ADVANCE; return NE; + +[+(),!;] ADVANCE; return yytext[0]; + +[ \t]+ ADVANCE; + +(#.*)?\n gPos += yyleng; ++gLine; gColumn = 1; + +. return BAD; diff --git a/edify/parser.yy b/edify/parser.yy new file mode 100644 index 0000000..5e1e847 --- /dev/null +++ b/edify/parser.yy @@ -0,0 +1,145 @@ +%{ +/* + * 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 +#include +#include +#include + +#include +#include +#include + +#include + +#include "edify/expr.h" +#include "yydefs.h" +#include "parser.h" + +extern int gLine; +extern int gColumn; + +void yyerror(std::unique_ptr* root, int* error_count, const char* s); +int yyparse(std::unique_ptr* root, int* error_count); + +struct yy_buffer_state; +void yy_switch_to_buffer(struct yy_buffer_state* new_buffer); +struct yy_buffer_state* yy_scan_string(const char* yystr); + +// Convenience function for building expressions with a fixed number +// of arguments. +static Expr* Build(Function fn, YYLTYPE loc, size_t count, ...) { + va_list v; + va_start(v, count); + Expr* e = new Expr(fn, "(operator)", loc.start, loc.end); + for (size_t i = 0; i < count; ++i) { + e->argv.emplace_back(va_arg(v, Expr*)); + } + va_end(v); + return e; +} + +%} + +%locations + +%union { + char* str; + Expr* expr; + std::vector>* args; +} + +%token AND OR SUBSTR SUPERSTR EQ NE IF THEN ELSE ENDIF +%token STRING BAD +%type expr +%type arglist + +%destructor { delete $$; } expr +%destructor { delete $$; } arglist + +%parse-param {std::unique_ptr* root} +%parse-param {int* error_count} +%define parse.error verbose + +/* declarations in increasing order of precedence */ +%left ';' +%left ',' +%left OR +%left AND +%left EQ NE +%left '+' +%right '!' + +%% + +input: expr { root->reset($1); } +; + +expr: STRING { + $$ = new Expr(Literal, $1, @$.start, @$.end); +} +| '(' expr ')' { $$ = $2; $$->start=@$.start; $$->end=@$.end; } +| expr ';' { $$ = $1; $$->start=@1.start; $$->end=@1.end; } +| expr ';' expr { $$ = Build(SequenceFn, @$, 2, $1, $3); } +| error ';' expr { $$ = $3; $$->start=@$.start; $$->end=@$.end; } +| expr '+' expr { $$ = Build(ConcatFn, @$, 2, $1, $3); } +| expr EQ expr { $$ = Build(EqualityFn, @$, 2, $1, $3); } +| expr NE expr { $$ = Build(InequalityFn, @$, 2, $1, $3); } +| expr AND expr { $$ = Build(LogicalAndFn, @$, 2, $1, $3); } +| expr OR expr { $$ = Build(LogicalOrFn, @$, 2, $1, $3); } +| '!' expr { $$ = Build(LogicalNotFn, @$, 1, $2); } +| IF expr THEN expr ENDIF { $$ = Build(IfElseFn, @$, 2, $2, $4); } +| IF expr THEN expr ELSE expr ENDIF { $$ = Build(IfElseFn, @$, 3, $2, $4, $6); } +| STRING '(' arglist ')' { + Function fn = FindFunction($1); + if (fn == nullptr) { + std::string msg = "unknown function \"" + std::string($1) + "\""; + yyerror(root, error_count, msg.c_str()); + YYERROR; + } + $$ = new Expr(fn, $1, @$.start, @$.end); + $$->argv = std::move(*$3); +} +; + +arglist: /* empty */ { + $$ = new std::vector>; +} +| expr { + $$ = new std::vector>; + $$->emplace_back($1); +} +| arglist ',' expr { + UNUSED($1); + $$->push_back(std::unique_ptr($3)); +} +; + +%% + +void yyerror(std::unique_ptr* root, int* error_count, const char* s) { + if (strlen(s) == 0) { + s = "syntax error"; + } + printf("line %d col %d: %s\n", gLine, gColumn, s); + ++*error_count; +} + +int ParseString(const std::string& str, std::unique_ptr* root, int* error_count) { + yy_switch_to_buffer(yy_scan_string(str.c_str())); + return yyparse(root, error_count); +} diff --git a/edify/yydefs.h b/edify/yydefs.h new file mode 100644 index 0000000..aca398f --- /dev/null +++ b/edify/yydefs.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#ifndef _YYDEFS_H_ +#define _YYDEFS_H_ + +#define YYLTYPE YYLTYPE +typedef struct { + int start, end; +} YYLTYPE; + +#define YYLLOC_DEFAULT(Current, Rhs, N) \ + do { \ + if (N) { \ + (Current).start = YYRHSLOC(Rhs, 1).start; \ + (Current).end = YYRHSLOC(Rhs, N).end; \ + } else { \ + (Current).start = YYRHSLOC(Rhs, 0).start; \ + (Current).end = YYRHSLOC(Rhs, 0).end; \ + } \ + } while (0) + +int yylex(); + +#endif diff --git a/tests/Android.bp b/tests/Android.bp new file mode 100644 index 0000000..f805db2 --- /dev/null +++ b/tests/Android.bp @@ -0,0 +1,133 @@ +// Copyright (C) 2024 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. + + +cc_test_host { + name: "recovery_host_test", + isolated: true, + + include_dirs: [ + "bootable/deprecated-ota", + "bootable/recovery/tests", + ], + + defaults: [ + "recovery_test_defaults", + "libupdater_defaults", + ], + + tidy_timeout_srcs: [ + "unit/host/imgdiff_test.cpp", + ], + + srcs: [ + "unit/host/*", + ], + + static_libs: [ + "libupdater_host", + "libupdater_core", + "libimgdiff", + "libbsdiff", + "libdivsufsort64", + "libdivsufsort", + "libfstab", + "libc++fs", + ], + + test_suites: ["general-tests"], + test_config: "RecoveryHostTest.xml", + + data: ["testdata/*"], + + target: { + darwin: { + // libapplypatch in "libupdater_defaults" is not available on the Mac. + enabled: false, + }, + }, +} + +// libapplypatch, libapplypatch_modes +libapplypatch_static_libs = [ + "libapplypatch_modes", + "libapplypatch", + "libedify", + "libotautil", + "libbsdiff", + "libbspatch", + "libdivsufsort", + "libdivsufsort64", + "libutils", + "libbase", + "libbrotli", + "libbz", + "libz_stable", + "libziparchive", +] + +cc_test { + name: "non_ab_unit_tests", + isolated: true, + require_root: true, + include_dirs: [ + "bootable/deprecated-ota", + "bootable/recovery/tests", + ], + + defaults: [ + "recovery_test_defaults", + "libupdater_defaults", + "libupdater_device_defaults", + ], + + test_suites: ["device-tests"], + + tidy_timeout_srcs: [ + "unit/commands_test.cpp", + ], + + srcs: [ + "unit/*.cpp", + ], + + shared_libs: [ + "libbinder_ndk", + ], + + static_libs: libapplypatch_static_libs + [ + "android.hardware.health-translate-ndk", + "android.hardware.health-V3-ndk", + "libhealthshim", + "librecovery_ui", + "libfusesideload", + "libminui", + "librecovery_utils", + "libotautil", + "libupdater_device", + "libupdater_core", + "libupdate_verifier", + + "libprotobuf-cpp-lite", + ], + header_libs: [ + "libgtest_prod_headers", + ], + + data: [ + "testdata/*", + ":recovery_image", + ":res-testdata", + ], +} diff --git a/tests/RecoveryHostTest.xml b/tests/RecoveryHostTest.xml new file mode 100644 index 0000000..0ac75e4 --- /dev/null +++ b/tests/RecoveryHostTest.xml @@ -0,0 +1,28 @@ + + + + diff --git a/tests/testdata/deflate_src.zip b/tests/testdata/deflate_src.zip new file mode 100644 index 0000000..bdb2b32 Binary files /dev/null and b/tests/testdata/deflate_src.zip differ diff --git a/tests/testdata/deflate_tgt.zip b/tests/testdata/deflate_tgt.zip new file mode 100644 index 0000000..2a21760 Binary files /dev/null and b/tests/testdata/deflate_tgt.zip differ diff --git a/tests/testdata/gzipped_source b/tests/testdata/gzipped_source new file mode 100644 index 0000000..6d425d0 Binary files /dev/null and b/tests/testdata/gzipped_source differ diff --git a/tests/testdata/gzipped_target b/tests/testdata/gzipped_target new file mode 100644 index 0000000..5621262 Binary files /dev/null and b/tests/testdata/gzipped_target differ diff --git a/tests/unit/applypatch_modes_test.cpp b/tests/unit/applypatch_modes_test.cpp new file mode 100644 index 0000000..08414b7 --- /dev/null +++ b/tests/unit/applypatch_modes_test.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agree to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#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(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& 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 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 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 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(src_content.data()), src_content.size(), + reinterpret_cast(tgt_content.data()), tgt_content.size(), + patch_file.path, nullptr)); + + std::vector 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 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 args{ + "applypatch", "--flash", from_testdata_base("recovery.img"), "--target", target, + }; + ASSERT_EQ(0, InvokeApplyPatchModes(args)); + VerifyPatchedTarget(target); +} + +TEST_F(ApplyPatchModesTest, FlashModeInvalidArgs) { + std::vector 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 0000000..218a224 --- /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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "applypatch/applypatch.h" +#include "common/test_constants.h" +#include "edify/expr.h" +#include "otautil/paths.h" +#include "otautil/print_sha1.h" + +using namespace std::string_literals; + +class ApplyPatchTest : public ::testing::Test { + protected: + void SetUp() override { + source_file = from_testdata_base("boot.img"); + FileContents boot_fc; + ASSERT_TRUE(LoadFileContents(source_file, &boot_fc)); + source_size = boot_fc.data.size(); + source_sha1 = print_sha1(boot_fc.sha1); + + target_file = from_testdata_base("recovery.img"); + FileContents recovery_fc; + ASSERT_TRUE(LoadFileContents(target_file, &recovery_fc)); + target_size = recovery_fc.data.size(); + target_sha1 = print_sha1(recovery_fc.sha1); + + source_partition = Partition(source_file, source_size, source_sha1); + target_partition = Partition(partition_file.path, target_size, target_sha1); + + srand(time(nullptr)); + bad_sha1_a = android::base::StringPrintf("%040x", rand()); + bad_sha1_b = android::base::StringPrintf("%040x", rand()); + + // Reset the cache backup file. + Paths::Get().set_cache_temp_source(cache_temp_source.path); + } + + void TearDown() override { + ASSERT_TRUE(android::base::RemoveFileIfExists(cache_temp_source.path)); + } + + std::string source_file; + std::string source_sha1; + size_t source_size; + + std::string target_file; + std::string target_sha1; + size_t target_size; + + std::string bad_sha1_a; + std::string bad_sha1_b; + + Partition source_partition; + Partition target_partition; + + private: + TemporaryFile partition_file; + TemporaryFile cache_temp_source; +}; + +TEST_F(ApplyPatchTest, CheckPartition) { + ASSERT_TRUE(CheckPartition(source_partition)); +} + +TEST_F(ApplyPatchTest, CheckPartition_Mismatching) { + ASSERT_FALSE(CheckPartition(Partition(source_file, target_size, target_sha1))); + ASSERT_FALSE(CheckPartition(Partition(source_file, source_size, bad_sha1_a))); + + ASSERT_FALSE(CheckPartition(Partition(source_file, source_size - 1, source_sha1))); + ASSERT_FALSE(CheckPartition(Partition(source_file, source_size + 1, source_sha1))); +} + +TEST_F(ApplyPatchTest, PatchPartitionCheck) { + ASSERT_TRUE(PatchPartitionCheck(target_partition, source_partition)); + + ASSERT_TRUE( + PatchPartitionCheck(Partition(source_file, source_size - 1, source_sha1), source_partition)); + + ASSERT_TRUE( + PatchPartitionCheck(Partition(source_file, source_size + 1, source_sha1), source_partition)); +} + +TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup) { + ASSERT_FALSE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); + + Paths::Get().set_cache_temp_source(source_file); + ASSERT_TRUE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); +} + +TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup_BothCorrupted) { + ASSERT_FALSE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); + + Paths::Get().set_cache_temp_source(target_file); + ASSERT_FALSE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); +} + +TEST_F(ApplyPatchTest, PatchPartition) { + FileContents patch_fc; + ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot.p"), &patch_fc)); + Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend())); + + FileContents bonus_fc; + ASSERT_TRUE(LoadFileContents(from_testdata_base("bonus.file"), &bonus_fc)); + Value bonus(Value::Type::BLOB, std::string(bonus_fc.data.cbegin(), bonus_fc.data.cend())); + + ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, &bonus, false)); +} + +// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has +// everything). +TEST_F(ApplyPatchTest, PatchPartitionWithoutBonusFile) { + FileContents patch_fc; + ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot-with-bonus.p"), &patch_fc)); + Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend())); + + ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, nullptr, false)); +} + +class FreeCacheTest : public ::testing::Test { + protected: + static constexpr size_t PARTITION_SIZE = 4096 * 10; + + // Returns a sorted list of files in |dirname|. + static std::vector FindFilesInDir(const std::string& dirname) { + std::vector file_list; + + std::unique_ptr 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& 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 files = FindFilesInDir(dirname); + return PARTITION_SIZE - 4096 * files.size(); + } + + TemporaryDir mock_cache; + TemporaryDir mock_log_dir; + + private: + std::vector temporary_files_; +}; + +TEST_F(FreeCacheTest, FreeCacheSmoke) { + std::vector 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{ "file3" }, FindFilesInDir(mock_cache.path)); + ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_cache.path)); +} + +TEST_F(FreeCacheTest, FreeCacheFreeSpaceCheckerError) { + std::vector 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 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{ "file1" }, FindFilesInDir(mock_cache.path)); +} + +TEST_F(FreeCacheTest, FreeCacheLogsSmoke) { + std::vector 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 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 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 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 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 expected = { "block.map", "command", "last_install" }; + ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path)); +} diff --git a/tests/unit/commands_test.cpp b/tests/unit/commands_test.cpp new file mode 100644 index 0000000..8a54df7 --- /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 +#include + +#include +#include +#include + +#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 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 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 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 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 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 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(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 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* block_buffer) -> int { + std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a'); + return 0; + }; + auto stash_reader = [](const std::string&, std::vector*) -> int { return 0; }; + std::vector 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* block_buffer) -> int { + std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a'); + return 0; + }; + auto stash_reader = [](const std::string&, std::vector* stash_buffer) -> int { + std::fill_n(stash_buffer->begin(), kBlockSize, 'b'); + return 0; + }; + std::vector 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*) -> int { return 0; }; + auto stash_reader = [](const std::string&, std::vector*) -> int { return 0; }; + std::vector 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 buffer(source.blocks() * kBlockSize); + auto failing_block_reader = [](const RangeSet&, std::vector*) -> int { return -1; }; + auto stash_reader = [](const std::string&, std::vector*) -> int { return 0; }; + ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, failing_block_reader, stash_reader)); + + auto block_reader = [](const RangeSet&, std::vector*) -> int { return 0; }; + auto failing_stash_reader = [](const std::string&, std::vector*) -> int { return -1; }; + ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, failing_stash_reader)); +} + +TEST(TransferListTest, Parse) { + std::vector 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(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 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(transfer_list)); +} + +TEST(TransferListTest, Parse_ZeroTotalBlocks) { + std::vector 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(transfer_list)); + ASSERT_EQ(4, transfer_list.version()); + ASSERT_EQ(0, transfer_list.total_blocks()); + ASSERT_EQ(0, transfer_list.stash_max_entries()); + ASSERT_EQ(0, transfer_list.stash_max_blocks()); + ASSERT_TRUE(transfer_list.commands().empty()); +} diff --git a/tests/unit/edify_test.cpp b/tests/unit/edify_test.cpp new file mode 100644 index 0000000..8397bd3 --- /dev/null +++ b/tests/unit/edify_test.cpp @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include "edify/expr.h" + +static void expect(const std::string& expr_str, const char* expected) { + std::unique_ptr 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; + int error_count = 0; + EXPECT_EQ(1, ParseString(script1, &expr, &error_count)); + EXPECT_EQ(1, error_count); + + const char* script2 = "abc; unknown_function()"; + error_count = 0; + EXPECT_EQ(1, ParseString(script2, &expr, &error_count)); + EXPECT_EQ(1, error_count); + + const char* script3 = "unknown_function1() || yes"; + error_count = 0; + EXPECT_EQ(1, ParseString(script3, &expr, &error_count)); + EXPECT_EQ(1, error_count); +} diff --git a/tests/unit/host/imgdiff_test.cpp b/tests/unit/host/imgdiff_test.cpp new file mode 100644 index 0000000..ddc397d --- /dev/null +++ b/tests/unit/host/imgdiff_test.cpp @@ -0,0 +1,1113 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/test_constants.h" + +using android::base::get_unaligned; + +static void verify_patch_header(const std::string& patch, size_t* num_normal, size_t* num_raw, + size_t* num_deflate) { + const size_t size = patch.size(); + const char* data = patch.data(); + + ASSERT_GE(size, 12U); + ASSERT_EQ("IMGDIFF2", std::string(data, 8)); + + const int num_chunks = get_unaligned(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(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(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(src.data()), src.size(), + reinterpret_cast(patch.data()), patch.size(), + [&](const unsigned char* data, size_t len) { + patched->append(reinterpret_cast(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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(src.data()), src.size(), + reinterpret_cast(patch.data()), patch.size(), + [](const unsigned char* /*data*/, size_t len) { return len; })); +} + +static void construct_store_entry(const std::vector>& 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>& 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 original 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 ConstructImageChunks( + const std::vector& content, const std::vector>& info) { + std::vector 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 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 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(content.begin(), content.begin() + 82520)); + + tgt_image.DumpChunks(); + + ZipModeImage src_image(true, 4096 * 10); + std::vector 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(content.begin(), content.begin() + 137880)); + + std::vector split_tgt_images; + std::vector split_src_images; + std::vector 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(4), split_tgt_images.size()); + + ASSERT_EQ(static_cast(1), split_tgt_images[0].NumOfChunks()); + ASSERT_EQ(static_cast(12288), split_tgt_images[0][0].DataLengthForPatch()); + ASSERT_EQ("4,0,3,15,20", split_src_ranges[0].ToString()); + + ASSERT_EQ(static_cast(1), split_tgt_images[1].NumOfChunks()); + ASSERT_EQ(static_cast(12288), split_tgt_images[1][0].DataLengthForPatch()); + ASSERT_EQ("2,3,13", split_src_ranges[1].ToString()); + + ASSERT_EQ(static_cast(1), split_tgt_images[2].NumOfChunks()); + ASSERT_EQ(static_cast(40960), split_tgt_images[2][0].DataLengthForPatch()); + ASSERT_EQ("2,20,30", split_src_ranges[2].ToString()); + + ASSERT_EQ(static_cast(1), split_tgt_images[3].NumOfChunks()); + ASSERT_EQ(static_cast(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 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 split_tgt_images; + std::vector split_src_images; + std::vector 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(5), split_tgt_images.size()); + + ASSERT_EQ(static_cast(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(1), split_src_images[1].NumOfChunks()); + ASSERT_EQ("c", split_src_images[1][0].GetEntryName()); + + ASSERT_EQ(static_cast(0), split_src_images[2].NumOfChunks()); + ASSERT_EQ(static_cast(0), split_src_images[3].NumOfChunks()); + ASSERT_EQ(static_cast(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 info_list = + android::base::Split(android::base::Trim(split_info_string), "\n"); + + ASSERT_EQ(static_cast(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(160385), tgt.size()); + std::vector 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 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 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 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 args = { + "imgdiff", "-z", "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(), + src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + + // Expect two split src images: + // src_piece 0: a-0 10 blocks + // src_piece 1: a-0 1 block, CD + GenerateAndCheckSplitTarget(debug_dir.path, 2, tgt); +} diff --git a/tests/unit/host/update_simulator_test.cpp b/tests/unit/host/update_simulator_test.cpp new file mode 100644 index 0000000..1603982 --- /dev/null +++ b/tests/unit/host/update_simulator_test.cpp @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "otautil/paths.h" +#include "otautil/print_sha1.h" +#include "updater/blockimg.h" +#include "updater/build_info.h" +#include "updater/install.h" +#include "updater/simulator_runtime.h" +#include "updater/target_files.h" +#include "updater/updater.h" + +using std::string; + +// echo -n "system.img" > system.img && img2simg system.img sparse_system_string_.img 4096 && +// hexdump -v -e '" " 12/1 "0x%02x, " "\n"' sparse_system_string_.img +// The total size of the result sparse image is 4136 bytes; and we can append 0s in the end to get +// the full image. +constexpr uint8_t SPARSE_SYSTEM_HEADER[] = { + 0x3a, 0xff, 0x26, 0xed, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, 0xca, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x10, 0x00, 0x00, 0x73, 0x79, 0x73, 0x74, 0x65, + 0x6d, 0x2e, 0x69, 0x6d, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static void AddZipEntries(int fd, const std::map& entries) { + FILE* zip_file = fdopen(fd, "w"); + ZipWriter writer(zip_file); + for (const auto& pair : entries) { + ASSERT_EQ(0, writer.StartEntry(pair.first.c_str(), 0)); + ASSERT_EQ(0, writer.WriteBytes(pair.second.data(), pair.second.size())); + ASSERT_EQ(0, writer.FinishEntry()); + } + ASSERT_EQ(0, writer.Finish()); + ASSERT_EQ(0, fclose(zip_file)); +} + +static string CalculateSha1(const string& data) { + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast(data.c_str()), data.size(), digest); + return print_sha1(digest); +} + +static void CreateBsdiffPatch(const string& src, const string& tgt, string* patch) { + TemporaryFile patch_file; + ASSERT_EQ(0, bsdiff::bsdiff(reinterpret_cast(src.data()), src.size(), + reinterpret_cast(tgt.data()), tgt.size(), + patch_file.path, nullptr)); + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, patch)); +} + +static void RunSimulation(std::string_view src_tf, std::string_view ota_package, bool expected) { + TemporaryFile cmd_pipe; + TemporaryFile temp_saved_source; + TemporaryFile temp_last_command; + TemporaryDir temp_stash_base; + + Paths::Get().set_cache_temp_source(temp_saved_source.path); + Paths::Get().set_last_command_file(temp_last_command.path); + Paths::Get().set_stash_directory_base(temp_stash_base.path); + + // Configure edify's functions. + RegisterBuiltins(); + RegisterInstallFunctions(); + RegisterBlockImageFunctions(); + + // Run the update simulation and check the result. + TemporaryDir work_dir; + BuildInfo build_info(work_dir.path, false); + ASSERT_TRUE(build_info.ParseTargetFile(src_tf, false)); + Updater updater(std::make_unique(&build_info)); + ASSERT_TRUE(updater.Init(cmd_pipe.release(), ota_package, false)); + ASSERT_EQ(expected, updater.RunUpdate()); + // TODO(xunchang) check the recovery&system has the expected contents. +} + +class DISABLED_UpdateSimulatorTest : public ::testing::Test { + protected: + void SetUp() override { + std::vector props = { + "import /oem/oem.prop oem*", + "# begin build properties", + "# autogenerated by buildinfo.sh", + "ro.build.id=OPR1.170510.001", + "ro.build.display.id=OPR1.170510.001 dev-keys", + "ro.build.version.incremental=3993052", + "ro.build.version.release=O", + "ro.build.date=Wed May 10 11:10:29 UTC 2017", + "ro.build.date.utc=1494414629", + "ro.build.type=user", + "ro.build.tags=dev-keys", + "ro.build.flavor=angler-user", + "ro.product.system.brand=google", + "ro.product.system.name=angler", + "ro.product.system.device=angler", + }; + build_prop_string_ = android::base::Join(props, "\n"); + + fstab_content_ = R"( +# +# More comments..... + +/dev/block/by-name/system /system ext4 ro,barrier=1 wait +/dev/block/by-name/vendor /vendor ext4 ro wait,verify=/dev/metadata +/dev/block/by-name/cache /cache ext4 noatime,errors=panic wait,check +/dev/block/by-name/modem /firmware vfat ro,uid=1000,gid=1000, wait +/dev/block/by-name/boot /boot emmc defaults defaults +/dev/block/by-name/recovery /recovery emmc defaults defaults +/dev/block/by-name/misc /misc emmc defaults +/dev/block/by-name/modem /modem emmc defaults defaults)"; + + raw_system_string_ = "system.img" + string(4086, '\0'); // raw image is 4096 bytes in total + sparse_system_string_ = string(SPARSE_SYSTEM_HEADER, std::end(SPARSE_SYSTEM_HEADER)) + + string(4136 - sizeof(SPARSE_SYSTEM_HEADER), '\0'); + } + + string build_prop_string_; + string fstab_content_; + string raw_system_string_; + string sparse_system_string_; +}; + +TEST_F(DISABLED_UpdateSimulatorTest, TargetFile_ExtractImage) { + TemporaryFile zip_file; + AddZipEntries(zip_file.release(), { { "META/misc_info.txt", "extfs_sparse_flag=-s" }, + { "IMAGES/system.img", sparse_system_string_ } }); + TargetFile target_file(zip_file.path, false); + ASSERT_TRUE(target_file.Open()); + + TemporaryDir temp_dir; + TemporaryFile raw_image; + ASSERT_TRUE(target_file.ExtractImage( + "IMAGES/system.img", FstabInfo("/dev/system", "system", "ext4"), temp_dir.path, &raw_image)); + + // Check the raw image has expected contents. + string content; + ASSERT_TRUE(android::base::ReadFileToString(raw_image.path, &content)); + string expected_content = "system.img" + string(4086, '\0'); + ASSERT_EQ(expected_content, content); +} + +TEST_F(DISABLED_UpdateSimulatorTest, TargetFile_ParseFstabInfo) { + TemporaryFile zip_file; + AddZipEntries(zip_file.release(), + { { "META/misc_info.txt", "" }, + { "RECOVERY/RAMDISK/system/etc/recovery.fstab", fstab_content_ } }); + TargetFile target_file(zip_file.path, false); + ASSERT_TRUE(target_file.Open()); + + std::vector fstab_info; + EXPECT_TRUE(target_file.ParseFstabInfo(&fstab_info)); + + std::vector> transformed; + std::transform(fstab_info.begin(), fstab_info.end(), std::back_inserter(transformed), + [](const FstabInfo& info) { + return std::vector{ info.blockdev_name, info.mount_point, info.fs_type }; + }); + + std::vector> expected = { + { "/dev/block/by-name/system", "/system", "ext4" }, + { "/dev/block/by-name/vendor", "/vendor", "ext4" }, + { "/dev/block/by-name/cache", "/cache", "ext4" }, + { "/dev/block/by-name/boot", "/boot", "emmc" }, + { "/dev/block/by-name/recovery", "/recovery", "emmc" }, + { "/dev/block/by-name/misc", "/misc", "emmc" }, + { "/dev/block/by-name/modem", "/modem", "emmc" }, + }; + EXPECT_EQ(expected, transformed); +} + +TEST_F(DISABLED_UpdateSimulatorTest, BuildInfo_ParseTargetFile) { + std::map entries = { + { "META/misc_info.txt", "" }, + { "SYSTEM/build.prop", build_prop_string_ }, + { "RECOVERY/RAMDISK/system/etc/recovery.fstab", fstab_content_ }, + { "IMAGES/recovery.img", "" }, + { "IMAGES/boot.img", "" }, + { "IMAGES/misc.img", "" }, + { "IMAGES/system.map", "" }, + { "IMAGES/system.img", sparse_system_string_ }, + }; + + TemporaryFile zip_file; + AddZipEntries(zip_file.release(), entries); + + TemporaryDir temp_dir; + BuildInfo build_info(temp_dir.path, false); + ASSERT_TRUE(build_info.ParseTargetFile(zip_file.path, false)); + + std::map expected_result = { + { "ro.build.id", "OPR1.170510.001" }, + { "ro.build.display.id", "OPR1.170510.001 dev-keys" }, + { "ro.build.version.incremental", "3993052" }, + { "ro.build.version.release", "O" }, + { "ro.build.date", "Wed May 10 11:10:29 UTC 2017" }, + { "ro.build.date.utc", "1494414629" }, + { "ro.build.type", "user" }, + { "ro.build.tags", "dev-keys" }, + { "ro.build.flavor", "angler-user" }, + { "ro.product.brand", "google" }, + { "ro.product.name", "angler" }, + { "ro.product.device", "angler" }, + }; + + for (const auto& [key, value] : expected_result) { + ASSERT_EQ(value, build_info.GetProperty(key, "")); + } + + // Check that the temp files for each block device are created successfully. + for (auto name : { "/dev/block/by-name/system", "/dev/block/by-name/recovery", + "/dev/block/by-name/boot", "/dev/block/by-name/misc" }) { + ASSERT_EQ(0, access(build_info.FindBlockDeviceName(name).c_str(), R_OK)); + } +} + +TEST_F(DISABLED_UpdateSimulatorTest, RunUpdateSmoke) { + string recovery_img_string = "recovery.img"; + string boot_img_string = "boot.img"; + + std::map src_entries{ + { "META/misc_info.txt", "extfs_sparse_flag=-s" }, + { "RECOVERY/RAMDISK/etc/recovery.fstab", fstab_content_ }, + { "SYSTEM/build.prop", build_prop_string_ }, + { "IMAGES/recovery.img", "" }, + { "IMAGES/boot.img", boot_img_string }, + { "IMAGES/system.img", sparse_system_string_ }, + }; + + // Construct the source target-files. + TemporaryFile src_tf; + AddZipEntries(src_tf.release(), src_entries); + + string recovery_from_boot; + CreateBsdiffPatch(boot_img_string, recovery_img_string, &recovery_from_boot); + + // Set up the apply patch commands to patch the recovery image. + string recovery_sha1 = CalculateSha1(recovery_img_string); + string boot_sha1 = CalculateSha1(boot_img_string); + string apply_patch_source_string = android::base::StringPrintf( + "EMMC:/dev/block/by-name/boot:%zu:%s", boot_img_string.size(), boot_sha1.c_str()); + string apply_patch_target_string = android::base::StringPrintf( + "EMMC:/dev/block/by-name/recovery:%zu:%s", recovery_img_string.size(), recovery_sha1.c_str()); + string check_command = android::base::StringPrintf( + R"(patch_partition_check("%s", "%s") || abort("check failed");)", + apply_patch_target_string.c_str(), apply_patch_source_string.c_str()); + string patch_command = android::base::StringPrintf( + R"(patch_partition("%s", "%s", package_extract_file("patch.p")) || abort("patch failed");)", + apply_patch_target_string.c_str(), apply_patch_source_string.c_str()); + + // Add the commands to update the system image. Test common commands: + // * getprop + // * ui_print + // * patch_partition + // * package_extract_file (single argument) + // * block_image_verify, block_image_update + string tgt_system_string = string(4096, 'a'); + string system_patch; + CreateBsdiffPatch(raw_system_string_, tgt_system_string, &system_patch); + + string tgt_system_hash = CalculateSha1(tgt_system_string); + string src_system_hash = CalculateSha1(raw_system_string_); + + std::vector transfer_list = { + "4", + "1", + "0", + "0", + android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,1 1 2,0,1", system_patch.size(), + src_system_hash.c_str(), tgt_system_hash.c_str()), + }; + + // Construct the updater_script. + std::vector updater_commands = { + R"(getprop("ro.product.device") == "angler" || abort("This package is for \"angler\"");)", + R"(ui_print("Source: angler/OPR1.170510.001");)", + check_command, + patch_command, + R"(block_image_verify("/dev/block/by-name/system", )" + R"(package_extract_file("system.transfer.list"), "system.new.dat", "system.patch.dat") || )" + R"(abort("Failed to verify system.");)", + R"(block_image_update("/dev/block/by-name/system", )" + R"(package_extract_file("system.transfer.list"), "system.new.dat", "system.patch.dat") || )" + R"(abort("Failed to verify system.");)", + }; + string updater_script = android::base::Join(updater_commands, '\n'); + + // Construct the ota update package. + std::map ota_entries{ + { "system.new.dat", "" }, + { "system.patch.dat", system_patch }, + { "system.transfer.list", android::base::Join(transfer_list, '\n') }, + { "META-INF/com/google/android/updater-script", updater_script }, + { "patch.p", recovery_from_boot }, + }; + + TemporaryFile ota_package; + AddZipEntries(ota_package.release(), ota_entries); + + RunSimulation(src_tf.path, ota_package.path, true); +} + +TEST_F(DISABLED_UpdateSimulatorTest, RunUpdateUnrecognizedFunction) { + std::map src_entries{ + { "META/misc_info.txt", "extfs_sparse_flag=-s" }, + { "IMAGES/system.img", sparse_system_string_ }, + { "RECOVERY/RAMDISK/etc/recovery.fstab", fstab_content_ }, + { "SYSTEM/build.prop", build_prop_string_ }, + }; + + TemporaryFile src_tf; + AddZipEntries(src_tf.release(), src_entries); + + std::map ota_entries{ + { "system.new.dat", "" }, + { "system.patch.dat", "" }, + { "system.transfer.list", "" }, + { "META-INF/com/google/android/updater-script", R"(bad_function("");)" }, + }; + + TemporaryFile ota_package; + AddZipEntries(ota_package.release(), ota_entries); + + RunSimulation(src_tf.path, ota_package.path, false); +} + +TEST_F(DISABLED_UpdateSimulatorTest, RunUpdateApplyPatchFailed) { + string recovery_img_string = "recovery.img"; + string boot_img_string = "boot.img"; + + std::map src_entries{ + { "META/misc_info.txt", "extfs_sparse_flag=-s" }, + { "IMAGES/recovery.img", "" }, + { "IMAGES/boot.img", boot_img_string }, + { "IMAGES/system.img", sparse_system_string_ }, + { "RECOVERY/RAMDISK/etc/recovery.fstab", fstab_content_ }, + { "SYSTEM/build.prop", build_prop_string_ }, + }; + + TemporaryFile src_tf; + AddZipEntries(src_tf.release(), src_entries); + + string recovery_sha1 = CalculateSha1(recovery_img_string); + string boot_sha1 = CalculateSha1(boot_img_string); + string apply_patch_source_string = android::base::StringPrintf( + "EMMC:/dev/block/by-name/boot:%zu:%s", boot_img_string.size(), boot_sha1.c_str()); + string apply_patch_target_string = android::base::StringPrintf( + "EMMC:/dev/block/by-name/recovery:%zu:%s", recovery_img_string.size(), recovery_sha1.c_str()); + string check_command = android::base::StringPrintf( + R"(patch_partition_check("%s", "%s") || abort("check failed");)", + apply_patch_target_string.c_str(), apply_patch_source_string.c_str()); + string patch_command = android::base::StringPrintf( + R"(patch_partition("%s", "%s", package_extract_file("patch.p")) || abort("failed");)", + apply_patch_target_string.c_str(), apply_patch_source_string.c_str()); + + // Give an invalid recovery patch and expect the apply patch to fail. + // TODO(xunchang) check the cause code. + std::vector updater_commands = { + R"(ui_print("Source: angler/OPR1.170510.001");)", + check_command, + patch_command, + }; + + string updater_script = android::base::Join(updater_commands, '\n'); + std::map ota_entries{ + { "system.new.dat", "" }, + { "system.patch.dat", "" }, + { "system.transfer.list", "" }, + { "META-INF/com/google/android/updater-script", updater_script }, + { "patch.p", "random string" }, + }; + + TemporaryFile ota_package; + AddZipEntries(ota_package.release(), ota_entries); + + RunSimulation(src_tf.path, ota_package.path, false); +} diff --git a/tests/unit/updater_test.cpp b/tests/unit/updater_test.cpp new file mode 100644 index 0000000..8993dd8 --- /dev/null +++ b/tests/unit/updater_test.cpp @@ -0,0 +1,1227 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "applypatch/applypatch.h" +#include "common/test_constants.h" +#include "edify/expr.h" +#include "otautil/error_code.h" +#include "otautil/paths.h" +#include "otautil/print_sha1.h" +#include "otautil/sysutil.h" +#include "private/commands.h" +#include "updater/blockimg.h" +#include "updater/install.h" +#include "updater/updater.h" +#include "updater/updater_runtime.h" + +using namespace std::string_literals; + +using PackageEntries = std::unordered_map; + +static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code, + Updater* updater) { + std::unique_ptr e; + int error_count = 0; + ASSERT_EQ(0, ParseString(expr_str, &e, &error_count)); + ASSERT_EQ(0, error_count); + + State state(expr_str, updater); + + std::string result; + bool status = Evaluate(&state, e, &result); + + if (expected == nullptr) { + ASSERT_FALSE(status); + } else { + ASSERT_TRUE(status) << "Evaluate() finished with error message: " << state.errmsg; + ASSERT_STREQ(expected, result.c_str()); + } + + // Error code is set in updater/updater.cpp only, by parsing State.errmsg. + ASSERT_EQ(kNoError, state.error_code); + + // Cause code should always be available. + ASSERT_EQ(cause_code, state.cause_code); +} + +static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code) { + Updater updater(std::make_unique(nullptr)); + expect(expected, expr_str, cause_code, &updater); +} + +static void BuildUpdatePackage(const PackageEntries& entries, int fd) { + FILE* zip_file_ptr = fdopen(fd, "wb"); + ZipWriter zip_writer(zip_file_ptr); + + for (const auto& entry : entries) { + // All the entries are written as STORED. + ASSERT_EQ(0, zip_writer.StartEntry(entry.first.c_str(), 0)); + if (!entry.second.empty()) { + ASSERT_EQ(0, zip_writer.WriteBytes(entry.second.data(), entry.second.size())); + } + ASSERT_EQ(0, zip_writer.FinishEntry()); + } + + ASSERT_EQ(0, zip_writer.Finish()); + ASSERT_EQ(0, fclose(zip_file_ptr)); +} + +static std::string GetSha1(std::string_view content) { + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast(content.data()), content.size(), digest); + return print_sha1(digest); +} + +static Value* BlobToString(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + + std::vector> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + + if (args[0]->type != Value::Type::BLOB) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects a BLOB argument", name); + } + + args[0]->type = Value::Type::STRING; + return args[0].release(); +} + +class UpdaterTestBase { + protected: + UpdaterTestBase() : updater_(std::make_unique(nullptr)) {} + + void SetUp() { + RegisterBuiltins(); + RegisterInstallFunctions(); + RegisterBlockImageFunctions(); + + // Each test is run in a separate process (isolated mode). Shared temporary files won't cause + // conflicts. + Paths::Get().set_cache_temp_source(temp_saved_source_.path); + Paths::Get().set_last_command_file(temp_last_command_.path); + Paths::Get().set_stash_directory_base(temp_stash_base_.path); + + last_command_file_ = temp_last_command_.path; + image_file_ = image_temp_file_.path; + } + + void TearDown() { + // Clean up the last_command_file if any. + ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_)); + + // Clear partition updated marker if any. + std::string updated_marker{ temp_stash_base_.path }; + updated_marker += "/" + GetSha1(image_temp_file_.path) + ".UPDATED"; + ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker)); + } + + void RunBlockImageUpdate(bool is_verify, PackageEntries entries, const std::string& image_file, + const std::string& result, CauseCode cause_code = kNoCause) { + CHECK(entries.find("transfer_list") != entries.end()); + std::string new_data = + entries.find("new_data.br") != entries.end() ? "new_data.br" : "new_data"; + std::string script = is_verify ? "block_image_verify" : "block_image_update"; + script += R"((")" + image_file + R"(", package_extract_file("transfer_list"), ")" + new_data + + R"(", "patch_data"))"; + entries.emplace(Updater::SCRIPT_NAME, script); + + // Build the update package. + TemporaryFile zip_file; + BuildUpdatePackage(entries, zip_file.release()); + + // Set up the handler, command_pipe, patch offset & length. + TemporaryFile temp_pipe; + ASSERT_TRUE(updater_.Init(temp_pipe.release(), zip_file.path, false)); + ASSERT_TRUE(updater_.RunUpdate()); + ASSERT_EQ(result, updater_.GetResult()); + + // Parse the cause code written to the command pipe. + int received_cause_code = kNoCause; + std::string pipe_content; + ASSERT_TRUE(android::base::ReadFileToString(temp_pipe.path, &pipe_content)); + auto lines = android::base::Split(pipe_content, "\n"); + for (std::string_view line : lines) { + if (android::base::ConsumePrefix(&line, "log cause: ")) { + ASSERT_TRUE(android::base::ParseInt(line.data(), &received_cause_code)); + } + } + ASSERT_EQ(cause_code, received_cause_code); + } + + TemporaryFile temp_saved_source_; + TemporaryDir temp_stash_base_; + std::string last_command_file_; + std::string image_file_; + + Updater updater_; + + private: + TemporaryFile temp_last_command_; + TemporaryFile image_temp_file_; +}; + +class UpdaterTest : public UpdaterTestBase, public ::testing::Test { + protected: + void SetUp() override { + UpdaterTestBase::SetUp(); + + RegisterFunction("blob_to_string", BlobToString); + // Enable a special command "abort" to simulate interruption. + Command::abort_allowed_ = true; + } + + void TearDown() override { + UpdaterTestBase::TearDown(); + } + + void SetUpdaterCmdPipe(int fd) { + FILE* cmd_pipe = fdopen(fd, "w"); + ASSERT_NE(nullptr, cmd_pipe); + updater_.cmd_pipe_.reset(cmd_pipe); + } + + void SetUpdaterOtaPackageHandle(ZipArchiveHandle handle) { + updater_.package_handle_ = handle; + } + + void FlushUpdaterCommandPipe() const { + fflush(updater_.cmd_pipe_.get()); + } +}; + +TEST_F(UpdaterTest, getprop) { + expect(android::base::GetProperty("ro.product.device", "").c_str(), + "getprop(\"ro.product.device\")", + kNoCause); + + expect(android::base::GetProperty("ro.build.fingerprint", "").c_str(), + "getprop(\"ro.build.fingerprint\")", + kNoCause); + + // getprop() accepts only one parameter. + expect(nullptr, "getprop()", kArgsParsingFailure); + expect(nullptr, "getprop(\"arg1\", \"arg2\")", kArgsParsingFailure); +} + +TEST_F(UpdaterTest, patch_partition_check) { + // Zero argument is not valid. + expect(nullptr, "patch_partition_check()", kArgsParsingFailure); + + std::string source_file = from_testdata_base("boot.img"); + std::string source_content; + ASSERT_TRUE(android::base::ReadFileToString(source_file, &source_content)); + size_t source_size = source_content.size(); + std::string source_hash = GetSha1(source_content); + Partition source(source_file, source_size, source_hash); + + std::string target_file = from_testdata_base("recovery.img"); + std::string target_content; + ASSERT_TRUE(android::base::ReadFileToString(target_file, &target_content)); + size_t target_size = target_content.size(); + std::string target_hash = GetSha1(target_content); + Partition target(target_file, target_size, target_hash); + + // One argument is not valid. + expect(nullptr, "patch_partition_check(\"" + source.ToString() + "\")", kArgsParsingFailure); + expect(nullptr, "patch_partition_check(\"" + target.ToString() + "\")", kArgsParsingFailure); + + // Both of the source and target have the desired checksum. + std::string cmd = + "patch_partition_check(\"" + source.ToString() + "\", \"" + target.ToString() + "\")"; + expect("t", cmd, kNoCause); + + // Only source partition has the desired checksum. + Partition bad_target(target_file, target_size - 1, target_hash); + cmd = "patch_partition_check(\"" + source.ToString() + "\", \"" + bad_target.ToString() + "\")"; + expect("t", cmd, kNoCause); + + // Only target partition has the desired checksum. + Partition bad_source(source_file, source_size + 1, source_hash); + cmd = "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + target.ToString() + "\")"; + expect("t", cmd, kNoCause); + + // Neither of the source or target has the desired checksum. + cmd = + "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + bad_target.ToString() + "\")"; + expect("", cmd, kNoCause); +} + +TEST_F(UpdaterTest, file_getprop) { + // file_getprop() expects two arguments. + expect(nullptr, "file_getprop()", kArgsParsingFailure); + expect(nullptr, "file_getprop(\"arg1\")", kArgsParsingFailure); + expect(nullptr, "file_getprop(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure); + + // File doesn't exist. + expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFreadFailure); + + // Reject too large files (current limit = 65536). + TemporaryFile temp_file1; + std::string buffer(65540, '\0'); + ASSERT_TRUE(android::base::WriteStringToFile(buffer, temp_file1.path)); + + // Read some keys. + TemporaryFile temp_file2; + std::string content("ro.product.name=tardis\n" + "# comment\n\n\n" + "ro.product.model\n" + "ro.product.board = magic \n"); + ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file2.path)); + + std::string script1("file_getprop(\"" + std::string(temp_file2.path) + + "\", \"ro.product.name\")"); + expect("tardis", script1, kNoCause); + + std::string script2("file_getprop(\"" + std::string(temp_file2.path) + + "\", \"ro.product.board\")"); + expect("magic", script2, kNoCause); + + // No match. + std::string script3("file_getprop(\"" + std::string(temp_file2.path) + + "\", \"ro.product.wrong\")"); + expect("", script3, kNoCause); + + std::string script4("file_getprop(\"" + std::string(temp_file2.path) + + "\", \"ro.product.name=\")"); + expect("", script4, kNoCause); + + std::string script5("file_getprop(\"" + std::string(temp_file2.path) + + "\", \"ro.product.nam\")"); + expect("", script5, kNoCause); + + std::string script6("file_getprop(\"" + std::string(temp_file2.path) + + "\", \"ro.product.model\")"); + expect("", script6, kNoCause); +} + +// TODO: Test extracting to block device. +TEST_F(UpdaterTest, package_extract_file) { + // package_extract_file expects 1 or 2 arguments. + expect(nullptr, "package_extract_file()", kArgsParsingFailure); + expect(nullptr, "package_extract_file(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure); + + std::string zip_path = from_testdata_base("ziptest_valid.zip"); + ZipArchiveHandle handle; + ASSERT_EQ(0, OpenArchive(zip_path.c_str(), &handle)); + + // Need to set up the ziphandle. + SetUpdaterOtaPackageHandle(handle); + + // Two-argument version. + TemporaryFile temp_file1; + std::string script("package_extract_file(\"a.txt\", \"" + std::string(temp_file1.path) + "\")"); + expect("t", script, kNoCause, &updater_); + + // Verify the extracted entry. + std::string data; + ASSERT_TRUE(android::base::ReadFileToString(temp_file1.path, &data)); + ASSERT_EQ(kATxtContents, data); + + // Now extract another entry to the same location, which should overwrite. + script = "package_extract_file(\"b.txt\", \"" + std::string(temp_file1.path) + "\")"; + expect("t", script, kNoCause, &updater_); + + ASSERT_TRUE(android::base::ReadFileToString(temp_file1.path, &data)); + ASSERT_EQ(kBTxtContents, data); + + // Missing zip entry. The two-argument version doesn't abort. + script = "package_extract_file(\"doesntexist\", \"" + std::string(temp_file1.path) + "\")"; + expect("", script, kNoCause, &updater_); + + // Extract to /dev/full should fail. + script = "package_extract_file(\"a.txt\", \"/dev/full\")"; + expect("", script, kNoCause, &updater_); + + // One-argument version. package_extract_file() gives a VAL_BLOB, which needs to be converted to + // VAL_STRING for equality test. + script = "blob_to_string(package_extract_file(\"a.txt\")) == \"" + kATxtContents + "\""; + expect("t", script, kNoCause, &updater_); + + script = "blob_to_string(package_extract_file(\"b.txt\")) == \"" + kBTxtContents + "\""; + expect("t", script, kNoCause, &updater_); + + // Missing entry. The one-argument version aborts the evaluation. + script = "package_extract_file(\"doesntexist\")"; + expect(nullptr, script, kPackageExtractFileFailure, &updater_); +} + +TEST_F(UpdaterTest, read_file) { + // read_file() expects one argument. + expect(nullptr, "read_file()", kArgsParsingFailure); + expect(nullptr, "read_file(\"arg1\", \"arg2\")", kArgsParsingFailure); + + // Write some value to file and read back. + TemporaryFile temp_file; + std::string script("write_value(\"foo\", \""s + temp_file.path + "\");"); + expect("t", script, kNoCause); + + script = "read_file(\""s + temp_file.path + "\") == \"foo\""; + expect("t", script, kNoCause); + + script = "read_file(\""s + temp_file.path + "\") == \"bar\""; + expect("", script, kNoCause); + + // It should fail gracefully when read fails. + script = "read_file(\"/doesntexist\")"; + expect("", script, kNoCause); +} + +TEST_F(UpdaterTest, compute_hash_tree_smoke) { + std::string data; + for (unsigned char i = 0; i < 128; i++) { + data += std::string(4096, i); + } + // Appends an additional block for verity data. + data += std::string(4096, 0); + ASSERT_EQ(129 * 4096, data.size()); + ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_)); + + std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7"; + std::string expected_root_hash = + "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca"; + // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash + std::vector 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 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 salt_bytes; + ASSERT_TRUE(HashTreeBuilder::ParseBytesArrayFromString(salt, &salt_bytes)); + std::vector hash_tree = std::move(salt_bytes); + hash_tree.insert(hash_tree.end(), updated.begin() + 128 * 4096, updated.end()); + + std::vector 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 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 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 "). + 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 "). + ASSERT_EQ(3U, android::base::Split(cmd, " ").size()); +} + +TEST_F(UpdaterTest, block_image_update_parsing_error) { + std::vector 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(source.data()), source.size(), + reinterpret_cast(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 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 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 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 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(brotli_new_data.data()), &encoded_size, + reinterpret_cast(const_cast(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 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 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 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 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 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 { + 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 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 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(bdc.data()), bdc.size(), + reinterpret_cast(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(abcd.data()), abcd.size(), + reinterpret_cast(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 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 g_transfer_list = GenerateTransferList(); + +INSTANTIATE_TEST_CASE_P(InterruptAfterEachCommand, ResumableUpdaterTest, + ::testing::Range(static_cast(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 transfer_list_copy{ g_transfer_list }; + transfer_list_copy[TransferList::kTransferListHeaderLines + index_] = "abort"; + + g_entries["transfer_list"] = android::base::Join(transfer_list_copy, '\n'); + + // Run update that's expected to fail. + RunBlockImageUpdate(false, g_entries, image_file_, ""); + + std::string last_command_expected; + + // Assert the last_command_file. + if (index_ == 0) { + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); + } else { + last_command_expected = std::to_string(index_ - 1) + "\n" + + g_transfer_list[TransferList::kTransferListHeaderLines + index_ - 1]; + std::string last_command_actual; + ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual)); + ASSERT_EQ(last_command_expected, last_command_actual); + } + + g_entries["transfer_list"] = android::base::Join(g_transfer_list, '\n'); + + // Resume the interrupted update, by doing verification first. + RunBlockImageUpdate(true, g_entries, image_file_, "t"); + + // last_command_file should remain intact. + if (index_ == 0) { + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); + } else { + std::string last_command_actual; + ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual)); + ASSERT_EQ(last_command_expected, last_command_actual); + } + + // Resume the update. + RunBlockImageUpdate(false, g_entries, image_file_, "t"); + + // last_command_file should be gone after successful update. + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); + + std::string updated_image_actual; + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_image_actual)); + ASSERT_EQ(g_target_image, updated_image_actual); +} diff --git a/updater/Android.bp b/updater/Android.bp new file mode 100644 index 0000000..4ec2860 --- /dev/null +++ b/updater/Android.bp @@ -0,0 +1,191 @@ +// 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. + +cc_defaults { + name: "libupdater_static_libs", + + static_libs: [ + "libapplypatch", + "libbootloader_message", + "libbspatch", + "libedify", + "libotautil", + "libext4_utils", + "libdm", + "libfec", + "libfec_rs", + "libavb", + "libverity_tree", + "liblog", + "liblp", + "libselinux", + "libsparse", + "libsquashfs_utils", + "libbrotli", + "libbz", + "libziparchive", + "libz_stable", + "libbase", + "libcrypto_utils", + "libcutils", + "libutils", + ], + header_libs: [ + "libgtest_prod_headers", + ], +} + +cc_defaults { + name: "libupdater_defaults", + + defaults: [ + "recovery_defaults", + "libupdater_static_libs", + ], + + shared_libs: [ + "libcrypto", + ], +} + +cc_defaults { + name: "libupdater_device_defaults", + + static_libs: [ + "libfs_mgr", + "libtune2fs", + + "libext2_com_err", + "libext2_blkid", + "libext2_quota", + "libext2_uuid", + "libext2_e2p", + "libext2fs", + ], +} + +cc_library_static { + name: "libupdater_core", + + host_supported: true, + + defaults: [ + "recovery_defaults", + "libupdater_defaults", + ], + + srcs: [ + "blockimg.cpp", + "commands.cpp", + "install.cpp", + "mounts.cpp", + "updater.cpp", + ], + + target: { + darwin: { + enabled: false, + }, + }, + + export_include_dirs: [ + "include", + ], +} + +cc_library_static { + name: "libupdater_device", + + defaults: [ + "recovery_defaults", + "libupdater_defaults", + "libupdater_device_defaults", + ], + + srcs: [ + "dynamic_partitions.cpp", + "updater_runtime.cpp", + "updater_runtime_dynamic_partitions.cpp", + ], + + static_libs: [ + "libupdater_core", + ], + + include_dirs: [ + "external/e2fsprogs/misc", + ], + + export_include_dirs: [ + "include", + ], +} + +cc_library_host_static { + name: "libupdater_host", + + defaults: [ + "recovery_defaults", + "libupdater_defaults", + ], + + srcs: [ + "build_info.cpp", + "dynamic_partitions.cpp", + "simulator_runtime.cpp", + "target_files.cpp", + ], + + static_libs: [ + "libupdater_core", + "libfstab", + "libc++fs", + ], + + target: { + darwin: { + enabled: false, + }, + }, + + export_include_dirs: [ + "include", + ], +} + +cc_binary_host { + name: "update_host_simulator", + defaults: ["libupdater_static_libs"], + + srcs: ["update_simulator_main.cpp"], + + cflags: [ + "-Wall", + "-Werror", + ], + + static_libs: [ + "libupdater_host", + "libupdater_core", + "libcrypto_static", + "libfstab", + "libc++fs", + ], + + target: { + darwin: { + enabled: false, + }, + }, +} diff --git a/updater/Android.mk b/updater/Android.mk new file mode 100644 index 0000000..2fd5639 --- /dev/null +++ b/updater/Android.mk @@ -0,0 +1,118 @@ +# Copyright 2009 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +tune2fs_static_libraries := \ + libext2_com_err \ + libext2_blkid \ + libext2_quota \ + libext2_uuid \ + libext2_e2p \ + libext2fs + +updater_common_static_libraries := \ + libapplypatch \ + libbootloader_message \ + libbspatch \ + libedify \ + libotautil \ + libext4_utils \ + libdm \ + libfec \ + libfec_rs \ + libavb \ + libverity_tree \ + liblog \ + liblp \ + libselinux \ + libsparse \ + libsquashfs_utils \ + libbrotli \ + libbz \ + libziparchive \ + libz_stable \ + libbase \ + libcrypto_static \ + libcrypto_utils \ + libcutils \ + libutils + + +# Each library in TARGET_RECOVERY_UPDATER_LIBS should have a function +# named "Register_()". Here we emit a little C function that +# gets #included by updater.cpp. It calls all those registration +# functions. +# $(1): the path to the register.inc file +# $(2): a list of TARGET_RECOVERY_UPDATER_LIBS +define generate-register-inc + $(hide) mkdir -p $(dir $(1)) + $(hide) echo "" > $(1) + $(hide) $(foreach lib,$(2),echo "extern void Register_$(lib)(void);" >> $(1);) + $(hide) echo "void RegisterDeviceExtensions() {" >> $(1) + $(hide) $(foreach lib,$(2),echo " Register_$(lib)();" >> $(1);) + $(hide) echo "}" >> $(1) +endef + + +# updater (static executable) +# =============================== +include $(CLEAR_VARS) + +LOCAL_MODULE := updater +LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 +LOCAL_LICENSE_CONDITIONS := notice +LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../NOTICE + +LOCAL_SRC_FILES := \ + updater_main.cpp + +LOCAL_C_INCLUDES := \ + $(LOCAL_PATH)/include + +LOCAL_CFLAGS := \ + -Wall \ + -Werror + +LOCAL_STATIC_LIBRARIES := \ + libupdater_device \ + libupdater_core \ + $(TARGET_RECOVERY_UPDATER_LIBS) \ + $(TARGET_RECOVERY_UPDATER_EXTRA_LIBS) \ + $(updater_common_static_libraries) \ + libfs_mgr \ + libtune2fs \ + $(tune2fs_static_libraries) + +LOCAL_HEADER_LIBRARIES := libgtest_prod_headers + +LOCAL_MODULE_CLASS := EXECUTABLES +inc := $(call local-generated-sources-dir)/register.inc + +# Devices can also add libraries to TARGET_RECOVERY_UPDATER_EXTRA_LIBS. +# These libs are also linked in with updater, but we don't try to call +# any sort of registration function for these. Use this variable for +# any subsidiary static libraries required for your registered +# extension libs. +$(inc) : libs := $(TARGET_RECOVERY_UPDATER_LIBS) +$(inc) : + $(call generate-register-inc,$@,$(libs)) + +LOCAL_GENERATED_SOURCES := $(inc) + +inc := + +LOCAL_FORCE_STATIC_EXECUTABLE := true + +include $(BUILD_EXECUTABLE) diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp new file mode 100644 index 0000000..b472a65 --- /dev/null +++ b/updater/blockimg.cpp @@ -0,0 +1,2303 @@ +/* + * Copyright (C) 2014 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "edify/expr.h" +#include "edify/updater_interface.h" +#include "otautil/dirutil.h" +#include "otautil/error_code.h" +#include "otautil/paths.h" +#include "otautil/print_sha1.h" +#include "otautil/rangeset.h" +#include "private/commands.h" +#include "updater/install.h" + +#ifdef __ANDROID__ +#include +// Set this to 0 to interpret 'erase' transfers to mean do a BLKDISCARD ioctl (the normal behavior). +// Set to 1 to interpret erase to mean fill the region with zeroes. +#define DEBUG_ERASE 0 +#else +#define DEBUG_ERASE 1 +#define AID_SYSTEM -1 +#endif // __ANDROID__ + +static constexpr size_t BLOCKSIZE = 4096; +static constexpr mode_t STASH_DIRECTORY_MODE = 0700; +static constexpr mode_t STASH_FILE_MODE = 0600; +static constexpr mode_t MARKER_DIRECTORY_MODE = 0700; + +static CauseCode failure_type = kNoCause; +static bool is_retry = false; +static std::unordered_map stash_map; + +static void DeleteLastCommandFile() { + const std::string& last_command_file = Paths::Get().last_command_file(); + if (unlink(last_command_file.c_str()) == -1 && errno != ENOENT) { + PLOG(ERROR) << "Failed to unlink: " << last_command_file; + } +} + +// Parse the last command index of the last update and save the result to |last_command_index|. +// Return true if we successfully read the index. +static bool ParseLastCommandFile(size_t* last_command_index) { + const std::string& last_command_file = Paths::Get().last_command_file(); + android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(last_command_file.c_str(), O_RDONLY))); + if (fd == -1) { + if (errno != ENOENT) { + PLOG(ERROR) << "Failed to open " << last_command_file; + return false; + } + + LOG(INFO) << last_command_file << " doesn't exist."; + return false; + } + + // Now that the last_command file exists, parse the last command index of previous update. + std::string content; + if (!android::base::ReadFdToString(fd.get(), &content)) { + LOG(ERROR) << "Failed to read: " << last_command_file; + return false; + } + + std::vector lines = android::base::Split(android::base::Trim(content), "\n"); + if (lines.size() != 2) { + LOG(ERROR) << "Unexpected line counts in last command file: " << content; + return false; + } + + if (!android::base::ParseUint(lines[0], last_command_index)) { + LOG(ERROR) << "Failed to parse integer in: " << lines[0]; + return false; + } + + return true; +} + +static bool FsyncDir(const std::string& dirname) { + android::base::unique_fd dfd(TEMP_FAILURE_RETRY(open(dirname.c_str(), O_RDONLY | O_DIRECTORY))); + if (dfd == -1) { + failure_type = errno == EIO ? kEioFailure : kFileOpenFailure; + PLOG(ERROR) << "Failed to open " << dirname; + return false; + } + if (fsync(dfd) == -1) { + failure_type = errno == EIO ? kEioFailure : kFsyncFailure; + PLOG(ERROR) << "Failed to fsync " << dirname; + return false; + } + return true; +} + +// Update the last executed command index in the last_command_file. +static bool UpdateLastCommandIndex(size_t command_index, const std::string& command_string) { + const std::string& last_command_file = Paths::Get().last_command_file(); + std::string last_command_tmp = last_command_file + ".tmp"; + std::string content = std::to_string(command_index) + "\n" + command_string; + android::base::unique_fd wfd( + TEMP_FAILURE_RETRY(open(last_command_tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0660))); + if (wfd == -1 || !android::base::WriteStringToFd(content, wfd)) { + PLOG(ERROR) << "Failed to update last command"; + return false; + } + + if (fsync(wfd) == -1) { + PLOG(ERROR) << "Failed to fsync " << last_command_tmp; + return false; + } + + if (chown(last_command_tmp.c_str(), AID_SYSTEM, AID_SYSTEM) == -1) { + PLOG(ERROR) << "Failed to change owner for " << last_command_tmp; + return false; + } + + if (rename(last_command_tmp.c_str(), last_command_file.c_str()) == -1) { + PLOG(ERROR) << "Failed to rename" << last_command_tmp; + return false; + } + + if (!FsyncDir(android::base::Dirname(last_command_file))) { + return false; + } + + return true; +} + +bool SetUpdatedMarker(const std::string& marker) { + auto dirname = android::base::Dirname(marker); + auto res = mkdir(dirname.c_str(), MARKER_DIRECTORY_MODE); + if (res == -1 && errno != EEXIST) { + PLOG(ERROR) << "Failed to create directory for marker: " << dirname; + return false; + } + + if (!android::base::WriteStringToFile("", marker)) { + PLOG(ERROR) << "Failed to write to marker file " << marker; + return false; + } + if (!FsyncDir(dirname)) { + return false; + } + LOG(INFO) << "Wrote updated marker to " << marker; + return true; +} + +static bool discard_blocks(int fd, off64_t offset, uint64_t size, bool force = false) { + // Don't discard blocks unless the update is a retry run or force == true + if (!is_retry && !force) { + return true; + } + + uint64_t args[2] = { static_cast(offset), size }; + if (ioctl(fd, BLKDISCARD, &args) == -1) { + // On devices that does not support BLKDISCARD, ignore the error. + if (errno == EOPNOTSUPP) { + return true; + } + PLOG(ERROR) << "BLKDISCARD ioctl failed"; + return false; + } + return true; +} + +static bool check_lseek(int fd, off64_t offset, int whence) { + off64_t rc = TEMP_FAILURE_RETRY(lseek64(fd, offset, whence)); + if (rc == -1) { + failure_type = kLseekFailure; + PLOG(ERROR) << "lseek64 failed"; + return false; + } + return true; +} + +static void allocate(size_t size, std::vector* buffer) { + // If the buffer's big enough, reuse it. + if (size <= buffer->size()) return; + buffer->resize(size); +} + +/** + * RangeSinkWriter reads data from the given FD, and writes them to the destination specified by the + * given RangeSet. + */ +class RangeSinkWriter { + public: + RangeSinkWriter(int fd, const RangeSet& tgt) + : fd_(fd), + tgt_(tgt), + next_range_(0), + current_range_left_(0), + bytes_written_(0) { + CHECK_NE(tgt.size(), static_cast(0)); + }; + + bool Finished() const { + return next_range_ == tgt_.size() && current_range_left_ == 0; + } + + size_t AvailableSpace() const { + return tgt_.blocks() * BLOCKSIZE - bytes_written_; + } + + // Return number of bytes written; and 0 indicates a writing failure. + size_t Write(const uint8_t* data, size_t size) { + if (Finished()) { + LOG(ERROR) << "range sink write overrun; can't write " << size << " bytes"; + return 0; + } + + size_t written = 0; + while (size > 0) { + // Move to the next range as needed. + if (!SeekToOutputRange()) { + break; + } + + size_t write_now = size; + if (current_range_left_ < write_now) { + write_now = current_range_left_; + } + + if (!android::base::WriteFully(fd_, data, write_now)) { + failure_type = errno == EIO ? kEioFailure : kFwriteFailure; + PLOG(ERROR) << "Failed to write " << write_now << " bytes of data"; + break; + } + + data += write_now; + size -= write_now; + + current_range_left_ -= write_now; + written += write_now; + } + + bytes_written_ += written; + return written; + } + + size_t BytesWritten() const { + return bytes_written_; + } + + private: + // Set up the output cursor, move to next range if needed. + bool SeekToOutputRange() { + // We haven't finished the current range yet. + if (current_range_left_ != 0) { + return true; + } + // We can't write any more; let the write function return how many bytes have been written + // so far. + if (next_range_ >= tgt_.size()) { + return false; + } + + const Range& range = tgt_[next_range_]; + off64_t offset = static_cast(range.first) * BLOCKSIZE; + current_range_left_ = (range.second - range.first) * BLOCKSIZE; + next_range_++; + + if (!discard_blocks(fd_, offset, current_range_left_)) { + return false; + } + if (!check_lseek(fd_, offset, SEEK_SET)) { + return false; + } + return true; + } + + // The output file descriptor. + int fd_; + // The destination ranges for the data. + const RangeSet& tgt_; + // The next range that we should write to. + size_t next_range_; + // The number of bytes to write before moving to the next range. + size_t current_range_left_; + // Total bytes written by the writer. + size_t bytes_written_; +}; + +/** + * All of the data for all the 'new' transfers is contained in one file in the update package, + * concatenated together in the order in which transfers.list will need it. We want to stream it out + * of the archive (it's compressed) without writing it to a temp file, but we can't write each + * section until it's that transfer's turn to go. + * + * To achieve this, we expand the new data from the archive in a background thread, and block that + * threads 'receive uncompressed data' function until the main thread has reached a point where we + * want some new data to be written. We signal the background thread with the destination for the + * data and block the main thread, waiting for the background thread to complete writing that + * section. Then it signals the main thread to wake up and goes back to blocking waiting for a + * transfer. + * + * NewThreadInfo is the struct used to pass information back and forth between the two threads. When + * the main thread wants some data written, it sets writer to the destination location and signals + * the condition. When the background thread is done writing, it clears writer and signals the + * condition again. + */ +struct NewThreadInfo { + ZipArchiveHandle za; + ZipEntry64 entry{}; + bool brotli_compressed; + + std::unique_ptr writer; + BrotliDecoderState* brotli_decoder_state; + bool receiver_available; + + pthread_mutex_t mu; + pthread_cond_t cv; +}; + +static bool receive_new_data(const uint8_t* data, size_t size, void* cookie) { + NewThreadInfo* nti = static_cast(cookie); + + while (size > 0) { + // Wait for nti->writer to be non-null, indicating some of this data is wanted. + pthread_mutex_lock(&nti->mu); + while (nti->writer == nullptr) { + // End the new data receiver if we encounter an error when performing block image update. + if (!nti->receiver_available) { + pthread_mutex_unlock(&nti->mu); + return false; + } + pthread_cond_wait(&nti->cv, &nti->mu); + } + pthread_mutex_unlock(&nti->mu); + + // At this point nti->writer is set, and we own it. The main thread is waiting for it to + // disappear from nti. + size_t write_now = std::min(size, nti->writer->AvailableSpace()); + if (nti->writer->Write(data, write_now) != write_now) { + LOG(ERROR) << "Failed to write " << write_now << " bytes."; + return false; + } + + data += write_now; + size -= write_now; + + if (nti->writer->Finished()) { + // We have written all the bytes desired by this writer. + + pthread_mutex_lock(&nti->mu); + nti->writer = nullptr; + pthread_cond_broadcast(&nti->cv); + pthread_mutex_unlock(&nti->mu); + } + } + + return true; +} + +static bool receive_brotli_new_data(const uint8_t* data, size_t size, void* cookie) { + NewThreadInfo* nti = static_cast(cookie); + + while (size > 0 || BrotliDecoderHasMoreOutput(nti->brotli_decoder_state)) { + // Wait for nti->writer to be non-null, indicating some of this data is wanted. + pthread_mutex_lock(&nti->mu); + while (nti->writer == nullptr) { + // End the receiver if we encounter an error when performing block image update. + if (!nti->receiver_available) { + pthread_mutex_unlock(&nti->mu); + return false; + } + pthread_cond_wait(&nti->cv, &nti->mu); + } + pthread_mutex_unlock(&nti->mu); + + // At this point nti->writer is set, and we own it. The main thread is waiting for it to + // disappear from nti. + + size_t buffer_size = std::min(32768, nti->writer->AvailableSpace()); + if (buffer_size == 0) { + LOG(ERROR) << "No space left in output range"; + return false; + } + uint8_t buffer[buffer_size]; + size_t available_in = size; + size_t available_out = buffer_size; + uint8_t* next_out = buffer; + + // The brotli decoder will update |data|, |available_in|, |next_out| and |available_out|. + BrotliDecoderResult result = BrotliDecoderDecompressStream( + nti->brotli_decoder_state, &available_in, &data, &available_out, &next_out, nullptr); + + if (result == BROTLI_DECODER_RESULT_ERROR) { + LOG(ERROR) << "Decompression failed with " + << BrotliDecoderErrorString(BrotliDecoderGetErrorCode(nti->brotli_decoder_state)); + return false; + } + + LOG(DEBUG) << "bytes to write: " << buffer_size - available_out << ", bytes consumed " + << size - available_in << ", decoder status " << result; + + size_t write_now = buffer_size - available_out; + if (nti->writer->Write(buffer, write_now) != write_now) { + LOG(ERROR) << "Failed to write " << write_now << " bytes."; + return false; + } + + // Update the remaining size. The input data ptr is already updated by brotli decoder function. + size = available_in; + + if (nti->writer->Finished()) { + // We have written all the bytes desired by this writer. + + pthread_mutex_lock(&nti->mu); + nti->writer = nullptr; + pthread_cond_broadcast(&nti->cv); + pthread_mutex_unlock(&nti->mu); + } + } + + return true; +} + +static void* unzip_new_data(void* cookie) { + NewThreadInfo* nti = static_cast(cookie); + if (nti->brotli_compressed) { + ProcessZipEntryContents(nti->za, &nti->entry, receive_brotli_new_data, nti); + } else { + ProcessZipEntryContents(nti->za, &nti->entry, receive_new_data, nti); + } + pthread_mutex_lock(&nti->mu); + nti->receiver_available = false; + if (nti->writer != nullptr) { + pthread_cond_broadcast(&nti->cv); + } + pthread_mutex_unlock(&nti->mu); + return nullptr; +} + +static int ReadBlocks(const RangeSet& src, std::vector* buffer, int fd) { + size_t p = 0; + for (const auto& [begin, end] : src) { + if (!check_lseek(fd, static_cast(begin) * BLOCKSIZE, SEEK_SET)) { + return -1; + } + + size_t size = (end - begin) * BLOCKSIZE; + if (!android::base::ReadFully(fd, buffer->data() + p, size)) { + failure_type = errno == EIO ? kEioFailure : kFreadFailure; + PLOG(ERROR) << "Failed to read " << size << " bytes of data"; + return -1; + } + + p += size; + } + + return 0; +} + +static int WriteBlocks(const RangeSet& tgt, const std::vector& buffer, int fd) { + size_t written = 0; + for (const auto& [begin, end] : tgt) { + off64_t offset = static_cast(begin) * BLOCKSIZE; + size_t size = (end - begin) * BLOCKSIZE; + if (!discard_blocks(fd, offset, size)) { + return -1; + } + + if (!check_lseek(fd, offset, SEEK_SET)) { + return -1; + } + + if (!android::base::WriteFully(fd, buffer.data() + written, size)) { + failure_type = errno == EIO ? kEioFailure : kFwriteFailure; + PLOG(ERROR) << "Failed to write " << size << " bytes of data"; + return -1; + } + + written += size; + } + + return 0; +} + +// Parameters for transfer list command functions +struct CommandParameters { + std::vector tokens; + size_t cpos; + std::string cmdname; + std::string cmdline; + std::string freestash; + std::string stashbase; + bool canwrite; + int createdstash; + android::base::unique_fd fd; + bool foundwrites; + bool isunresumable; + int version; + size_t written; + size_t stashed; + NewThreadInfo nti; + pthread_t thread; + std::vector buffer; + uint8_t* patch_start; + bool target_verified; // The target blocks have expected contents already. +}; + +// Print the hash in hex for corrupted source blocks (excluding the stashed blocks which is +// handled separately). +static void PrintHashForCorruptedSourceBlocks(const CommandParameters& params, + const std::vector& buffer) { + LOG(INFO) << "unexpected contents of source blocks in cmd:\n" << params.cmdline; + CHECK(params.tokens[0] == "move" || params.tokens[0] == "bsdiff" || + params.tokens[0] == "imgdiff"); + + size_t pos = 0; + // Command example: + // move [ ] + // bsdiff + // [ ] + if (params.tokens[0] == "move") { + // src_range for move starts at the 4th position. + if (params.tokens.size() < 5) { + LOG(ERROR) << "failed to parse source range in cmd:\n" << params.cmdline; + return; + } + pos = 4; + } else { + // src_range for diff starts at the 7th position. + if (params.tokens.size() < 8) { + LOG(ERROR) << "failed to parse source range in cmd:\n" << params.cmdline; + return; + } + pos = 7; + } + + // Source blocks in stash only, no work to do. + if (params.tokens[pos] == "-") { + return; + } + + RangeSet src = RangeSet::Parse(params.tokens[pos++]); + if (!src) { + LOG(ERROR) << "Failed to parse range in " << params.cmdline; + return; + } + + RangeSet locs; + // If there's no stashed blocks, content in the buffer is consecutive and has the same + // order as the source blocks. + if (pos == params.tokens.size()) { + locs = RangeSet(std::vector{ Range{ 0, src.blocks() } }); + } else { + // Otherwise, the next token is the offset of the source blocks in the target range. + // Example: for the tokens <4,63946,63947,63948,63979> <4,6,7,8,39> ; + // We want to print SHA-1 for the data in buffer[6], buffer[8], buffer[9] ... buffer[38]; + // this corresponds to the 32 src blocks #63946, #63948, #63949 ... #63978. + locs = RangeSet::Parse(params.tokens[pos++]); + CHECK_EQ(src.blocks(), locs.blocks()); + } + + LOG(INFO) << "printing hash in hex for " << src.blocks() << " source blocks"; + for (size_t i = 0; i < src.blocks(); i++) { + size_t block_num = src.GetBlockNumber(i); + size_t buffer_index = locs.GetBlockNumber(i); + CHECK_LE((buffer_index + 1) * BLOCKSIZE, buffer.size()); + + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(buffer.data() + buffer_index * BLOCKSIZE, BLOCKSIZE, digest); + std::string hexdigest = print_sha1(digest); + LOG(INFO) << " block number: " << block_num << ", SHA-1: " << hexdigest; + } +} + +// If the calculated hash for the whole stash doesn't match the stash id, print the SHA-1 +// in hex for each block. +static void PrintHashForCorruptedStashedBlocks(const std::string& id, + const std::vector& buffer, + const RangeSet& src) { + LOG(INFO) << "printing hash in hex for stash_id: " << id; + CHECK_EQ(src.blocks() * BLOCKSIZE, buffer.size()); + + for (size_t i = 0; i < src.blocks(); i++) { + size_t block_num = src.GetBlockNumber(i); + + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(buffer.data() + i * BLOCKSIZE, BLOCKSIZE, digest); + std::string hexdigest = print_sha1(digest); + LOG(INFO) << " block number: " << block_num << ", SHA-1: " << hexdigest; + } +} + +// If the stash file doesn't exist, read the source blocks this stash contains and print the +// SHA-1 for these blocks. +static void PrintHashForMissingStashedBlocks(const std::string& id, int fd) { + if (stash_map.find(id) == stash_map.end()) { + LOG(ERROR) << "No stash saved for id: " << id; + return; + } + + LOG(INFO) << "print hash in hex for source blocks in missing stash: " << id; + const RangeSet& src = stash_map[id]; + std::vector buffer(src.blocks() * BLOCKSIZE); + if (ReadBlocks(src, &buffer, fd) == -1) { + LOG(ERROR) << "failed to read source blocks for stash: " << id; + return; + } + PrintHashForCorruptedStashedBlocks(id, buffer, src); +} + +static int VerifyBlocks(const std::string& expected, const std::vector& buffer, + const size_t blocks, bool printerror) { + uint8_t digest[SHA_DIGEST_LENGTH]; + const uint8_t* data = buffer.data(); + + SHA1(data, blocks * BLOCKSIZE, digest); + + std::string hexdigest = print_sha1(digest); + + if (hexdigest != expected) { + if (printerror) { + LOG(ERROR) << "failed to verify blocks (expected " << expected << ", read " << hexdigest + << ")"; + } + return -1; + } + + return 0; +} + +static std::string GetStashFileName(const std::string& base, const std::string& id, + const std::string& postfix) { + if (base.empty()) { + return ""; + } + std::string filename = Paths::Get().stash_directory_base() + "/" + base; + if (id.empty() && postfix.empty()) { + return filename; + } + return filename + "/" + id + postfix; +} + +// Does a best effort enumeration of stash files. Ignores possible non-file items in the stash +// directory and continues despite of errors. Calls the 'callback' function for each file. +static void EnumerateStash(const std::string& dirname, + const std::function& callback) { + if (dirname.empty()) return; + + std::unique_ptr directory(opendir(dirname.c_str()), closedir); + + if (directory == nullptr) { + if (errno != ENOENT) { + PLOG(ERROR) << "opendir \"" << dirname << "\" failed"; + } + return; + } + + dirent* item; + while ((item = readdir(directory.get())) != nullptr) { + if (item->d_type != DT_REG) continue; + callback(dirname + "/" + item->d_name); + } +} + +// Deletes the stash directory and all files in it. Assumes that it only +// contains files. There is nothing we can do about unlikely, but possible +// errors, so they are merely logged. +static void DeleteFile(const std::string& fn) { + if (fn.empty()) return; + + LOG(INFO) << "deleting " << fn; + + if (unlink(fn.c_str()) == -1 && errno != ENOENT) { + PLOG(ERROR) << "unlink \"" << fn << "\" failed"; + } +} + +static void DeleteStash(const std::string& base) { + if (base.empty()) return; + + LOG(INFO) << "deleting stash " << base; + + std::string dirname = GetStashFileName(base, "", ""); + EnumerateStash(dirname, DeleteFile); + + if (rmdir(dirname.c_str()) == -1) { + if (errno != ENOENT && errno != ENOTDIR) { + PLOG(ERROR) << "rmdir \"" << dirname << "\" failed"; + } + } +} + +static int LoadStash(const CommandParameters& params, const std::string& id, bool verify, + std::vector* buffer, bool printnoent) { + // In verify mode, if source range_set was saved for the given hash, check contents in the source + // blocks first. If the check fails, search for the stashed files on /cache as usual. + if (!params.canwrite) { + if (stash_map.find(id) != stash_map.end()) { + const RangeSet& src = stash_map[id]; + allocate(src.blocks() * BLOCKSIZE, buffer); + + if (ReadBlocks(src, buffer, params.fd) == -1) { + LOG(ERROR) << "failed to read source blocks in stash map."; + return -1; + } + if (VerifyBlocks(id, *buffer, src.blocks(), true) != 0) { + LOG(ERROR) << "failed to verify loaded source blocks in stash map."; + if (!is_retry) { + PrintHashForCorruptedStashedBlocks(id, *buffer, src); + } + return -1; + } + return 0; + } + } + + std::string fn = GetStashFileName(params.stashbase, id, ""); + + struct stat sb; + if (stat(fn.c_str(), &sb) == -1) { + if (errno != ENOENT || printnoent) { + PLOG(ERROR) << "stat \"" << fn << "\" failed"; + PrintHashForMissingStashedBlocks(id, params.fd); + } + return -1; + } + + LOG(INFO) << " loading " << fn; + + if ((sb.st_size % BLOCKSIZE) != 0) { + LOG(ERROR) << fn << " size " << sb.st_size << " not multiple of block size " << BLOCKSIZE; + return -1; + } + + android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(fn.c_str(), O_RDONLY))); + if (fd == -1) { + failure_type = errno == EIO ? kEioFailure : kFileOpenFailure; + PLOG(ERROR) << "open \"" << fn << "\" failed"; + return -1; + } + + allocate(sb.st_size, buffer); + + if (!android::base::ReadFully(fd, buffer->data(), sb.st_size)) { + failure_type = errno == EIO ? kEioFailure : kFreadFailure; + PLOG(ERROR) << "Failed to read " << sb.st_size << " bytes of data"; + return -1; + } + + size_t blocks = sb.st_size / BLOCKSIZE; + if (verify && VerifyBlocks(id, *buffer, blocks, true) != 0) { + LOG(ERROR) << "unexpected contents in " << fn; + if (stash_map.find(id) == stash_map.end()) { + LOG(ERROR) << "failed to find source blocks number for stash " << id + << " when executing command: " << params.cmdname; + } else { + const RangeSet& src = stash_map[id]; + PrintHashForCorruptedStashedBlocks(id, *buffer, src); + } + DeleteFile(fn); + return -1; + } + + return 0; +} + +static int WriteStash(const std::string& base, const std::string& id, int blocks, + const std::vector& buffer, bool checkspace, bool* exists) { + if (base.empty()) { + return -1; + } + + if (checkspace && !CheckAndFreeSpaceOnCache(blocks * BLOCKSIZE)) { + LOG(ERROR) << "not enough space to write stash"; + return -1; + } + + std::string fn = GetStashFileName(base, id, ".partial"); + std::string cn = GetStashFileName(base, id, ""); + + if (exists) { + struct stat sb; + int res = stat(cn.c_str(), &sb); + + if (res == 0) { + // The file already exists and since the name is the hash of the contents, + // it's safe to assume the contents are identical (accidental hash collisions + // are unlikely) + LOG(INFO) << " skipping " << blocks << " existing blocks in " << cn; + *exists = true; + return 0; + } + + *exists = false; + } + + LOG(INFO) << " writing " << blocks << " blocks to " << cn; + + android::base::unique_fd fd( + TEMP_FAILURE_RETRY(open(fn.c_str(), O_WRONLY | O_CREAT | O_TRUNC, STASH_FILE_MODE))); + if (fd == -1) { + failure_type = errno == EIO ? kEioFailure : kFileOpenFailure; + PLOG(ERROR) << "failed to create \"" << fn << "\""; + return -1; + } + + if (fchown(fd, AID_SYSTEM, AID_SYSTEM) != 0) { // system user + PLOG(ERROR) << "failed to chown \"" << fn << "\""; + return -1; + } + + if (!android::base::WriteFully(fd, buffer.data(), blocks * BLOCKSIZE)) { + failure_type = errno == EIO ? kEioFailure : kFwriteFailure; + PLOG(ERROR) << "Failed to write " << blocks * BLOCKSIZE << " bytes of data"; + return -1; + } + + if (fsync(fd) == -1) { + failure_type = errno == EIO ? kEioFailure : kFsyncFailure; + PLOG(ERROR) << "fsync \"" << fn << "\" failed"; + return -1; + } + + if (rename(fn.c_str(), cn.c_str()) == -1) { + PLOG(ERROR) << "rename(\"" << fn << "\", \"" << cn << "\") failed"; + return -1; + } + + std::string dname = GetStashFileName(base, "", ""); + if (!FsyncDir(dname)) { + return -1; + } + + return 0; +} + +// Creates a directory for storing stash files and checks if the /cache partition +// hash enough space for the expected amount of blocks we need to store. Returns +// >0 if we created the directory, zero if it existed already, and <0 of failure. +static int CreateStash(State* state, size_t maxblocks, const std::string& base) { + std::string dirname = GetStashFileName(base, "", ""); + struct stat sb; + int res = stat(dirname.c_str(), &sb); + if (res == -1 && errno != ENOENT) { + ErrorAbort(state, kStashCreationFailure, "stat \"%s\" failed: %s", dirname.c_str(), + strerror(errno)); + return -1; + } + + size_t max_stash_size = maxblocks * BLOCKSIZE; + if (res == -1) { + LOG(INFO) << "creating stash " << dirname; + res = mkdir_recursively(dirname, STASH_DIRECTORY_MODE, false, nullptr); + + if (res != 0) { + ErrorAbort(state, kStashCreationFailure, "mkdir \"%s\" failed: %s", dirname.c_str(), + strerror(errno)); + return -1; + } + + if (chown(dirname.c_str(), AID_SYSTEM, AID_SYSTEM) != 0) { // system user + ErrorAbort(state, kStashCreationFailure, "chown \"%s\" failed: %s", dirname.c_str(), + strerror(errno)); + return -1; + } + + if (!CheckAndFreeSpaceOnCache(max_stash_size)) { + ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu needed)", + max_stash_size); + return -1; + } + + return 1; // Created directory + } + + LOG(INFO) << "using existing stash " << dirname; + + // If the directory already exists, calculate the space already allocated to stash files and check + // if there's enough for all required blocks. Delete any partially completed stash files first. + EnumerateStash(dirname, [](const std::string& fn) { + if (android::base::EndsWith(fn, ".partial")) { + DeleteFile(fn); + } + }); + + size_t existing = 0; + EnumerateStash(dirname, [&existing](const std::string& fn) { + if (fn.empty()) return; + struct stat sb; + if (stat(fn.c_str(), &sb) == -1) { + PLOG(ERROR) << "stat \"" << fn << "\" failed"; + return; + } + existing += static_cast(sb.st_size); + }); + + if (max_stash_size > existing) { + size_t needed = max_stash_size - existing; + if (!CheckAndFreeSpaceOnCache(needed)) { + ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu more needed)", + needed); + return -1; + } + } + + return 0; // Using existing directory +} + +static int FreeStash(const std::string& base, const std::string& id) { + if (base.empty() || id.empty()) { + return -1; + } + + DeleteFile(GetStashFileName(base, id, "")); + + return 0; +} + +// Source contains packed data, which we want to move to the locations given in locs in the dest +// buffer. source and dest may be the same buffer. +static void MoveRange(std::vector& dest, const RangeSet& locs, + const std::vector& source) { + const uint8_t* from = source.data(); + uint8_t* to = dest.data(); + size_t start = locs.blocks(); + // Must do the movement backward. + for (auto it = locs.crbegin(); it != locs.crend(); it++) { + size_t blocks = it->second - it->first; + start -= blocks; + memmove(to + (it->first * BLOCKSIZE), from + (start * BLOCKSIZE), blocks * BLOCKSIZE); + } +} + +/** + * We expect to parse the remainder of the parameter tokens as one of: + * + * + * (loads data from source image only) + * + * - <[stash_id:stash_range] ...> + * (loads data from stashes only) + * + * <[stash_id:stash_range] ...> + * (loads data from both source image and stashes) + * + * On return, params.buffer is filled with the loaded source data (rearranged and combined with + * stashed data as necessary). buffer may be reallocated if needed to accommodate the source data. + * tgt is the target RangeSet for detecting overlaps. Any stashes required are loaded using + * LoadStash. + */ +static int LoadSourceBlocks(CommandParameters& params, const RangeSet& tgt, size_t* src_blocks, + bool* overlap) { + CHECK(src_blocks != nullptr); + CHECK(overlap != nullptr); + + // + const std::string& token = params.tokens[params.cpos++]; + if (!android::base::ParseUint(token, src_blocks)) { + LOG(ERROR) << "invalid src_block_count \"" << token << "\""; + return -1; + } + + allocate(*src_blocks * BLOCKSIZE, ¶ms.buffer); + + // "-" or [] + if (params.tokens[params.cpos] == "-") { + // no source ranges, only stashes + params.cpos++; + } else { + RangeSet src = RangeSet::Parse(params.tokens[params.cpos++]); + CHECK(static_cast(src)); + *overlap = src.Overlaps(tgt); + + if (ReadBlocks(src, ¶ms.buffer, params.fd) == -1) { + return -1; + } + + if (params.cpos >= params.tokens.size()) { + // no stashes, only source range + return 0; + } + + RangeSet locs = RangeSet::Parse(params.tokens[params.cpos++]); + CHECK(static_cast(locs)); + MoveRange(params.buffer, locs, params.buffer); + } + + // <[stash_id:stash_range]> + while (params.cpos < params.tokens.size()) { + // Each word is a an index into the stash table, a colon, and then a RangeSet describing where + // in the source block that stashed data should go. + std::vector tokens = android::base::Split(params.tokens[params.cpos++], ":"); + if (tokens.size() != 2) { + LOG(ERROR) << "invalid parameter"; + return -1; + } + + std::vector stash; + if (LoadStash(params, tokens[0], false, &stash, true) == -1) { + // These source blocks will fail verification if used later, but we + // will let the caller decide if this is a fatal failure + LOG(ERROR) << "failed to load stash " << tokens[0]; + continue; + } + + RangeSet locs = RangeSet::Parse(tokens[1]); + CHECK(static_cast(locs)); + MoveRange(params.buffer, locs, stash); + } + + return 0; +} + +/** + * Do a source/target load for move/bsdiff/imgdiff in version 3. + * + * We expect to parse the remainder of the parameter tokens as one of: + * + * + * (loads data from source image only) + * + * - <[stash_id:stash_range] ...> + * (loads data from stashes only) + * + * <[stash_id:stash_range] ...> + * (loads data from both source image and stashes) + * + * 'onehash' tells whether to expect separate source and targe block hashes, or if they are both the + * same and only one hash should be expected. params.isunresumable will be set to true if block + * verification fails in a way that the update cannot be resumed anymore. + * + * If the function is unable to load the necessary blocks or their contents don't match the hashes, + * the return value is -1 and the command should be aborted. + * + * If the return value is 1, the command has already been completed according to the contents of the + * target blocks, and should not be performed again. + * + * If the return value is 0, source blocks have expected content and the command can be performed. + */ +static int LoadSrcTgtVersion3(CommandParameters& params, RangeSet* tgt, size_t* src_blocks, + bool onehash) { + CHECK(src_blocks != nullptr); + + if (params.cpos >= params.tokens.size()) { + LOG(ERROR) << "missing source hash"; + return -1; + } + + std::string srchash = params.tokens[params.cpos++]; + std::string tgthash; + + if (onehash) { + tgthash = srchash; + } else { + if (params.cpos >= params.tokens.size()) { + LOG(ERROR) << "missing target hash"; + return -1; + } + tgthash = params.tokens[params.cpos++]; + } + + // At least it needs to provide three parameters: , and + // "-"/. + if (params.cpos + 2 >= params.tokens.size()) { + LOG(ERROR) << "invalid parameters"; + return -1; + } + + // + *tgt = RangeSet::Parse(params.tokens[params.cpos++]); + CHECK(static_cast(*tgt)); + + std::vector tgtbuffer(tgt->blocks() * BLOCKSIZE); + if (ReadBlocks(*tgt, &tgtbuffer, params.fd) == -1) { + return -1; + } + + // Return now if target blocks already have expected content. + if (VerifyBlocks(tgthash, tgtbuffer, tgt->blocks(), false) == 0) { + return 1; + } + + // Load source blocks. + bool overlap = false; + if (LoadSourceBlocks(params, *tgt, src_blocks, &overlap) == -1) { + return -1; + } + + if (VerifyBlocks(srchash, params.buffer, *src_blocks, true) == 0) { + // If source and target blocks overlap, stash the source blocks so we can resume from possible + // write errors. In verify mode, we can skip stashing because the source blocks won't be + // overwritten. + if (overlap && params.canwrite) { + LOG(INFO) << "stashing " << *src_blocks << " overlapping blocks to " << srchash; + + bool stash_exists = false; + if (WriteStash(params.stashbase, srchash, *src_blocks, params.buffer, true, + &stash_exists) != 0) { + LOG(ERROR) << "failed to stash overlapping source blocks"; + return -1; + } + + params.stashed += *src_blocks; + // Can be deleted when the write has completed. + if (!stash_exists) { + params.freestash = srchash; + } + } + + // Source blocks have expected content, command can proceed. + return 0; + } + + if (overlap && LoadStash(params, srchash, true, ¶ms.buffer, true) == 0) { + // Overlapping source blocks were previously stashed, command can proceed. We are recovering + // from an interrupted command, so we don't know if the stash can safely be deleted after this + // command. + return 0; + } + + // Valid source data not available, update cannot be resumed. + LOG(ERROR) << "partition has unexpected contents"; + PrintHashForCorruptedSourceBlocks(params, params.buffer); + + params.isunresumable = true; + + return -1; +} + +static int PerformCommandMove(CommandParameters& params) { + size_t blocks = 0; + RangeSet tgt; + int status = LoadSrcTgtVersion3(params, &tgt, &blocks, true); + + if (status == -1) { + LOG(ERROR) << "failed to read blocks for move"; + return -1; + } + + if (status == 0) { + params.foundwrites = true; + } else { + params.target_verified = true; + if (params.foundwrites) { + LOG(WARNING) << "warning: commands executed out of order [" << params.cmdname << "]"; + } + } + + if (params.canwrite) { + if (status == 0) { + LOG(INFO) << " moving " << blocks << " blocks"; + + if (WriteBlocks(tgt, params.buffer, params.fd) == -1) { + return -1; + } + } else { + LOG(INFO) << "skipping " << blocks << " already moved blocks"; + } + } + + if (!params.freestash.empty()) { + FreeStash(params.stashbase, params.freestash); + params.freestash.clear(); + } + + params.written += tgt.blocks(); + + return 0; +} + +static int PerformCommandStash(CommandParameters& params) { + // + if (params.cpos + 1 >= params.tokens.size()) { + LOG(ERROR) << "missing id and/or src range fields in stash command"; + return -1; + } + + const std::string& id = params.tokens[params.cpos++]; + if (LoadStash(params, id, true, ¶ms.buffer, false) == 0) { + // Stash file already exists and has expected contents. Do not read from source again, as the + // source may have been already overwritten during a previous attempt. + return 0; + } + + RangeSet src = RangeSet::Parse(params.tokens[params.cpos++]); + CHECK(static_cast(src)); + + size_t blocks = src.blocks(); + allocate(blocks * BLOCKSIZE, ¶ms.buffer); + if (ReadBlocks(src, ¶ms.buffer, params.fd) == -1) { + return -1; + } + stash_map[id] = src; + + if (VerifyBlocks(id, params.buffer, blocks, true) != 0) { + // Source blocks have unexpected contents. If we actually need this data later, this is an + // unrecoverable error. However, the command that uses the data may have already completed + // previously, so the possible failure will occur during source block verification. + LOG(ERROR) << "failed to load source blocks for stash " << id; + return 0; + } + + // In verify mode, we don't need to stash any blocks. + if (!params.canwrite) { + return 0; + } + + LOG(INFO) << "stashing " << blocks << " blocks to " << id; + int result = WriteStash(params.stashbase, id, blocks, params.buffer, false, nullptr); + if (result == 0) { + params.stashed += blocks; + } + return result; +} + +static int PerformCommandFree(CommandParameters& params) { + // + if (params.cpos >= params.tokens.size()) { + LOG(ERROR) << "missing stash id in free command"; + return -1; + } + + const std::string& id = params.tokens[params.cpos++]; + stash_map.erase(id); + + if (params.createdstash || params.canwrite) { + return FreeStash(params.stashbase, id); + } + + return 0; +} + +static int PerformCommandZero(CommandParameters& params) { + if (params.cpos >= params.tokens.size()) { + LOG(ERROR) << "missing target blocks for zero"; + return -1; + } + + RangeSet tgt = RangeSet::Parse(params.tokens[params.cpos++]); + CHECK(static_cast(tgt)); + + LOG(INFO) << " zeroing " << tgt.blocks() << " blocks"; + + allocate(BLOCKSIZE, ¶ms.buffer); + memset(params.buffer.data(), 0, BLOCKSIZE); + + if (params.canwrite) { + for (const auto& [begin, end] : tgt) { + off64_t offset = static_cast(begin) * BLOCKSIZE; + size_t size = (end - begin) * BLOCKSIZE; + if (!discard_blocks(params.fd, offset, size)) { + return -1; + } + + if (!check_lseek(params.fd, offset, SEEK_SET)) { + return -1; + } + + for (size_t j = begin; j < end; ++j) { + if (!android::base::WriteFully(params.fd, params.buffer.data(), BLOCKSIZE)) { + failure_type = errno == EIO ? kEioFailure : kFwriteFailure; + PLOG(ERROR) << "Failed to write " << BLOCKSIZE << " bytes of data"; + return -1; + } + } + } + } + + if (params.cmdname[0] == 'z') { + // Update only for the zero command, as the erase command will call + // this if DEBUG_ERASE is defined. + params.written += tgt.blocks(); + } + + return 0; +} + +static int PerformCommandNew(CommandParameters& params) { + if (params.cpos >= params.tokens.size()) { + LOG(ERROR) << "missing target blocks for new"; + return -1; + } + + RangeSet tgt = RangeSet::Parse(params.tokens[params.cpos++]); + CHECK(static_cast(tgt)); + + if (params.canwrite) { + LOG(INFO) << " writing " << tgt.blocks() << " blocks of new data"; + + pthread_mutex_lock(¶ms.nti.mu); + params.nti.writer = std::make_unique(params.fd, tgt); + pthread_cond_broadcast(¶ms.nti.cv); + + while (params.nti.writer != nullptr) { + if (!params.nti.receiver_available) { + LOG(ERROR) << "missing " << (tgt.blocks() * BLOCKSIZE - params.nti.writer->BytesWritten()) + << " bytes of new data"; + pthread_mutex_unlock(¶ms.nti.mu); + return -1; + } + pthread_cond_wait(¶ms.nti.cv, ¶ms.nti.mu); + } + + pthread_mutex_unlock(¶ms.nti.mu); + } + + params.written += tgt.blocks(); + + return 0; +} + +static int PerformCommandDiff(CommandParameters& params) { + // + if (params.cpos + 1 >= params.tokens.size()) { + LOG(ERROR) << "missing patch offset or length for " << params.cmdname; + return -1; + } + + size_t offset; + if (!android::base::ParseUint(params.tokens[params.cpos++], &offset)) { + LOG(ERROR) << "invalid patch offset"; + return -1; + } + + size_t len; + if (!android::base::ParseUint(params.tokens[params.cpos++], &len)) { + LOG(ERROR) << "invalid patch len"; + return -1; + } + + RangeSet tgt; + size_t blocks = 0; + int status = LoadSrcTgtVersion3(params, &tgt, &blocks, false); + + if (status == -1) { + LOG(ERROR) << "failed to read blocks for diff"; + return -1; + } + + if (status == 0) { + params.foundwrites = true; + } else { + params.target_verified = true; + if (params.foundwrites) { + LOG(WARNING) << "warning: commands executed out of order [" << params.cmdname << "]"; + } + } + + if (params.canwrite) { + if (status == 0) { + LOG(INFO) << "patching " << blocks << " blocks to " << tgt.blocks(); + Value patch_value( + Value::Type::BLOB, + std::string(reinterpret_cast(params.patch_start + offset), len)); + + RangeSinkWriter writer(params.fd, tgt); + if (params.cmdname[0] == 'i') { // imgdiff + if (ApplyImagePatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value, + std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1, + std::placeholders::_2), + nullptr) != 0) { + LOG(ERROR) << "Failed to apply image patch."; + failure_type = kPatchApplicationFailure; + return -1; + } + } else { + if (ApplyBSDiffPatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value, 0, + std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1, + std::placeholders::_2)) != 0) { + LOG(ERROR) << "Failed to apply bsdiff patch."; + failure_type = kPatchApplicationFailure; + return -1; + } + } + + // We expect the output of the patcher to fill the tgt ranges exactly. + if (!writer.Finished()) { + LOG(ERROR) << "Failed to fully write target blocks (range sink underrun): Missing " + << writer.AvailableSpace() << " bytes"; + failure_type = kPatchApplicationFailure; + return -1; + } + } else { + LOG(INFO) << "skipping " << blocks << " blocks already patched to " << tgt.blocks() << " [" + << params.cmdline << "]"; + } + } + + if (!params.freestash.empty()) { + FreeStash(params.stashbase, params.freestash); + params.freestash.clear(); + } + + params.written += tgt.blocks(); + + return 0; +} + +static int PerformCommandErase(CommandParameters& params) { + if (DEBUG_ERASE) { + return PerformCommandZero(params); + } + + struct stat sb; + if (fstat(params.fd, &sb) == -1) { + PLOG(ERROR) << "failed to fstat device to erase"; + return -1; + } + + if (!S_ISBLK(sb.st_mode)) { + LOG(ERROR) << "not a block device; skipping erase"; + return -1; + } + + if (params.cpos >= params.tokens.size()) { + LOG(ERROR) << "missing target blocks for erase"; + return -1; + } + + RangeSet tgt = RangeSet::Parse(params.tokens[params.cpos++]); + CHECK(static_cast(tgt)); + + if (params.canwrite) { + LOG(INFO) << " erasing " << tgt.blocks() << " blocks"; + + for (const auto& [begin, end] : tgt) { + off64_t offset = static_cast(begin) * BLOCKSIZE; + size_t size = (end - begin) * BLOCKSIZE; + if (!discard_blocks(params.fd, offset, size, true /* force */)) { + return -1; + } + } + } + + return 0; +} + +static int PerformCommandAbort(CommandParameters&) { + LOG(INFO) << "Aborting as instructed"; + return -1; +} + +// Computes the hash_tree bytes based on the parameters, checks if the root hash of the tree +// matches the expected hash and writes the result to the specified range on the block_device. +// Hash_tree computation arguments: +// hash_tree_ranges +// source_ranges +// hash_algorithm +// salt_hex +// root_hash +static int PerformCommandComputeHashTree(CommandParameters& params) { + if (params.cpos + 5 != params.tokens.size()) { + LOG(ERROR) << "Invalid arguments count in hash computation " << params.cmdline; + return -1; + } + + // Expects the hash_tree data to be contiguous. + RangeSet hash_tree_ranges = RangeSet::Parse(params.tokens[params.cpos++]); + if (!hash_tree_ranges || hash_tree_ranges.size() != 1) { + LOG(ERROR) << "Invalid hash tree ranges in " << params.cmdline; + return -1; + } + + RangeSet source_ranges = RangeSet::Parse(params.tokens[params.cpos++]); + if (!source_ranges) { + LOG(ERROR) << "Invalid source ranges in " << params.cmdline; + return -1; + } + + auto hash_function = HashTreeBuilder::HashFunction(params.tokens[params.cpos++]); + if (hash_function == nullptr) { + LOG(ERROR) << "Invalid hash algorithm in " << params.cmdline; + return -1; + } + + std::vector salt; + std::string salt_hex = params.tokens[params.cpos++]; + if (salt_hex.empty() || !HashTreeBuilder::ParseBytesArrayFromString(salt_hex, &salt)) { + LOG(ERROR) << "Failed to parse salt in " << params.cmdline; + return -1; + } + + std::string expected_root_hash = params.tokens[params.cpos++]; + if (expected_root_hash.empty()) { + LOG(ERROR) << "Invalid root hash in " << params.cmdline; + return -1; + } + + // Starts the hash_tree computation. + HashTreeBuilder builder(BLOCKSIZE, hash_function); + if (!builder.Initialize(static_cast(source_ranges.blocks()) * BLOCKSIZE, salt)) { + LOG(ERROR) << "Failed to initialize hash tree computation, source " << source_ranges.ToString() + << ", salt " << salt_hex; + return -1; + } + + // Iterates through every block in the source_ranges and updates the hash tree structure + // accordingly. + for (const auto& [begin, end] : source_ranges) { + uint8_t buffer[BLOCKSIZE]; + if (!check_lseek(params.fd, static_cast(begin) * BLOCKSIZE, SEEK_SET)) { + PLOG(ERROR) << "Failed to seek to block: " << begin; + return -1; + } + + for (size_t i = begin; i < end; i++) { + if (!android::base::ReadFully(params.fd, buffer, BLOCKSIZE)) { + failure_type = errno == EIO ? kEioFailure : kFreadFailure; + LOG(ERROR) << "Failed to read data in " << begin << ":" << end; + return -1; + } + + if (!builder.Update(reinterpret_cast(buffer), BLOCKSIZE)) { + LOG(ERROR) << "Failed to update hash tree builder"; + return -1; + } + } + } + + if (!builder.BuildHashTree()) { + LOG(ERROR) << "Failed to build hash tree"; + return -1; + } + + std::string root_hash_hex = HashTreeBuilder::BytesArrayToString(builder.root_hash()); + if (root_hash_hex != expected_root_hash) { + LOG(ERROR) << "Root hash of the verity hash tree doesn't match the expected value. Expected: " + << expected_root_hash << ", actual: " << root_hash_hex; + return -1; + } + + uint64_t write_offset = static_cast(hash_tree_ranges.GetBlockNumber(0)) * BLOCKSIZE; + if (params.canwrite && !builder.WriteHashTreeToFd(params.fd, write_offset)) { + LOG(ERROR) << "Failed to write hash tree to output"; + return -1; + } + + // TODO(xunchang) validates the written bytes + + return 0; +} + +using CommandFunction = std::function; + +using CommandMap = std::unordered_map; + +static bool Sha1DevicePath(const std::string& path, uint8_t digest[SHA_DIGEST_LENGTH]) { + auto device_name = android::base::Basename(path); + auto dm_target_name_path = "/sys/block/" + device_name + "/dm/name"; + + struct stat sb; + if (stat(dm_target_name_path.c_str(), &sb) == 0) { + // This is a device mapper target. Use partition name as part of the hash instead. Do not + // include extents as part of the hash, because the size of a partition may be shrunk after + // the patches are applied. + std::string dm_target_name; + if (!android::base::ReadFileToString(dm_target_name_path, &dm_target_name)) { + PLOG(ERROR) << "Cannot read " << dm_target_name_path; + return false; + } + SHA1(reinterpret_cast(dm_target_name.data()), dm_target_name.size(), digest); + return true; + } + + if (errno != ENOENT) { + // This is a device mapper target, but its name cannot be retrieved. + PLOG(ERROR) << "Cannot get dm target name for " << path; + return false; + } + + // This doesn't appear to be a device mapper target, but if its name starts with dm-, something + // else might have gone wrong. + if (android::base::StartsWith(device_name, "dm-")) { + LOG(WARNING) << "Device " << path << " starts with dm- but is not mapped by device-mapper."; + } + + // Stash directory should be different for each partition to avoid conflicts when updating + // multiple partitions at the same time, so we use the hash of the block device name as the base + // directory. + SHA1(reinterpret_cast(path.data()), path.size(), digest); + return true; +} + +static Value* PerformBlockImageUpdate(const char* name, State* state, + const std::vector>& argv, + const CommandMap& command_map, bool dryrun) { + CommandParameters params{}; + stash_map.clear(); + params.canwrite = !dryrun; + + LOG(INFO) << "performing " << (dryrun ? "verification" : "update"); + if (state->is_retry) { + is_retry = true; + LOG(INFO) << "This update is a retry."; + } + if (argv.size() != 4) { + ErrorAbort(state, kArgsParsingFailure, "block_image_update expects 4 arguments, got %zu", + argv.size()); + return StringValue(""); + } + + std::vector> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + + // args: + // - block device (or file) to modify in-place + // - transfer list (blob) + // - new data stream (filename within package.zip) + // - patch stream (filename within package.zip, must be uncompressed) + const std::unique_ptr& blockdev_filename = args[0]; + const std::unique_ptr& transfer_list_value = args[1]; + const std::unique_ptr& new_data_fn = args[2]; + const std::unique_ptr& patch_data_fn = args[3]; + + if (blockdev_filename->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "blockdev_filename argument to %s must be string", name); + return StringValue(""); + } + if (transfer_list_value->type != Value::Type::BLOB) { + ErrorAbort(state, kArgsParsingFailure, "transfer_list argument to %s must be blob", name); + return StringValue(""); + } + if (new_data_fn->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "new_data_fn argument to %s must be string", name); + return StringValue(""); + } + if (patch_data_fn->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "patch_data_fn argument to %s must be string", name); + return StringValue(""); + } + + auto updater = state->updater; + auto block_device_path = updater->FindBlockDeviceName(blockdev_filename->data); + if (block_device_path.empty()) { + LOG(ERROR) << "Block device path for " << blockdev_filename->data << " not found. " << name + << " failed."; + return StringValue(""); + } + + ZipArchiveHandle za = updater->GetPackageHandle(); + if (za == nullptr) { + return StringValue(""); + } + + std::string_view path_data(patch_data_fn->data); + ZipEntry64 patch_entry; + if (FindEntry(za, path_data, &patch_entry) != 0) { + LOG(ERROR) << name << "(): no file \"" << patch_data_fn->data << "\" in package"; + return StringValue(""); + } + params.patch_start = updater->GetMappedPackageAddress() + patch_entry.offset; + + std::string_view new_data(new_data_fn->data); + ZipEntry64 new_entry; + if (FindEntry(za, new_data, &new_entry) != 0) { + LOG(ERROR) << name << "(): no file \"" << new_data_fn->data << "\" in package"; + return StringValue(""); + } + + params.fd.reset(TEMP_FAILURE_RETRY(open(block_device_path.c_str(), O_RDWR))); + if (params.fd == -1) { + failure_type = errno == EIO ? kEioFailure : kFileOpenFailure; + PLOG(ERROR) << "open \"" << block_device_path << "\" failed"; + return StringValue(""); + } + + uint8_t digest[SHA_DIGEST_LENGTH]; + if (!Sha1DevicePath(block_device_path, digest)) { + return StringValue(""); + } + params.stashbase = print_sha1(digest); + + // Possibly do return early on retry, by checking the marker. If the update on this partition has + // been finished (but interrupted at a later point), there could be leftover on /cache that would + // fail the no-op retry. + std::string updated_marker = GetStashFileName(params.stashbase + ".UPDATED", "", ""); + if (is_retry) { + struct stat sb; + int result = stat(updated_marker.c_str(), &sb); + if (result == 0) { + LOG(INFO) << "Skipping already updated partition " << block_device_path << " based on marker"; + return StringValue("t"); + } + } else { + // Delete the obsolete marker if any. + std::string err; + if (!android::base::RemoveFileIfExists(updated_marker, &err)) { + LOG(ERROR) << "Failed to remove partition updated marker " << updated_marker << ": " << err; + return StringValue(""); + } + } + + static constexpr size_t kTransferListHeaderLines = 4; + std::vector lines = android::base::Split(transfer_list_value->data, "\n"); + if (lines.size() < kTransferListHeaderLines) { + ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]", + lines.size()); + return StringValue(""); + } + + // First line in transfer list is the version number. + if (!android::base::ParseInt(lines[0], ¶ms.version, 3, 4)) { + LOG(ERROR) << "unexpected transfer list version [" << lines[0] << "]"; + return StringValue(""); + } + + LOG(INFO) << "blockimg version is " << params.version; + + // Second line in transfer list is the total number of blocks we expect to write. + size_t total_blocks; + if (!android::base::ParseUint(lines[1], &total_blocks)) { + ErrorAbort(state, kArgsParsingFailure, "unexpected block count [%s]", lines[1].c_str()); + return StringValue(""); + } + + if (total_blocks == 0) { + return StringValue("t"); + } + + // Third line is how many stash entries are needed simultaneously. + LOG(INFO) << "maximum stash entries " << lines[2]; + + // Fourth line is the maximum number of blocks that will be stashed simultaneously + size_t stash_max_blocks; + if (!android::base::ParseUint(lines[3], &stash_max_blocks)) { + ErrorAbort(state, kArgsParsingFailure, "unexpected maximum stash blocks [%s]", + lines[3].c_str()); + return StringValue(""); + } + + int res = CreateStash(state, stash_max_blocks, params.stashbase); + if (res == -1) { + return StringValue(""); + } + params.createdstash = res; + + // Set up the new data writer. + if (params.canwrite) { + params.nti.za = za; + params.nti.entry = new_entry; + params.nti.brotli_compressed = android::base::EndsWith(new_data_fn->data, ".br"); + if (params.nti.brotli_compressed) { + // Initialize brotli decoder state. + params.nti.brotli_decoder_state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr); + } + params.nti.receiver_available = true; + + pthread_mutex_init(¶ms.nti.mu, nullptr); + pthread_cond_init(¶ms.nti.cv, nullptr); + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + + int error = pthread_create(¶ms.thread, &attr, unzip_new_data, ¶ms.nti); + if (error != 0) { + LOG(ERROR) << "pthread_create failed: " << strerror(error); + return StringValue(""); + } + } + + // When performing an update, save the index and cmdline of the current command into the + // last_command_file. + // Upon resuming an update, read the saved index first; then + // 1. In verification mode, check if the 'move' or 'diff' commands before the saved index has + // the expected target blocks already. If not, these commands cannot be skipped and we need + // to attempt to execute them again. Therefore, we will delete the last_command_file so that + // the update will resume from the start of the transfer list. + // 2. In update mode, skip all commands before the saved index. Therefore, we can avoid deleting + // stashes with duplicate id unintentionally (b/69858743); and also speed up the update. + // If an update succeeds or is unresumable, delete the last_command_file. + bool skip_executed_command = true; + size_t saved_last_command_index; + if (!ParseLastCommandFile(&saved_last_command_index)) { + DeleteLastCommandFile(); + // We failed to parse the last command. Disallow skipping executed commands. + skip_executed_command = false; + } + + int rc = -1; + + // Subsequent lines are all individual transfer commands + for (size_t i = kTransferListHeaderLines; i < lines.size(); i++) { + const std::string& line = lines[i]; + if (line.empty()) continue; + + size_t cmdindex = i - kTransferListHeaderLines; + params.tokens = android::base::Split(line, " "); + params.cpos = 0; + params.cmdname = params.tokens[params.cpos++]; + params.cmdline = line; + params.target_verified = false; + + Command::Type cmd_type = Command::ParseType(params.cmdname); + if (cmd_type == Command::Type::LAST) { + LOG(ERROR) << "unexpected command [" << params.cmdname << "]"; + goto pbiudone; + } + + const CommandFunction& performer = command_map.at(cmd_type); + + // Skip the command if we explicitly set the corresponding function pointer to nullptr, e.g. + // "erase" during block_image_verify. + if (performer == nullptr) { + LOG(DEBUG) << "skip executing command [" << line << "]"; + continue; + } + + // Skip all commands before the saved last command index when resuming an update, except for + // "new" command. Because new commands read in the data sequentially. + if (params.canwrite && skip_executed_command && cmdindex <= saved_last_command_index && + cmd_type != Command::Type::NEW) { + LOG(INFO) << "Skipping already executed command: " << cmdindex + << ", last executed command for previous update: " << saved_last_command_index; + continue; + } + + if (performer(params) == -1) { + LOG(ERROR) << "failed to execute command [" << line << "]"; + if (cmd_type == Command::Type::COMPUTE_HASH_TREE && failure_type == kNoCause) { + failure_type = kHashTreeComputationFailure; + } + goto pbiudone; + } + + // In verify mode, check if the commands before the saved last_command_index have been executed + // correctly. If some target blocks have unexpected contents, delete the last command file so + // that we will resume the update from the first command in the transfer list. + if (!params.canwrite && skip_executed_command && cmdindex <= saved_last_command_index) { + // TODO(xunchang) check that the cmdline of the saved index is correct. + if ((cmd_type == Command::Type::MOVE || cmd_type == Command::Type::BSDIFF || + cmd_type == Command::Type::IMGDIFF) && + !params.target_verified) { + LOG(WARNING) << "Previously executed command " << saved_last_command_index << ": " + << params.cmdline << " doesn't produce expected target blocks."; + skip_executed_command = false; + DeleteLastCommandFile(); + } + } + + if (params.canwrite) { + if (fsync(params.fd) == -1) { + failure_type = errno == EIO ? kEioFailure : kFsyncFailure; + PLOG(ERROR) << "fsync failed"; + goto pbiudone; + } + + if (!UpdateLastCommandIndex(cmdindex, params.cmdline)) { + LOG(WARNING) << "Failed to update the last command file."; + } + + updater->WriteToCommandPipe( + android::base::StringPrintf("set_progress %.4f", + static_cast(params.written) / total_blocks), + true); + } + } + + rc = 0; + +pbiudone: + if (params.canwrite) { + pthread_mutex_lock(¶ms.nti.mu); + if (params.nti.receiver_available) { + LOG(WARNING) << "new data receiver is still available after executing all commands."; + } + params.nti.receiver_available = false; + pthread_cond_broadcast(¶ms.nti.cv); + pthread_mutex_unlock(¶ms.nti.mu); + int ret = pthread_join(params.thread, nullptr); + if (ret != 0) { + LOG(WARNING) << "pthread join returned with " << strerror(ret); + } + + if (rc == 0) { + LOG(INFO) << "wrote " << params.written << " blocks; expected " << total_blocks; + LOG(INFO) << "stashed " << params.stashed << " blocks"; + LOG(INFO) << "max alloc needed was " << params.buffer.size(); + + const char* partition = strrchr(block_device_path.c_str(), '/'); + if (partition != nullptr && *(partition + 1) != 0) { + updater->WriteToCommandPipe( + android::base::StringPrintf("log bytes_written_%s: %" PRIu64, partition + 1, + static_cast(params.written) * BLOCKSIZE)); + updater->WriteToCommandPipe( + android::base::StringPrintf("log bytes_stashed_%s: %" PRIu64, partition + 1, + static_cast(params.stashed) * BLOCKSIZE), + true); + } + // Delete stash only after successfully completing the update, as it may contain blocks needed + // to complete the update later. + DeleteStash(params.stashbase); + DeleteLastCommandFile(); + + // Create a marker on /cache partition, which allows skipping the update on this partition on + // retry. The marker will be removed once booting into normal boot, or before starting next + // fresh install. + if (!SetUpdatedMarker(updated_marker)) { + LOG(WARNING) << "Failed to set updated marker; continuing"; + } + } + + pthread_mutex_destroy(¶ms.nti.mu); + pthread_cond_destroy(¶ms.nti.cv); + } else if (rc == 0) { + LOG(INFO) << "verified partition contents; update may be resumed"; + } + + if (fsync(params.fd) == -1) { + failure_type = errno == EIO ? kEioFailure : kFsyncFailure; + PLOG(ERROR) << "fsync failed"; + } + // params.fd will be automatically closed because it's a unique_fd. + + if (params.nti.brotli_decoder_state != nullptr) { + BrotliDecoderDestroyInstance(params.nti.brotli_decoder_state); + } + + // Delete the last command file if the update cannot be resumed. + if (params.isunresumable) { + DeleteLastCommandFile(); + } + + // Only delete the stash if the update cannot be resumed, or it's a verification run and we + // created the stash. + if (params.isunresumable || (!params.canwrite && params.createdstash)) { + DeleteStash(params.stashbase); + } + + if (failure_type != kNoCause && state->cause_code == kNoCause) { + state->cause_code = failure_type; + } + + return StringValue(rc == 0 ? "t" : ""); +} + +/** + * The transfer list is a text file containing commands to transfer data from one place to another + * on the target partition. We parse it and execute the commands in order: + * + * zero [rangeset] + * - Fill the indicated blocks with zeros. + * + * new [rangeset] + * - Fill the blocks with data read from the new_data file. + * + * erase [rangeset] + * - Mark the given blocks as empty. + * + * move <...> + * bsdiff <...> + * imgdiff <...> + * - Read the source blocks, apply a patch (or not in the case of move), write result to target + * blocks. bsdiff or imgdiff specifies the type of patch; move means no patch at all. + * + * See the comments in LoadSrcTgtVersion3() for a description of the <...> format. + * + * stash + * - Load the given source range and stash the data in the given slot of the stash table. + * + * free + * - Free the given stash data. + * + * The creator of the transfer list will guarantee that no block is read (ie, used as the source for + * a patch or move) after it has been written. + * + * The creator will guarantee that a given stash is loaded (with a stash command) before it's used + * in a move/bsdiff/imgdiff command. + * + * Within one command the source and target ranges may overlap so in general we need to read the + * entire source into memory before writing anything to the target blocks. + * + * All the patch data is concatenated into one patch_data file in the update package. It must be + * stored uncompressed because we memory-map it in directly from the archive. (Since patches are + * already compressed, we lose very little by not compressing their concatenation.) + * + * Commands that read data from the partition (i.e. move/bsdiff/imgdiff/stash) have one or more + * additional hashes before the range parameters, which are used to check if the command has already + * been completed and verify the integrity of the source data. + */ +Value* BlockImageVerifyFn(const char* name, State* state, + const std::vector>& argv) { + // Commands which are not allowed are set to nullptr to skip them completely. + const CommandMap command_map{ + // clang-format off + { Command::Type::ABORT, PerformCommandAbort }, + { Command::Type::BSDIFF, PerformCommandDiff }, + { Command::Type::COMPUTE_HASH_TREE, nullptr }, + { Command::Type::ERASE, nullptr }, + { Command::Type::FREE, PerformCommandFree }, + { Command::Type::IMGDIFF, PerformCommandDiff }, + { Command::Type::MOVE, PerformCommandMove }, + { Command::Type::NEW, nullptr }, + { Command::Type::STASH, PerformCommandStash }, + { Command::Type::ZERO, nullptr }, + // clang-format on + }; + CHECK_EQ(static_cast(Command::Type::LAST), command_map.size()); + + // Perform a dry run without writing to test if an update can proceed. + return PerformBlockImageUpdate(name, state, argv, command_map, true); +} + +Value* BlockImageUpdateFn(const char* name, State* state, + const std::vector>& argv) { + const CommandMap command_map{ + // clang-format off + { Command::Type::ABORT, PerformCommandAbort }, + { Command::Type::BSDIFF, PerformCommandDiff }, + { Command::Type::COMPUTE_HASH_TREE, PerformCommandComputeHashTree }, + { Command::Type::ERASE, PerformCommandErase }, + { Command::Type::FREE, PerformCommandFree }, + { Command::Type::IMGDIFF, PerformCommandDiff }, + { Command::Type::MOVE, PerformCommandMove }, + { Command::Type::NEW, PerformCommandNew }, + { Command::Type::STASH, PerformCommandStash }, + { Command::Type::ZERO, PerformCommandZero }, + // clang-format on + }; + CHECK_EQ(static_cast(Command::Type::LAST), command_map.size()); + + return PerformBlockImageUpdate(name, state, argv, command_map, false); +} + +Value* RangeSha1Fn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 2) { + ErrorAbort(state, kArgsParsingFailure, "range_sha1 expects 2 arguments, got %zu", argv.size()); + return StringValue(""); + } + + std::vector> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + + const std::unique_ptr& blockdev_filename = args[0]; + const std::unique_ptr& ranges = args[1]; + + if (blockdev_filename->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "blockdev_filename argument to %s must be string", name); + return StringValue(""); + } + if (ranges->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "ranges argument to %s must be string", name); + return StringValue(""); + } + + auto block_device_path = state->updater->FindBlockDeviceName(blockdev_filename->data); + if (block_device_path.empty()) { + LOG(ERROR) << "Block device path for " << blockdev_filename->data << " not found. " << name + << " failed."; + return StringValue(""); + } + + android::base::unique_fd fd(open(block_device_path.c_str(), O_RDWR)); + if (fd == -1) { + CauseCode cause_code = errno == EIO ? kEioFailure : kFileOpenFailure; + ErrorAbort(state, cause_code, "open \"%s\" failed: %s", block_device_path.c_str(), + strerror(errno)); + return StringValue(""); + } + + RangeSet rs = RangeSet::Parse(ranges->data); + CHECK(static_cast(rs)); + + SHA_CTX ctx; + SHA1_Init(&ctx); + + std::vector buffer(BLOCKSIZE); + for (const auto& [begin, end] : rs) { + if (!check_lseek(fd, static_cast(begin) * BLOCKSIZE, SEEK_SET)) { + ErrorAbort(state, kLseekFailure, "failed to seek %s: %s", block_device_path.c_str(), + strerror(errno)); + return StringValue(""); + } + + for (size_t j = begin; j < end; ++j) { + if (!android::base::ReadFully(fd, buffer.data(), BLOCKSIZE)) { + CauseCode cause_code = errno == EIO ? kEioFailure : kFreadFailure; + ErrorAbort(state, cause_code, "failed to read %s: %s", block_device_path.c_str(), + strerror(errno)); + return StringValue(""); + } + + SHA1_Update(&ctx, buffer.data(), BLOCKSIZE); + } + } + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1_Final(digest, &ctx); + + return StringValue(print_sha1(digest)); +} + +// This function checks if a device has been remounted R/W prior to an incremental +// OTA update. This is a common cause of update abortion. The function reads the +// 1st block of each partition and check for mounting time/count. It return string "t" +// if executes successfully and an empty string otherwise. + +Value* CheckFirstBlockFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 1) { + ErrorAbort(state, kArgsParsingFailure, "check_first_block expects 1 argument, got %zu", + argv.size()); + return StringValue(""); + } + + std::vector> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + + const std::unique_ptr& arg_filename = args[0]; + + if (arg_filename->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "filename argument to %s must be string", name); + return StringValue(""); + } + + auto block_device_path = state->updater->FindBlockDeviceName(arg_filename->data); + if (block_device_path.empty()) { + LOG(ERROR) << "Block device path for " << arg_filename->data << " not found. " << name + << " failed."; + return StringValue(""); + } + + android::base::unique_fd fd(open(block_device_path.c_str(), O_RDONLY)); + if (fd == -1) { + CauseCode cause_code = errno == EIO ? kEioFailure : kFileOpenFailure; + ErrorAbort(state, cause_code, "open \"%s\" failed: %s", block_device_path.c_str(), + strerror(errno)); + return StringValue(""); + } + + RangeSet blk0(std::vector{ Range{ 0, 1 } }); + std::vector block0_buffer(BLOCKSIZE); + + if (ReadBlocks(blk0, &block0_buffer, fd) == -1) { + CauseCode cause_code = errno == EIO ? kEioFailure : kFreadFailure; + ErrorAbort(state, cause_code, "failed to read %s: %s", block_device_path.c_str(), + strerror(errno)); + return StringValue(""); + } + + // https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout + // Super block starts from block 0, offset 0x400 + // 0x2C: len32 Mount time + // 0x30: len32 Write time + // 0x34: len16 Number of mounts since the last fsck + // 0x38: len16 Magic signature 0xEF53 + + time_t mount_time = *reinterpret_cast(&block0_buffer[0x400 + 0x2C]); + uint16_t mount_count = *reinterpret_cast(&block0_buffer[0x400 + 0x34]); + + if (mount_count > 0) { + state->updater->UiPrint( + android::base::StringPrintf("Device was remounted R/W %" PRIu16 " times", mount_count)); + state->updater->UiPrint( + android::base::StringPrintf("Last remount happened on %s", ctime(&mount_time))); + } + + return StringValue("t"); +} + +Value* BlockImageRecoverFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 2) { + ErrorAbort(state, kArgsParsingFailure, "block_image_recover expects 2 arguments, got %zu", + argv.size()); + return StringValue(""); + } + + std::vector> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + + const std::unique_ptr& filename = args[0]; + const std::unique_ptr& ranges = args[1]; + + if (filename->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "filename argument to %s must be string", name); + return StringValue(""); + } + if (ranges->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "ranges argument to %s must be string", name); + return StringValue(""); + } + RangeSet rs = RangeSet::Parse(ranges->data); + if (!rs) { + ErrorAbort(state, kArgsParsingFailure, "failed to parse ranges: %s", ranges->data.c_str()); + return StringValue(""); + } + + auto block_device_path = state->updater->FindBlockDeviceName(filename->data); + if (block_device_path.empty()) { + LOG(ERROR) << "Block device path for " << filename->data << " not found. " << name + << " failed."; + return StringValue(""); + } + + // Output notice to log when recover is attempted + LOG(INFO) << block_device_path << " image corrupted, attempting to recover..."; + + // When opened with O_RDWR, libfec rewrites corrupted blocks when they are read + fec::io fh(block_device_path, O_RDWR); + + if (!fh) { + ErrorAbort(state, kLibfecFailure, "fec_open \"%s\" failed: %s", block_device_path.c_str(), + strerror(errno)); + return StringValue(""); + } + + if (!fh.has_ecc() || !fh.has_verity()) { + ErrorAbort(state, kLibfecFailure, "unable to use metadata to correct errors"); + return StringValue(""); + } + + fec_status status; + if (!fh.get_status(status)) { + ErrorAbort(state, kLibfecFailure, "failed to read FEC status"); + return StringValue(""); + } + + uint8_t buffer[BLOCKSIZE]; + for (const auto& [begin, end] : rs) { + for (size_t j = begin; j < end; ++j) { + // Stay within the data area, libfec validates and corrects metadata + if (status.data_size <= static_cast(j) * BLOCKSIZE) { + continue; + } + + if (fh.pread(buffer, BLOCKSIZE, static_cast(j) * BLOCKSIZE) != BLOCKSIZE) { + ErrorAbort(state, kLibfecFailure, "failed to recover %s (block %zu): %s", + block_device_path.c_str(), j, strerror(errno)); + return StringValue(""); + } + + // If we want to be able to recover from a situation where rewriting a corrected + // block doesn't guarantee the same data will be returned when re-read later, we + // can save a copy of corrected blocks to /cache. Note: + // + // 1. Maximum space required from /cache is the same as the maximum number of + // corrupted blocks we can correct. For RS(255, 253) and a 2 GiB partition, + // this would be ~16 MiB, for example. + // + // 2. To find out if this block was corrupted, call fec_get_status after each + // read and check if the errors field value has increased. + } + } + LOG(INFO) << "..." << block_device_path << " image recovered successfully."; + return StringValue("t"); +} + +void RegisterBlockImageFunctions() { + RegisterFunction("block_image_verify", BlockImageVerifyFn); + RegisterFunction("block_image_update", BlockImageUpdateFn); + RegisterFunction("block_image_recover", BlockImageRecoverFn); + RegisterFunction("check_first_block", CheckFirstBlockFn); + RegisterFunction("range_sha1", RangeSha1Fn); +} diff --git a/updater/build_info.cpp b/updater/build_info.cpp new file mode 100644 index 0000000..f168008 --- /dev/null +++ b/updater/build_info.cpp @@ -0,0 +1,139 @@ +/* + * 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 "updater/build_info.h" + +#include + +#include +#include + +#include +#include +#include + +#include "updater/target_files.h" + +bool BuildInfo::ParseTargetFile(const std::string_view target_file_path, bool extracted_input) { + TargetFile target_file(std::string(target_file_path), extracted_input); + if (!target_file.Open()) { + return false; + } + + if (!target_file.GetBuildProps(&build_props_)) { + return false; + } + + std::vector fstab_info_list; + if (!target_file.ParseFstabInfo(&fstab_info_list)) { + return false; + } + + for (const auto& fstab_info : fstab_info_list) { + for (const auto& directory : { "IMAGES", "RADIO" }) { + std::string entry_name = directory + fstab_info.mount_point + ".img"; + if (!target_file.EntryExists(entry_name)) { + LOG(WARNING) << "Failed to find the image entry in the target file: " << entry_name; + continue; + } + + temp_files_.emplace_back(work_dir_); + auto& image_file = temp_files_.back(); + if (!target_file.ExtractImage(entry_name, fstab_info, work_dir_, &image_file)) { + LOG(ERROR) << "Failed to set up source image files."; + return false; + } + + std::string mapped_path = image_file.path; + // Rename the images to more readable ones if we want to keep the image. + if (keep_images_) { + mapped_path = work_dir_ + fstab_info.mount_point + ".img"; + image_file.release(); + if (rename(image_file.path, mapped_path.c_str()) != 0) { + PLOG(ERROR) << "Failed to rename " << image_file.path << " to " << mapped_path; + return false; + } + } + + LOG(INFO) << "Mounted " << fstab_info.mount_point << "\nMapping: " << fstab_info.blockdev_name + << " to " << mapped_path; + + blockdev_map_.emplace( + fstab_info.blockdev_name, + FakeBlockDevice(fstab_info.blockdev_name, fstab_info.mount_point, mapped_path)); + break; + } + } + + return true; +} + +std::string BuildInfo::GetProperty(const std::string_view key, + const std::string_view default_value) const { + // The logic to parse the ro.product properties should be in line with the generation script. + // More details in common.py BuildInfo.GetBuildProp. + // TODO(xunchang) handle the oem property and the source order defined in + // ro.product.property_source_order + const std::set> ro_product_props = { + "ro.product.brand", "ro.product.device", "ro.product.manufacturer", "ro.product.model", + "ro.product.name" + }; + const std::vector source_order = { + "product", "odm", "vendor", "system_ext", "system", + }; + if (ro_product_props.find(key) != ro_product_props.end()) { + std::string_view key_suffix(key); + CHECK(android::base::ConsumePrefix(&key_suffix, "ro.product")); + for (const auto& source : source_order) { + std::string resolved_key = "ro.product." + source + std::string(key_suffix); + if (auto entry = build_props_.find(resolved_key); entry != build_props_.end()) { + return entry->second; + } + } + LOG(WARNING) << "Failed to find property: " << key; + return std::string(default_value); + } else if (key == "ro.build.fingerprint") { + // clang-format off + return android::base::StringPrintf("%s/%s/%s:%s/%s/%s:%s/%s", + GetProperty("ro.product.brand", "").c_str(), + GetProperty("ro.product.name", "").c_str(), + GetProperty("ro.product.device", "").c_str(), + GetProperty("ro.build.version.release", "").c_str(), + GetProperty("ro.build.id", "").c_str(), + GetProperty("ro.build.version.incremental", "").c_str(), + GetProperty("ro.build.type", "").c_str(), + GetProperty("ro.build.tags", "").c_str()); + // clang-format on + } + + auto entry = build_props_.find(key); + if (entry == build_props_.end()) { + LOG(WARNING) << "Failed to find property: " << key; + return std::string(default_value); + } + + return entry->second; +} + +std::string BuildInfo::FindBlockDeviceName(const std::string_view name) const { + auto entry = blockdev_map_.find(name); + if (entry == blockdev_map_.end()) { + LOG(WARNING) << "Failed to find path to block device " << name; + return ""; + } + + return entry->second.mounted_file_path; +} diff --git a/updater/commands.cpp b/updater/commands.cpp new file mode 100644 index 0000000..1a7c272 --- /dev/null +++ b/updater/commands.cpp @@ -0,0 +1,453 @@ +/* + * 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 "private/commands.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "otautil/print_sha1.h" +#include "otautil/rangeset.h" + +using namespace std::string_literals; + +bool Command::abort_allowed_ = false; + +Command::Command(Type type, size_t index, std::string cmdline, HashTreeInfo hash_tree_info) + : type_(type), + index_(index), + cmdline_(std::move(cmdline)), + hash_tree_info_(std::move(hash_tree_info)) { + CHECK(type == Type::COMPUTE_HASH_TREE); +} + +Command::Type Command::ParseType(const std::string& type_str) { + if (type_str == "abort") { + if (!abort_allowed_) { + LOG(ERROR) << "ABORT disallowed"; + return Type::LAST; + } + return Type::ABORT; + } else if (type_str == "bsdiff") { + return Type::BSDIFF; + } else if (type_str == "compute_hash_tree") { + return Type::COMPUTE_HASH_TREE; + } else if (type_str == "erase") { + return Type::ERASE; + } else if (type_str == "free") { + return Type::FREE; + } else if (type_str == "imgdiff") { + return Type::IMGDIFF; + } else if (type_str == "move") { + return Type::MOVE; + } else if (type_str == "new") { + return Type::NEW; + } else if (type_str == "stash") { + return Type::STASH; + } else if (type_str == "zero") { + return Type::ZERO; + } + return Type::LAST; +}; + +bool Command::ParseTargetInfoAndSourceInfo(const std::vector& tokens, + const std::string& tgt_hash, TargetInfo* target, + const std::string& src_hash, SourceInfo* source, + std::string* err) { + // We expect the given args (in 'tokens' vector) in one of the following formats. + // + // - <[stash_id:location] ...> + // (loads data from stashes only) + // + // + // (loads data from source image only) + // + // <[stash_id:location] ...> + // (loads data from both of source image and stashes) + + // At least it needs to provide three args: , and "-"/. + if (tokens.size() < 3) { + *err = "invalid number of args"; + return false; + } + + size_t pos = 0; + RangeSet tgt_ranges = RangeSet::Parse(tokens[pos++]); + if (!tgt_ranges) { + *err = "invalid target ranges"; + return false; + } + *target = TargetInfo(tgt_hash, tgt_ranges); + + // + const std::string& token = tokens[pos++]; + size_t src_blocks; + if (!android::base::ParseUint(token, &src_blocks)) { + *err = "invalid src_block_count \""s + token + "\""; + return false; + } + + RangeSet src_ranges; + RangeSet src_ranges_location; + // "-" or [] + if (tokens[pos] == "-") { + // no source ranges, only stashes + pos++; + } else { + src_ranges = RangeSet::Parse(tokens[pos++]); + if (!src_ranges) { + *err = "invalid source ranges"; + return false; + } + + if (pos >= tokens.size()) { + // No stashes, only source ranges. + SourceInfo result(src_hash, src_ranges, {}, {}); + + if (result.blocks() != src_blocks) { + *err = + android::base::StringPrintf("mismatching block count: %zu (%s) vs %zu", result.blocks(), + src_ranges.ToString().c_str(), src_blocks); + return false; + } + + *source = result; + return true; + } + + src_ranges_location = RangeSet::Parse(tokens[pos++]); + if (!src_ranges_location) { + *err = "invalid source ranges location"; + return false; + } + } + + // <[stash_id:stash_location]> + std::vector stashes; + while (pos < tokens.size()) { + // Each word is a an index into the stash table, a colon, and then a RangeSet describing where + // in the source block that stashed data should go. + std::vector pairs = android::base::Split(tokens[pos++], ":"); + if (pairs.size() != 2) { + *err = "invalid stash info"; + return false; + } + RangeSet stash_location = RangeSet::Parse(pairs[1]); + if (!stash_location) { + *err = "invalid stash location"; + return false; + } + stashes.emplace_back(pairs[0], stash_location); + } + + SourceInfo result(src_hash, src_ranges, src_ranges_location, stashes); + if (src_blocks != result.blocks()) { + *err = android::base::StringPrintf("mismatching block count: %zu (%s) vs %zu", result.blocks(), + src_ranges.ToString().c_str(), src_blocks); + return false; + } + + *source = result; + return true; +} + +Command Command::Parse(const std::string& line, size_t index, std::string* err) { + std::vector tokens = android::base::Split(line, " "); + size_t pos = 0; + // tokens.size() will be 1 at least. + Type op = ParseType(tokens[pos++]); + if (op == Type::LAST) { + *err = "invalid type"; + return {}; + } + + PatchInfo patch_info; + TargetInfo target_info; + SourceInfo source_info; + StashInfo stash_info; + + if (op == Type::ZERO || op == Type::NEW || op == Type::ERASE) { + // zero/new/erase + if (pos + 1 != tokens.size()) { + *err = android::base::StringPrintf("invalid number of args: %zu (expected 1)", + tokens.size() - pos); + return {}; + } + RangeSet tgt_ranges = RangeSet::Parse(tokens[pos++]); + if (!tgt_ranges) { + return {}; + } + static const std::string kUnknownHash{ "unknown-hash" }; + target_info = TargetInfo(kUnknownHash, tgt_ranges); + } else if (op == Type::STASH) { + // stash + if (pos + 2 != tokens.size()) { + *err = android::base::StringPrintf("invalid number of args: %zu (expected 2)", + tokens.size() - pos); + return {}; + } + const std::string& id = tokens[pos++]; + RangeSet src_ranges = RangeSet::Parse(tokens[pos++]); + if (!src_ranges) { + *err = "invalid token"; + return {}; + } + stash_info = StashInfo(id, src_ranges); + } else if (op == Type::FREE) { + // free + if (pos + 1 != tokens.size()) { + *err = android::base::StringPrintf("invalid number of args: %zu (expected 1)", + tokens.size() - pos); + return {}; + } + stash_info = StashInfo(tokens[pos++], {}); + } else if (op == Type::MOVE) { + // + if (pos + 1 > tokens.size()) { + *err = "missing hash"; + return {}; + } + std::string hash = tokens[pos++]; + if (!ParseTargetInfoAndSourceInfo( + std::vector(tokens.cbegin() + pos, tokens.cend()), hash, &target_info, + hash, &source_info, err)) { + return {}; + } + } else if (op == Type::BSDIFF || op == Type::IMGDIFF) { + // + if (pos + 4 > tokens.size()) { + *err = android::base::StringPrintf("invalid number of args: %zu (expected 4+)", + tokens.size() - pos); + return {}; + } + size_t offset; + size_t length; + if (!android::base::ParseUint(tokens[pos++], &offset) || + !android::base::ParseUint(tokens[pos++], &length)) { + *err = "invalid patch offset/length"; + return {}; + } + patch_info = PatchInfo(offset, length); + + std::string src_hash = tokens[pos++]; + std::string dst_hash = tokens[pos++]; + if (!ParseTargetInfoAndSourceInfo( + std::vector(tokens.cbegin() + pos, tokens.cend()), dst_hash, &target_info, + src_hash, &source_info, err)) { + return {}; + } + } else if (op == Type::ABORT) { + // Abort takes no arguments, so there's nothing else to check. + if (pos != tokens.size()) { + *err = android::base::StringPrintf("invalid number of args: %zu (expected 0)", + tokens.size() - pos); + return {}; + } + } else if (op == Type::COMPUTE_HASH_TREE) { + // + if (pos + 5 != tokens.size()) { + *err = android::base::StringPrintf("invalid number of args: %zu (expected 5)", + tokens.size() - pos); + return {}; + } + + // Expects the hash_tree data to be contiguous. + RangeSet hash_tree_ranges = RangeSet::Parse(tokens[pos++]); + if (!hash_tree_ranges || hash_tree_ranges.size() != 1) { + *err = "invalid hash tree ranges in: " + line; + return {}; + } + + RangeSet source_ranges = RangeSet::Parse(tokens[pos++]); + if (!source_ranges) { + *err = "invalid source ranges in: " + line; + return {}; + } + + std::string hash_algorithm = tokens[pos++]; + std::string salt_hex = tokens[pos++]; + std::string root_hash = tokens[pos++]; + if (hash_algorithm.empty() || salt_hex.empty() || root_hash.empty()) { + *err = "invalid hash tree arguments in " + line; + return {}; + } + + HashTreeInfo hash_tree_info(std::move(hash_tree_ranges), std::move(source_ranges), + std::move(hash_algorithm), std::move(salt_hex), + std::move(root_hash)); + return Command(op, index, line, std::move(hash_tree_info)); + } else { + *err = "invalid op"; + return {}; + } + + return Command(op, index, line, patch_info, target_info, source_info, stash_info); +} + +bool SourceInfo::Overlaps(const TargetInfo& target) const { + return ranges_.Overlaps(target.ranges()); +} + +// Moves blocks in the 'source' vector to the specified locations (as in 'locs') in the 'dest' +// vector. Note that source and dest may be the same buffer. +static void MoveRange(std::vector* dest, const RangeSet& locs, + const std::vector& source, size_t block_size) { + const uint8_t* from = source.data(); + uint8_t* to = dest->data(); + size_t start = locs.blocks(); + // Must do the movement backward. + for (auto it = locs.crbegin(); it != locs.crend(); it++) { + size_t blocks = it->second - it->first; + start -= blocks; + memmove(to + (it->first * block_size), from + (start * block_size), blocks * block_size); + } +} + +bool SourceInfo::ReadAll( + std::vector* buffer, size_t block_size, + const std::function*)>& block_reader, + const std::function*)>& stash_reader) const { + if (buffer->size() < blocks() * block_size) { + return false; + } + + // Read in the source ranges. + if (ranges_) { + if (block_reader(ranges_, buffer) != 0) { + return false; + } + if (location_) { + MoveRange(buffer, location_, *buffer, block_size); + } + } + + // Read in the stashes. + for (const StashInfo& stash : stashes_) { + std::vector stash_buffer(stash.blocks() * block_size); + if (stash_reader(stash.id(), &stash_buffer) != 0) { + return false; + } + MoveRange(buffer, stash.ranges(), stash_buffer, block_size); + } + return true; +} + +void SourceInfo::DumpBuffer(const std::vector& buffer, size_t block_size) const { + LOG(INFO) << "Dumping hashes in hex for " << ranges_.blocks() << " source blocks"; + + const RangeSet& location = location_ ? location_ : RangeSet({ Range{ 0, ranges_.blocks() } }); + for (size_t i = 0; i < ranges_.blocks(); i++) { + size_t block_num = ranges_.GetBlockNumber(i); + size_t buffer_index = location.GetBlockNumber(i); + CHECK_LE((buffer_index + 1) * block_size, buffer.size()); + + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(buffer.data() + buffer_index * block_size, block_size, digest); + std::string hexdigest = print_sha1(digest); + LOG(INFO) << " block number: " << block_num << ", SHA-1: " << hexdigest; + } +} + +std::ostream& operator<<(std::ostream& os, const Command& command) { + os << command.index() << ": " << command.cmdline(); + return os; +} + +std::ostream& operator<<(std::ostream& os, const TargetInfo& target) { + os << target.blocks() << " blocks (" << target.hash_ << "): " << target.ranges_.ToString(); + return os; +} + +std::ostream& operator<<(std::ostream& os, const StashInfo& stash) { + os << stash.blocks() << " blocks (" << stash.id_ << "): " << stash.ranges_.ToString(); + return os; +} + +std::ostream& operator<<(std::ostream& os, const SourceInfo& source) { + os << source.blocks_ << " blocks (" << source.hash_ << "): "; + if (source.ranges_) { + os << source.ranges_.ToString(); + if (source.location_) { + os << " (location: " << source.location_.ToString() << ")"; + } + } + if (!source.stashes_.empty()) { + os << " " << source.stashes_.size() << " stash(es)"; + } + return os; +} + +TransferList TransferList::Parse(const std::string& transfer_list_str, std::string* err) { + TransferList result{}; + + std::vector lines = android::base::Split(transfer_list_str, "\n"); + if (lines.size() < kTransferListHeaderLines) { + *err = android::base::StringPrintf("too few lines in the transfer list [%zu]", lines.size()); + return TransferList{}; + } + + // First line in transfer list is the version number. + if (!android::base::ParseInt(lines[0], &result.version_, 3, 4)) { + *err = "unexpected transfer list version ["s + lines[0] + "]"; + return TransferList{}; + } + + // Second line in transfer list is the total number of blocks we expect to write. + if (!android::base::ParseUint(lines[1], &result.total_blocks_)) { + *err = "unexpected block count ["s + lines[1] + "]"; + return TransferList{}; + } + + // Third line is how many stash entries are needed simultaneously. + if (!android::base::ParseUint(lines[2], &result.stash_max_entries_)) { + return TransferList{}; + } + + // Fourth line is the maximum number of blocks that will be stashed simultaneously. + if (!android::base::ParseUint(lines[3], &result.stash_max_blocks_)) { + *err = "unexpected maximum stash blocks ["s + lines[3] + "]"; + return TransferList{}; + } + + // Subsequent lines are all individual transfer commands. + for (size_t i = kTransferListHeaderLines; i < lines.size(); i++) { + const std::string& line = lines[i]; + if (line.empty()) continue; + + size_t cmdindex = i - kTransferListHeaderLines; + std::string parsing_error; + Command command = Command::Parse(line, cmdindex, &parsing_error); + if (!command) { + *err = android::base::StringPrintf("Failed to parse command %zu [%s]: %s", cmdindex, + line.c_str(), parsing_error.c_str()); + return TransferList{}; + } + result.commands_.push_back(command); + } + + return result; +} diff --git a/updater/dynamic_partitions.cpp b/updater/dynamic_partitions.cpp new file mode 100644 index 0000000..a340116 --- /dev/null +++ b/updater/dynamic_partitions.cpp @@ -0,0 +1,140 @@ +/* + * 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 "updater/dynamic_partitions.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "edify/expr.h" +#include "edify/updater_runtime_interface.h" +#include "otautil/error_code.h" +#include "otautil/paths.h" +#include "private/utils.h" + +static std::vector ReadStringArgs(const char* name, State* state, + const std::vector>& argv, + const std::vector& arg_names) { + if (argv.size() != arg_names.size()) { + ErrorAbort(state, kArgsParsingFailure, "%s expects %zu arguments, got %zu", name, + arg_names.size(), argv.size()); + return {}; + } + + std::vector> args; + if (!ReadValueArgs(state, argv, &args)) { + return {}; + } + + CHECK_EQ(args.size(), arg_names.size()); + + for (size_t i = 0; i < arg_names.size(); ++i) { + if (args[i]->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "%s argument to %s must be string", + arg_names[i].c_str(), name); + return {}; + } + } + + std::vector ret; + std::transform(args.begin(), args.end(), std::back_inserter(ret), + [](const auto& arg) { return arg->data; }); + return ret; +} + +Value* UnmapPartitionFn(const char* name, State* state, + const std::vector>& argv) { + auto args = ReadStringArgs(name, state, argv, { "name" }); + if (args.empty()) return StringValue(""); + + auto updater_runtime = state->updater->GetRuntime(); + return updater_runtime->UnmapPartitionOnDeviceMapper(args[0]) ? StringValue("t") + : StringValue(""); +} + +Value* MapPartitionFn(const char* name, State* state, + const std::vector>& argv) { + auto args = ReadStringArgs(name, state, argv, { "name" }); + if (args.empty()) return StringValue(""); + + std::string path; + auto updater_runtime = state->updater->GetRuntime(); + bool result = updater_runtime->MapPartitionOnDeviceMapper(args[0], &path); + return result ? StringValue(path) : StringValue(""); +} + +static constexpr char kMetadataUpdatedMarker[] = "/dynamic_partition_metadata.UPDATED"; + +Value* UpdateDynamicPartitionsFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 1) { + ErrorAbort(state, kArgsParsingFailure, "%s expects 1 arguments, got %zu", name, argv.size()); + return StringValue(""); + } + std::vector> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + const std::unique_ptr& op_list_value = args[0]; + if (op_list_value->type != Value::Type::BLOB) { + ErrorAbort(state, kArgsParsingFailure, "op_list argument to %s must be blob", name); + return StringValue(""); + } + + std::string updated_marker = Paths::Get().stash_directory_base() + kMetadataUpdatedMarker; + if (state->is_retry) { + struct stat sb; + int result = stat(updated_marker.c_str(), &sb); + if (result == 0) { + LOG(INFO) << "Skipping already updated dynamic partition metadata based on marker"; + return StringValue("t"); + } + } else { + // Delete the obsolete marker if any. + std::string err; + if (!android::base::RemoveFileIfExists(updated_marker, &err)) { + LOG(ERROR) << "Failed to remove dynamic partition metadata updated marker " << updated_marker + << ": " << err; + return StringValue(""); + } + } + + auto updater_runtime = state->updater->GetRuntime(); + if (!updater_runtime->UpdateDynamicPartitions(op_list_value->data)) { + return StringValue(""); + } + + if (!SetUpdatedMarker(updated_marker)) { + LOG(ERROR) << "Failed to set metadata updated marker."; + return StringValue(""); + } + + return StringValue("t"); +} + +void RegisterDynamicPartitionsFunctions() { + RegisterFunction("unmap_partition", UnmapPartitionFn); + RegisterFunction("map_partition", MapPartitionFn); + RegisterFunction("update_dynamic_partitions", UpdateDynamicPartitionsFn); +} diff --git a/updater/include/private/commands.h b/updater/include/private/commands.h new file mode 100644 index 0000000..7a23bb7 --- /dev/null +++ b/updater/include/private/commands.h @@ -0,0 +1,475 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include // FRIEND_TEST + +#include "otautil/rangeset.h" + +// Represents the target info used in a Command. TargetInfo contains the ranges of the blocks and +// the expected hash. +class TargetInfo { + public: + TargetInfo() = default; + + TargetInfo(std::string hash, RangeSet ranges) + : hash_(std::move(hash)), ranges_(std::move(ranges)) {} + + const std::string& hash() const { + return hash_; + } + + const RangeSet& ranges() const { + return ranges_; + } + + size_t blocks() const { + return ranges_.blocks(); + } + + bool operator==(const TargetInfo& other) const { + return hash_ == other.hash_ && ranges_ == other.ranges_; + } + + private: + friend std::ostream& operator<<(std::ostream& os, const TargetInfo& source); + + // The hash of the data represented by the object. + std::string hash_; + // The block ranges that the data should be written to. + RangeSet ranges_; +}; + +std::ostream& operator<<(std::ostream& os, const TargetInfo& source); + +// Represents the stash info used in a Command. +class StashInfo { + public: + StashInfo() = default; + + StashInfo(std::string id, RangeSet ranges) : id_(std::move(id)), ranges_(std::move(ranges)) {} + + size_t blocks() const { + return ranges_.blocks(); + } + + const std::string& id() const { + return id_; + } + + const RangeSet& ranges() const { + return ranges_; + } + + bool operator==(const StashInfo& other) const { + return id_ == other.id_ && ranges_ == other.ranges_; + } + + private: + friend std::ostream& operator<<(std::ostream& os, const StashInfo& stash); + + // The id (i.e. hash) of the stash. + std::string id_; + // The matching location of the stash. + RangeSet ranges_; +}; + +std::ostream& operator<<(std::ostream& os, const StashInfo& stash); + +// Represents the source info in a Command, whose data could come from source image, stashed blocks, +// or both. +class SourceInfo { + public: + SourceInfo() = default; + + SourceInfo(std::string hash, RangeSet ranges, RangeSet location, std::vector stashes) + : hash_(std::move(hash)), + ranges_(std::move(ranges)), + location_(std::move(location)), + stashes_(std::move(stashes)) { + blocks_ = ranges_.blocks(); + for (const auto& stash : stashes_) { + blocks_ += stash.ranges().blocks(); + } + } + + // Reads all the data specified by this SourceInfo object into the given 'buffer', by calling the + // given readers. Caller needs to specify the block size for the represented blocks. The given + // buffer needs to be sufficiently large. Otherwise it returns false. 'block_reader' and + // 'stash_reader' read the specified data into the given buffer (guaranteed to be large enough) + // respectively. The readers should return 0 on success, or -1 on error. + bool ReadAll( + std::vector* buffer, size_t block_size, + const std::function*)>& block_reader, + const std::function*)>& stash_reader) const; + + // Whether this SourceInfo overlaps with the given TargetInfo object. + bool Overlaps(const TargetInfo& target) const; + + // Dumps the hashes in hex for the given buffer that's loaded from this SourceInfo object + // (excluding the stashed blocks which are handled separately). + void DumpBuffer(const std::vector& buffer, size_t block_size) const; + + const std::string& hash() const { + return hash_; + } + + size_t blocks() const { + return blocks_; + } + + bool operator==(const SourceInfo& other) const { + return hash_ == other.hash_ && ranges_ == other.ranges_ && location_ == other.location_ && + stashes_ == other.stashes_; + } + + private: + friend std::ostream& operator<<(std::ostream& os, const SourceInfo& source); + + // The hash of the data represented by the object. + std::string hash_; + // The block ranges from the source image to read data from. This could be a subset of all the + // blocks represented by the object, or empty if all the data should be loaded from stash. + RangeSet ranges_; + // The location in the buffer to load ranges_ into. Empty if ranges_ alone covers all the blocks + // (i.e. nothing needs to be loaded from stash). + RangeSet location_; + // The info for the stashed blocks that are part of the source. Empty if there's none. + std::vector stashes_; + // Total number of blocks represented by the object. + size_t blocks_{ 0 }; +}; + +std::ostream& operator<<(std::ostream& os, const SourceInfo& source); + +class PatchInfo { + public: + PatchInfo() = default; + + PatchInfo(size_t offset, size_t length) : offset_(offset), length_(length) {} + + size_t offset() const { + return offset_; + } + + size_t length() const { + return length_; + } + + bool operator==(const PatchInfo& other) const { + return offset_ == other.offset_ && length_ == other.length_; + } + + private: + size_t offset_{ 0 }; + size_t length_{ 0 }; +}; + +// The arguments to build a hash tree from blocks on the block device. +class HashTreeInfo { + public: + HashTreeInfo() = default; + + HashTreeInfo(RangeSet hash_tree_ranges, RangeSet source_ranges, std::string hash_algorithm, + std::string salt_hex, std::string root_hash) + : hash_tree_ranges_(std::move(hash_tree_ranges)), + source_ranges_(std::move(source_ranges)), + hash_algorithm_(std::move(hash_algorithm)), + salt_hex_(std::move(salt_hex)), + root_hash_(std::move(root_hash)) {} + + const RangeSet& hash_tree_ranges() const { + return hash_tree_ranges_; + } + const RangeSet& source_ranges() const { + return source_ranges_; + } + + const std::string& hash_algorithm() const { + return hash_algorithm_; + } + const std::string& salt_hex() const { + return salt_hex_; + } + const std::string& root_hash() const { + return root_hash_; + } + + bool operator==(const HashTreeInfo& other) const { + return hash_tree_ranges_ == other.hash_tree_ranges_ && source_ranges_ == other.source_ranges_ && + hash_algorithm_ == other.hash_algorithm_ && salt_hex_ == other.salt_hex_ && + root_hash_ == other.root_hash_; + } + + private: + RangeSet hash_tree_ranges_; + RangeSet source_ranges_; + std::string hash_algorithm_; + std::string salt_hex_; + std::string root_hash_; +}; + +// Command class holds the info for an update command that performs block-based OTA (BBOTA). Each +// command consists of one or several args, namely TargetInfo, SourceInfo, StashInfo and PatchInfo. +// The currently used BBOTA version is v4. +// +// zero +// - Fill the indicated blocks with zeros. +// - Meaningful args: TargetInfo +// +// new +// - Fill the blocks with data read from the new_data file. +// - Meaningful args: TargetInfo +// +// erase +// - Mark the given blocks as empty. +// - Meaningful args: TargetInfo +// +// move <...> +// - Read the source blocks, write result to target blocks. +// - Meaningful args: TargetInfo, SourceInfo +// +// See the note below for <...>. +// +// bsdiff <...> +// imgdiff <...> +// - Read the source blocks, apply a patch, and write result to target blocks. +// - Meaningful args: PatchInfo, TargetInfo, SourceInfo +// +// It expects <...> in one of the following formats: +// +// - <[stash_id:stash_location] ...> +// (loads data from stashes only) +// +// +// (loads data from source image only) +// +// +// <[stash_id:stash_location] ...> +// (loads data from both of source image and stashes) +// +// stash +// - Load the given source blocks and stash the data in the given slot of the stash table. +// - Meaningful args: StashInfo +// +// free +// - Free the given stash data. +// - Meaningful args: StashInfo +// +// compute_hash_tree +// - Computes the hash_tree bytes and writes the result to the specified range on the +// block_device. +// +// abort +// - Abort the current update. Allowed for testing code only. +// +class Command { + public: + enum class Type { + ABORT, + BSDIFF, + COMPUTE_HASH_TREE, + ERASE, + FREE, + IMGDIFF, + MOVE, + NEW, + STASH, + ZERO, + LAST, // Not a valid type. + }; + + Command() = default; + + Command(Type type, size_t index, std::string cmdline, PatchInfo patch, TargetInfo target, + SourceInfo source, StashInfo stash) + : type_(type), + index_(index), + cmdline_(std::move(cmdline)), + patch_(patch), + target_(std::move(target)), + source_(std::move(source)), + stash_(std::move(stash)) {} + + Command(Type type, size_t index, std::string cmdline, HashTreeInfo hash_tree_info); + + // Parses the given command 'line' into a Command object and returns it. The 'index' is specified + // by the caller to index the object. On parsing error, it returns an empty Command object that + // evaluates to false, and the specific error message will be set in 'err'. + static Command Parse(const std::string& line, size_t index, std::string* err); + + // Parses the command type from the given string. + static Type ParseType(const std::string& type_str); + + Type type() const { + return type_; + } + + size_t index() const { + return index_; + } + + const std::string& cmdline() const { + return cmdline_; + } + + const PatchInfo& patch() const { + return patch_; + } + + const TargetInfo& target() const { + return target_; + } + + const SourceInfo& source() const { + return source_; + } + + const StashInfo& stash() const { + return stash_; + } + + const HashTreeInfo& hash_tree_info() const { + return hash_tree_info_; + } + + size_t block_size() const { + return block_size_; + } + + constexpr explicit operator bool() const { + return type_ != Type::LAST; + } + + private: + friend class ResumableUpdaterTest; + friend class UpdaterTest; + + FRIEND_TEST(CommandsTest, Parse_ABORT_Allowed); + FRIEND_TEST(CommandsTest, Parse_InvalidNumberOfArgs); + FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput); + FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly); + FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes); + FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly); + + // Parses the target and source info from the given 'tokens' vector. Saves the parsed info into + // 'target' and 'source' objects. Returns the parsing result. Error message will be set in 'err' + // on parsing error, and the contents in 'target' and 'source' will be undefined. + static bool ParseTargetInfoAndSourceInfo(const std::vector& tokens, + const std::string& tgt_hash, TargetInfo* target, + const std::string& src_hash, SourceInfo* source, + std::string* err); + + // Allows parsing ABORT command, which should be used for testing purpose only. + static bool abort_allowed_; + + // The type of the command. + Type type_{ Type::LAST }; + // The index of the Command object, which is specified by the caller. + size_t index_{ 0 }; + // The input string that the Command object is parsed from. + std::string cmdline_; + // The patch info. Only meaningful for BSDIFF and IMGDIFF commands. + PatchInfo patch_; + // The target info, where the command should be written to. + TargetInfo target_; + // The source info to load the source blocks for the command. + SourceInfo source_; + // The stash info. Only meaningful for STASH and FREE commands. Note that although SourceInfo may + // also load data from stash, such info will be owned and managed by SourceInfo (i.e. in source_). + StashInfo stash_; + // The hash_tree info. Only meaningful for COMPUTE_HASH_TREE. + HashTreeInfo hash_tree_info_; + // The unit size of each block to be used in this command. + size_t block_size_{ 4096 }; +}; + +std::ostream& operator<<(std::ostream& os, const Command& command); + +// TransferList represents the info for a transfer list, which is parsed from input text lines +// containing commands to transfer data from one place to another on the target partition. +// +// The creator of the transfer list will guarantee that no block is read (i.e., used as the source +// for a patch or move) after it has been written. +// +// The creator will guarantee that a given stash is loaded (with a stash command) before it's used +// in a move/bsdiff/imgdiff command. +// +// Within one command the source and target ranges may overlap so in general we need to read the +// entire source into memory before writing anything to the target blocks. +// +// All the patch data is concatenated into one patch_data file in the update package. It must be +// stored uncompressed because we memory-map it in directly from the archive. (Since patches are +// already compressed, we lose very little by not compressing their concatenation.) +// +// Commands that read data from the partition (i.e. move/bsdiff/imgdiff/stash) have one or more +// additional hashes before the range parameters, which are used to check if the command has +// already been completed and verify the integrity of the source data. +class TransferList { + public: + // Number of header lines. + static constexpr size_t kTransferListHeaderLines = 4; + + TransferList() = default; + + // Parses the given input string and returns a TransferList object. Sets error message if any. + static TransferList Parse(const std::string& transfer_list_str, std::string* err); + + int version() const { + return version_; + } + + size_t total_blocks() const { + return total_blocks_; + } + + size_t stash_max_entries() const { + return stash_max_entries_; + } + + size_t stash_max_blocks() const { + return stash_max_blocks_; + } + + const std::vector& commands() const { + return commands_; + } + + // Returns whether the TransferList is valid. + constexpr explicit operator bool() const { + return version_ != 0; + } + + private: + // BBOTA version. + int version_{ 0 }; + // Total number of blocks to be written in this transfer. + size_t total_blocks_; + // Maximum number of stashes that exist at the same time. + size_t stash_max_entries_; + // Maximum number of blocks to be stashed. + size_t stash_max_blocks_; + // Commands in this transfer. + std::vector commands_; +}; diff --git a/updater/include/private/utils.h b/updater/include/private/utils.h new file mode 100644 index 0000000..33cf615 --- /dev/null +++ b/updater/include/private/utils.h @@ -0,0 +1,21 @@ +/* + * 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. + */ + +#pragma once + +#include + +bool SetUpdatedMarker(const std::string& marker); diff --git a/updater/include/updater/blockimg.h b/updater/include/updater/blockimg.h new file mode 100644 index 0000000..71733b3 --- /dev/null +++ b/updater/include/updater/blockimg.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 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. + */ + +#ifndef _UPDATER_BLOCKIMG_H_ +#define _UPDATER_BLOCKIMG_H_ + +#include + +void RegisterBlockImageFunctions(); + +#endif diff --git a/updater/include/updater/build_info.h b/updater/include/updater/build_info.h new file mode 100644 index 0000000..0073bfa --- /dev/null +++ b/updater/include/updater/build_info.h @@ -0,0 +1,74 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +// This class serves as the aggregation of the fake block device information during update +// simulation on host. In specific, it has the name of the block device, its mount point, and the +// path to the temporary file that fakes this block device. +class FakeBlockDevice { + public: + FakeBlockDevice(std::string block_device, std::string mount_point, std::string temp_file_path) + : blockdev_name(std::move(block_device)), + mount_point(std::move(mount_point)), + mounted_file_path(std::move(temp_file_path)) {} + + std::string blockdev_name; + std::string mount_point; + std::string mounted_file_path; // path to the temp file that mocks the block device +}; + +// This class stores the information of the source build. For example, it creates and maintains +// the temporary files to simulate the block devices on host. Therefore, the simulator runtime can +// query the information and run the update on host. +class BuildInfo { + public: + BuildInfo(const std::string_view work_dir, bool keep_images) + : work_dir_(work_dir), keep_images_(keep_images) {} + // Returns the value of the build properties. + std::string GetProperty(const std::string_view key, const std::string_view default_value) const; + // Returns the path to the mock block device. + std::string FindBlockDeviceName(const std::string_view name) const; + // Parses the given target-file, initializes the build properties and extracts the images. + bool ParseTargetFile(const std::string_view target_file_path, bool extracted_input); + + std::string GetOemSettings() const { + return oem_settings_; + } + void SetOemSettings(const std::string_view oem_settings) { + oem_settings_ = oem_settings; + } + + private: + // A map to store the system properties during simulation. + std::map> build_props_; + // A file that contains the oem properties. + std::string oem_settings_; + // A map from the blockdev_name to the FakeBlockDevice object, which contains the path to the + // temporary file. + std::map> blockdev_map_; + + std::list temp_files_; + std::string work_dir_; // A temporary directory to store the extracted image files + bool keep_images_; +}; diff --git a/updater/include/updater/dynamic_partitions.h b/updater/include/updater/dynamic_partitions.h new file mode 100644 index 0000000..31cf859 --- /dev/null +++ b/updater/include/updater/dynamic_partitions.h @@ -0,0 +1,19 @@ +/* + * 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. + */ + +#pragma once + +void RegisterDynamicPartitionsFunctions(); diff --git a/updater/include/updater/install.h b/updater/include/updater/install.h new file mode 100644 index 0000000..9fe2031 --- /dev/null +++ b/updater/include/updater/install.h @@ -0,0 +1,19 @@ +/* + * 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. + */ + +#pragma once + +void RegisterInstallFunctions(); diff --git a/updater/include/updater/simulator_runtime.h b/updater/include/updater/simulator_runtime.h new file mode 100644 index 0000000..fa878db --- /dev/null +++ b/updater/include/updater/simulator_runtime.h @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "edify/updater_runtime_interface.h" +#include "updater/build_info.h" + +class SimulatorRuntime : public UpdaterRuntimeInterface { + public: + explicit SimulatorRuntime(BuildInfo* source) : source_(source) {} + + bool IsSimulator() const override { + return true; + } + + std::string GetProperty(const std::string_view key, + const std::string_view default_value) const override; + + int Mount(const std::string_view location, const std::string_view mount_point, + const std::string_view fs_type, const std::string_view mount_options) override; + bool IsMounted(const std::string_view mount_point) const override; + std::pair Unmount(const std::string_view mount_point) override; + + bool ReadFileToString(const std::string_view filename, std::string* content) const override; + bool WriteStringToFile(const std::string_view content, + const std::string_view filename) const override; + + int WipeBlockDevice(const std::string_view filename, size_t len) const override; + int RunProgram(const std::vector& args, bool is_vfork) const override; + int Tune2Fs(const std::vector& args) const override; + + bool MapPartitionOnDeviceMapper(const std::string& partition_name, std::string* path) override; + bool UnmapPartitionOnDeviceMapper(const std::string& partition_name) override; + bool UpdateDynamicPartitions(const std::string_view op_list_value) override; + std::string AddSlotSuffix(const std::string_view arg) const override; + + private: + std::string FindBlockDeviceName(const std::string_view name) const override; + + BuildInfo* source_; + std::map> mounted_partitions_; +}; diff --git a/updater/include/updater/target_files.h b/updater/include/updater/target_files.h new file mode 100644 index 0000000..f185eaf --- /dev/null +++ b/updater/include/updater/target_files.h @@ -0,0 +1,71 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +// This class represents the mount information for each line in a fstab file. +class FstabInfo { + public: + FstabInfo(std::string blockdev_name, std::string mount_point, std::string fs_type) + : blockdev_name(std::move(blockdev_name)), + mount_point(std::move(mount_point)), + fs_type(std::move(fs_type)) {} + + std::string blockdev_name; + std::string mount_point; + std::string fs_type; +}; + +// This class parses a target file from a zip file or an extracted directory. It also provides the +// function to read its content for simulation. +class TargetFile { + public: + TargetFile(std::string path, bool extracted_input) + : path_(std::move(path)), extracted_input_(extracted_input) {} + + // Opens the input target file (or extracted directory) and parses the misc_info.txt. + bool Open(); + // Parses the build properties in all possible locations and save them in |props_map| + bool GetBuildProps(std::map>* props_map) const; + // Parses the fstab and save the information about each partition to mount into |fstab_info_list|. + bool ParseFstabInfo(std::vector* fstab_info_list) const; + // Returns true if the given entry exists in the target file. + bool EntryExists(const std::string_view name) const; + // Extracts the image file |entry_name|. Returns true on success. + bool ExtractImage(const std::string_view entry_name, const FstabInfo& fstab_info, + const std::string_view work_dir, TemporaryFile* image_file) const; + + private: + // Wrapper functions to read the entry from either the zipped target-file, or the extracted input + // directory. + bool ReadEntryToString(const std::string_view name, std::string* content) const; + bool ExtractEntryToTempFile(const std::string_view name, TemporaryFile* temp_file) const; + + std::string path_; // Path to the zipped target-file or an extracted directory. + bool extracted_input_; // True if the target-file has been extracted. + ZipArchiveHandle handle_{ nullptr }; + + // The properties under META/misc_info.txt + std::map> misc_info_; +}; diff --git a/updater/include/updater/updater.h b/updater/include/updater/updater.h new file mode 100644 index 0000000..8676b60 --- /dev/null +++ b/updater/include/updater/updater.h @@ -0,0 +1,96 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include + +#include "edify/expr.h" +#include "edify/updater_interface.h" +#include "otautil/error_code.h" +#include "otautil/sysutil.h" + +class Updater : public UpdaterInterface { + public: + explicit Updater(std::unique_ptr run_time) + : runtime_(std::move(run_time)) {} + + ~Updater() override; + + // Memory-maps the OTA package and opens it as a zip file. Also sets up the command pipe and + // UpdaterRuntime. + bool Init(int fd, const std::string_view package_filename, bool is_retry); + + // Parses and evaluates the updater-script in the OTA package. Reports the error code if the + // evaluation fails. + bool RunUpdate(); + + // Writes the message to command pipe, adds a new line in the end. + void WriteToCommandPipe(const std::string_view message, bool flush = false) const override; + + // Sends over the message to recovery to print it on the screen. + void UiPrint(const std::string_view message) const override; + + std::string FindBlockDeviceName(const std::string_view name) const override; + + UpdaterRuntimeInterface* GetRuntime() const override { + return runtime_.get(); + } + ZipArchiveHandle GetPackageHandle() const override { + return package_handle_; + } + std::string GetResult() const override { + return result_; + } + uint8_t* GetMappedPackageAddress() const override { + return mapped_package_.addr; + } + size_t GetMappedPackageLength() const override { + return mapped_package_.length; + } + + private: + friend class UpdaterTestBase; + friend class UpdaterTest; + // Where in the package we expect to find the edify script to execute. + // (Note it's "updateR-script", not the older "update-script".) + static constexpr const char* SCRIPT_NAME = "META-INF/com/google/android/updater-script"; + + // Reads the entry |name| in the zip archive and put the result in |content|. + bool ReadEntryToString(ZipArchiveHandle za, const std::string& entry_name, std::string* content); + + // Parses the error code embedded in state->errmsg; and reports the error code and cause code. + void ParseAndReportErrorCode(State* state); + + std::unique_ptr runtime_; + + MemMapping mapped_package_; + ZipArchiveHandle package_handle_{ nullptr }; + std::string updater_script_; + + bool is_retry_{ false }; + std::unique_ptr cmd_pipe_{ nullptr, fclose }; + + std::string result_; + std::vector skipped_functions_; +}; diff --git a/updater/include/updater/updater_runtime.h b/updater/include/updater/updater_runtime.h new file mode 100644 index 0000000..b943dfc --- /dev/null +++ b/updater/include/updater/updater_runtime.h @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "edify/updater_runtime_interface.h" + +struct selabel_handle; + +class UpdaterRuntime : public UpdaterRuntimeInterface { + public: + explicit UpdaterRuntime(struct selabel_handle* sehandle) : sehandle_(sehandle) {} + ~UpdaterRuntime() override = default; + + bool IsSimulator() const override { + return false; + } + + std::string GetProperty(const std::string_view key, + const std::string_view default_value) const override; + + std::string FindBlockDeviceName(const std::string_view name) const override; + + int Mount(const std::string_view location, const std::string_view mount_point, + const std::string_view fs_type, const std::string_view mount_options) override; + bool IsMounted(const std::string_view mount_point) const override; + std::pair Unmount(const std::string_view mount_point) override; + + bool ReadFileToString(const std::string_view filename, std::string* content) const override; + bool WriteStringToFile(const std::string_view content, + const std::string_view filename) const override; + + int WipeBlockDevice(const std::string_view filename, size_t len) const override; + int RunProgram(const std::vector& args, bool is_vfork) const override; + int Tune2Fs(const std::vector& args) const override; + + bool MapPartitionOnDeviceMapper(const std::string& partition_name, std::string* path) override; + bool UnmapPartitionOnDeviceMapper(const std::string& partition_name) override; + bool UpdateDynamicPartitions(const std::string_view op_list_value) override; + std::string AddSlotSuffix(const std::string_view arg) const override; + + private: + struct selabel_handle* sehandle_{ nullptr }; +}; diff --git a/updater/install.cpp b/updater/install.cpp new file mode 100644 index 0000000..2959650 --- /dev/null +++ b/updater/install.cpp @@ -0,0 +1,910 @@ +/* + * 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 "updater/install.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "edify/expr.h" +#include "edify/updater_interface.h" +#include "edify/updater_runtime_interface.h" +#include "otautil/dirutil.h" +#include "otautil/error_code.h" +#include "otautil/print_sha1.h" +#include "otautil/sysutil.h" + +#ifndef __ANDROID__ +#include // for strlcpy +#endif + +static bool UpdateBlockDeviceNameForPartition(UpdaterInterface* updater, Partition* partition) { + CHECK(updater); + std::string name = updater->FindBlockDeviceName(partition->name); + if (name.empty()) { + LOG(ERROR) << "Failed to find the block device " << partition->name; + return false; + } + + partition->name = std::move(name); + return true; +} + +// This is the updater side handler for ui_print() in edify script. Contents will be sent over to +// the recovery side for on-screen display. +Value* UIPrintFn(const char* name, State* state, const std::vector>& argv) { + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name); + } + + std::string buffer = android::base::Join(args, ""); + state->updater->UiPrint(buffer); + return StringValue(buffer); +} + +// package_extract_file(package_file[, dest_file]) +// Extracts a single package_file from the update package and writes it to dest_file, +// overwriting existing files if necessary. Without the dest_file argument, returns the +// contents of the package file as a binary blob. +Value* PackageExtractFileFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() < 1 || argv.size() > 2) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 or 2 args, got %zu", name, + argv.size()); + } + + if (argv.size() == 2) { + // The two-argument version extracts to a file. + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse %zu args", name, + argv.size()); + } + const std::string& zip_path = args[0]; + std::string dest_path = args[1]; + + ZipArchiveHandle za = state->updater->GetPackageHandle(); + ZipEntry64 entry; + if (FindEntry(za, zip_path, &entry) != 0) { + LOG(ERROR) << name << ": no " << zip_path << " in package"; + return StringValue(""); + } + + // Update the destination of package_extract_file if it's a block device. During simulation the + // destination will map to a fake file. + if (std::string block_device_name = state->updater->FindBlockDeviceName(dest_path); + !block_device_name.empty()) { + dest_path = block_device_name; + } + + android::base::unique_fd fd(TEMP_FAILURE_RETRY( + open(dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR))); + if (fd == -1) { + PLOG(ERROR) << name << ": can't open " << dest_path << " for write"; + return StringValue(""); + } + + bool success = true; + int32_t ret = ExtractEntryToFile(za, &entry, fd); + if (ret != 0) { + LOG(ERROR) << name << ": Failed to extract entry \"" << zip_path << "\" (" + << entry.uncompressed_length << " bytes) to \"" << dest_path + << "\": " << ErrorCodeString(ret); + success = false; + } + if (fsync(fd) == -1) { + PLOG(ERROR) << "fsync of \"" << dest_path << "\" failed"; + success = false; + } + + if (close(fd.release()) != 0) { + PLOG(ERROR) << "close of \"" << dest_path << "\" failed"; + success = false; + } + + return StringValue(success ? "t" : ""); + } else { + // The one-argument version returns the contents of the file as the result. + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse %zu args", name, + argv.size()); + } + const std::string& zip_path = args[0]; + + ZipArchiveHandle za = state->updater->GetPackageHandle(); + ZipEntry64 entry; + if (FindEntry(za, zip_path, &entry) != 0) { + return ErrorAbort(state, kPackageExtractFileFailure, "%s(): no %s in package", name, + zip_path.c_str()); + } + + std::string buffer; + if (entry.uncompressed_length > std::numeric_limits::max()) { + return ErrorAbort(state, kPackageExtractFileFailure, + "%s(): Entry `%s` Uncompressed size exceeds size of address space.", name, + zip_path.c_str()); + } + buffer.resize(entry.uncompressed_length); + + int32_t ret = + ExtractToMemory(za, &entry, reinterpret_cast(&buffer[0]), buffer.size()); + if (ret != 0) { + return ErrorAbort(state, kPackageExtractFileFailure, + "%s: Failed to extract entry \"%s\" (%zu bytes) to memory: %s", name, + zip_path.c_str(), buffer.size(), ErrorCodeString(ret)); + } + + return new Value(Value::Type::BLOB, buffer); + } +} + +// patch_partition_check(target_partition, source_partition) +// Checks if the target and source partitions have the desired checksums to be patched. It returns +// directly, if the target partition already has the expected checksum. Otherwise it in turn +// checks the integrity of the source partition and the backup file on /cache. +// +// For example, patch_partition_check( +// "EMMC:/dev/block/boot:12342568:8aaacf187a6929d0e9c3e9e46ea7ff495b43424d", +// "EMMC:/dev/block/boot:12363048:06b0b16299dcefc94900efed01e0763ff644ffa4") +Value* PatchPartitionCheckFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 2) { + return ErrorAbort(state, kArgsParsingFailure, + "%s(): Invalid number of args (expected 2, got %zu)", name, argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args, 0, 2)) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name); + } + + std::string err; + auto target = Partition::Parse(args[0], &err); + if (!target) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse target \"%s\": %s", name, + args[0].c_str(), err.c_str()); + } + + auto source = Partition::Parse(args[1], &err); + if (!source) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse source \"%s\": %s", name, + args[1].c_str(), err.c_str()); + } + + if (!UpdateBlockDeviceNameForPartition(state->updater, &source) || + !UpdateBlockDeviceNameForPartition(state->updater, &target)) { + return StringValue(""); + } + + bool result = PatchPartitionCheck(target, source); + return StringValue(result ? "t" : ""); +} + +// patch_partition(target, source, patch) +// Applies the given patch to the source partition, and writes the result to the target partition. +// +// For example, patch_partition( +// "EMMC:/dev/block/boot:12342568:8aaacf187a6929d0e9c3e9e46ea7ff495b43424d", +// "EMMC:/dev/block/boot:12363048:06b0b16299dcefc94900efed01e0763ff644ffa4", +// package_extract_file("boot.img.p")) +Value* PatchPartitionFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 3) { + return ErrorAbort(state, kArgsParsingFailure, + "%s(): Invalid number of args (expected 3, got %zu)", name, argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args, 0, 2)) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name); + } + + std::string err; + auto target = Partition::Parse(args[0], &err); + if (!target) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse target \"%s\": %s", name, + args[0].c_str(), err.c_str()); + } + + auto source = Partition::Parse(args[1], &err); + if (!source) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse source \"%s\": %s", name, + args[1].c_str(), err.c_str()); + } + + std::vector> values; + if (!ReadValueArgs(state, argv, &values, 2, 1) || values[0]->type != Value::Type::BLOB) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Invalid patch arg", name); + } + + if (!UpdateBlockDeviceNameForPartition(state->updater, &source) || + !UpdateBlockDeviceNameForPartition(state->updater, &target)) { + return StringValue(""); + } + + bool result = PatchPartition(target, source, *values[0], nullptr, true); + return StringValue(result ? "t" : ""); +} + +// mount(fs_type, partition_type, location, mount_point) +// mount(fs_type, partition_type, location, mount_point, mount_options) + +// fs_type="ext4" partition_type="EMMC" location=device +Value* MountFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 4 && argv.size() != 5) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 4-5 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& fs_type = args[0]; + const std::string& partition_type = args[1]; + const std::string& location = args[2]; + const std::string& mount_point = args[3]; + std::string mount_options; + + if (argv.size() == 5) { + mount_options = args[4]; + } + + if (fs_type.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "fs_type argument to %s() can't be empty", name); + } + if (partition_type.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "partition_type argument to %s() can't be empty", + name); + } + if (location.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "location argument to %s() can't be empty", name); + } + if (mount_point.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "mount_point argument to %s() can't be empty", + name); + } + + auto updater = state->updater; + if (updater->GetRuntime()->Mount(location, mount_point, fs_type, mount_options) != 0) { + updater->UiPrint(android::base::StringPrintf("%s: Failed to mount %s at %s: %s", name, + location.c_str(), mount_point.c_str(), + strerror(errno))); + return StringValue(""); + } + + return StringValue(mount_point); +} + +// is_mounted(mount_point) +Value* IsMountedFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& mount_point = args[0]; + if (mount_point.empty()) { + return ErrorAbort(state, kArgsParsingFailure, + "mount_point argument to unmount() can't be empty"); + } + + auto updater_runtime = state->updater->GetRuntime(); + if (!updater_runtime->IsMounted(mount_point)) { + return StringValue(""); + } + + return StringValue(mount_point); +} + +Value* UnmountFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& mount_point = args[0]; + if (mount_point.empty()) { + return ErrorAbort(state, kArgsParsingFailure, + "mount_point argument to unmount() can't be empty"); + } + + auto updater = state->updater; + auto [mounted, result] = updater->GetRuntime()->Unmount(mount_point); + if (!mounted) { + updater->UiPrint( + android::base::StringPrintf("Failed to unmount %s: No such volume", mount_point.c_str())); + return nullptr; + } else if (result != 0) { + updater->UiPrint(android::base::StringPrintf("Failed to unmount %s: %s", mount_point.c_str(), + strerror(errno))); + } + + return StringValue(mount_point); +} + +// format(fs_type, partition_type, location, fs_size, mount_point) +// +// fs_type="ext4" partition_type="EMMC" location=device fs_size= mount_point= +// fs_type="f2fs" partition_type="EMMC" location=device fs_size= mount_point= +// if fs_size == 0, then make fs uses the entire partition. +// if fs_size > 0, that is the size to use +// if fs_size < 0, then reserve that many bytes at the end of the partition (not for "f2fs") +Value* FormatFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 5) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 5 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& fs_type = args[0]; + const std::string& partition_type = args[1]; + const std::string& location = args[2]; + const std::string& fs_size = args[3]; + const std::string& mount_point = args[4]; + + if (fs_type.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "fs_type argument to %s() can't be empty", name); + } + if (partition_type.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "partition_type argument to %s() can't be empty", + name); + } + if (location.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "location argument to %s() can't be empty", name); + } + if (mount_point.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "mount_point argument to %s() can't be empty", + name); + } + + int64_t size; + if (!android::base::ParseInt(fs_size, &size)) { + return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse int in %s", name, + fs_size.c_str()); + } + + auto updater_runtime = state->updater->GetRuntime(); + if (fs_type == "ext4") { + std::vector mke2fs_args = { + "/system/bin/mke2fs", "-t", "ext4", "-b", "4096", location + }; + if (size != 0) { + mke2fs_args.push_back(std::to_string(size / 4096LL)); + } + + if (auto status = updater_runtime->RunProgram(mke2fs_args, true); status != 0) { + LOG(ERROR) << name << ": mke2fs failed (" << status << ") on " << location; + return StringValue(""); + } + + if (auto status = updater_runtime->RunProgram( + { "/system/bin/e2fsdroid", "-e", "-a", mount_point, location }, true); + status != 0) { + LOG(ERROR) << name << ": e2fsdroid failed (" << status << ") on " << location; + return StringValue(""); + } + return StringValue(location); + } + + if (fs_type == "f2fs") { + if (size < 0) { + LOG(ERROR) << name << ": fs_size can't be negative for f2fs: " << fs_size; + return StringValue(""); + } + std::vector f2fs_args = { + "/system/bin/make_f2fs", "-g", "android", "-w", "512", location + }; + if (size >= 512) { + f2fs_args.push_back(std::to_string(size / 512)); + } + if (auto status = updater_runtime->RunProgram(f2fs_args, true); status != 0) { + LOG(ERROR) << name << ": make_f2fs failed (" << status << ") on " << location; + return StringValue(""); + } + + if (auto status = updater_runtime->RunProgram( + { "/system/bin/sload_f2fs", "-t", mount_point, location }, true); + status != 0) { + LOG(ERROR) << name << ": sload_f2fs failed (" << status << ") on " << location; + return StringValue(""); + } + + return StringValue(location); + } + + LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \"" + << partition_type << "\""; + return nullptr; +} + +Value* ShowProgressFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 2) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 2 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& frac_str = args[0]; + const std::string& sec_str = args[1]; + + double frac; + if (!android::base::ParseDouble(frac_str.c_str(), &frac)) { + return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse double in %s", name, + frac_str.c_str()); + } + int sec; + if (!android::base::ParseInt(sec_str.c_str(), &sec)) { + return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse int in %s", name, + sec_str.c_str()); + } + + state->updater->WriteToCommandPipe(android::base::StringPrintf("progress %f %d", frac, sec)); + + return StringValue(frac_str); +} + +Value* SetProgressFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& frac_str = args[0]; + + double frac; + if (!android::base::ParseDouble(frac_str.c_str(), &frac)) { + return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse double in %s", name, + frac_str.c_str()); + } + + state->updater->WriteToCommandPipe(android::base::StringPrintf("set_progress %f", frac)); + + return StringValue(frac_str); +} + +Value* GetPropFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + std::string key; + if (!Evaluate(state, argv[0], &key)) { + return nullptr; + } + + auto updater_runtime = state->updater->GetRuntime(); + std::string value = updater_runtime->GetProperty(key, ""); + + return StringValue(value); +} + +// file_getprop(file, key) +// +// interprets 'file' as a getprop-style file (key=value pairs, one +// per line. # comment lines, blank lines, lines without '=' ignored), +// and returns the value for 'key' (or "" if it isn't defined). +Value* FileGetPropFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 2) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 2 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& filename = args[0]; + const std::string& key = args[1]; + + std::string buffer; + auto updater_runtime = state->updater->GetRuntime(); + if (!updater_runtime->ReadFileToString(filename, &buffer)) { + ErrorAbort(state, kFreadFailure, "%s: failed to read %s", name, filename.c_str()); + return nullptr; + } + + std::vector lines = android::base::Split(buffer, "\n"); + for (size_t i = 0; i < lines.size(); i++) { + std::string line = android::base::Trim(lines[i]); + + // comment or blank line: skip to next line + if (line.empty() || line[0] == '#') { + continue; + } + size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + continue; + } + + // trim whitespace between key and '=' + std::string str = android::base::Trim(line.substr(0, equal_pos)); + + // not the key we're looking for + if (key != str) continue; + + return StringValue(android::base::Trim(line.substr(equal_pos + 1))); + } + + return StringValue(""); +} + +// apply_patch_space(bytes) +Value* ApplyPatchSpaceFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 args, got %zu", name, + argv.size()); + } + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& bytes_str = args[0]; + + size_t bytes; + if (!android::base::ParseUint(bytes_str.c_str(), &bytes)) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): can't parse \"%s\" as byte count", name, + bytes_str.c_str()); + } + + // Skip the cache size check if the update is a retry. + if (state->is_retry || CheckAndFreeSpaceOnCache(bytes)) { + return StringValue("t"); + } + return StringValue(""); +} + +Value* WipeCacheFn(const char* name, State* state, const std::vector>& argv) { + if (!argv.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects no args, got %zu", name, + argv.size()); + } + + state->updater->WriteToCommandPipe("wipe_cache"); + return StringValue("t"); +} + +Value* RunProgramFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() < 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects at least 1 arg", name); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + + auto updater_runtime = state->updater->GetRuntime(); + auto status = updater_runtime->RunProgram(args, false); + return StringValue(std::to_string(status)); +} + +// read_file(filename) +// Reads a local file 'filename' and returns its contents as a string Value. +Value* ReadFileFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name); + } + const std::string& filename = args[0]; + + std::string contents; + auto updater_runtime = state->updater->GetRuntime(); + if (updater_runtime->ReadFileToString(filename, &contents)) { + return new Value(Value::Type::STRING, std::move(contents)); + } + + // Leave it to caller to handle the failure. + PLOG(ERROR) << name << ": Failed to read " << filename; + return StringValue(""); +} + +// write_value(value, filename) +// Writes 'value' to 'filename'. +// Example: write_value("960000", "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq") +Value* WriteValueFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 2) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 2 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name); + } + + const std::string& filename = args[1]; + if (filename.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Filename cannot be empty", name); + } + + const std::string& value = args[0]; + auto updater_runtime = state->updater->GetRuntime(); + if (!updater_runtime->WriteStringToFile(value, filename)) { + PLOG(ERROR) << name << ": Failed to write to \"" << filename << "\""; + return StringValue(""); + } + return StringValue("t"); +} + +// Immediately reboot the device. Recovery is not finished normally, +// so if you reboot into recovery it will re-start applying the +// current package (because nothing has cleared the copy of the +// arguments stored in the BCB). +// +// The argument is the partition name passed to the android reboot +// property. It can be "recovery" to boot from the recovery +// partition, or "" (empty string) to boot from the regular boot +// partition. +Value* RebootNowFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 2) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 2 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name); + } + const std::string& filename = args[0]; + const std::string& property = args[1]; + + // Zero out the 'command' field of the bootloader message. Leave the rest intact. + bootloader_message boot; + std::string err; + if (!read_bootloader_message_from(&boot, filename, &err)) { + LOG(ERROR) << name << "(): Failed to read from \"" << filename << "\": " << err; + return StringValue(""); + } + memset(boot.command, 0, sizeof(boot.command)); + if (!write_bootloader_message_to(boot, filename, &err)) { + LOG(ERROR) << name << "(): Failed to write to \"" << filename << "\": " << err; + return StringValue(""); + } + + Reboot(property); + + return ErrorAbort(state, kRebootFailure, "%s() failed to reboot", name); +} + +// Store a string value somewhere that future invocations of recovery +// can access it. This value is called the "stage" and can be used to +// drive packages that need to do reboots in the middle of +// installation and keep track of where they are in the multi-stage +// install. +// +// The first argument is the block device for the misc partition +// ("/misc" in the fstab), which is where this value is stored. The +// second argument is the string to store; it should not exceed 31 +// bytes. +Value* SetStageFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 2) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 2 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& filename = args[0]; + const std::string& stagestr = args[1]; + + // Store this value in the misc partition, immediately after the + // bootloader message that the main recovery uses to save its + // arguments in case of the device restarting midway through + // package installation. + bootloader_message boot; + std::string err; + if (!read_bootloader_message_from(&boot, filename, &err)) { + LOG(ERROR) << name << "(): Failed to read from \"" << filename << "\": " << err; + return StringValue(""); + } + strlcpy(boot.stage, stagestr.c_str(), sizeof(boot.stage)); + if (!write_bootloader_message_to(boot, filename, &err)) { + LOG(ERROR) << name << "(): Failed to write to \"" << filename << "\": " << err; + return StringValue(""); + } + + return StringValue(filename); +} + +// Return the value most recently saved with SetStageFn. The argument +// is the block device for the misc partition. +Value* GetStageFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& filename = args[0]; + + bootloader_message boot; + std::string err; + if (!read_bootloader_message_from(&boot, filename, &err)) { + LOG(ERROR) << name << "(): Failed to read from \"" << filename << "\": " << err; + return StringValue(""); + } + + return StringValue(boot.stage); +} + +Value* WipeBlockDeviceFn(const char* name, State* state, const std::vector>& argv) { + if (argv.size() != 2) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 2 args, got %zu", name, + argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& filename = args[0]; + const std::string& len_str = args[1]; + + size_t len; + if (!android::base::ParseUint(len_str.c_str(), &len)) { + return nullptr; + } + + auto updater_runtime = state->updater->GetRuntime(); + int status = updater_runtime->WipeBlockDevice(filename, len); + return StringValue(status == 0 ? "t" : ""); +} + +Value* EnableRebootFn(const char* name, State* state, const std::vector>& argv) { + if (!argv.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects no args, got %zu", name, + argv.size()); + } + state->updater->WriteToCommandPipe("enable_reboot"); + return StringValue("t"); +} + +Value* Tune2FsFn(const char* name, State* state, const std::vector>& argv) { + if (argv.empty()) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects args, got %zu", name, argv.size()); + } + + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() could not read args", name); + } + + // tune2fs expects the program name as its first arg. + args.insert(args.begin(), "tune2fs"); + auto updater_runtime = state->updater->GetRuntime(); + if (auto result = updater_runtime->Tune2Fs(args); result != 0) { + return ErrorAbort(state, kTune2FsFailure, "%s() returned error code %d", name, result); + } + return StringValue("t"); +} + +Value* AddSlotSuffixFn(const char* name, State* state, + const std::vector>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + std::vector args; + if (!ReadArgs(state, argv, &args)) { + return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); + } + const std::string& arg = args[0]; + auto updater_runtime = state->updater->GetRuntime(); + return StringValue(updater_runtime->AddSlotSuffix(arg)); +} + +void RegisterInstallFunctions() { + RegisterFunction("mount", MountFn); + RegisterFunction("is_mounted", IsMountedFn); + RegisterFunction("unmount", UnmountFn); + RegisterFunction("format", FormatFn); + RegisterFunction("show_progress", ShowProgressFn); + RegisterFunction("set_progress", SetProgressFn); + RegisterFunction("package_extract_file", PackageExtractFileFn); + + RegisterFunction("getprop", GetPropFn); + RegisterFunction("file_getprop", FileGetPropFn); + + RegisterFunction("apply_patch_space", ApplyPatchSpaceFn); + RegisterFunction("patch_partition", PatchPartitionFn); + RegisterFunction("patch_partition_check", PatchPartitionCheckFn); + + RegisterFunction("wipe_block_device", WipeBlockDeviceFn); + + RegisterFunction("read_file", ReadFileFn); + RegisterFunction("write_value", WriteValueFn); + + RegisterFunction("wipe_cache", WipeCacheFn); + + RegisterFunction("ui_print", UIPrintFn); + + RegisterFunction("run_program", RunProgramFn); + + RegisterFunction("reboot_now", RebootNowFn); + RegisterFunction("get_stage", GetStageFn); + RegisterFunction("set_stage", SetStageFn); + + RegisterFunction("enable_reboot", EnableRebootFn); + RegisterFunction("tune2fs", Tune2FsFn); + + RegisterFunction("add_slot_suffix", AddSlotSuffixFn); +} diff --git a/updater/mounts.cpp b/updater/mounts.cpp new file mode 100644 index 0000000..943d35c --- /dev/null +++ b/updater/mounts.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2007 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 "mounts.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +struct MountedVolume { + std::string device; + std::string mount_point; + std::string filesystem; + std::string flags; +}; + +static std::vector g_mounts_state; + +bool scan_mounted_volumes() { + for (size_t i = 0; i < g_mounts_state.size(); ++i) { + delete g_mounts_state[i]; + } + g_mounts_state.clear(); + + // Open and read mount table entries. + FILE* fp = setmntent("/proc/mounts", "re"); + if (fp == NULL) { + return false; + } + mntent* e; + while ((e = getmntent(fp)) != NULL) { + MountedVolume* v = new MountedVolume; + v->device = e->mnt_fsname; + v->mount_point = e->mnt_dir; + v->filesystem = e->mnt_type; + v->flags = e->mnt_opts; + g_mounts_state.push_back(v); + } + endmntent(fp); + return true; +} + +MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point) { + for (size_t i = 0; i < g_mounts_state.size(); ++i) { + if (g_mounts_state[i]->mount_point == mount_point) return g_mounts_state[i]; + } + return nullptr; +} + +int unmount_mounted_volume(MountedVolume* volume) { + // Intentionally pass the empty string to umount if the caller tries to unmount a volume they + // already unmounted using this function. + std::string mount_point = volume->mount_point; + volume->mount_point.clear(); + int result = umount(mount_point.c_str()); + if (result == -1) { + PLOG(WARNING) << "Failed to umount " << mount_point; + } + return result; +} diff --git a/updater/mounts.h b/updater/mounts.h new file mode 100644 index 0000000..6786c8d --- /dev/null +++ b/updater/mounts.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2007 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. + */ + +#pragma once + +struct MountedVolume; + +bool scan_mounted_volumes(); + +MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point); + +int unmount_mounted_volume(MountedVolume* volume); diff --git a/updater/simulator_runtime.cpp b/updater/simulator_runtime.cpp new file mode 100644 index 0000000..57dfb32 --- /dev/null +++ b/updater/simulator_runtime.cpp @@ -0,0 +1,137 @@ +/* + * 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 "updater/simulator_runtime.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "mounts.h" +#include "otautil/sysutil.h" + +std::string SimulatorRuntime::GetProperty(const std::string_view key, + const std::string_view default_value) const { + return source_->GetProperty(key, default_value); +} + +int SimulatorRuntime::Mount(const std::string_view location, const std::string_view mount_point, + const std::string_view /* fs_type */, + const std::string_view /* mount_options */) { + if (auto mounted_location = mounted_partitions_.find(mount_point); + mounted_location != mounted_partitions_.end() && mounted_location->second != location) { + LOG(ERROR) << mount_point << " has been mounted at " << mounted_location->second; + return -1; + } + + mounted_partitions_.emplace(mount_point, location); + return 0; +} + +bool SimulatorRuntime::IsMounted(const std::string_view mount_point) const { + return mounted_partitions_.find(mount_point) != mounted_partitions_.end(); +} + +std::pair SimulatorRuntime::Unmount(const std::string_view mount_point) { + if (!IsMounted(mount_point)) { + return { false, -1 }; + } + + mounted_partitions_.erase(std::string(mount_point)); + return { true, 0 }; +} + +std::string SimulatorRuntime::FindBlockDeviceName(const std::string_view name) const { + return source_->FindBlockDeviceName(name); +} + +// TODO(xunchang) implement the utility functions in simulator. +int SimulatorRuntime::RunProgram(const std::vector& args, bool /* is_vfork */) const { + LOG(INFO) << "Running program with args " << android::base::Join(args, " "); + return 0; +} + +int SimulatorRuntime::Tune2Fs(const std::vector& args) const { + LOG(INFO) << "Running Tune2Fs with args " << android::base::Join(args, " "); + return 0; +} + +int SimulatorRuntime::WipeBlockDevice(const std::string_view filename, size_t /* len */) const { + LOG(INFO) << "SKip wiping block device " << filename; + return 0; +} + +bool SimulatorRuntime::ReadFileToString(const std::string_view filename, + std::string* content) const { + if (android::base::EndsWith(filename, "oem.prop")) { + return android::base::ReadFileToString(source_->GetOemSettings(), content); + } + + LOG(INFO) << "SKip reading filename " << filename; + return true; +} + +bool SimulatorRuntime::WriteStringToFile(const std::string_view content, + const std::string_view filename) const { + LOG(INFO) << "SKip writing " << content.size() << " bytes to file " << filename; + return true; +} + +bool SimulatorRuntime::MapPartitionOnDeviceMapper(const std::string& partition_name, + std::string* path) { + *path = partition_name; + return true; +} + +bool SimulatorRuntime::UnmapPartitionOnDeviceMapper(const std::string& partition_name) { + LOG(INFO) << "Skip unmapping " << partition_name; + return true; +} + +bool SimulatorRuntime::UpdateDynamicPartitions(const std::string_view op_list_value) { + const std::unordered_set commands{ + "resize", "remove", "add", "move", + "add_group", "resize_group", "remove_group", "remove_all_groups", + }; + + std::vector lines = android::base::Split(std::string(op_list_value), "\n"); + for (const auto& line : lines) { + if (line.empty() || line[0] == '#') continue; + auto tokens = android::base::Split(line, " "); + if (commands.find(tokens[0]) == commands.end()) { + LOG(ERROR) << "Unknown operation in op_list: " << line; + return false; + } + } + return true; +} + +std::string SimulatorRuntime::AddSlotSuffix(const std::string_view arg) const { + LOG(INFO) << "Skip adding slot suffix to " << arg; + return std::string(arg); +} diff --git a/updater/target_files.cpp b/updater/target_files.cpp new file mode 100644 index 0000000..207146f --- /dev/null +++ b/updater/target_files.cpp @@ -0,0 +1,294 @@ +/* + * 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 "updater/target_files.h" + +#include + +#include +#include +#include + +#include +#include +#include + +static bool SimgToImg(int input_fd, int output_fd) { + if (lseek64(input_fd, 0, SEEK_SET) == -1) { + PLOG(ERROR) << "Failed to lseek64 on the input sparse image"; + return false; + } + + if (lseek64(output_fd, 0, SEEK_SET) == -1) { + PLOG(ERROR) << "Failed to lseek64 on the output raw image"; + return false; + } + + std::unique_ptr s_file( + sparse_file_import(input_fd, true, false), sparse_file_destroy); + if (!s_file) { + LOG(ERROR) << "Failed to import the sparse image."; + return false; + } + + if (sparse_file_write(s_file.get(), output_fd, false, false, false) < 0) { + PLOG(ERROR) << "Failed to output the raw image file."; + return false; + } + + return true; +} + +static bool ParsePropertyFile(const std::string_view prop_content, + std::map>* props_map) { + LOG(INFO) << "Start parsing build property\n"; + std::vector lines = android::base::Split(std::string(prop_content), "\n"); + for (const auto& line : lines) { + if (line.empty() || line[0] == '#') continue; + auto pos = line.find('='); + if (pos == std::string::npos) continue; + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + LOG(INFO) << key << ": " << value; + props_map->emplace(key, value); + } + + return true; +} + +static bool ParseFstab(const std::string_view fstab, std::vector* fstab_info_list) { + LOG(INFO) << "parsing fstab\n"; + std::vector lines = android::base::Split(std::string(fstab), "\n"); + for (const auto& line : lines) { + if (line.empty() || line[0] == '#') continue; + + // optional: + std::vector tokens = android::base::Split(line, " "); + tokens.erase(std::remove(tokens.begin(), tokens.end(), ""), tokens.end()); + if (tokens.size() != 4 && tokens.size() != 5) { + LOG(ERROR) << "Unexpected token size: " << tokens.size() << std::endl + << "Error parsing fstab line: " << line; + return false; + } + + const auto& blockdev = tokens[0]; + const auto& mount_point = tokens[1]; + const auto& fs_type = tokens[2]; + if (!android::base::StartsWith(mount_point, "/")) { + LOG(WARNING) << "mount point '" << mount_point << "' does not start with '/'"; + continue; + } + + // The simulator only supports ext4 and emmc for now. + if (fs_type != "ext4" && fs_type != "emmc") { + LOG(WARNING) << "Unsupported fs_type in " << line; + continue; + } + + fstab_info_list->emplace_back(blockdev, mount_point, fs_type); + } + + return true; +} + +bool TargetFile::EntryExists(const std::string_view name) const { + if (extracted_input_) { + std::string entry_path = path_ + "/" + std::string(name); + if (access(entry_path.c_str(), O_RDONLY) != 0) { + PLOG(WARNING) << "Failed to access " << entry_path; + return false; + } + return true; + } + + CHECK(handle_); + ZipEntry64 img_entry; + return FindEntry(handle_, name, &img_entry) == 0; +} + +bool TargetFile::ReadEntryToString(const std::string_view name, std::string* content) const { + if (extracted_input_) { + std::string entry_path = path_ + "/" + std::string(name); + return android::base::ReadFileToString(entry_path, content); + } + + CHECK(handle_); + ZipEntry64 entry; + if (auto find_err = FindEntry(handle_, name, &entry); find_err != 0) { + LOG(ERROR) << "failed to find " << name << " in the package: " << ErrorCodeString(find_err); + return false; + } + + if (entry.uncompressed_length == 0) { + content->clear(); + return true; + } + + if (entry.uncompressed_length > std::numeric_limits::max()) { + LOG(ERROR) << "Failed to extract " << name + << " because's uncompressed size exceeds size of address space. " + << entry.uncompressed_length; + return false; + } + + content->resize(entry.uncompressed_length); + if (auto extract_err = ExtractToMemory( + handle_, &entry, reinterpret_cast(&content->at(0)), entry.uncompressed_length); + extract_err != 0) { + LOG(ERROR) << "failed to read " << name << " from package: " << ErrorCodeString(extract_err); + return false; + } + + return true; +} + +bool TargetFile::ExtractEntryToTempFile(const std::string_view name, + TemporaryFile* temp_file) const { + if (extracted_input_) { + std::string entry_path = path_ + "/" + std::string(name); + return std::filesystem::copy_file(entry_path, temp_file->path, + std::filesystem::copy_options::overwrite_existing); + } + + CHECK(handle_); + ZipEntry64 entry; + if (auto find_err = FindEntry(handle_, name, &entry); find_err != 0) { + LOG(ERROR) << "failed to find " << name << " in the package: " << ErrorCodeString(find_err); + return false; + } + + if (auto status = ExtractEntryToFile(handle_, &entry, temp_file->fd); status != 0) { + LOG(ERROR) << "Failed to extract zip entry " << name << " : " << ErrorCodeString(status); + return false; + } + return true; +} + +bool TargetFile::Open() { + if (!extracted_input_) { + if (auto ret = OpenArchive(path_.c_str(), &handle_); ret != 0) { + LOG(ERROR) << "failed to open source target file " << path_ << ": " << ErrorCodeString(ret); + return false; + } + } + + // Parse the misc info. + std::string misc_info_content; + if (!ReadEntryToString("META/misc_info.txt", &misc_info_content)) { + return false; + } + if (!ParsePropertyFile(misc_info_content, &misc_info_)) { + return false; + } + + return true; +} + +bool TargetFile::GetBuildProps(std::map>* props_map) const { + props_map->clear(); + // Parse the source zip to mock the system props and block devices. We try all the possible + // locations for build props. + constexpr std::string_view kPropLocations[] = { + "SYSTEM/build.prop", + "VENDOR/build.prop", + "PRODUCT/build.prop", + "SYSTEM_EXT/build.prop", + "SYSTEM/vendor/build.prop", + "SYSTEM/product/build.prop", + "SYSTEM/system_ext/build.prop", + "ODM/build.prop", // legacy + "ODM/etc/build.prop", + "VENDOR/odm/build.prop", // legacy + "VENDOR/odm/etc/build.prop", + }; + for (const auto& name : kPropLocations) { + std::string build_prop_content; + if (!ReadEntryToString(name, &build_prop_content)) { + continue; + } + std::map> props; + if (!ParsePropertyFile(build_prop_content, &props)) { + LOG(ERROR) << "Failed to parse build prop in " << name; + return false; + } + for (const auto& [key, value] : props) { + if (auto it = props_map->find(key); it != props_map->end() && it->second != value) { + LOG(WARNING) << "Property " << key << " has different values in property files, we got " + << it->second << " and " << value; + } + props_map->emplace(key, value); + } + } + + return true; +} + +bool TargetFile::ExtractImage(const std::string_view entry_name, const FstabInfo& fstab_info, + const std::string_view work_dir, TemporaryFile* image_file) const { + if (!EntryExists(entry_name)) { + return false; + } + + // We don't need extra work for 'emmc'; use the image file as the block device. + if (fstab_info.fs_type == "emmc" || misc_info_.find("extfs_sparse_flag") == misc_info_.end()) { + if (!ExtractEntryToTempFile(entry_name, image_file)) { + return false; + } + } else { // treated as ext4 sparse image + TemporaryFile sparse_image{ std::string(work_dir) }; + if (!ExtractEntryToTempFile(entry_name, &sparse_image)) { + return false; + } + + // Convert the sparse image to raw. + if (!SimgToImg(sparse_image.fd, image_file->fd)) { + LOG(ERROR) << "Failed to convert " << fstab_info.mount_point << " to raw."; + return false; + } + } + + return true; +} + +bool TargetFile::ParseFstabInfo(std::vector* fstab_info_list) const { + // Parse the fstab file and extract the image files. The location of the fstab actually depends + // on some flags e.g. "no_recovery", "recovery_as_boot". Here we just try all possibilities. + constexpr std::string_view kRecoveryFstabLocations[] = { + "RECOVERY/RAMDISK/system/etc/recovery.fstab", + "RECOVERY/RAMDISK/etc/recovery.fstab", + "BOOT/RAMDISK/system/etc/recovery.fstab", + "BOOT/RAMDISK/etc/recovery.fstab", + }; + std::string fstab_content; + for (const auto& name : kRecoveryFstabLocations) { + if (std::string content; ReadEntryToString(name, &content)) { + fstab_content = std::move(content); + break; + } + } + if (fstab_content.empty()) { + LOG(ERROR) << "Failed to parse the recovery fstab file"; + return false; + } + + // Extract the images and convert them to raw. + if (!ParseFstab(fstab_content, fstab_info_list)) { + LOG(ERROR) << "Failed to mount the block devices for source build."; + return false; + } + + return true; +} diff --git a/updater/update_simulator_main.cpp b/updater/update_simulator_main.cpp new file mode 100644 index 0000000..6c6989b --- /dev/null +++ b/updater/update_simulator_main.cpp @@ -0,0 +1,167 @@ +/* + * 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 +#include +#include + +#include +#include + +#include +#include +#include + +#include "edify/expr.h" +#include "otautil/error_code.h" +#include "otautil/paths.h" +#include "updater/blockimg.h" +#include "updater/build_info.h" +#include "updater/dynamic_partitions.h" +#include "updater/install.h" +#include "updater/simulator_runtime.h" +#include "updater/updater.h" + +using namespace std::string_literals; + +void Usage(std::string_view name) { + LOG(INFO) << "Usage: " << name << "[--oem_settings ]" + << "[--skip_functions ]" + << " --source " + << " --ota_package "; +} + +Value* SimulatorPlaceHolderFn(const char* name, State* /* state */, + const std::vector>& /* argv */) { + LOG(INFO) << "Skip function " << name << " in host simulation"; + return StringValue("t"); +} + +int main(int argc, char** argv) { + // Write the logs to stdout. + android::base::InitLogging(argv, &android::base::StderrLogger); + + std::string oem_settings; + std::string skip_function_file; + std::string source_target_file; + std::string package_name; + std::string work_dir; + bool keep_images = false; + + constexpr struct option OPTIONS[] = { + { "keep_images", no_argument, nullptr, 0 }, + { "oem_settings", required_argument, nullptr, 0 }, + { "ota_package", required_argument, nullptr, 0 }, + { "skip_functions", required_argument, nullptr, 0 }, + { "source", required_argument, nullptr, 0 }, + { "work_dir", required_argument, nullptr, 0 }, + { nullptr, 0, nullptr, 0 }, + }; + + int arg; + int option_index; + while ((arg = getopt_long(argc, argv, "", OPTIONS, &option_index)) != -1) { + if (arg != 0) { + LOG(ERROR) << "Invalid command argument"; + Usage(argv[0]); + return EXIT_FAILURE; + } + auto option_name = OPTIONS[option_index].name; + // The same oem property file used during OTA generation. It's needed for file_getprop() to + // return the correct value for the source build. + if (option_name == "oem_settings"s) { + oem_settings = optarg; + } else if (option_name == "skip_functions"s) { + skip_function_file = optarg; + } else if (option_name == "source"s) { + source_target_file = optarg; + } else if (option_name == "ota_package"s) { + package_name = optarg; + } else if (option_name == "keep_images"s) { + keep_images = true; + } else if (option_name == "work_dir"s) { + work_dir = optarg; + } else { + Usage(argv[0]); + return EXIT_FAILURE; + } + } + + if (source_target_file.empty() || package_name.empty()) { + Usage(argv[0]); + return EXIT_FAILURE; + } + + // Configure edify's functions. + RegisterBuiltins(); + RegisterInstallFunctions(); + RegisterBlockImageFunctions(); + RegisterDynamicPartitionsFunctions(); + + if (!skip_function_file.empty()) { + std::string content; + if (!android::base::ReadFileToString(skip_function_file, &content)) { + PLOG(ERROR) << "Failed to read " << skip_function_file; + return EXIT_FAILURE; + } + + auto lines = android::base::Split(content, "\n"); + for (const auto& line : lines) { + if (line.empty() || android::base::StartsWith(line, "#")) { + continue; + } + RegisterFunction(line, SimulatorPlaceHolderFn); + } + } + + TemporaryFile temp_saved_source; + TemporaryFile temp_last_command; + TemporaryDir temp_stash_base; + + Paths::Get().set_cache_temp_source(temp_saved_source.path); + Paths::Get().set_last_command_file(temp_last_command.path); + Paths::Get().set_stash_directory_base(temp_stash_base.path); + + TemporaryFile cmd_pipe; + TemporaryDir source_temp_dir; + if (work_dir.empty()) { + work_dir = source_temp_dir.path; + } + + BuildInfo source_build_info(work_dir, keep_images); + if (!source_build_info.ParseTargetFile(source_target_file, false)) { + LOG(ERROR) << "Failed to parse the target file " << source_target_file; + return EXIT_FAILURE; + } + + if (!oem_settings.empty()) { + CHECK_EQ(0, access(oem_settings.c_str(), R_OK)); + source_build_info.SetOemSettings(oem_settings); + } + + Updater updater(std::make_unique(&source_build_info)); + if (!updater.Init(cmd_pipe.release(), package_name, false)) { + return EXIT_FAILURE; + } + + if (!updater.RunUpdate()) { + return EXIT_FAILURE; + } + + LOG(INFO) << "\nscript succeeded, result: " << updater.GetResult(); + + return 0; +} diff --git a/updater/updater.cpp b/updater/updater.cpp new file mode 100644 index 0000000..c526734 --- /dev/null +++ b/updater/updater.cpp @@ -0,0 +1,189 @@ +/* + * 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 "updater/updater.h" + +#include +#include + +#include + +#include +#include + +#include "edify/updater_runtime_interface.h" + +Updater::~Updater() { + if (package_handle_) { + CloseArchive(package_handle_); + } +} + +bool Updater::Init(int fd, const std::string_view package_filename, bool is_retry) { + // Set up the pipe for sending commands back to the parent process. + cmd_pipe_.reset(fdopen(fd, "wb")); + if (!cmd_pipe_) { + LOG(ERROR) << "Failed to open the command pipe"; + return false; + } + + setlinebuf(cmd_pipe_.get()); + + if (!mapped_package_.MapFile(std::string(package_filename))) { + LOG(ERROR) << "failed to map package " << package_filename; + return false; + } + if (int open_err = OpenArchiveFromMemory(mapped_package_.addr, mapped_package_.length, + std::string(package_filename).c_str(), &package_handle_); + open_err != 0) { + LOG(ERROR) << "failed to open package " << package_filename << ": " + << ErrorCodeString(open_err); + return false; + } + if (!ReadEntryToString(package_handle_, SCRIPT_NAME, &updater_script_)) { + return false; + } + + is_retry_ = is_retry; + + return true; +} + +bool Updater::RunUpdate() { + CHECK(runtime_); + + // Parse the script. + std::unique_ptr root; + int error_count = 0; + int error = ParseString(updater_script_, &root, &error_count); + if (error != 0 || error_count > 0) { + LOG(ERROR) << error_count << " parse errors"; + return false; + } + + // Evaluate the parsed script. + State state(updater_script_, this); + state.is_retry = is_retry_; + + bool status = Evaluate(&state, root, &result_); + if (status) { + fprintf(cmd_pipe_.get(), "ui_print script succeeded: result was [%s]\n", result_.c_str()); + // Even though the script doesn't abort, still log the cause code if result is empty. + if (result_.empty() && state.cause_code != kNoCause) { + fprintf(cmd_pipe_.get(), "log cause: %d\n", state.cause_code); + } + for (const auto& func : skipped_functions_) { + LOG(WARNING) << "Skipped executing function " << func; + } + return true; + } + + ParseAndReportErrorCode(&state); + return false; +} + +void Updater::WriteToCommandPipe(const std::string_view message, bool flush) const { + fprintf(cmd_pipe_.get(), "%s\n", std::string(message).c_str()); + if (flush) { + fflush(cmd_pipe_.get()); + } +} + +void Updater::UiPrint(const std::string_view message) const { + // "line1\nline2\n" will be split into 3 tokens: "line1", "line2" and "". + // so skip sending empty strings to ui. + std::vector lines = android::base::Split(std::string(message), "\n"); + for (const auto& line : lines) { + if (!line.empty()) { + fprintf(cmd_pipe_.get(), "ui_print %s\n", line.c_str()); + } + } + + // on the updater side, we need to dump the contents to stderr (which has + // been redirected to the log file). because the recovery will only print + // the contents to screen when processing pipe command ui_print. + LOG(INFO) << message; +} + +std::string Updater::FindBlockDeviceName(const std::string_view name) const { + return runtime_->FindBlockDeviceName(name); +} + +void Updater::ParseAndReportErrorCode(State* state) { + CHECK(state); + if (state->errmsg.empty()) { + LOG(ERROR) << "script aborted (no error message)"; + fprintf(cmd_pipe_.get(), "ui_print script aborted (no error message)\n"); + } else { + LOG(ERROR) << "script aborted: " << state->errmsg; + const std::vector lines = android::base::Split(state->errmsg, "\n"); + for (const std::string& line : lines) { + // Parse the error code in abort message. + // Example: "E30: This package is for bullhead devices." + if (!line.empty() && line[0] == 'E') { + if (sscanf(line.c_str(), "E%d: ", &state->error_code) != 1) { + LOG(ERROR) << "Failed to parse error code: [" << line << "]"; + } + } + fprintf(cmd_pipe_.get(), "ui_print %s\n", line.c_str()); + } + } + + // Installation has been aborted. Set the error code to kScriptExecutionFailure unless + // a more specific code has been set in errmsg. + if (state->error_code == kNoError) { + state->error_code = kScriptExecutionFailure; + } + fprintf(cmd_pipe_.get(), "log error: %d\n", state->error_code); + // Cause code should provide additional information about the abort. + if (state->cause_code != kNoCause) { + fprintf(cmd_pipe_.get(), "log cause: %d\n", state->cause_code); + if (state->cause_code == kPatchApplicationFailure) { + LOG(INFO) << "Patch application failed, retry update."; + fprintf(cmd_pipe_.get(), "retry_update\n"); + } else if (state->cause_code == kEioFailure) { + LOG(INFO) << "Update failed due to EIO, retry update."; + fprintf(cmd_pipe_.get(), "retry_update\n"); + } + } +} + +bool Updater::ReadEntryToString(ZipArchiveHandle za, const std::string& entry_name, + std::string* content) { + ZipEntry64 entry; + int find_err = FindEntry(za, entry_name, &entry); + if (find_err != 0) { + LOG(ERROR) << "failed to find " << entry_name + << " in the package: " << ErrorCodeString(find_err); + return false; + } + if (entry.uncompressed_length > std::numeric_limits::max()) { + LOG(ERROR) << "Failed to extract " << entry_name + << " because's uncompressed size exceeds size of address space. " + << entry.uncompressed_length; + return false; + } + content->resize(entry.uncompressed_length); + int extract_err = ExtractToMemory(za, &entry, reinterpret_cast(&content->at(0)), + entry.uncompressed_length); + if (extract_err != 0) { + LOG(ERROR) << "failed to read " << entry_name + << " from package: " << ErrorCodeString(extract_err); + return false; + } + + return true; +} diff --git a/updater/updater_main.cpp b/updater/updater_main.cpp new file mode 100644 index 0000000..33d5b5b --- /dev/null +++ b/updater/updater_main.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 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 +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "edify/expr.h" +#include "updater/blockimg.h" +#include "updater/dynamic_partitions.h" +#include "updater/install.h" +#include "updater/updater.h" +#include "updater/updater_runtime.h" + +// Generated by the makefile, this function defines the +// RegisterDeviceExtensions() function, which calls all the +// registration functions for device-specific extensions. +#include "register.inc" + +static void UpdaterLogger(android::base::LogId /* id */, android::base::LogSeverity /* severity */, + const char* /* tag */, const char* /* file */, unsigned int /* line */, + const char* message) { + fprintf(stdout, "%s\n", message); +} + +int main(int argc, char** argv) { + // Various things log information to stdout or stderr more or less + // at random (though we've tried to standardize on stdout). The + // log file makes more sense if buffering is turned off so things + // appear in the right order. + setbuf(stdout, nullptr); + setbuf(stderr, nullptr); + + // We don't have logcat yet under recovery. Update logs will always be written to stdout + // (which is redirected to recovery.log). + android::base::InitLogging(argv, &UpdaterLogger); + + // Run the libcrypto KAT(known answer tests) based self tests. + if (BORINGSSL_self_test() != 1) { + LOG(ERROR) << "Failed to run the boringssl self tests"; + return EXIT_FAILURE; + } + + if (argc != 4 && argc != 5) { + LOG(ERROR) << "unexpected number of arguments: " << argc; + return EXIT_FAILURE; + } + + char* version = argv[1]; + if ((version[0] != '1' && version[0] != '2' && version[0] != '3') || version[1] != '\0') { + // We support version 1, 2, or 3. + LOG(ERROR) << "wrong updater binary API; expected 1, 2, or 3; got " << argv[1]; + return EXIT_FAILURE; + } + + int fd; + if (!android::base::ParseInt(argv[2], &fd)) { + LOG(ERROR) << "Failed to parse fd in " << argv[2]; + return EXIT_FAILURE; + } + + std::string package_name = argv[3]; + + bool is_retry = false; + if (argc == 5) { + if (strcmp(argv[4], "retry") == 0) { + is_retry = true; + } else { + LOG(ERROR) << "unexpected argument: " << argv[4]; + return EXIT_FAILURE; + } + } + + // Configure edify's functions. + RegisterBuiltins(); + RegisterInstallFunctions(); + RegisterBlockImageFunctions(); + RegisterDynamicPartitionsFunctions(); + RegisterDeviceExtensions(); + + auto sehandle = selinux_android_file_context_handle(); + selinux_android_set_sehandle(sehandle); + + Updater updater(std::make_unique(sehandle)); + if (!updater.Init(fd, package_name, is_retry)) { + return EXIT_FAILURE; + } + + if (!updater.RunUpdate()) { + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/updater/updater_runtime.cpp b/updater/updater_runtime.cpp new file mode 100644 index 0000000..bac078c --- /dev/null +++ b/updater/updater_runtime.cpp @@ -0,0 +1,189 @@ +/* + * 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 "updater/updater_runtime.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mounts.h" +#include "otautil/sysutil.h" + +std::string UpdaterRuntime::GetProperty(const std::string_view key, + const std::string_view default_value) const { + return android::base::GetProperty(std::string(key), std::string(default_value)); +} + +std::string UpdaterRuntime::FindBlockDeviceName(const std::string_view name) const { + return std::string(name); +} + +static bool setMountFlag(const std::string& flag, unsigned* mount_flags) { + static constexpr std::pair mount_flags_list[] = { + { "noatime", MS_NOATIME }, + { "noexec", MS_NOEXEC }, + { "nosuid", MS_NOSUID }, + { "nodev", MS_NODEV }, + { "nodiratime", MS_NODIRATIME }, + { "ro", MS_RDONLY }, + { "rw", 0 }, + { "remount", MS_REMOUNT }, + { "bind", MS_BIND }, + { "rec", MS_REC }, + { "unbindable", MS_UNBINDABLE }, + { "private", MS_PRIVATE }, + { "slave", MS_SLAVE }, + { "shared", MS_SHARED }, + { "defaults", 0 }, + }; + + for (const auto& [name, value] : mount_flags_list) { + if (flag == name) { + *mount_flags |= value; + return true; + } + } + return false; +} + +static bool parseMountFlags(const std::string& flags, unsigned* mount_flags, + std::string* fs_options) { + bool is_flag_set = false; + std::vector flag_list; + for (const auto& flag : android::base::Split(flags, ",")) { + if (!setMountFlag(flag, mount_flags)) { + // Unknown flag, so it must be a filesystem specific option. + flag_list.push_back(flag); + } else { + is_flag_set = true; + } + } + *fs_options = android::base::Join(flag_list, ','); + return is_flag_set; +} + +int UpdaterRuntime::Mount(const std::string_view location, const std::string_view mount_point, + const std::string_view fs_type, const std::string_view mount_options) { + std::string mount_point_string(mount_point); + std::string mount_options_string(mount_options); + char* secontext = nullptr; + unsigned mount_flags = 0; + std::string fs_options; + + if (sehandle_) { + selabel_lookup(sehandle_, &secontext, mount_point_string.c_str(), 0755); + setfscreatecon(secontext); + } + + mkdir(mount_point_string.c_str(), 0755); + + if (secontext) { + freecon(secontext); + setfscreatecon(nullptr); + } + + if (!parseMountFlags(mount_options_string, &mount_flags, &fs_options)) { + // Fall back to default + mount_flags = MS_NOATIME | MS_NODEV | MS_NODIRATIME; + } + + return mount(std::string(location).c_str(), mount_point_string.c_str(), + std::string(fs_type).c_str(), mount_flags, fs_options.c_str()); +} + +bool UpdaterRuntime::IsMounted(const std::string_view mount_point) const { + scan_mounted_volumes(); + MountedVolume* vol = find_mounted_volume_by_mount_point(std::string(mount_point).c_str()); + return vol != nullptr; +} + +std::pair UpdaterRuntime::Unmount(const std::string_view mount_point) { + scan_mounted_volumes(); + MountedVolume* vol = find_mounted_volume_by_mount_point(std::string(mount_point).c_str()); + if (vol == nullptr) { + return { false, -1 }; + } + + int ret = unmount_mounted_volume(vol); + return { true, ret }; +} + +bool UpdaterRuntime::ReadFileToString(const std::string_view filename, std::string* content) const { + return android::base::ReadFileToString(std::string(filename), content); +} + +bool UpdaterRuntime::WriteStringToFile(const std::string_view content, + const std::string_view filename) const { + return android::base::WriteStringToFile(std::string(content), std::string(filename)); +} + +int UpdaterRuntime::WipeBlockDevice(const std::string_view filename, size_t len) const { + android::base::unique_fd fd(open(std::string(filename).c_str(), O_WRONLY)); + if (fd == -1) { + PLOG(ERROR) << "Failed to open " << filename; + return false; + } + // The wipe_block_device function in ext4_utils returns 0 on success and 1 for failure. + return wipe_block_device(fd, len); +} + +int UpdaterRuntime::RunProgram(const std::vector& args, bool is_vfork) const { + CHECK(!args.empty()); + auto argv = StringVectorToNullTerminatedArray(args); + LOG(INFO) << "about to run program [" << args[0] << "] with " << argv.size() << " args"; + + pid_t child = is_vfork ? vfork() : fork(); + if (child == 0) { + execv(argv[0], argv.data()); + PLOG(ERROR) << "run_program: execv failed"; + _exit(EXIT_FAILURE); + } + + int status; + waitpid(child, &status, 0); + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != 0) { + LOG(ERROR) << "run_program: child exited with status " << WEXITSTATUS(status); + } + } else if (WIFSIGNALED(status)) { + LOG(ERROR) << "run_program: child terminated by signal " << WTERMSIG(status); + } + + return status; +} + +int UpdaterRuntime::Tune2Fs(const std::vector& args) const { + auto tune2fs_args = StringVectorToNullTerminatedArray(args); + // tune2fs changes the filesystem parameters on an ext2 filesystem; it returns 0 on success. + return tune2fs_main(tune2fs_args.size() - 1, tune2fs_args.data()); +} + +std::string UpdaterRuntime::AddSlotSuffix(const std::string_view arg) const { + return std::string(arg) + fs_mgr_get_slot_suffix(); +} diff --git a/updater/updater_runtime_dynamic_partitions.cpp b/updater/updater_runtime_dynamic_partitions.cpp new file mode 100644 index 0000000..6570cff --- /dev/null +++ b/updater/updater_runtime_dynamic_partitions.cpp @@ -0,0 +1,356 @@ +/* + * 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 "updater/updater_runtime.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using android::dm::DeviceMapper; +using android::dm::DmDeviceState; +using android::fs_mgr::CreateLogicalPartition; +using android::fs_mgr::CreateLogicalPartitionParams; +using android::fs_mgr::DestroyLogicalPartition; +using android::fs_mgr::LpMetadata; +using android::fs_mgr::MetadataBuilder; +using android::fs_mgr::Partition; +using android::fs_mgr::PartitionOpener; +using android::fs_mgr::SlotNumberForSlotSuffix; + +static constexpr std::chrono::milliseconds kMapTimeout{ 1000 }; + +static std::string GetSuperDevice() { + return "/dev/block/by-name/" + fs_mgr_get_super_partition_name(); +} + +static std::string AddSlotSuffix(const std::string& partition_name) { + return partition_name + fs_mgr_get_slot_suffix(); +} + +static bool UnmapPartitionWithSuffixOnDeviceMapper(const std::string& partition_name_suffix) { + auto state = DeviceMapper::Instance().GetState(partition_name_suffix); + if (state == DmDeviceState::INVALID) { + return true; + } + if (state == DmDeviceState::ACTIVE) { + return DestroyLogicalPartition(partition_name_suffix); + } + LOG(ERROR) << "Unknown device mapper state: " + << static_cast>(state); + return false; +} + +bool UpdaterRuntime::MapPartitionOnDeviceMapper(const std::string& partition_name, + std::string* path) { + auto partition_name_suffix = AddSlotSuffix(partition_name); + auto state = DeviceMapper::Instance().GetState(partition_name_suffix); + if (state == DmDeviceState::INVALID) { + CreateLogicalPartitionParams params = { + .block_device = GetSuperDevice(), + // If device supports A/B, apply non-A/B update to the partition at current slot. Otherwise, + // SlotNumberForSlotSuffix("") returns 0. + .metadata_slot = SlotNumberForSlotSuffix(fs_mgr_get_slot_suffix()), + // If device supports A/B, apply non-A/B update to the partition at current slot. Otherwise, + // fs_mgr_get_slot_suffix() returns empty string. + .partition_name = partition_name_suffix, + .force_writable = true, + .timeout_ms = kMapTimeout, + }; + return CreateLogicalPartition(params, path); + } + + if (state == DmDeviceState::ACTIVE) { + return DeviceMapper::Instance().GetDmDevicePathByName(partition_name_suffix, path); + } + LOG(ERROR) << "Unknown device mapper state: " + << static_cast>(state); + return false; +} + +bool UpdaterRuntime::UnmapPartitionOnDeviceMapper(const std::string& partition_name) { + return ::UnmapPartitionWithSuffixOnDeviceMapper(AddSlotSuffix(partition_name)); +} + +namespace { // Ops + +struct OpParameters { + std::vector tokens; + MetadataBuilder* builder; + + bool ExpectArgSize(size_t size) const { + CHECK(!tokens.empty()); + auto actual = tokens.size() - 1; + if (actual != size) { + LOG(ERROR) << "Op " << op() << " expects " << size << " args, got " << actual; + return false; + } + return true; + } + const std::string& op() const { + CHECK(!tokens.empty()); + return tokens[0]; + } + const std::string& arg(size_t pos) const { + CHECK_LE(pos + 1, tokens.size()); + return tokens[pos + 1]; + } + std::optional uint_arg(size_t pos, const std::string& name) const { + auto str = arg(pos); + uint64_t ret; + if (!android::base::ParseUint(str, &ret)) { + LOG(ERROR) << "Op " << op() << " expects uint64 for argument " << name << ", got " << str; + return std::nullopt; + } + return ret; + } +}; + +using OpFunction = std::function; +using OpMap = std::map; + +bool PerformOpResize(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name_suffix = AddSlotSuffix(params.arg(0)); + auto size = params.uint_arg(1, "size"); + if (!size.has_value()) return false; + + auto partition = params.builder->FindPartition(partition_name_suffix); + if (partition == nullptr) { + LOG(ERROR) << "Failed to find partition " << partition_name_suffix + << " in dynamic partition metadata."; + return false; + } + if (!UnmapPartitionWithSuffixOnDeviceMapper(partition_name_suffix)) { + LOG(ERROR) << "Cannot unmap " << partition_name_suffix << " before resizing."; + return false; + } + if (!params.builder->ResizePartition(partition, size.value())) { + LOG(ERROR) << "Failed to resize partition " << partition_name_suffix << " to size " << *size + << "."; + return false; + } + return true; +} + +bool PerformOpRemove(const OpParameters& params) { + if (!params.ExpectArgSize(1)) return false; + const auto& partition_name_suffix = AddSlotSuffix(params.arg(0)); + + if (!UnmapPartitionWithSuffixOnDeviceMapper(partition_name_suffix)) { + LOG(ERROR) << "Cannot unmap " << partition_name_suffix << " before removing."; + return false; + } + params.builder->RemovePartition(partition_name_suffix); + return true; +} + +bool PerformOpAdd(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name_suffix = AddSlotSuffix(params.arg(0)); + const auto& group_name_suffix = AddSlotSuffix(params.arg(1)); + + if (params.builder->AddPartition(partition_name_suffix, group_name_suffix, + LP_PARTITION_ATTR_READONLY) == nullptr) { + LOG(ERROR) << "Failed to add partition " << partition_name_suffix << " to group " + << group_name_suffix << "."; + return false; + } + return true; +} + +bool PerformOpMove(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name_suffix = AddSlotSuffix(params.arg(0)); + const auto& new_group_name_suffix = AddSlotSuffix(params.arg(1)); + + auto partition = params.builder->FindPartition(partition_name_suffix); + if (partition == nullptr) { + LOG(ERROR) << "Cannot move partition " << partition_name_suffix << " to group " + << new_group_name_suffix << " because it is not found."; + return false; + } + + auto old_group_name_suffix = partition->group_name(); + if (old_group_name_suffix != new_group_name_suffix) { + if (!params.builder->ChangePartitionGroup(partition, new_group_name_suffix)) { + LOG(ERROR) << "Cannot move partition " << partition_name_suffix << " from group " + << old_group_name_suffix << " to group " << new_group_name_suffix << "."; + return false; + } + } + return true; +} + +bool PerformOpAddGroup(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& group_name_suffix = AddSlotSuffix(params.arg(0)); + auto maximum_size = params.uint_arg(1, "maximum_size"); + if (!maximum_size.has_value()) return false; + + auto group = params.builder->FindGroup(group_name_suffix); + if (group != nullptr) { + LOG(ERROR) << "Cannot add group " << group_name_suffix << " because it already exists."; + return false; + } + + if (maximum_size.value() == 0) { + LOG(WARNING) << "Adding group " << group_name_suffix << " with no size limits."; + } + + if (!params.builder->AddGroup(group_name_suffix, maximum_size.value())) { + LOG(ERROR) << "Failed to add group " << group_name_suffix << " with maximum size " + << maximum_size.value() << "."; + return false; + } + return true; +} + +bool PerformOpResizeGroup(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& group_name_suffix = AddSlotSuffix(params.arg(0)); + auto new_size = params.uint_arg(1, "maximum_size"); + if (!new_size.has_value()) return false; + + auto group = params.builder->FindGroup(group_name_suffix); + if (group == nullptr) { + LOG(ERROR) << "Cannot resize group " << group_name_suffix << " because it is not found."; + return false; + } + + auto old_size = group->maximum_size(); + if (old_size != new_size.value()) { + if (!params.builder->ChangeGroupSize(group_name_suffix, new_size.value())) { + LOG(ERROR) << "Cannot resize group " << group_name_suffix << " from " << old_size << " to " + << new_size.value() << "."; + return false; + } + } + return true; +} + +std::vector ListPartitionNamesInGroup(MetadataBuilder* builder, + const std::string& group_name_suffix) { + auto partitions = builder->ListPartitionsInGroup(group_name_suffix); + std::vector partition_names; + std::transform(partitions.begin(), partitions.end(), std::back_inserter(partition_names), + [](Partition* partition) { return partition->name(); }); + return partition_names; +} + +bool PerformOpRemoveGroup(const OpParameters& params) { + if (!params.ExpectArgSize(1)) return false; + const auto& group_name_suffix = AddSlotSuffix(params.arg(0)); + + auto partition_names = ListPartitionNamesInGroup(params.builder, group_name_suffix); + if (!partition_names.empty()) { + LOG(ERROR) << "Cannot remove group " << group_name_suffix + << " because it still contains partitions [" + << android::base::Join(partition_names, ", ") << "]"; + return false; + } + params.builder->RemoveGroupAndPartitions(group_name_suffix); + return true; +} + +bool PerformOpRemoveAllGroups(const OpParameters& params) { + if (!params.ExpectArgSize(0)) return false; + + auto group_names = params.builder->ListGroups(); + for (const auto& group_name_suffix : group_names) { + auto partition_names = ListPartitionNamesInGroup(params.builder, group_name_suffix); + for (const auto& partition_name_suffix : partition_names) { + if (!UnmapPartitionWithSuffixOnDeviceMapper(partition_name_suffix)) { + LOG(ERROR) << "Cannot unmap " << partition_name_suffix << " before removing group " + << group_name_suffix << "."; + return false; + } + } + params.builder->RemoveGroupAndPartitions(group_name_suffix); + } + return true; +} + +} // namespace + +bool UpdaterRuntime::UpdateDynamicPartitions(const std::string_view op_list_value) { + auto super_device = GetSuperDevice(); + auto builder = MetadataBuilder::New(PartitionOpener(), super_device, 0); + if (builder == nullptr) { + LOG(ERROR) << "Failed to load dynamic partition metadata."; + return false; + } + + static const OpMap op_map{ + // clang-format off + {"resize", PerformOpResize}, + {"remove", PerformOpRemove}, + {"add", PerformOpAdd}, + {"move", PerformOpMove}, + {"add_group", PerformOpAddGroup}, + {"resize_group", PerformOpResizeGroup}, + {"remove_group", PerformOpRemoveGroup}, + {"remove_all_groups", PerformOpRemoveAllGroups}, + // clang-format on + }; + + std::vector lines = android::base::Split(std::string(op_list_value), "\n"); + for (const auto& line : lines) { + auto comment_idx = line.find('#'); + auto op_and_args = comment_idx == std::string::npos ? line : line.substr(0, comment_idx); + op_and_args = android::base::Trim(op_and_args); + if (op_and_args.empty()) continue; + + auto tokens = android::base::Split(op_and_args, " "); + const auto& op = tokens[0]; + auto it = op_map.find(op); + if (it == op_map.end()) { + LOG(ERROR) << "Unknown operation in op_list: " << op; + return false; + } + OpParameters params; + params.tokens = tokens; + params.builder = builder.get(); + if (!it->second(params)) { + return false; + } + } + + auto metadata = builder->Export(); + if (metadata == nullptr) { + LOG(ERROR) << "Failed to export metadata."; + return false; + } + + if (!UpdatePartitionTable(super_device, *metadata, 0)) { + LOG(ERROR) << "Failed to write metadata."; + return false; + } + + return true; +}