Restore applypatch/updater/edify for non-A/B OTA support

Google deleted this source from bootable/recovery outright (commit c7ebad5f,
"rm -rf non-AB code", 2024-04-05) — confirmed present at android-14, absent
at android-16.0.0_r4. The runtime side (install.cpp's SetUpNonAbUpdateCommands)
and build-side packaging (ota_from_target_files.py's GenerateNonAbOtaPackage)
both survived; only the source that builds the update-binary executable
itself was removed. Sourced from TeamWin's android_bootable_recovery
(android-14.1), the only living copy found, with install/ZipUtil.cpp,
get_args.cpp, and set_metadata.cpp intentionally left out (only wired into
TeamWin's modified install.cpp, which PawletOS isn't adopting — twrpinstall
carries its own independent copies).

Kept as its own repo, not folded into a recovery fork, so it's buildable
independent of whichever recovery UI ends up in use.
This commit is contained in:
2026-07-11 03:21:24 -07:00
commit 042ffdb2d5
56 changed files with 12691 additions and 0 deletions
+201
View File
@@ -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",
],
}
+192
View File
@@ -0,0 +1,192 @@
# 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.
LOCAL_PATH := $(call my-dir)
# libapplypatch (static library)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
applypatch.cpp \
bspatch.cpp \
freecache.cpp \
imgpatch.cpp
LOCAL_MODULE := libapplypatch
LOCAL_MODULE_TAGS := optional
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(commands_recovery_local_path)
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
LOCAL_STATIC_LIBRARIES := \
libotafault \
libbase \
libcrypto \
libbspatch \
libbz \
libz
LOCAL_WHOLE_STATIC_LIBRARIES += libmtdutils
LOCAL_CFLAGS := \
-DZLIB_CONST \
-Werror
BOARD_RECOVERY_DEFINES := BOARD_BML_BOOT BOARD_BML_RECOVERY
$(foreach board_define,$(BOARD_RECOVERY_DEFINES), \
$(if $($(board_define)), \
$(eval LOCAL_CFLAGS += -D$(board_define)=\"$($(board_define))\") \
) \
)
include $(BUILD_STATIC_LIBRARY)
# libimgpatch (static library)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
bspatch.cpp \
imgpatch.cpp
LOCAL_MODULE := libimgpatch
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(commands_recovery_local_path)
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
LOCAL_STATIC_LIBRARIES := \
libcrypto \
libbspatch \
libbase \
libbz \
libz
LOCAL_CFLAGS := \
-DZLIB_CONST \
-Werror
include $(BUILD_STATIC_LIBRARY)
# libimgpatch (host static library)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
bspatch.cpp \
imgpatch.cpp
LOCAL_MODULE := libimgpatch
LOCAL_MODULE_HOST_OS := linux
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(commands_recovery_local_path)
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
LOCAL_STATIC_LIBRARIES := \
libcrypto \
libbspatch \
libbase \
libbz \
libz
LOCAL_CFLAGS := \
-DZLIB_CONST \
-Werror
include $(BUILD_HOST_STATIC_LIBRARY)
# libapplypatch_modes (static library)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
applypatch_modes.cpp
LOCAL_MODULE := libapplypatch_modes
LOCAL_C_INCLUDES := $(commands_recovery_local_path)
LOCAL_STATIC_LIBRARIES := \
libapplypatch \
libbase \
libedify \
libcrypto
LOCAL_CFLAGS := -Werror
include $(BUILD_STATIC_LIBRARY)
# applypatch (target executable)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := applypatch_main.cpp
LOCAL_MODULE := applypatch
LOCAL_C_INCLUDES := $(commands_recovery_local_path)
LOCAL_STATIC_LIBRARIES := \
libapplypatch_modes \
libapplypatch \
libbase \
libedify \
libotafault \
libcrypto \
libbspatch \
libbz
LOCAL_SHARED_LIBRARIES := \
libbase \
libz \
libcutils
LOCAL_CFLAGS := -Werror
include $(BUILD_EXECUTABLE)
libimgdiff_src_files := imgdiff.cpp
# libbsdiff is compiled with -D_FILE_OFFSET_BITS=64.
libimgdiff_cflags := \
-Werror \
-D_FILE_OFFSET_BITS=64
libimgdiff_static_libraries := \
libbsdiff \
libdivsufsort \
libdivsufsort64 \
libziparchive \
libutils \
liblog \
libbase \
libz
# libimgdiff (static library)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
$(libimgdiff_src_files)
LOCAL_MODULE := libimgdiff
LOCAL_CFLAGS := \
$(libimgdiff_cflags)
LOCAL_STATIC_LIBRARIES := \
$(libimgdiff_static_libraries)
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
include $(BUILD_STATIC_LIBRARY)
# libimgdiff (host static library)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
$(libimgdiff_src_files)
LOCAL_MODULE := libimgdiff
LOCAL_CFLAGS := \
$(libimgdiff_cflags)
LOCAL_STATIC_LIBRARIES := \
$(libimgdiff_static_libraries)
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
include $(BUILD_HOST_STATIC_LIBRARY)
# imgdiff (host static executable)
# ===============================
include $(CLEAR_VARS)
LOCAL_SRC_FILES := imgdiff_main.cpp
LOCAL_MODULE := imgdiff
LOCAL_CFLAGS := -Werror
LOCAL_STATIC_LIBRARIES := \
libimgdiff \
$(libimgdiff_static_libraries) \
libbz
include $(BUILD_HOST_EXECUTABLE)
+41
View File
@@ -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.
+453
View File
@@ -0,0 +1,453 @@
/*
* 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 <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
#include <openssl/sha.h>
#include "bmlutils/bmlutils.h"
#include "mtdutils/mtdutils.h"
#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<unsigned char>(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<unsigned char> 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;
}
const char* partition = pieces[1].c_str();
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<const uint8_t*>(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<const uint8_t*>(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<std::string> 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 "<invalid-partition>";
}
std::ostream& operator<<(std::ostream& os, const Partition& partition) {
os << partition.ToString();
return os;
}
+25
View File
@@ -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 <android-base/logging.h>
// See the comments for applypatch() function.
int main(int argc, char** argv) {
android::base::InitLogging(argv);
return applypatch_modes(argc, argv);
}
+187
View File
@@ -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 <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <memory>
#include <string>
#include <vector>
#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
#include <openssl/sha.h>
#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<Value> 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>(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:<target-file>:<target-size>:<target-sha1>\n\n"
"flash mode\n"
" applypatch --flash <source-file>\n"
" --target EMMC:<target-file>:<target-size>:<target-sha1>\n\n"
"patch mode\n"
" applypatch [--bonus <bonus-file>]\n"
" --patch <patch-file>\n"
" --target EMMC:<target-file>:<target-size>:<target-sha1>\n"
" --source EMMC:<source-file>:<source-size>:<source-sha1>\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;
}
+22
View File
@@ -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
+87
View File
@@ -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 <stdio.h>
#include <sys/types.h>
#include <string>
#include <android-base/logging.h>
#include <bsdiff/bspatch.h>
#include <openssl/sha.h>
#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<const uint8_t*>(&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<const uint8_t*>(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;
}
+247
View File
@@ -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 <dirent.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <unistd.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <set>
#include <string>
#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include "applypatch/applypatch.h"
#include "otautil/paths.h"
static int EliminateOpenFiles(const std::string& dirname, std::set<std::string>* files) {
std::unique_ptr<DIR, decltype(&closedir)> 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<DIR, decltype(&closedir)> 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<std::string> FindExpendableFiles(
const std::string& dirname, const std::function<bool(const std::string&)>& name_filter) {
std::unique_ptr<DIR, decltype(&closedir)> 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<std::string> 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<std::string>(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<unsigned int>::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<int64_t>(sf.f_bsize);
auto free_space = sf.f_bsize * sf.f_bavail;
if (f_bsize == 0 || free_space / f_bsize != static_cast<int64_t>(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<std::string> 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<int64_t(const std::string&)>& space_checker) {
// The requested size cannot exceed max int64_t.
if (static_cast<uint64_t>(bytes_needed) >
static_cast<uint64_t>(std::numeric_limits<int64_t>::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<int64_t>(bytes_needed)) {
return true;
}
std::vector<std::string> 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<int64_t>(bytes_needed)) {
return true;
}
}
return false;
}
File diff suppressed because it is too large Load Diff
+21
View File
@@ -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<const char**>(argv));
}
+293
View File
@@ -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 <applypatch/imgpatch.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/cdefs.h>
#include <sys/stat.h>
#include <unistd.h>
#include <memory>
#include <string>
#include <vector>
#include <android-base/logging.h>
#include <android-base/memory.h>
#include <applypatch/applypatch.h>
#include <applypatch/imgdiff.h>
#include <openssl/sha.h>
#include <zlib.h>
#include "edify/expr.h"
#include "otautil/print_sha1.h"
static inline int64_t Read8(const void *address) {
return android::base::get_unaligned<int64_t>(address);
}
static inline int32_t Read4(const void *address) {
return android::base::get_unaligned<int32_t>(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<size_t>(Read8(deflate_header + 32));
CHECK_GT(expected_target_length, static_cast<size_t>(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<uint8_t> 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<const char*>(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<size_t>(Read8(normal_header));
size_t src_len = static_cast<size_t>(Read8(normal_header + 8));
size_t patch_offset = static_cast<size_t>(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<size_t>(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<const unsigned char*>(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<size_t>(Read8(deflate_header));
size_t src_len = static_cast<size_t>(Read8(deflate_header + 8));
size_t patch_offset = static_cast<size_t>(Read8(deflate_header + 16));
size_t expanded_len = static_cast<size_t>(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<unsigned char> 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;
}
+131
View File
@@ -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 <stdint.h>
#include <functional>
#include <memory>
#include <ostream>
#include <string>
#include <vector>
#include <openssl/sha.h>
// Forward declaration to avoid including "edify/expr.h" in the header.
struct Value;
struct FileContents {
uint8_t sha1[SHA_DIGEST_LENGTH];
std::vector<unsigned char> data;
};
using SinkFn = std::function<size_t(const unsigned char*, size_t)>;
// 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 "<digest>:<anything>". 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:<device>:<size>:<hash>". 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<int64_t(const std::string&)>& space_checker);
#endif
+39
View File
@@ -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 <stddef.h>
// 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
@@ -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 <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <string>
#include <vector>
#include <bsdiff/bsdiff.h>
#include <ziparchive/zip_archive.h>
#include <zlib.h>
#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<uint8_t>* 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<uint8_t> data);
bool SetBonusData(const std::vector<uint8_t>& 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<uint8_t>* 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<uint8_t>* 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<uint8_t> 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<uint8_t> 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<PatchChunk>& 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<uint8_t> 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<ImageChunk>::iterator begin() {
return chunks_.begin();
}
std::vector<ImageChunk>::iterator end() {
return chunks_.end();
}
std::vector<ImageChunk>::const_iterator cbegin() const {
return chunks_.cbegin();
}
std::vector<ImageChunk>::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<uint8_t>* file_content);
bool is_source_; // True if it's for source chunks.
std::vector<ImageChunk> chunks_; // Internal storage of ImageChunk.
std::vector<uint8_t> 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<ImageChunk>& chunks, const std::vector<uint8_t>& 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<ZipModeImage>& split_tgt_images,
const std::vector<ZipModeImage>& split_src_images,
const std::vector<SortedRangeSet>& 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<ZipModeImage>* split_tgt_images,
std::vector<ZipModeImage>* split_src_images,
std::vector<SortedRangeSet>* 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<ZipModeImage>& split_tgt_images,
const std::vector<ZipModeImage>& split_src_images,
std::vector<SortedRangeSet>& 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<ImageChunk>& split_tgt_chunks,
const std::vector<ImageChunk>& split_src_chunks,
std::vector<ZipModeImage>* split_tgt_images,
std::vector<ZipModeImage>* 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<PatchChunk>* 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<uint8_t>& 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
+29
View File
@@ -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 <sys/types.h>
#include <functional>
using SinkFn = std::function<size_t(const unsigned char*, size_t)>;
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
+3
View File
@@ -0,0 +1,3 @@
service vendor_flash_recovery /vendor/bin/install-recovery.sh
class main
oneshot