From 801ba0ccaacf306d8e8882aecfad5c3eb034fcb2 Mon Sep 17 00:00:00 2001 From: Kelvin Zhang Date: Fri, 5 Apr 2024 09:48:47 -0700 Subject: [PATCH] Move non-AB code from bootable/recovery to bootable/deprecated-ota Bug: 324360816 Test: th Change-Id: I3d82d9031446be355d8a1d077ab83283c7cc769c --- NOTICE | 190 ++ OWNERS | 3 + applypatch/Android.bp | 201 ++ applypatch/NOTICE | 41 + applypatch/applypatch.cpp | 457 ++++ applypatch/applypatch_main.cpp | 25 + applypatch/applypatch_modes.cpp | 187 ++ applypatch/applypatch_modes.h | 22 + applypatch/bspatch.cpp | 87 + applypatch/freecache.cpp | 247 ++ applypatch/imgdiff.cpp | 1641 ++++++++++++ applypatch/imgdiff_main.cpp | 21 + applypatch/imgpatch.cpp | 293 +++ applypatch/include/applypatch/applypatch.h | 131 + applypatch/include/applypatch/imgdiff.h | 39 + applypatch/include/applypatch/imgdiff_image.h | 308 +++ applypatch/include/applypatch/imgpatch.h | 29 + applypatch/vendor_flash_recovery.rc | 4 + edify/Android.bp | 56 + edify/README.md | 111 + edify/expr.cpp | 425 +++ edify/include/edify/expr.h | 159 ++ edify/include/edify/updater_interface.h | 48 + .../include/edify/updater_runtime_interface.h | 77 + edify/lexer.ll | 112 + edify/parser.yy | 145 ++ edify/yydefs.h | 38 + tests/Android.bp | 133 + tests/RecoveryHostTest.xml | 28 + tests/testdata/deflate_src.zip | Bin 0 -> 164491 bytes tests/testdata/deflate_tgt.zip | Bin 0 -> 160385 bytes tests/testdata/gzipped_source | Bin 0 -> 1436 bytes tests/testdata/gzipped_target | Bin 0 -> 1502 bytes tests/unit/applypatch_modes_test.cpp | 198 ++ tests/unit/applypatch_test.cpp | 290 +++ tests/unit/commands_test.cpp | 554 ++++ tests/unit/edify_test.cpp | 167 ++ tests/unit/host/imgdiff_test.cpp | 1113 ++++++++ tests/unit/host/update_simulator_test.cpp | 403 +++ tests/unit/updater_test.cpp | 1227 +++++++++ updater/Android.bp | 191 ++ updater/Android.mk | 118 + updater/blockimg.cpp | 2303 +++++++++++++++++ updater/build_info.cpp | 139 + updater/commands.cpp | 453 ++++ updater/dynamic_partitions.cpp | 140 + updater/include/private/commands.h | 475 ++++ updater/include/private/utils.h | 21 + updater/include/updater/blockimg.h | 24 + updater/include/updater/build_info.h | 74 + updater/include/updater/dynamic_partitions.h | 19 + updater/include/updater/install.h | 19 + updater/include/updater/simulator_runtime.h | 63 + updater/include/updater/target_files.h | 71 + updater/include/updater/updater.h | 96 + updater/include/updater/updater_runtime.h | 63 + updater/install.cpp | 910 +++++++ updater/mounts.cpp | 82 + updater/mounts.h | 25 + updater/simulator_runtime.cpp | 137 + updater/target_files.cpp | 294 +++ updater/update_simulator_main.cpp | 167 ++ updater/updater.cpp | 189 ++ updater/updater_main.cpp | 116 + updater/updater_runtime.cpp | 189 ++ .../updater_runtime_dynamic_partitions.cpp | 356 +++ 66 files changed, 15944 insertions(+) create mode 100644 NOTICE create mode 100644 OWNERS create mode 100644 applypatch/Android.bp create mode 100644 applypatch/NOTICE create mode 100644 applypatch/applypatch.cpp create mode 100644 applypatch/applypatch_main.cpp create mode 100644 applypatch/applypatch_modes.cpp create mode 100644 applypatch/applypatch_modes.h create mode 100644 applypatch/bspatch.cpp create mode 100644 applypatch/freecache.cpp create mode 100644 applypatch/imgdiff.cpp create mode 100644 applypatch/imgdiff_main.cpp create mode 100644 applypatch/imgpatch.cpp create mode 100644 applypatch/include/applypatch/applypatch.h create mode 100644 applypatch/include/applypatch/imgdiff.h create mode 100644 applypatch/include/applypatch/imgdiff_image.h create mode 100644 applypatch/include/applypatch/imgpatch.h create mode 100644 applypatch/vendor_flash_recovery.rc create mode 100644 edify/Android.bp create mode 100644 edify/README.md create mode 100644 edify/expr.cpp create mode 100644 edify/include/edify/expr.h create mode 100644 edify/include/edify/updater_interface.h create mode 100644 edify/include/edify/updater_runtime_interface.h create mode 100644 edify/lexer.ll create mode 100644 edify/parser.yy create mode 100644 edify/yydefs.h create mode 100644 tests/Android.bp create mode 100644 tests/RecoveryHostTest.xml create mode 100644 tests/testdata/deflate_src.zip create mode 100644 tests/testdata/deflate_tgt.zip create mode 100644 tests/testdata/gzipped_source create mode 100644 tests/testdata/gzipped_target create mode 100644 tests/unit/applypatch_modes_test.cpp create mode 100644 tests/unit/applypatch_test.cpp create mode 100644 tests/unit/commands_test.cpp create mode 100644 tests/unit/edify_test.cpp create mode 100644 tests/unit/host/imgdiff_test.cpp create mode 100644 tests/unit/host/update_simulator_test.cpp create mode 100644 tests/unit/updater_test.cpp create mode 100644 updater/Android.bp create mode 100644 updater/Android.mk create mode 100644 updater/blockimg.cpp create mode 100644 updater/build_info.cpp create mode 100644 updater/commands.cpp create mode 100644 updater/dynamic_partitions.cpp create mode 100644 updater/include/private/commands.h create mode 100644 updater/include/private/utils.h create mode 100644 updater/include/updater/blockimg.h create mode 100644 updater/include/updater/build_info.h create mode 100644 updater/include/updater/dynamic_partitions.h create mode 100644 updater/include/updater/install.h create mode 100644 updater/include/updater/simulator_runtime.h create mode 100644 updater/include/updater/target_files.h create mode 100644 updater/include/updater/updater.h create mode 100644 updater/include/updater/updater_runtime.h create mode 100644 updater/install.cpp create mode 100644 updater/mounts.cpp create mode 100644 updater/mounts.h create mode 100644 updater/simulator_runtime.cpp create mode 100644 updater/target_files.cpp create mode 100644 updater/update_simulator_main.cpp create mode 100644 updater/updater.cpp create mode 100644 updater/updater_main.cpp create mode 100644 updater/updater_runtime.cpp create mode 100644 updater/updater_runtime_dynamic_partitions.cpp 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 0000000000000000000000000000000000000000..bdb2b321689f5e548370ff2b1c38f48ece597572 GIT binary patch literal 164491 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

=szk^qy5?5Fr2!HRucq0V?{bM9MNk>bo{Ay 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

m|~eM(_q^Fu0MY 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/gzipped_target b/tests/testdata/gzipped_target new file mode 100644 index 0000000000000000000000000000000000000000..562126286188b6c1bc554b6bda430af186aaa6df GIT binary patch literal 1502 zcmV<41tIz$iwFQ|0|r|F1MOFBZ`(Ey{;pqfQ@mx~YCDdTG;?DQ=$Z`yT6AdF0Yh6D zv~;q$NTf-~jsxlQ0-&aEBVwp26+n+7VEUQhgrCX;)T$6NB^z5%E z7cXXU@B365B?=}i4U4=KZjJRp+{d5j`Jd-6UR*pmpY?8QgS(NwVq;{=gOz1-B@?z$ zWaS5**+S%bROpmvQDs)FlAGBiN-HDPYGK(`rQgqI5gaVFg_cN{3t?7v7Dd-C@&wdi1Ck6r9Sc2$JTb73DaCgQQ-!f()RA zeZTbE^}beps^ieARRx6YUY2bXODDAwaMA`mZVe14x+qwsoNlCt_qP=adDG@*Bb&4` zQ8`@*CzFlrsFm5OWHZ?E{qb)$mbcs-f97W3WU+mTll$v8xf#^C>C;Z8#`j7C+o$_P z#RE&d!}t-|wanbTPRMA#U2K`JohyIvhv&22W*0l0tTn}zx>WjF5xh^4W_afJ(ouks ztw``seOQ!g#Zd(9893`n|Kqo|A4M{w*Wl|dPPmI{HlsKukjJgrGv<Qu|3=LA+e`EIh@Y0^)y@f+i1lmkN3Bx!`D3FFJ zWnT~@E~?yTcl#m~0&bmbZP}6&nceuJW+O$eno6a8fZguw2ZI_Mk4)ZiCmcWUpS$Po zQM(VSuN0>~OBTGXrA>YL8fZ%l7NN>YkzAq+&C;qUZhR*hjGqnRY%qoA+ zrL;0ZXF3~s>D*HvW)=vinpjpAF%f1VU`J$$Ol4ZZOXWb2r7ur*&7q98yHbKgF5gvR zNUt2Rk_EaO71A%R@F|K1^sbUVrPi4$Mai*Aq!T_)s`6YEiKa6-uboSKHj;BvxS~I> z_6+v$dtyPpNo(A(ju0ohA)t8TeWN!c3#P)A8aSlFz$gu`eN#8U1{+s8uS(~^yz(J~ zi%4K1^SlKSkyVscD=8c)f3qF@45#x_pnCw=0Ns#qf%EqXmyRo+11Rex5uCt7Qk4>^ z^S%&h>3IoG0%#8=u9H-F8hB{@EJQ*C$dB&irG?J@1%<~@0x=t)uZm&Z!Jjw~sGbR- zZ@-D!XUn~?nO1QxtU7cIP?d-?%&fS#J;FNl>q#Hi&8|c0q|5m;CU+=GWgR-*!d3}t z-!2OL@|`69)j!Qb0n|dqgdTpugYP{cLAM7l>^3j-5~77-cdgAO3NaS|YJA+Gnqs)- zTBHk*D`T_p8HQ?@CT&~$7x~$#{y%)5!{0A-f&WwdpQN}RIkcI581BIg6<&uHhRTAK z|Nlcei_!IVAZZQmAC!x*(P81A$yc1YXoKz3ZeOc?LZ9CUK;bC|llpZLMZfsTyf>cGBrFf7bP$%4DP27K z{rSt67muHwhrOeYz2hkzPL9LJNr%G2jnC7~Nk4UHjt{1EbTDqe#ycwi0H={AB&QAl E0Dy+#Q~&?~ literal 0 HcmV?d00001 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; +}

OdhZ!&7jGQa{t)HwTX`jyC#OjR9)fHkR%9;?H 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 sYa#!QU)=x$nfx#Q|1ZOTHq)$ literal 0 HcmV?d00001 diff --git a/tests/testdata/deflate_tgt.zip b/tests/testdata/deflate_tgt.zip new file mode 100644 index 0000000000000000000000000000000000000000..2a21760ec529d31d9dc5c0380ce8a5e22aa2a86b GIT binary patch literal 160385 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