FastDeploy refactor: 2+GB APK support, optimizations, tests.

- removed 2GB apk size cap,
- removed zip archive parsing on device (1.1M->236K agent size reduction),
- optimized matching entries search,
- added more robust matching entries search based on hash of CDr entry,
- reduced patch size by reusing Local File Header of matched entries,
- removed extra manifest parsing and extra agent calls,
- added device-side tests for agent,
- fix for Windows patch creation.

Test: atest adb_test fastdeploy_test FastDeployTests

Total time for 0-size patch reduction for 1.7G apk: 1m1.778s->0m36.234s.

Change-Id: I66d2cef1adf5b2be3325e355a7e72e9c99992369
This commit is contained in:
Alex Buynytskyy 2019-09-16 12:10:54 -07:00
parent c63ef7fc36
commit 665f3ff5a7
34 changed files with 1867 additions and 749 deletions

View file

@ -27,7 +27,6 @@ cc_defaults {
"-DADB_HOST=1", // overridden by adbd_defaults
"-DALLOW_ADBD_ROOT=0", // overridden by adbd_defaults
"-DANDROID_BASE_UNIQUE_FD_DISABLE_IMPLICIT_CONVERSION=1",
"-DENABLE_FASTDEPLOY=1", // enable fast deploy
],
cpp_std: "experimental",
@ -691,6 +690,7 @@ cc_library_host_static {
name: "libfastdeploy_host",
defaults: ["adb_defaults"],
srcs: [
"fastdeploy/deploypatchgenerator/apk_archive.cpp",
"fastdeploy/deploypatchgenerator/deploy_patch_generator.cpp",
"fastdeploy/deploypatchgenerator/patch_utils.cpp",
"fastdeploy/proto/ApkEntry.proto",
@ -727,6 +727,7 @@ cc_test_host {
name: "fastdeploy_test",
defaults: ["adb_defaults"],
srcs: [
"fastdeploy/deploypatchgenerator/apk_archive_test.cpp",
"fastdeploy/deploypatchgenerator/deploy_patch_generator_test.cpp",
"fastdeploy/deploypatchgenerator/patch_utils_test.cpp",
],
@ -754,6 +755,9 @@ cc_test_host {
},
},
data: [
"fastdeploy/testdata/rotating_cube-metadata-release.data",
"fastdeploy/testdata/rotating_cube-release.apk",
"fastdeploy/testdata/sample.apk",
"fastdeploy/testdata/sample.cd",
],
}

View file

@ -37,9 +37,7 @@
#include "commandline.h"
#include "fastdeploy.h"
#if defined(ENABLE_FASTDEPLOY)
static constexpr int kFastDeployMinApi = 24;
#endif
namespace {
@ -146,15 +144,7 @@ static void read_status_line(int fd, char* buf, size_t count) {
*buf = '\0';
}
#if defined(ENABLE_FASTDEPLOY)
static int delete_device_patch_file(const char* apkPath) {
std::string patchDevicePath = get_patch_path(apkPath);
return delete_device_file(patchDevicePath);
}
#endif
static int install_app_streamed(int argc, const char** argv, bool use_fastdeploy,
bool use_localagent) {
static int install_app_streamed(int argc, const char** argv, bool use_fastdeploy) {
printf("Performing Streamed Install\n");
// The last argument must be the APK file
@ -176,31 +166,15 @@ static int install_app_streamed(int argc, const char** argv, bool use_fastdeploy
error_exit("--fastdeploy doesn't support .apex files");
}
if (use_fastdeploy == true) {
#if defined(ENABLE_FASTDEPLOY)
TemporaryFile metadataTmpFile;
std::string patchTmpFilePath;
{
TemporaryFile patchTmpFile;
patchTmpFile.DoNotRemove();
patchTmpFilePath = patchTmpFile.path;
if (use_fastdeploy) {
auto metadata = extract_metadata(file);
if (metadata.has_value()) {
// pass all but 1st (command) and last (apk path) parameters through to pm for
// session creation
std::vector<const char*> pm_args{argv + 1, argv + argc - 1};
auto patchFd = install_patch(pm_args.size(), pm_args.data());
return stream_patch(file, std::move(metadata.value()), std::move(patchFd));
}
FILE* metadataFile = fopen(metadataTmpFile.path, "wb");
extract_metadata(file, metadataFile);
fclose(metadataFile);
create_patch(file, metadataTmpFile.path, patchTmpFilePath.c_str());
// pass all but 1st (command) and last (apk path) parameters through to pm for
// session creation
std::vector<const char*> pm_args{argv + 1, argv + argc - 1};
install_patch(file, patchTmpFilePath.c_str(), pm_args.size(), pm_args.data());
adb_unlink(patchTmpFilePath.c_str());
delete_device_patch_file(file);
return 0;
#else
error_exit("fastdeploy is disabled");
#endif
}
struct stat sb;
@ -265,8 +239,7 @@ static int install_app_streamed(int argc, const char** argv, bool use_fastdeploy
return 1;
}
static int install_app_legacy(int argc, const char** argv, bool use_fastdeploy,
bool use_localagent) {
static int install_app_legacy(int argc, const char** argv, bool use_fastdeploy) {
printf("Performing Push Install\n");
// Find last APK argument.
@ -287,35 +260,26 @@ static int install_app_legacy(int argc, const char** argv, bool use_fastdeploy,
int result = -1;
std::vector<const char*> apk_file = {argv[last_apk]};
std::string apk_dest = "/data/local/tmp/" + android::base::Basename(argv[last_apk]);
if (use_fastdeploy == true) {
#if defined(ENABLE_FASTDEPLOY)
TemporaryFile metadataTmpFile;
TemporaryFile patchTmpFile;
FILE* metadataFile = fopen(metadataTmpFile.path, "wb");
extract_metadata(apk_file[0], metadataFile);
fclose(metadataFile);
create_patch(apk_file[0], metadataTmpFile.path, patchTmpFile.path);
apply_patch_on_device(apk_file[0], patchTmpFile.path, apk_dest.c_str());
#else
error_exit("fastdeploy is disabled");
#endif
} else {
if (!do_sync_push(apk_file, apk_dest.c_str(), false)) goto cleanup_apk;
}
argv[last_apk] = apk_dest.c_str(); /* destination name, not source location */
result = pm_command(argc, argv);
cleanup_apk:
if (use_fastdeploy == true) {
#if defined(ENABLE_FASTDEPLOY)
delete_device_patch_file(apk_file[0]);
#endif
if (use_fastdeploy) {
auto metadata = extract_metadata(apk_file[0]);
if (metadata.has_value()) {
auto patchFd = apply_patch_on_device(apk_dest.c_str());
int status = stream_patch(apk_file[0], std::move(metadata.value()), std::move(patchFd));
result = pm_command(argc, argv);
delete_device_file(apk_dest);
return status;
}
}
delete_device_file(apk_dest);
if (do_sync_push(apk_file, apk_dest.c_str(), false)) {
result = pm_command(argc, argv);
delete_device_file(apk_dest);
}
return result;
}
@ -324,7 +288,6 @@ int install_app(int argc, const char** argv) {
InstallMode installMode = INSTALL_DEFAULT;
bool use_fastdeploy = false;
bool is_reinstall = false;
bool use_localagent = false;
FastDeploy_AgentUpdateStrategy agent_update_strategy = FastDeploy_AgentUpdateDifferentVersion;
for (int i = 1; i < argc; i++) {
@ -353,11 +316,6 @@ int install_app(int argc, const char** argv) {
} else if (!strcmp(argv[i], "--version-check-agent")) {
processedArgIndicies.push_back(i);
agent_update_strategy = FastDeploy_AgentUpdateDifferentVersion;
#ifndef _WIN32
} else if (!strcmp(argv[i], "--local-agent")) {
processedArgIndicies.push_back(i);
use_localagent = true;
#endif
}
}
@ -369,14 +327,13 @@ int install_app(int argc, const char** argv) {
error_exit("Attempting to use streaming install on unsupported device");
}
#if defined(ENABLE_FASTDEPLOY)
if (use_fastdeploy == true && get_device_api_level() < kFastDeployMinApi) {
if (use_fastdeploy && get_device_api_level() < kFastDeployMinApi) {
printf("Fast Deploy is only compatible with devices of API version %d or higher, "
"ignoring.\n",
kFastDeployMinApi);
use_fastdeploy = false;
}
#endif
fastdeploy_set_agent_update_strategy(agent_update_strategy);
std::vector<const char*> passthrough_argv;
for (int i = 0; i < argc; i++) {
@ -389,26 +346,13 @@ int install_app(int argc, const char** argv) {
error_exit("install requires an apk argument");
}
if (use_fastdeploy == true) {
#if defined(ENABLE_FASTDEPLOY)
fastdeploy_set_local_agent(use_localagent);
update_agent(agent_update_strategy);
// The last argument must be the APK file
const char* file = passthrough_argv.back();
use_fastdeploy = find_package(file);
#else
error_exit("fastdeploy is disabled");
#endif
}
switch (installMode) {
case INSTALL_PUSH:
return install_app_legacy(passthrough_argv.size(), passthrough_argv.data(),
use_fastdeploy, use_localagent);
use_fastdeploy);
case INSTALL_STREAM:
return install_app_streamed(passthrough_argv.size(), passthrough_argv.data(),
use_fastdeploy, use_localagent);
use_fastdeploy);
case INSTALL_DEFAULT:
default:
return 1;

View file

@ -255,13 +255,8 @@ static void stdin_raw_restore() {
}
#endif
// Reads from |fd| and prints received data. If |use_shell_protocol| is true
// this expects that incoming data will use the shell protocol, in which case
// stdout/stderr are routed independently and the remote exit code will be
// returned.
// if |callback| is non-null, stdout/stderr output will be handled by it.
int read_and_dump(borrowed_fd fd, bool use_shell_protocol = false,
StandardStreamsCallbackInterface* callback = &DEFAULT_STANDARD_STREAMS_CALLBACK) {
int read_and_dump(borrowed_fd fd, bool use_shell_protocol,
StandardStreamsCallbackInterface* callback) {
int exit_code = 0;
if (fd < 0) return exit_code;

View file

@ -109,6 +109,14 @@ int send_shell_command(
const std::string& command, bool disable_shell_protocol = false,
StandardStreamsCallbackInterface* callback = &DEFAULT_STANDARD_STREAMS_CALLBACK);
// Reads from |fd| and prints received data. If |use_shell_protocol| is true
// this expects that incoming data will use the shell protocol, in which case
// stdout/stderr are routed independently and the remote exit code will be
// returned.
// if |callback| is non-null, stdout/stderr output will be handled by it.
int read_and_dump(borrowed_fd fd, bool use_shell_protocol = false,
StandardStreamsCallbackInterface* callback = &DEFAULT_STANDARD_STREAMS_CALLBACK);
// Connects to the device "abb" service with |command| and returns the fd.
template <typename ContainerT>
unique_fd send_abb_exec_command(const ContainerT& command_args, std::string* error) {

View file

@ -30,112 +30,114 @@
#include "deployagent.inc" // Generated include via build rule.
#include "deployagentscript.inc" // Generated include via build rule.
#include "fastdeploy/deploypatchgenerator/deploy_patch_generator.h"
#include "fastdeploy/deploypatchgenerator/patch_utils.h"
#include "fastdeploy/proto/ApkEntry.pb.h"
#include "fastdeploycallbacks.h"
#include "sysdeps.h"
#include "adb_utils.h"
static constexpr long kRequiredAgentVersion = 0x00000002;
static constexpr long kRequiredAgentVersion = 0x00000003;
static constexpr int kPackageMissing = 3;
static constexpr int kInvalidAgentVersion = 4;
static constexpr const char* kDeviceAgentPath = "/data/local/tmp/";
static constexpr const char* kDeviceAgentFile = "/data/local/tmp/deployagent.jar";
static constexpr const char* kDeviceAgentScript = "/data/local/tmp/deployagent";
static bool g_use_localagent = false;
static constexpr bool g_verbose_timings = false;
static FastDeploy_AgentUpdateStrategy g_agent_update_strategy =
FastDeploy_AgentUpdateDifferentVersion;
long get_agent_version() {
std::vector<char> versionOutputBuffer;
std::vector<char> versionErrorBuffer;
using APKMetaData = com::android::fastdeploy::APKMetaData;
int statusCode = capture_shell_command("/data/local/tmp/deployagent version",
&versionOutputBuffer, &versionErrorBuffer);
long version = -1;
namespace {
if (statusCode == 0 && versionOutputBuffer.size() > 0) {
version = strtol((char*)versionOutputBuffer.data(), NULL, 16);
struct TimeReporter {
TimeReporter(const char* label) : label_(label) {}
~TimeReporter() {
if (g_verbose_timings) {
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_);
fprintf(stderr, "%s finished in %lldms\n", label_,
static_cast<long long>(duration.count()));
}
}
return version;
}
private:
const char* label_;
std::chrono::steady_clock::time_point start_ = std::chrono::steady_clock::now();
};
#define REPORT_FUNC_TIME() TimeReporter reporter(__func__)
struct FileDeleter {
FileDeleter(const char* path) : path_(path) {}
~FileDeleter() { adb_unlink(path_); }
private:
const char* const path_;
};
} // namespace
int get_device_api_level() {
std::vector<char> sdkVersionOutputBuffer;
std::vector<char> sdkVersionErrorBuffer;
REPORT_FUNC_TIME();
std::vector<char> sdk_version_output_buffer;
std::vector<char> sdk_version_error_buffer;
int api_level = -1;
int statusCode = capture_shell_command("getprop ro.build.version.sdk", &sdkVersionOutputBuffer,
&sdkVersionErrorBuffer);
if (statusCode == 0 && sdkVersionOutputBuffer.size() > 0) {
api_level = strtol((char*)sdkVersionOutputBuffer.data(), NULL, 10);
int statusCode = capture_shell_command("getprop ro.build.version.sdk",
&sdk_version_output_buffer, &sdk_version_error_buffer);
if (statusCode == 0 && sdk_version_output_buffer.size() > 0) {
api_level = strtol((char*)sdk_version_output_buffer.data(), NULL, 10);
}
return api_level;
}
void fastdeploy_set_local_agent(bool use_localagent) {
g_use_localagent = use_localagent;
void fastdeploy_set_agent_update_strategy(FastDeploy_AgentUpdateStrategy agent_update_strategy) {
g_agent_update_strategy = agent_update_strategy;
}
static bool deploy_agent(bool checkTimeStamps) {
static void push_to_device(const void* data, size_t byte_count, const char* dst, bool sync) {
std::vector<const char*> srcs;
// TODO: Deploy agent from bin2c directly instead of writing to disk first.
TemporaryFile tempAgent;
android::base::WriteFully(tempAgent.fd, kDeployAgent, sizeof(kDeployAgent));
srcs.push_back(tempAgent.path);
if (!do_sync_push(srcs, kDeviceAgentFile, checkTimeStamps)) {
{
TemporaryFile temp;
android::base::WriteFully(temp.fd, data, byte_count);
srcs.push_back(temp.path);
// On Windows, the file needs to be flushed before pushing to device.
// closing the file flushes its content, but we still need to remove it after push.
// FileDeleter does exactly that.
temp.DoNotRemove();
}
FileDeleter temp_deleter(srcs.back());
if (!do_sync_push(srcs, dst, sync)) {
error_exit("Failed to push fastdeploy agent to device.");
}
srcs.clear();
// TODO: Deploy agent from bin2c directly instead of writing to disk first.
TemporaryFile tempAgentScript;
android::base::WriteFully(tempAgentScript.fd, kDeployAgentScript, sizeof(kDeployAgentScript));
srcs.push_back(tempAgentScript.path);
if (!do_sync_push(srcs, kDeviceAgentScript, checkTimeStamps)) {
error_exit("Failed to push fastdeploy agent script to device.");
}
srcs.clear();
}
static bool deploy_agent(bool check_time_stamps) {
REPORT_FUNC_TIME();
push_to_device(kDeployAgent, sizeof(kDeployAgent), kDeviceAgentFile, check_time_stamps);
push_to_device(kDeployAgentScript, sizeof(kDeployAgentScript), kDeviceAgentScript,
check_time_stamps);
// on windows the shell script might have lost execute permission
// so need to set this explicitly
const char* kChmodCommandPattern = "chmod 777 %s";
std::string chmodCommand =
std::string chmod_command =
android::base::StringPrintf(kChmodCommandPattern, kDeviceAgentScript);
int ret = send_shell_command(chmodCommand);
int ret = send_shell_command(chmod_command);
if (ret != 0) {
error_exit("Error executing %s returncode: %d", chmodCommand.c_str(), ret);
error_exit("Error executing %s returncode: %d", chmod_command.c_str(), ret);
}
return true;
}
void update_agent(FastDeploy_AgentUpdateStrategy agentUpdateStrategy) {
long agent_version = get_agent_version();
switch (agentUpdateStrategy) {
case FastDeploy_AgentUpdateAlways:
deploy_agent(false);
break;
case FastDeploy_AgentUpdateNewerTimeStamp:
deploy_agent(true);
break;
case FastDeploy_AgentUpdateDifferentVersion:
if (agent_version != kRequiredAgentVersion) {
if (agent_version < 0) {
printf("Could not detect agent on device, deploying\n");
} else {
printf("Device agent version is (%ld), (%ld) is required, re-deploying\n",
agent_version, kRequiredAgentVersion);
}
deploy_agent(false);
}
break;
}
agent_version = get_agent_version();
if (agent_version != kRequiredAgentVersion) {
error_exit("After update agent version remains incorrect! Expected %ld but version is %ld",
kRequiredAgentVersion, agent_version);
}
}
static std::string get_string_from_utf16(const char16_t* input, int input_len) {
ssize_t utf8_length = utf16_to_utf8_length(input, input_len);
if (utf8_length <= 0) {
@ -147,29 +149,29 @@ static std::string get_string_from_utf16(const char16_t* input, int input_len) {
return utf8;
}
static std::string get_packagename_from_apk(const char* apkPath) {
static std::string get_package_name_from_apk(const char* apk_path) {
#undef open
std::unique_ptr<android::ZipFileRO> zipFile(android::ZipFileRO::open(apkPath));
std::unique_ptr<android::ZipFileRO> zip_file((android::ZipFileRO::open)(apk_path));
#define open ___xxx_unix_open
if (zipFile == nullptr) {
perror_exit("Could not open %s", apkPath);
if (zip_file == nullptr) {
perror_exit("Could not open %s", apk_path);
}
android::ZipEntryRO entry = zipFile->findEntryByName("AndroidManifest.xml");
android::ZipEntryRO entry = zip_file->findEntryByName("AndroidManifest.xml");
if (entry == nullptr) {
error_exit("Could not find AndroidManifest.xml inside %s", apkPath);
error_exit("Could not find AndroidManifest.xml inside %s", apk_path);
}
uint32_t manifest_len = 0;
if (!zipFile->getEntryInfo(entry, NULL, &manifest_len, NULL, NULL, NULL, NULL)) {
error_exit("Could not read AndroidManifest.xml inside %s", apkPath);
if (!zip_file->getEntryInfo(entry, NULL, &manifest_len, NULL, NULL, NULL, NULL)) {
error_exit("Could not read AndroidManifest.xml inside %s", apk_path);
}
std::vector<char> manifest_data(manifest_len);
if (!zipFile->uncompressEntry(entry, manifest_data.data(), manifest_len)) {
error_exit("Could not uncompress AndroidManifest.xml inside %s", apkPath);
if (!zip_file->uncompressEntry(entry, manifest_data.data(), manifest_len)) {
error_exit("Could not uncompress AndroidManifest.xml inside %s", apk_path);
}
android::ResXMLTree tree;
android::status_t setto_status = tree.setTo(manifest_data.data(), manifest_len, true);
if (setto_status != android::OK) {
error_exit("Could not parse AndroidManifest.xml inside %s", apkPath);
error_exit("Could not parse AndroidManifest.xml inside %s", apk_path);
}
android::ResXMLParser::event_code_t code;
while ((code = tree.next()) != android::ResXMLParser::BAD_DOCUMENT &&
@ -210,80 +212,97 @@ static std::string get_packagename_from_apk(const char* apkPath) {
break;
}
}
error_exit("Could not find package name tag in AndroidManifest.xml inside %s", apkPath);
error_exit("Could not find package name tag in AndroidManifest.xml inside %s", apk_path);
}
void extract_metadata(const char* apkPath, FILE* outputFp) {
std::string packageName = get_packagename_from_apk(apkPath);
const char* kAgentExtractCommandPattern = "/data/local/tmp/deployagent extract %s";
std::string extractCommand =
android::base::StringPrintf(kAgentExtractCommandPattern, packageName.c_str());
static long parse_agent_version(const std::vector<char>& version_buffer) {
long version = -1;
if (!version_buffer.empty()) {
version = strtol((char*)version_buffer.data(), NULL, 16);
}
return version;
}
std::vector<char> extractErrorBuffer;
DeployAgentFileCallback cb(outputFp, &extractErrorBuffer);
int returnCode = send_shell_command(extractCommand, false, &cb);
static void update_agent_if_necessary() {
switch (g_agent_update_strategy) {
case FastDeploy_AgentUpdateAlways:
deploy_agent(/*check_time_stamps=*/false);
break;
case FastDeploy_AgentUpdateNewerTimeStamp:
deploy_agent(/*check_time_stamps=*/true);
break;
default:
break;
}
}
std::optional<APKMetaData> extract_metadata(const char* apk_path) {
// Update agent if there is a command line argument forcing to do so.
update_agent_if_necessary();
REPORT_FUNC_TIME();
std::string package_name = get_package_name_from_apk(apk_path);
// Dump apk command checks the required vs current agent version and if they match then returns
// the APK dump for package. Doing this in a single call saves round-trip and agent launch time.
constexpr const char* kAgentDumpCommandPattern = "/data/local/tmp/deployagent dump %ld %s";
std::string dump_command = android::base::StringPrintf(
kAgentDumpCommandPattern, kRequiredAgentVersion, package_name.c_str());
std::vector<char> dump_out_buffer;
std::vector<char> dump_error_buffer;
int returnCode =
capture_shell_command(dump_command.c_str(), &dump_out_buffer, &dump_error_buffer);
if (returnCode >= kInvalidAgentVersion) {
// Agent has wrong version or missing.
long agent_version = parse_agent_version(dump_out_buffer);
if (agent_version < 0) {
printf("Could not detect agent on device, deploying\n");
} else {
printf("Device agent version is (%ld), (%ld) is required, re-deploying\n",
agent_version, kRequiredAgentVersion);
}
deploy_agent(/*check_time_stamps=*/false);
// Retry with new agent.
dump_out_buffer.clear();
dump_error_buffer.clear();
returnCode =
capture_shell_command(dump_command.c_str(), &dump_out_buffer, &dump_error_buffer);
}
if (returnCode != 0) {
fprintf(stderr, "Executing %s returned %d\n", extractCommand.c_str(), returnCode);
fprintf(stderr, "%*s\n", int(extractErrorBuffer.size()), extractErrorBuffer.data());
if (returnCode == kInvalidAgentVersion) {
long agent_version = parse_agent_version(dump_out_buffer);
error_exit(
"After update agent version remains incorrect! Expected %ld but version is %ld",
kRequiredAgentVersion, agent_version);
}
if (returnCode == kPackageMissing) {
fprintf(stderr, "Package %s not found, falling back to install\n",
package_name.c_str());
return {};
}
fprintf(stderr, "Executing %s returned %d\n", dump_command.c_str(), returnCode);
fprintf(stderr, "%*s\n", int(dump_error_buffer.size()), dump_error_buffer.data());
error_exit("Aborting");
}
com::android::fastdeploy::APKDump dump;
if (!dump.ParseFromArray(dump_out_buffer.data(), dump_out_buffer.size())) {
fprintf(stderr, "Can't parse output of %s\n", dump_command.c_str());
error_exit("Aborting");
}
return PatchUtils::GetDeviceAPKMetaData(dump);
}
void create_patch(const char* apkPath, const char* metadataPath, const char* patchPath) {
DeployPatchGenerator generator(false);
unique_fd patchFd(adb_open(patchPath, O_WRONLY | O_CREAT | O_CLOEXEC));
if (patchFd < 0) {
perror_exit("adb: failed to create %s", patchPath);
}
bool success = generator.CreatePatch(apkPath, metadataPath, patchFd);
if (!success) {
error_exit("Failed to create patch for %s", apkPath);
}
}
unique_fd install_patch(int argc, const char** argv) {
REPORT_FUNC_TIME();
constexpr char kAgentApplyServicePattern[] = "shell:/data/local/tmp/deployagent apply - -pm %s";
std::string get_patch_path(const char* apkPath) {
std::string packageName = get_packagename_from_apk(apkPath);
std::string patchDevicePath =
android::base::StringPrintf("%s%s.patch", kDeviceAgentPath, packageName.c_str());
return patchDevicePath;
}
void apply_patch_on_device(const char* apkPath, const char* patchPath, const char* outputPath) {
const std::string kAgentApplyCommandPattern = "/data/local/tmp/deployagent apply %s %s -o %s";
std::string packageName = get_packagename_from_apk(apkPath);
std::string patchDevicePath = get_patch_path(apkPath);
std::vector<const char*> srcs = {patchPath};
bool push_ok = do_sync_push(srcs, patchDevicePath.c_str(), false);
if (!push_ok) {
error_exit("Error pushing %s to %s returned", patchPath, patchDevicePath.c_str());
}
std::string applyPatchCommand =
android::base::StringPrintf(kAgentApplyCommandPattern.c_str(), packageName.c_str(),
patchDevicePath.c_str(), outputPath);
int returnCode = send_shell_command(applyPatchCommand);
if (returnCode != 0) {
error_exit("Executing %s returned %d", applyPatchCommand.c_str(), returnCode);
}
}
void install_patch(const char* apkPath, const char* patchPath, int argc, const char** argv) {
const std::string kAgentApplyCommandPattern = "/data/local/tmp/deployagent apply %s %s -pm %s";
std::string packageName = get_packagename_from_apk(apkPath);
std::string patchDevicePath =
android::base::StringPrintf("%s%s.patch", kDeviceAgentPath, packageName.c_str());
std::vector<const char*> srcs{patchPath};
bool push_ok = do_sync_push(srcs, patchDevicePath.c_str(), false);
if (!push_ok) {
error_exit("Error pushing %s to %s returned", patchPath, patchDevicePath.c_str());
}
std::vector<unsigned char> applyOutputBuffer;
std::vector<unsigned char> applyErrorBuffer;
std::vector<unsigned char> apply_output_buffer;
std::vector<unsigned char> apply_error_buffer;
std::string argsString;
bool rSwitchPresent = false;
@ -298,17 +317,42 @@ void install_patch(const char* apkPath, const char* patchPath, int argc, const c
argsString.append("-r");
}
std::string applyPatchCommand =
android::base::StringPrintf(kAgentApplyCommandPattern.c_str(), packageName.c_str(),
patchDevicePath.c_str(), argsString.c_str());
int returnCode = send_shell_command(applyPatchCommand);
if (returnCode != 0) {
error_exit("Executing %s returned %d", applyPatchCommand.c_str(), returnCode);
std::string error;
std::string apply_patch_service_string =
android::base::StringPrintf(kAgentApplyServicePattern, argsString.c_str());
unique_fd fd{adb_connect(apply_patch_service_string, &error)};
if (fd < 0) {
error_exit("Executing %s returned %s", apply_patch_service_string.c_str(), error.c_str());
}
return fd;
}
unique_fd apply_patch_on_device(const char* output_path) {
REPORT_FUNC_TIME();
constexpr char kAgentApplyServicePattern[] = "shell:/data/local/tmp/deployagent apply - -o %s";
std::string error;
std::string apply_patch_service_string =
android::base::StringPrintf(kAgentApplyServicePattern, output_path);
unique_fd fd{adb_connect(apply_patch_service_string, &error)};
if (fd < 0) {
error_exit("Executing %s returned %s", apply_patch_service_string.c_str(), error.c_str());
}
return fd;
}
static void create_patch(const char* apk_path, APKMetaData metadata, borrowed_fd patch_fd) {
REPORT_FUNC_TIME();
DeployPatchGenerator generator(/*is_verbose=*/false);
bool success = generator.CreatePatch(apk_path, std::move(metadata), patch_fd);
if (!success) {
error_exit("Failed to create patch for %s", apk_path);
}
}
bool find_package(const char* apkPath) {
const std::string findCommand =
"/data/local/tmp/deployagent find " + get_packagename_from_apk(apkPath);
return !send_shell_command(findCommand);
int stream_patch(const char* apk_path, APKMetaData metadata, unique_fd patch_fd) {
create_patch(apk_path, std::move(metadata), patch_fd);
REPORT_FUNC_TIME();
return read_and_dump(patch_fd.get());
}

View file

@ -16,6 +16,11 @@
#pragma once
#include "adb_unique_fd.h"
#include "fastdeploy/proto/ApkEntry.pb.h"
#include <optional>
#include <string>
enum FastDeploy_AgentUpdateStrategy {
@ -24,12 +29,11 @@ enum FastDeploy_AgentUpdateStrategy {
FastDeploy_AgentUpdateDifferentVersion
};
void fastdeploy_set_local_agent(bool use_localagent);
void fastdeploy_set_agent_update_strategy(FastDeploy_AgentUpdateStrategy agent_update_strategy);
int get_device_api_level();
void update_agent(FastDeploy_AgentUpdateStrategy agentUpdateStrategy);
void extract_metadata(const char* apkPath, FILE* outputFp);
void create_patch(const char* apkPath, const char* metadataPath, const char* patchPath);
void apply_patch_on_device(const char* apkPath, const char* patchPath, const char* outputPath);
void install_patch(const char* apkPath, const char* patchPath, int argc, const char** argv);
std::string get_patch_path(const char* apkPath);
bool find_package(const char* apkPath);
std::optional<com::android::fastdeploy::APKMetaData> extract_metadata(const char* apk_path);
unique_fd install_patch(int argc, const char** argv);
unique_fd apply_patch_on_device(const char* output_path);
int stream_patch(const char* apk_path, com::android::fastdeploy::APKMetaData metadata,
unique_fd patch_fd);

View file

@ -49,35 +49,7 @@ class DeployAgentBufferCallback : public StandardStreamsCallbackInterface {
int capture_shell_command(const char* command, std::vector<char>* outBuffer,
std::vector<char>* errBuffer) {
DeployAgentBufferCallback cb(outBuffer, errBuffer);
return send_shell_command(command, false, &cb);
}
DeployAgentFileCallback::DeployAgentFileCallback(FILE* outputFile, std::vector<char>* errBuffer) {
mpOutFile = outputFile;
mpErrBuffer = errBuffer;
mBytesWritten = 0;
}
void DeployAgentFileCallback::OnStdout(const char* buffer, int length) {
if (mpOutFile != NULL) {
int bytes_written = fwrite(buffer, 1, length, mpOutFile);
if (bytes_written != length) {
printf("Write error %d\n", bytes_written);
}
mBytesWritten += bytes_written;
}
}
void DeployAgentFileCallback::OnStderr(const char* buffer, int length) {
appendBuffer(mpErrBuffer, buffer, length);
}
int DeployAgentFileCallback::Done(int status) {
return status;
}
int DeployAgentFileCallback::getBytesWritten() {
return mBytesWritten;
return send_shell_command(command, /*disable_shell_protocol=*/false, &cb);
}
DeployAgentBufferCallback::DeployAgentBufferCallback(std::vector<char>* outBuffer,

View file

@ -17,23 +17,6 @@
#pragma once
#include <vector>
#include "commandline.h"
class DeployAgentFileCallback : public StandardStreamsCallbackInterface {
public:
DeployAgentFileCallback(FILE* outputFile, std::vector<char>* errBuffer);
virtual void OnStdout(const char* buffer, int length);
virtual void OnStderr(const char* buffer, int length);
virtual int Done(int status);
int getBytesWritten();
private:
FILE* mpOutFile;
std::vector<char>* mpErrBuffer;
int mBytesWritten;
};
int capture_shell_command(const char* command, std::vector<char>* outBuffer,
std::vector<char>* errBuffer);

View file

@ -13,15 +13,76 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
java_binary {
name: "deployagent",
java_library {
name: "deployagent_lib",
sdk_version: "24",
srcs: ["deployagent/src/**/*.java", "deploylib/src/**/*.java", "proto/**/*.proto"],
static_libs: ["apkzlib_zip"],
srcs: [
"deployagent/src/**/*.java",
"proto/**/*.proto",
],
proto: {
type: "lite",
},
}
java_binary {
name: "deployagent",
static_libs: [
"deployagent_lib",
],
dex_preopt: {
enabled: false,
}
}
}
android_test {
name: "FastDeployTests",
manifest: "AndroidManifest.xml",
srcs: [
"deployagent/test/com/android/fastdeploy/ApkArchiveTest.java",
],
static_libs: [
"androidx.test.core",
"androidx.test.runner",
"androidx.test.rules",
"deployagent_lib",
"mockito-target-inline-minus-junit4",
],
libs: [
"android.test.runner",
"android.test.base",
"android.test.mock",
],
data: [
"testdata/sample.apk",
"testdata/sample.cd",
],
optimize: {
enabled: false,
},
}
java_test_host {
name: "FastDeployHostTests",
srcs: [
"deployagent/test/com/android/fastdeploy/FastDeployTest.java",
],
data: [
"testdata/helloworld5.apk",
"testdata/helloworld7.apk",
],
libs: [
"compatibility-host-util",
"cts-tradefed",
"tradefed",
],
test_suites: [
"general-tests",
],
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.fastdeploytests">
<application android:testOnly="true"
android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.fastdeploytests"
android:label="FastDeploy Tests" />
</manifest>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<configuration description="Runs Device Tests for FastDeploy.">
<option name="test-suite-tag" value="FastDeployTests"/>
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true"/>
<option name="install-arg" value="-t"/>
<option name="test-file-name" value="FastDeployTests.apk"/>
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
<option name="cleanup" value="false" />
<option name="push-file" key="sample.apk" value="/data/local/tmp/FastDeployTests/sample.apk" />
<option name="push-file" key="sample.cd" value="/data/local/tmp/FastDeployTests/sample.cd" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="package" value="com.android.fastdeploytests"/>
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
</test>
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
<option name="jar" value="FastDeployHostTests.jar" />
</test>
</configuration>

View file

@ -0,0 +1,193 @@
/*
* 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.
*/
package com.android.fastdeploy;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
/**
* Extremely light-weight APK parser class.
* Aware of Central Directory, Local File Headers and Signature.
* No Zip64 support yet.
*/
public final class ApkArchive {
private static final String TAG = "ApkArchive";
// Central Directory constants.
private static final int EOCD_SIGNATURE = 0x06054b50;
private static final int EOCD_MIN_SIZE = 22;
private static final long EOCD_MAX_SIZE = 65_535L + EOCD_MIN_SIZE;
private static final int CD_ENTRY_HEADER_SIZE_BYTES = 22;
private static final int CD_LOCAL_FILE_HEADER_SIZE_OFFSET = 12;
// Signature constants.
private static final int EOSIGNATURE_SIZE = 24;
public final static class Dump {
final byte[] cd;
final byte[] signature;
Dump(byte[] cd, byte[] signature) {
this.cd = cd;
this.signature = signature;
}
}
final static class Location {
final long offset;
final long size;
public Location(long offset, long size) {
this.offset = offset;
this.size = size;
}
}
private final RandomAccessFile mFile;
private final FileChannel mChannel;
public ApkArchive(File apk) throws IOException {
mFile = new RandomAccessFile(apk, "r");
mChannel = mFile.getChannel();
}
/**
* Extract the APK metadata: content of Central Directory and Signature.
*
* @return raw content from APK representing CD and Signature data.
*/
public Dump extractMetadata() throws IOException {
Location cdLoc = getCDLocation();
byte[] cd = readMetadata(cdLoc);
byte[] signature = null;
Location sigLoc = getSignatureLocation(cdLoc.offset);
if (sigLoc != null) {
signature = readMetadata(sigLoc);
long size = ByteBuffer.wrap(signature).order(ByteOrder.LITTLE_ENDIAN).getLong();
if (sigLoc.size != size) {
Log.e(TAG, "Mismatching signature sizes: " + sigLoc.size + " != " + size);
signature = null;
}
}
return new Dump(cd, signature);
}
private long findEndOfCDRecord() throws IOException {
final long fileSize = mChannel.size();
int sizeToRead = Math.toIntExact(Math.min(fileSize, EOCD_MAX_SIZE));
final long readOffset = fileSize - sizeToRead;
ByteBuffer buffer = mChannel.map(FileChannel.MapMode.READ_ONLY, readOffset,
sizeToRead).order(ByteOrder.LITTLE_ENDIAN);
buffer.position(sizeToRead - EOCD_MIN_SIZE);
while (true) {
int signature = buffer.getInt(); // Read 4 bytes.
if (signature == EOCD_SIGNATURE) {
return readOffset + buffer.position() - 4;
}
if (buffer.position() == 4) {
break;
}
buffer.position(buffer.position() - Integer.BYTES - 1); // Backtrack 5 bytes.
}
return -1L;
}
private Location findCDRecord(ByteBuffer buf) {
if (buf.order() != ByteOrder.LITTLE_ENDIAN) {
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
}
if (buf.remaining() < CD_ENTRY_HEADER_SIZE_BYTES) {
throw new IllegalArgumentException(
"Input too short. Need at least " + CD_ENTRY_HEADER_SIZE_BYTES
+ " bytes, available: " + buf.remaining() + "bytes.");
}
int originalPosition = buf.position();
int recordSignature = buf.getInt();
if (recordSignature != EOCD_SIGNATURE) {
throw new IllegalArgumentException(
"Not a Central Directory record. Signature: 0x"
+ Long.toHexString(recordSignature & 0xffffffffL));
}
buf.position(originalPosition + CD_LOCAL_FILE_HEADER_SIZE_OFFSET);
long size = buf.getInt() & 0xffffffffL;
long offset = buf.getInt() & 0xffffffffL;
return new Location(offset, size);
}
// Retrieve the location of the Central Directory Record.
Location getCDLocation() throws IOException {
long eocdRecord = findEndOfCDRecord();
if (eocdRecord < 0) {
throw new IllegalArgumentException("Unable to find End of Central Directory record.");
}
Location location = findCDRecord(mChannel.map(FileChannel.MapMode.READ_ONLY, eocdRecord,
CD_ENTRY_HEADER_SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN));
if (location == null) {
throw new IllegalArgumentException("Unable to find Central Directory File Header.");
}
return location;
}
// Retrieve the location of the signature block starting from Central
// Directory Record or null if signature is not found.
Location getSignatureLocation(long cdRecordOffset) throws IOException {
long signatureOffset = cdRecordOffset - EOSIGNATURE_SIZE;
if (signatureOffset < 0) {
Log.e(TAG, "Unable to find Signature.");
return null;
}
ByteBuffer signature = mChannel.map(FileChannel.MapMode.READ_ONLY, signatureOffset,
EOSIGNATURE_SIZE).order(ByteOrder.LITTLE_ENDIAN);
long size = signature.getLong();
byte[] sign = new byte[16];
signature.get(sign);
String signAsString = new String(sign);
if (!"APK Sig Block 42".equals(signAsString)) {
Log.e(TAG, "Signature magic does not match: " + signAsString);
return null;
}
long offset = cdRecordOffset - size - 8;
return new Location(offset, size);
}
private byte[] readMetadata(Location loc) throws IOException {
byte[] payload = new byte[(int) loc.size];
ByteBuffer buffer = mChannel.map(FileChannel.MapMode.READ_ONLY, loc.offset, loc.size);
buffer.get(payload);
return payload;
}
}

View file

@ -24,18 +24,22 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.Set;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import com.android.fastdeploy.PatchFormatException;
import com.android.fastdeploy.ApkArchive;
import com.android.fastdeploy.APKDump;
import com.android.fastdeploy.APKMetaData;
import com.android.fastdeploy.PatchUtils;
import com.google.protobuf.ByteString;
public final class DeployAgent {
private static final int BUFFER_SIZE = 128 * 1024;
private static final int AGENT_VERSION = 0x00000002;
private static final int AGENT_VERSION = 0x00000003;
public static void main(String[] args) {
int exitCode = 0;
@ -45,68 +49,70 @@ public final class DeployAgent {
}
String commandString = args[0];
switch (commandString) {
case "dump": {
if (args.length != 3) {
showUsage(1);
}
if (commandString.equals("extract")) {
if (args.length != 2) {
showUsage(1);
}
String packageName = args[1];
extractMetaData(packageName);
} else if (commandString.equals("find")) {
if (args.length != 2) {
showUsage(1);
}
String packageName = args[1];
if (getFilenameFromPackageName(packageName) == null) {
exitCode = 3;
}
} else if (commandString.equals("apply")) {
if (args.length < 4) {
showUsage(1);
}
String packageName = args[1];
String patchPath = args[2];
String outputParam = args[3];
InputStream deltaInputStream = null;
if (patchPath.compareTo("-") == 0) {
deltaInputStream = System.in;
} else {
deltaInputStream = new FileInputStream(patchPath);
}
if (outputParam.equals("-o")) {
OutputStream outputStream = null;
if (args.length > 4) {
String outputPath = args[4];
if (!outputPath.equals("-")) {
outputStream = new FileOutputStream(outputPath);
String requiredVersion = args[1];
if (AGENT_VERSION == Integer.parseInt(requiredVersion)) {
String packageName = args[2];
String packagePath = getFilenameFromPackageName(packageName);
if (packagePath != null) {
dumpApk(packageName, packagePath);
} else {
exitCode = 3;
}
} else {
System.out.printf("0x%08X\n", AGENT_VERSION);
exitCode = 4;
}
if (outputStream == null) {
outputStream = System.out;
}
File deviceFile = getFileFromPackageName(packageName);
writePatchToStream(
new RandomAccessFile(deviceFile, "r"), deltaInputStream, outputStream);
} else if (outputParam.equals("-pm")) {
String[] sessionArgs = null;
if (args.length > 4) {
int numSessionArgs = args.length-4;
sessionArgs = new String[numSessionArgs];
for (int i=0 ; i<numSessionArgs ; i++) {
sessionArgs[i] = args[i+4];
}
}
exitCode = applyPatch(packageName, deltaInputStream, sessionArgs);
break;
}
} else if (commandString.equals("version")) {
System.out.printf("0x%08X\n", AGENT_VERSION);
} else {
showUsage(1);
case "apply": {
if (args.length < 3) {
showUsage(1);
}
String patchPath = args[1];
String outputParam = args[2];
InputStream deltaInputStream = null;
if (patchPath.compareTo("-") == 0) {
deltaInputStream = System.in;
} else {
deltaInputStream = new FileInputStream(patchPath);
}
if (outputParam.equals("-o")) {
OutputStream outputStream = null;
if (args.length > 3) {
String outputPath = args[3];
if (!outputPath.equals("-")) {
outputStream = new FileOutputStream(outputPath);
}
}
if (outputStream == null) {
outputStream = System.out;
}
writePatchToStream(deltaInputStream, outputStream);
} else if (outputParam.equals("-pm")) {
String[] sessionArgs = null;
if (args.length > 3) {
int numSessionArgs = args.length - 3;
sessionArgs = new String[numSessionArgs];
for (int i = 0; i < numSessionArgs; i++) {
sessionArgs[i] = args[i + 3];
}
}
exitCode = applyPatch(deltaInputStream, sessionArgs);
}
break;
}
default:
showUsage(1);
break;
}
} catch (Exception e) {
System.err.println("Error: " + e);
@ -118,16 +124,16 @@ public final class DeployAgent {
private static void showUsage(int exitCode) {
System.err.println(
"usage: deployagent <command> [<args>]\n\n" +
"commands:\n" +
"version get the version\n" +
"find PKGNAME return zero if package found, else non-zero\n" +
"extract PKGNAME extract an installed package's metadata\n" +
"apply PKGNAME PATCHFILE [-o|-pm] apply a patch from PATCHFILE (- for stdin) to an installed package\n" +
" -o <FILE> directs output to FILE, default or - for stdout\n" +
" -pm <ARGS> directs output to package manager, passes <ARGS> to 'pm install-create'\n"
);
"usage: deployagent <command> [<args>]\n\n" +
"commands:\n" +
"dump VERSION PKGNAME dump info for an installed package given that " +
"VERSION equals current agent's version\n" +
"apply PATCHFILE [-o|-pm] apply a patch from PATCHFILE " +
"(- for stdin) to an installed package\n" +
" -o <FILE> directs output to FILE, default or - for stdout\n" +
" -pm <ARGS> directs output to package manager, passes <ARGS> to " +
"'pm install-create'\n"
);
System.exit(exitCode);
}
@ -162,32 +168,34 @@ public final class DeployAgent {
}
int equalsIndex = line.lastIndexOf(packageSuffix);
String fileName =
line.substring(packageIndex + packagePrefix.length(), equalsIndex);
line.substring(packageIndex + packagePrefix.length(), equalsIndex);
return fileName;
}
}
return null;
}
private static File getFileFromPackageName(String packageName) throws IOException {
String filename = getFilenameFromPackageName(packageName);
if (filename == null) {
// Should not happen (function is only called when we know the package exists)
throw new IOException("package not found");
}
return new File(filename);
}
private static void dumpApk(String packageName, String packagePath) throws IOException {
File apk = new File(packagePath);
ApkArchive.Dump dump = new ApkArchive(apk).extractMetadata();
private static void extractMetaData(String packageName) throws IOException {
File apkFile = getFileFromPackageName(packageName);
APKMetaData apkMetaData = PatchUtils.getAPKMetaData(apkFile);
apkMetaData.writeTo(System.out);
APKDump.Builder apkDumpBuilder = APKDump.newBuilder();
apkDumpBuilder.setName(packageName);
if (dump.cd != null) {
apkDumpBuilder.setCd(ByteString.copyFrom(dump.cd));
}
if (dump.signature != null) {
apkDumpBuilder.setSignature(ByteString.copyFrom(dump.signature));
}
apkDumpBuilder.setAbsolutePath(apk.getAbsolutePath());
apkDumpBuilder.build().writeTo(System.out);
}
private static int createInstallSession(String[] args) throws IOException {
StringBuilder commandBuilder = new StringBuilder();
commandBuilder.append("pm install-create ");
for (int i=0 ; args != null && i<args.length ; i++) {
for (int i = 0; args != null && i < args.length; i++) {
commandBuilder.append(args[i] + " ");
}
@ -199,7 +207,8 @@ public final class DeployAgent {
String successLineEnd = "]";
while ((line = reader.readLine()) != null) {
if (line.startsWith(successLineStart) && line.endsWith(successLineEnd)) {
return Integer.parseInt(line.substring(successLineStart.length(), line.lastIndexOf(successLineEnd)));
return Integer.parseInt(line.substring(successLineStart.length(),
line.lastIndexOf(successLineEnd)));
}
}
@ -213,16 +222,15 @@ public final class DeployAgent {
return p.exitValue();
}
private static int applyPatch(String packageName, InputStream deltaStream, String[] sessionArgs)
private static int applyPatch(InputStream deltaStream, String[] sessionArgs)
throws IOException, PatchFormatException {
File deviceFile = getFileFromPackageName(packageName);
int sessionId = createInstallSession(sessionArgs);
if (sessionId < 0) {
System.err.println("PM Create Session Failed");
return -1;
}
int writeExitCode = writePatchedDataToSession(new RandomAccessFile(deviceFile, "r"), deltaStream, sessionId);
int writeExitCode = writePatchedDataToSession(deltaStream, sessionId);
if (writeExitCode == 0) {
return commitInstallSession(sessionId);
} else {
@ -230,84 +238,94 @@ public final class DeployAgent {
}
}
private static long writePatchToStream(RandomAccessFile oldData, InputStream patchData,
OutputStream outputStream) throws IOException, PatchFormatException {
private static long writePatchToStream(InputStream patchData,
OutputStream outputStream) throws IOException, PatchFormatException {
long newSize = readPatchHeader(patchData);
long bytesWritten = writePatchedDataToStream(oldData, newSize, patchData, outputStream);
long bytesWritten = writePatchedDataToStream(newSize, patchData, outputStream);
outputStream.flush();
if (bytesWritten != newSize) {
throw new PatchFormatException(String.format(
"output size mismatch (expected %ld but wrote %ld)", newSize, bytesWritten));
"output size mismatch (expected %ld but wrote %ld)", newSize, bytesWritten));
}
return bytesWritten;
}
private static long readPatchHeader(InputStream patchData)
throws IOException, PatchFormatException {
throws IOException, PatchFormatException {
byte[] signatureBuffer = new byte[PatchUtils.SIGNATURE.length()];
try {
PatchUtils.readFully(patchData, signatureBuffer, 0, signatureBuffer.length);
PatchUtils.readFully(patchData, signatureBuffer);
} catch (IOException e) {
throw new PatchFormatException("truncated signature");
}
String signature = new String(signatureBuffer, 0, signatureBuffer.length, "US-ASCII");
String signature = new String(signatureBuffer);
if (!PatchUtils.SIGNATURE.equals(signature)) {
throw new PatchFormatException("bad signature");
}
long newSize = PatchUtils.readBsdiffLong(patchData);
if (newSize < 0 || newSize > Integer.MAX_VALUE) {
throw new PatchFormatException("bad newSize");
long newSize = PatchUtils.readLELong(patchData);
if (newSize < 0) {
throw new PatchFormatException("bad newSize: " + newSize);
}
return newSize;
}
// Note that this function assumes patchData has been seek'ed to the start of the delta stream
// (i.e. the signature has already been read by readPatchHeader). For a stream that points to the
// start of a patch file call writePatchToStream
private static long writePatchedDataToStream(RandomAccessFile oldData, long newSize,
InputStream patchData, OutputStream outputStream) throws IOException {
// (i.e. the signature has already been read by readPatchHeader). For a stream that points to
// the start of a patch file call writePatchToStream
private static long writePatchedDataToStream(long newSize, InputStream patchData,
OutputStream outputStream) throws IOException {
String deviceFile = PatchUtils.readString(patchData);
RandomAccessFile oldDataFile = new RandomAccessFile(deviceFile, "r");
FileChannel oldData = oldDataFile.getChannel();
WritableByteChannel newData = Channels.newChannel(outputStream);
long newDataBytesWritten = 0;
byte[] buffer = new byte[BUFFER_SIZE];
while (newDataBytesWritten < newSize) {
long copyLen = PatchUtils.readFormattedLong(patchData);
if (copyLen > 0) {
PatchUtils.pipe(patchData, outputStream, buffer, (int) copyLen);
long newDataLen = PatchUtils.readLELong(patchData);
if (newDataLen > 0) {
PatchUtils.pipe(patchData, outputStream, buffer, newDataLen);
}
long oldDataOffset = PatchUtils.readFormattedLong(patchData);
long oldDataLen = PatchUtils.readFormattedLong(patchData);
oldData.seek(oldDataOffset);
if (oldDataLen > 0) {
PatchUtils.pipe(oldData, outputStream, buffer, (int) oldDataLen);
long oldDataOffset = PatchUtils.readLELong(patchData);
long oldDataLen = PatchUtils.readLELong(patchData);
if (oldDataLen >= 0) {
long offset = oldDataOffset;
long len = oldDataLen;
while (len > 0) {
long chunkLen = Math.min(len, 1024*1024*1024);
oldData.transferTo(offset, chunkLen, newData);
offset += chunkLen;
len -= chunkLen;
}
}
newDataBytesWritten += copyLen + oldDataLen;
newDataBytesWritten += newDataLen + oldDataLen;
}
return newDataBytesWritten;
}
private static int writePatchedDataToSession(RandomAccessFile oldData, InputStream patchData, int sessionId)
private static int writePatchedDataToSession(InputStream patchData, int sessionId)
throws IOException, PatchFormatException {
try {
Process p;
long newSize = readPatchHeader(patchData);
StringBuilder commandBuilder = new StringBuilder();
commandBuilder.append(String.format("pm install-write -S %d %d -- -", newSize, sessionId));
String command = commandBuilder.toString();
String command = String.format("pm install-write -S %d %d -- -", newSize, sessionId);
p = Runtime.getRuntime().exec(command);
OutputStream sessionOutputStream = p.getOutputStream();
long bytesWritten = writePatchedDataToStream(oldData, newSize, patchData, sessionOutputStream);
long bytesWritten = writePatchedDataToStream(newSize, patchData, sessionOutputStream);
sessionOutputStream.flush();
p.waitFor();
if (bytesWritten != newSize) {
throw new PatchFormatException(
String.format("output size mismatch (expected %d but wrote %)", newSize, bytesWritten));
String.format("output size mismatch (expected %d but wrote %)", newSize,
bytesWritten));
}
return p.exitValue();
} catch (InterruptedException e) {

View file

@ -0,0 +1,74 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.fastdeploy;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
class PatchUtils {
public static final String SIGNATURE = "FASTDEPLOY";
/**
* Reads a 64-bit signed integer in Little Endian format from the specified {@link
* DataInputStream}.
*
* @param in the stream to read from.
*/
static long readLELong(InputStream in) throws IOException {
byte[] buffer = new byte[Long.BYTES];
readFully(in, buffer);
ByteBuffer buf = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
return buf.getLong();
}
static String readString(InputStream in) throws IOException {
int size = (int) readLELong(in);
byte[] buffer = new byte[size];
readFully(in, buffer);
return new String(buffer);
}
static void readFully(final InputStream in, final byte[] destination, final int startAt,
final int numBytes) throws IOException {
int numRead = 0;
while (numRead < numBytes) {
int readNow = in.read(destination, startAt + numRead, numBytes - numRead);
if (readNow == -1) {
throw new IOException("truncated input stream");
}
numRead += readNow;
}
}
static void readFully(final InputStream in, final byte[] destination) throws IOException {
readFully(in, destination, 0, destination.length);
}
static void pipe(final InputStream in, final OutputStream out, final byte[] buffer,
long copyLength) throws IOException {
while (copyLength > 0) {
int maxCopy = (int) Math.min(buffer.length, copyLength);
readFully(in, buffer, 0, maxCopy);
out.write(buffer, 0, maxCopy);
copyLength -= maxCopy;
}
}
}

View file

@ -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.
*/
package com.android.fastdeploy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.android.fastdeploy.ApkArchive;
import java.io.File;
import java.io.IOException;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ApkArchiveTest {
private static final File SAMPLE_APK = new File("/data/local/tmp/FastDeployTests/sample.apk");
private static final File WRONG_APK = new File("/data/local/tmp/FastDeployTests/sample.cd");
@Test
public void testApkArchiveSizes() throws IOException {
ApkArchive archive = new ApkArchive(SAMPLE_APK);
ApkArchive.Location cdLoc = archive.getCDLocation();
assertNotEquals(cdLoc, null);
assertEquals(cdLoc.offset, 2044145);
assertEquals(cdLoc.size, 49390);
// Check that block can be retrieved
ApkArchive.Location sigLoc = archive.getSignatureLocation(cdLoc.offset);
assertNotEquals(sigLoc, null);
assertEquals(sigLoc.offset, 2040049);
assertEquals(sigLoc.size, 4088);
}
@Test
public void testApkArchiveDump() throws IOException {
ApkArchive archive = new ApkArchive(SAMPLE_APK);
ApkArchive.Dump dump = archive.extractMetadata();
assertNotEquals(dump, null);
assertNotEquals(dump.cd, null);
assertNotEquals(dump.signature, null);
assertEquals(dump.cd.length, 49390);
assertEquals(dump.signature.length, 4088);
}
@Test(expected = IllegalArgumentException.class)
public void testApkArchiveDumpWrongApk() throws IOException {
ApkArchive archive = new ApkArchive(WRONG_APK);
archive.extractMetadata();
}
}

View file

@ -0,0 +1,85 @@
/*
* 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.
*/
package com.android.fastdeploy;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.Arrays;
@RunWith(DeviceJUnit4ClassRunner.class)
public class FastDeployTest extends BaseHostJUnit4Test {
private static final String TEST_APP_PACKAGE = "com.example.helloworld";
private static final String TEST_APK5_NAME = "helloworld5.apk";
private static final String TEST_APK7_NAME = "helloworld7.apk";
private String mTestApk5Path;
private String mTestApk7Path;
@Before
public void setUp() throws Exception {
CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
getDevice().uninstallPackage(TEST_APP_PACKAGE);
mTestApk5Path = buildHelper.getTestFile(TEST_APK5_NAME).getAbsolutePath();
mTestApk7Path = buildHelper.getTestFile(TEST_APK7_NAME).getAbsolutePath();
}
@Test
public void testAppInstalls() throws Exception {
fastInstallPackage(mTestApk5Path);
assertTrue(isAppInstalled(TEST_APP_PACKAGE));
getDevice().uninstallPackage(TEST_APP_PACKAGE);
assertFalse(isAppInstalled(TEST_APP_PACKAGE));
}
@Test
public void testAppPatch() throws Exception {
fastInstallPackage(mTestApk5Path);
assertTrue(isAppInstalled(TEST_APP_PACKAGE));
fastInstallPackage(mTestApk7Path);
assertTrue(isAppInstalled(TEST_APP_PACKAGE));
getDevice().uninstallPackage(TEST_APP_PACKAGE);
assertFalse(isAppInstalled(TEST_APP_PACKAGE));
}
private boolean isAppInstalled(String packageName) throws DeviceNotAvailableException {
final String commandResult = getDevice().executeShellCommand("pm list packages");
final int prefixLength = "package:".length();
return Arrays.stream(commandResult.split("\\r?\\n"))
.anyMatch(line -> line.substring(prefixLength).equals(packageName));
}
// Mostly copied from PkgInstallSignatureVerificationTest.java.
private String fastInstallPackage(String apkPath)
throws IOException, DeviceNotAvailableException {
return getDevice().executeAdbCommand("install", "-t", "--fastdeploy", "--force-agent",
apkPath);
}
}

View file

@ -1,186 +0,0 @@
/*
* 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.
*/
package com.android.fastdeploy;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import com.android.tools.build.apkzlib.zip.ZFile;
import com.android.tools.build.apkzlib.zip.ZFileOptions;
import com.android.tools.build.apkzlib.zip.StoredEntry;
import com.android.tools.build.apkzlib.zip.StoredEntryType;
import com.android.tools.build.apkzlib.zip.CentralDirectoryHeaderCompressInfo;
import com.android.tools.build.apkzlib.zip.CentralDirectoryHeader;
import com.android.fastdeploy.APKMetaData;
import com.android.fastdeploy.APKEntry;
class PatchUtils {
private static final long NEGATIVE_MASK = 1L << 63;
private static final long NEGATIVE_LONG_SIGN_MASK = 1L << 63;
public static final String SIGNATURE = "FASTDEPLOY";
private static long getOffsetFromEntry(StoredEntry entry) {
return entry.getCentralDirectoryHeader().getOffset() + entry.getLocalHeaderSize();
}
public static APKMetaData getAPKMetaData(File apkFile) throws IOException {
APKMetaData.Builder apkEntriesBuilder = APKMetaData.newBuilder();
ZFileOptions options = new ZFileOptions();
ZFile zFile = new ZFile(apkFile, options);
ArrayList<StoredEntry> metaDataEntries = new ArrayList<StoredEntry>();
for (StoredEntry entry : zFile.entries()) {
if (entry.getType() != StoredEntryType.FILE) {
continue;
}
metaDataEntries.add(entry);
}
Collections.sort(metaDataEntries, new Comparator<StoredEntry>() {
private long getOffsetFromEntry(StoredEntry entry) {
return PatchUtils.getOffsetFromEntry(entry);
}
@Override
public int compare(StoredEntry lhs, StoredEntry rhs) {
// -1 - less than, 1 - greater than, 0 - equal, all inversed for descending
return Long.compare(getOffsetFromEntry(lhs), getOffsetFromEntry(rhs));
}
});
for (StoredEntry entry : metaDataEntries) {
CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader();
CentralDirectoryHeaderCompressInfo cdhci = cdh.getCompressionInfoWithWait();
APKEntry.Builder entryBuilder = APKEntry.newBuilder();
entryBuilder.setCrc32(cdh.getCrc32());
entryBuilder.setFileName(cdh.getName());
entryBuilder.setCompressedSize(cdhci.getCompressedSize());
entryBuilder.setUncompressedSize(cdh.getUncompressedSize());
entryBuilder.setDataOffset(getOffsetFromEntry(entry));
apkEntriesBuilder.addEntries(entryBuilder);
apkEntriesBuilder.build();
}
return apkEntriesBuilder.build();
}
/**
* Writes a 64-bit signed integer to the specified {@link OutputStream}. The least significant
* byte is written first and the most significant byte is written last.
* @param value the value to write
* @param outputStream the stream to write to
*/
static void writeFormattedLong(final long value, OutputStream outputStream) throws IOException {
long y = value;
if (y < 0) {
y = (-y) | NEGATIVE_MASK;
}
for (int i = 0; i < 8; ++i) {
outputStream.write((byte) (y & 0xff));
y >>>= 8;
}
}
/**
* Reads a 64-bit signed integer written by {@link #writeFormattedLong(long, OutputStream)} from
* the specified {@link InputStream}.
* @param inputStream the stream to read from
*/
static long readFormattedLong(InputStream inputStream) throws IOException {
long result = 0;
for (int bitshift = 0; bitshift < 64; bitshift += 8) {
result |= ((long) inputStream.read()) << bitshift;
}
if ((result - NEGATIVE_MASK) > 0) {
result = (result & ~NEGATIVE_MASK) * -1;
}
return result;
}
static final long readBsdiffLong(InputStream in) throws PatchFormatException, IOException {
long result = 0;
for (int bitshift = 0; bitshift < 64; bitshift += 8) {
result |= ((long) in.read()) << bitshift;
}
if (result == NEGATIVE_LONG_SIGN_MASK) {
// "Negative zero", which is valid in signed-magnitude format.
// NB: No sane patch generator should ever produce such a value.
throw new PatchFormatException("read negative zero");
}
if ((result & NEGATIVE_LONG_SIGN_MASK) != 0) {
result = -(result & ~NEGATIVE_LONG_SIGN_MASK);
}
return result;
}
static void readFully(final InputStream in, final byte[] destination, final int startAt,
final int numBytes) throws IOException {
int numRead = 0;
while (numRead < numBytes) {
int readNow = in.read(destination, startAt + numRead, numBytes - numRead);
if (readNow == -1) {
throw new IOException("truncated input stream");
}
numRead += readNow;
}
}
static void pipe(final InputStream in, final OutputStream out, final byte[] buffer,
long copyLength) throws IOException {
while (copyLength > 0) {
int maxCopy = Math.min(buffer.length, (int) copyLength);
readFully(in, buffer, 0, maxCopy);
out.write(buffer, 0, maxCopy);
copyLength -= maxCopy;
}
}
static void pipe(final RandomAccessFile in, final OutputStream out, final byte[] buffer,
long copyLength) throws IOException {
while (copyLength > 0) {
int maxCopy = Math.min(buffer.length, (int) copyLength);
in.readFully(buffer, 0, maxCopy);
out.write(buffer, 0, maxCopy);
copyLength -= maxCopy;
}
}
static void fill(byte value, final OutputStream out, final byte[] buffer, long fillLength)
throws IOException {
while (fillLength > 0) {
int maxCopy = Math.min(buffer.length, (int) fillLength);
Arrays.fill(buffer, 0, maxCopy, value);
out.write(buffer, 0, maxCopy);
fillLength -= maxCopy;
}
}
}

View file

@ -0,0 +1,415 @@
/*
* 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.
*/
#define TRACE_TAG ADB
#include "apk_archive.h"
#include "adb_trace.h"
#include "sysdeps.h"
#include <android-base/endian.h>
#include <android-base/mapped_file.h>
#include <openssl/md5.h>
constexpr uint16_t kCompressStored = 0;
// mask value that signifies that the entry has a DD
static const uint32_t kGPBDDFlagMask = 0x0008;
namespace {
struct FileRegion {
FileRegion(borrowed_fd fd, off64_t offset, size_t length)
: mapped_(android::base::MappedFile::FromOsHandle(adb_get_os_handle(fd), offset, length,
PROT_READ)) {
if (mapped_.data() != nullptr) {
return;
}
// Mapped file failed, falling back to pread.
buffer_.resize(length);
if (auto err = adb_pread(fd.get(), buffer_.data(), length, offset); size_t(err) != length) {
fprintf(stderr, "Unable to read %lld bytes at offset %" PRId64 " \n",
static_cast<long long>(length), offset);
buffer_.clear();
return;
}
}
const char* data() const { return mapped_.data() ? mapped_.data() : buffer_.data(); }
size_t size() const { return mapped_.data() ? mapped_.size() : buffer_.size(); }
private:
FileRegion() = default;
DISALLOW_COPY_AND_ASSIGN(FileRegion);
android::base::MappedFile mapped_;
std::string buffer_;
};
} // namespace
using com::android::fastdeploy::APKDump;
ApkArchive::ApkArchive(const std::string& path) : path_(path), size_(0) {
fd_.reset(adb_open(path_.c_str(), O_RDONLY));
if (fd_ == -1) {
fprintf(stderr, "Unable to open file '%s'\n", path_.c_str());
return;
}
struct stat st;
if (stat(path_.c_str(), &st) == -1) {
fprintf(stderr, "Unable to stat file '%s'\n", path_.c_str());
return;
}
size_ = st.st_size;
}
ApkArchive::~ApkArchive() {}
APKDump ApkArchive::ExtractMetadata() {
D("ExtractMetadata");
if (!ready()) {
return {};
}
Location cdLoc = GetCDLocation();
if (!cdLoc.valid) {
return {};
}
APKDump dump;
dump.set_absolute_path(path_);
dump.set_cd(ReadMetadata(cdLoc));
Location sigLoc = GetSignatureLocation(cdLoc.offset);
if (sigLoc.valid) {
dump.set_signature(ReadMetadata(sigLoc));
}
return dump;
}
off_t ApkArchive::FindEndOfCDRecord() const {
constexpr int endOfCDSignature = 0x06054b50;
constexpr off_t endOfCDMinSize = 22;
constexpr off_t endOfCDMaxSize = 65535 + endOfCDMinSize;
auto sizeToRead = std::min(size_, endOfCDMaxSize);
auto readOffset = size_ - sizeToRead;
FileRegion mapped(fd_, readOffset, sizeToRead);
// Start scanning from the end
auto* start = mapped.data();
auto* cursor = start + mapped.size() - sizeof(endOfCDSignature);
// Search for End of Central Directory record signature.
while (cursor >= start) {
if (*(int32_t*)cursor == endOfCDSignature) {
return readOffset + (cursor - start);
}
cursor--;
}
return -1;
}
ApkArchive::Location ApkArchive::FindCDRecord(const char* cursor) {
struct ecdr_t {
int32_t signature;
uint16_t diskNumber;
uint16_t numDisk;
uint16_t diskEntries;
uint16_t numEntries;
uint32_t crSize;
uint32_t offsetToCdHeader;
uint16_t commentSize;
uint8_t comment[0];
} __attribute__((packed));
ecdr_t* header = (ecdr_t*)cursor;
Location location;
location.offset = header->offsetToCdHeader;
location.size = header->crSize;
location.valid = true;
return location;
}
ApkArchive::Location ApkArchive::GetCDLocation() {
constexpr off_t cdEntryHeaderSizeBytes = 22;
Location location;
// Find End of Central Directory Record
off_t eocdRecord = FindEndOfCDRecord();
if (eocdRecord < 0) {
fprintf(stderr, "Unable to find End of Central Directory record in file '%s'\n",
path_.c_str());
return location;
}
// Find Central Directory Record
FileRegion mapped(fd_, eocdRecord, cdEntryHeaderSizeBytes);
location = FindCDRecord(mapped.data());
if (!location.valid) {
fprintf(stderr, "Unable to find Central Directory File Header in file '%s'\n",
path_.c_str());
return location;
}
return location;
}
ApkArchive::Location ApkArchive::GetSignatureLocation(off_t cdRecordOffset) {
Location location;
// Signature constants.
constexpr off_t endOfSignatureSize = 24;
off_t signatureOffset = cdRecordOffset - endOfSignatureSize;
if (signatureOffset < 0) {
fprintf(stderr, "Unable to find signature in file '%s'\n", path_.c_str());
return location;
}
FileRegion mapped(fd_, signatureOffset, endOfSignatureSize);
uint64_t signatureSize = *(uint64_t*)mapped.data();
auto* signature = mapped.data() + sizeof(signatureSize);
// Check if there is a v2/v3 Signature block here.
if (memcmp(signature, "APK Sig Block 42", 16)) {
return location;
}
// This is likely a signature block.
location.size = signatureSize;
location.offset = cdRecordOffset - location.size - 8;
location.valid = true;
return location;
}
std::string ApkArchive::ReadMetadata(Location loc) const {
FileRegion mapped(fd_, loc.offset, loc.size);
return {mapped.data(), mapped.size()};
}
size_t ApkArchive::ParseCentralDirectoryRecord(const char* input, size_t size, std::string* md5Hash,
int64_t* localFileHeaderOffset, int64_t* dataSize) {
// A structure representing the fixed length fields for a single
// record in the central directory of the archive. In addition to
// the fixed length fields listed here, each central directory
// record contains a variable length "file_name" and "extra_field"
// whose lengths are given by |file_name_length| and |extra_field_length|
// respectively.
static constexpr int kCDFileHeaderMagic = 0x02014b50;
struct CentralDirectoryRecord {
// The start of record signature. Must be |kSignature|.
uint32_t record_signature;
// Source tool version. Top byte gives source OS.
uint16_t version_made_by;
// Tool version. Ignored by this implementation.
uint16_t version_needed;
// The "general purpose bit flags" for this entry. The only
// flag value that we currently check for is the "data descriptor"
// flag.
uint16_t gpb_flags;
// The compression method for this entry, one of |kCompressStored|
// and |kCompressDeflated|.
uint16_t compression_method;
// The file modification time and date for this entry.
uint16_t last_mod_time;
uint16_t last_mod_date;
// The CRC-32 checksum for this entry.
uint32_t crc32;
// The compressed size (in bytes) of this entry.
uint32_t compressed_size;
// The uncompressed size (in bytes) of this entry.
uint32_t uncompressed_size;
// The length of the entry file name in bytes. The file name
// will appear immediately after this record.
uint16_t file_name_length;
// The length of the extra field info (in bytes). This data
// will appear immediately after the entry file name.
uint16_t extra_field_length;
// The length of the entry comment (in bytes). This data will
// appear immediately after the extra field.
uint16_t comment_length;
// The start disk for this entry. Ignored by this implementation).
uint16_t file_start_disk;
// File attributes. Ignored by this implementation.
uint16_t internal_file_attributes;
// File attributes. For archives created on Unix, the top bits are the
// mode.
uint32_t external_file_attributes;
// The offset to the local file header for this entry, from the
// beginning of this archive.
uint32_t local_file_header_offset;
private:
CentralDirectoryRecord() = default;
DISALLOW_COPY_AND_ASSIGN(CentralDirectoryRecord);
} __attribute__((packed));
const CentralDirectoryRecord* cdr;
if (size < sizeof(*cdr)) {
return 0;
}
auto begin = input;
cdr = reinterpret_cast<const CentralDirectoryRecord*>(begin);
if (cdr->record_signature != kCDFileHeaderMagic) {
fprintf(stderr, "Invalid Central Directory Record signature\n");
return 0;
}
auto end = begin + sizeof(*cdr) + cdr->file_name_length + cdr->extra_field_length +
cdr->comment_length;
uint8_t md5Digest[MD5_DIGEST_LENGTH];
MD5((const unsigned char*)begin, end - begin, md5Digest);
md5Hash->assign((const char*)md5Digest, sizeof(md5Digest));
*localFileHeaderOffset = cdr->local_file_header_offset;
*dataSize = (cdr->compression_method == kCompressStored) ? cdr->uncompressed_size
: cdr->compressed_size;
return end - begin;
}
size_t ApkArchive::CalculateLocalFileEntrySize(int64_t localFileHeaderOffset,
int64_t dataSize) const {
// The local file header for a given entry. This duplicates information
// present in the central directory of the archive. It is an error for
// the information here to be different from the central directory
// information for a given entry.
static constexpr int kLocalFileHeaderMagic = 0x04034b50;
struct LocalFileHeader {
// The local file header signature, must be |kSignature|.
uint32_t lfh_signature;
// Tool version. Ignored by this implementation.
uint16_t version_needed;
// The "general purpose bit flags" for this entry. The only
// flag value that we currently check for is the "data descriptor"
// flag.
uint16_t gpb_flags;
// The compression method for this entry, one of |kCompressStored|
// and |kCompressDeflated|.
uint16_t compression_method;
// The file modification time and date for this entry.
uint16_t last_mod_time;
uint16_t last_mod_date;
// The CRC-32 checksum for this entry.
uint32_t crc32;
// The compressed size (in bytes) of this entry.
uint32_t compressed_size;
// The uncompressed size (in bytes) of this entry.
uint32_t uncompressed_size;
// The length of the entry file name in bytes. The file name
// will appear immediately after this record.
uint16_t file_name_length;
// The length of the extra field info (in bytes). This data
// will appear immediately after the entry file name.
uint16_t extra_field_length;
private:
LocalFileHeader() = default;
DISALLOW_COPY_AND_ASSIGN(LocalFileHeader);
} __attribute__((packed));
static constexpr int kLocalFileHeaderSize = sizeof(LocalFileHeader);
CHECK(ready()) << path_;
const LocalFileHeader* lfh;
if (localFileHeaderOffset + kLocalFileHeaderSize > size_) {
fprintf(stderr,
"Invalid Local File Header offset in file '%s' at offset %lld, file size %lld\n",
path_.c_str(), static_cast<long long>(localFileHeaderOffset),
static_cast<long long>(size_));
return 0;
}
FileRegion lfhMapped(fd_, localFileHeaderOffset, sizeof(LocalFileHeader));
lfh = reinterpret_cast<const LocalFileHeader*>(lfhMapped.data());
if (lfh->lfh_signature != kLocalFileHeaderMagic) {
fprintf(stderr, "Invalid Local File Header signature in file '%s' at offset %lld\n",
path_.c_str(), static_cast<long long>(localFileHeaderOffset));
return 0;
}
// The *optional* data descriptor start signature.
static constexpr int kOptionalDataDescriptorMagic = 0x08074b50;
struct DataDescriptor {
// CRC-32 checksum of the entry.
uint32_t crc32;
// Compressed size of the entry.
uint32_t compressed_size;
// Uncompressed size of the entry.
uint32_t uncompressed_size;
private:
DataDescriptor() = default;
DISALLOW_COPY_AND_ASSIGN(DataDescriptor);
} __attribute__((packed));
static constexpr int kDataDescriptorSize = sizeof(DataDescriptor);
off_t ddOffset = localFileHeaderOffset + kLocalFileHeaderSize + lfh->file_name_length +
lfh->extra_field_length + dataSize;
int64_t ddSize = 0;
int64_t localDataSize;
if (lfh->gpb_flags & kGPBDDFlagMask) {
// There is trailing data descriptor.
const DataDescriptor* dd;
if (ddOffset + int(sizeof(uint32_t)) > size_) {
fprintf(stderr,
"Error reading trailing data descriptor signature in file '%s' at offset %lld, "
"file size %lld\n",
path_.c_str(), static_cast<long long>(ddOffset), static_cast<long long>(size_));
return 0;
}
FileRegion ddMapped(fd_, ddOffset, sizeof(uint32_t) + sizeof(DataDescriptor));
off_t localDDOffset = 0;
if (kOptionalDataDescriptorMagic == *(uint32_t*)ddMapped.data()) {
ddOffset += sizeof(uint32_t);
localDDOffset += sizeof(uint32_t);
ddSize += sizeof(uint32_t);
}
if (ddOffset + kDataDescriptorSize > size_) {
fprintf(stderr,
"Error reading trailing data descriptor in file '%s' at offset %lld, file size "
"%lld\n",
path_.c_str(), static_cast<long long>(ddOffset), static_cast<long long>(size_));
return 0;
}
dd = reinterpret_cast<const DataDescriptor*>(ddMapped.data() + localDDOffset);
localDataSize = (lfh->compression_method == kCompressStored) ? dd->uncompressed_size
: dd->compressed_size;
ddSize += sizeof(*dd);
} else {
localDataSize = (lfh->compression_method == kCompressStored) ? lfh->uncompressed_size
: lfh->compressed_size;
}
if (localDataSize != dataSize) {
fprintf(stderr,
"Data sizes mismatch in file '%s' at offset %lld, CDr: %lld vs LHR/DD: %lld\n",
path_.c_str(), static_cast<long long>(localFileHeaderOffset),
static_cast<long long>(dataSize), static_cast<long long>(localDataSize));
return 0;
}
return kLocalFileHeaderSize + lfh->file_name_length + lfh->extra_field_length + dataSize +
ddSize;
}

View file

@ -0,0 +1,80 @@
/*
* 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 <memory>
#include <string>
#include <vector>
#include <adb_unique_fd.h>
#include "fastdeploy/proto/ApkEntry.pb.h"
class ApkArchiveTester;
// Manipulates an APK archive. Process it by mmaping it in order to minimize
// I/Os.
class ApkArchive {
public:
friend ApkArchiveTester;
// A convenience struct to store the result of search operation when
// locating the EoCDr, CDr, and Signature Block.
struct Location {
off_t offset = 0;
off_t size = 0;
bool valid = false;
};
ApkArchive(const std::string& path);
~ApkArchive();
com::android::fastdeploy::APKDump ExtractMetadata();
// Parses the CDr starting from |input| and returns number of bytes consumed.
// Extracts local file header offset, data size and calculates MD5 hash of the record.
// 0 indicates invalid CDr.
static size_t ParseCentralDirectoryRecord(const char* input, size_t size, std::string* md5Hash,
int64_t* localFileHeaderOffset, int64_t* dataSize);
// Calculates Local File Entry size including header using offset and data size from CDr.
// 0 indicates invalid Local File Entry.
size_t CalculateLocalFileEntrySize(int64_t localFileHeaderOffset, int64_t dataSize) const;
private:
std::string ReadMetadata(Location loc) const;
// Retrieve the location of the Central Directory Record.
Location GetCDLocation();
// Retrieve the location of the signature block starting from Central
// Directory Record
Location GetSignatureLocation(off_t cdRecordOffset);
// Find the End of Central Directory Record, starting from the end of the
// file.
off_t FindEndOfCDRecord() const;
// Find Central Directory Record, starting from the end of the file.
Location FindCDRecord(const char* cursor);
// Checks if the archive can be used.
bool ready() const { return fd_ >= 0; }
std::string path_;
off_t size_;
unique_fd fd_;
};

View file

@ -0,0 +1,73 @@
/*
* 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 <iostream>
#include <gtest/gtest.h>
#include "apk_archive.h"
// Friend test to get around private scope of ApkArchive private functions.
class ApkArchiveTester {
public:
ApkArchiveTester(const std::string& path) : archive_(path) {}
bool ready() { return archive_.ready(); }
auto ExtractMetadata() { return archive_.ExtractMetadata(); }
ApkArchive::Location GetCDLocation() { return archive_.GetCDLocation(); }
ApkArchive::Location GetSignatureLocation(size_t start) {
return archive_.GetSignatureLocation(start);
}
private:
ApkArchive archive_;
};
TEST(ApkArchiveTest, TestApkArchiveSizes) {
ApkArchiveTester archiveTester("fastdeploy/testdata/sample.apk");
EXPECT_TRUE(archiveTester.ready());
ApkArchive::Location cdLoc = archiveTester.GetCDLocation();
EXPECT_TRUE(cdLoc.valid);
ASSERT_EQ(cdLoc.offset, 2044145u);
ASSERT_EQ(cdLoc.size, 49390u);
// Check that block can be retrieved
ApkArchive::Location sigLoc = archiveTester.GetSignatureLocation(cdLoc.offset);
EXPECT_TRUE(sigLoc.valid);
ASSERT_EQ(sigLoc.offset, 2040049u);
ASSERT_EQ(sigLoc.size, 4088u);
}
TEST(ApkArchiveTest, TestApkArchiveDump) {
ApkArchiveTester archiveTester("fastdeploy/testdata/sample.apk");
EXPECT_TRUE(archiveTester.ready());
auto dump = archiveTester.ExtractMetadata();
ASSERT_EQ(dump.cd().size(), 49390u);
ASSERT_EQ(dump.signature().size(), 4088u);
}
TEST(ApkArchiveTest, WrongApk) {
ApkArchiveTester archiveTester("fastdeploy/testdata/sample.cd");
EXPECT_TRUE(archiveTester.ready());
auto dump = archiveTester.ExtractMetadata();
ASSERT_EQ(dump.cd().size(), 0u);
ASSERT_EQ(dump.signature().size(), 0u);
}

View file

@ -25,8 +25,12 @@
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <openssl/md5.h>
#include "adb_unique_fd.h"
#include "adb_utils.h"
#include "android-base/file.h"
#include "patch_utils.h"
#include "sysdeps.h"
@ -34,9 +38,6 @@
using namespace com::android::fastdeploy;
void DeployPatchGenerator::Log(const char* fmt, ...) {
if (!is_verbose_) {
return;
}
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
@ -44,19 +45,34 @@ void DeployPatchGenerator::Log(const char* fmt, ...) {
va_end(ap);
}
void DeployPatchGenerator::APKEntryToLog(const APKEntry& entry) {
Log("Filename: %s", entry.filename().c_str());
Log("CRC32: 0x%08" PRIX64, entry.crc32());
Log("Data Offset: %" PRId64, entry.dataoffset());
Log("Compressed Size: %" PRId64, entry.compressedsize());
Log("Uncompressed Size: %" PRId64, entry.uncompressedsize());
static std::string HexEncode(const void* in_buffer, unsigned int size) {
static const char kHexChars[] = "0123456789ABCDEF";
// Each input byte creates two output hex characters.
std::string out_buffer(size * 2, '\0');
for (unsigned int i = 0; i < size; ++i) {
char byte = ((const uint8_t*)in_buffer)[i];
out_buffer[(i << 1)] = kHexChars[(byte >> 4) & 0xf];
out_buffer[(i << 1) + 1] = kHexChars[byte & 0xf];
}
return out_buffer;
}
void DeployPatchGenerator::APKMetaDataToLog(const char* file, const APKMetaData& metadata) {
void DeployPatchGenerator::APKEntryToLog(const APKEntry& entry) {
if (!is_verbose_) {
return;
}
Log("APK Metadata: %s", file);
Log("MD5: %s", HexEncode(entry.md5().data(), entry.md5().size()).c_str());
Log("Data Offset: %" PRId64, entry.dataoffset());
Log("Data Size: %" PRId64, entry.datasize());
}
void DeployPatchGenerator::APKMetaDataToLog(const APKMetaData& metadata) {
if (!is_verbose_) {
return;
}
Log("APK Metadata: %s", metadata.absolute_path().c_str());
for (int i = 0; i < metadata.entries_size(); i++) {
const APKEntry& entry = metadata.entries(i);
APKEntryToLog(entry);
@ -65,49 +81,93 @@ void DeployPatchGenerator::APKMetaDataToLog(const char* file, const APKMetaData&
void DeployPatchGenerator::ReportSavings(const std::vector<SimpleEntry>& identicalEntries,
uint64_t totalSize) {
long totalEqualBytes = 0;
int totalEqualFiles = 0;
uint64_t totalEqualBytes = 0;
uint64_t totalEqualFiles = 0;
for (size_t i = 0; i < identicalEntries.size(); i++) {
if (identicalEntries[i].deviceEntry != nullptr) {
totalEqualBytes += identicalEntries[i].localEntry->compressedsize();
totalEqualBytes += identicalEntries[i].localEntry->datasize();
totalEqualFiles++;
}
}
float savingPercent = (totalEqualBytes * 100.0f) / totalSize;
fprintf(stderr, "Detected %d equal APK entries\n", totalEqualFiles);
fprintf(stderr, "%ld bytes are equal out of %" PRIu64 " (%.2f%%)\n", totalEqualBytes, totalSize,
savingPercent);
double savingPercent = (totalEqualBytes * 100.0f) / totalSize;
fprintf(stderr, "Detected %" PRIu64 " equal APK entries\n", totalEqualFiles);
fprintf(stderr, "%" PRIu64 " bytes are equal out of %" PRIu64 " (%.2f%%)\n", totalEqualBytes,
totalSize, savingPercent);
}
struct PatchEntry {
int64_t deltaFromDeviceDataStart = 0;
int64_t deviceDataOffset = 0;
int64_t deviceDataLength = 0;
};
static void WritePatchEntry(const PatchEntry& patchEntry, borrowed_fd input, borrowed_fd output,
size_t* realSizeOut) {
if (!(patchEntry.deltaFromDeviceDataStart | patchEntry.deviceDataOffset |
patchEntry.deviceDataLength)) {
return;
}
PatchUtils::WriteLong(patchEntry.deltaFromDeviceDataStart, output);
if (patchEntry.deltaFromDeviceDataStart > 0) {
PatchUtils::Pipe(input, output, patchEntry.deltaFromDeviceDataStart);
}
auto hostDataLength = patchEntry.deviceDataLength;
adb_lseek(input, hostDataLength, SEEK_CUR);
PatchUtils::WriteLong(patchEntry.deviceDataOffset, output);
PatchUtils::WriteLong(patchEntry.deviceDataLength, output);
*realSizeOut += patchEntry.deltaFromDeviceDataStart + hostDataLength;
}
void DeployPatchGenerator::GeneratePatch(const std::vector<SimpleEntry>& entriesToUseOnDevice,
const char* localApkPath, borrowed_fd output) {
unique_fd input(adb_open(localApkPath, O_RDONLY | O_CLOEXEC));
const std::string& localApkPath,
const std::string& deviceApkPath, borrowed_fd output) {
unique_fd input(adb_open(localApkPath.c_str(), O_RDONLY | O_CLOEXEC));
size_t newApkSize = adb_lseek(input, 0L, SEEK_END);
adb_lseek(input, 0L, SEEK_SET);
// Header.
PatchUtils::WriteSignature(output);
PatchUtils::WriteLong(newApkSize, output);
PatchUtils::WriteString(deviceApkPath, output);
size_t currentSizeOut = 0;
size_t realSizeOut = 0;
// Write data from the host upto the first entry we have that matches a device entry. Then write
// the metadata about the device entry and repeat for all entries that match on device. Finally
// write out any data left. If the device and host APKs are exactly the same this ends up
// writing out zip metadata from the local APK followed by offsets to the data to use from the
// device APK.
for (auto&& entry : entriesToUseOnDevice) {
int64_t deviceDataOffset = entry.deviceEntry->dataoffset();
PatchEntry patchEntry;
for (size_t i = 0, size = entriesToUseOnDevice.size(); i < size; ++i) {
auto&& entry = entriesToUseOnDevice[i];
int64_t hostDataOffset = entry.localEntry->dataoffset();
int64_t deviceDataLength = entry.deviceEntry->compressedsize();
int64_t hostDataLength = entry.localEntry->datasize();
int64_t deviceDataOffset = entry.deviceEntry->dataoffset();
// Both entries are the same, using host data length.
int64_t deviceDataLength = hostDataLength;
int64_t deltaFromDeviceDataStart = hostDataOffset - currentSizeOut;
PatchUtils::WriteLong(deltaFromDeviceDataStart, output);
if (deltaFromDeviceDataStart > 0) {
PatchUtils::Pipe(input, output, deltaFromDeviceDataStart);
WritePatchEntry(patchEntry, input, output, &realSizeOut);
patchEntry.deltaFromDeviceDataStart = deltaFromDeviceDataStart;
patchEntry.deviceDataOffset = deviceDataOffset;
patchEntry.deviceDataLength = deviceDataLength;
} else {
patchEntry.deviceDataLength += deviceDataLength;
}
PatchUtils::WriteLong(deviceDataOffset, output);
PatchUtils::WriteLong(deviceDataLength, output);
adb_lseek(input, deviceDataLength, SEEK_CUR);
currentSizeOut += deltaFromDeviceDataStart + deviceDataLength;
currentSizeOut += deltaFromDeviceDataStart + hostDataLength;
}
if (currentSizeOut != newApkSize) {
WritePatchEntry(patchEntry, input, output, &realSizeOut);
if (realSizeOut != currentSizeOut) {
fprintf(stderr, "Size mismatch current %lld vs real %lld\n",
static_cast<long long>(currentSizeOut), static_cast<long long>(realSizeOut));
error_exit("Aborting");
}
if (newApkSize > currentSizeOut) {
PatchUtils::WriteLong(newApkSize - currentSizeOut, output);
PatchUtils::Pipe(input, output, newApkSize - currentSizeOut);
PatchUtils::WriteLong(0, output);
@ -115,44 +175,72 @@ void DeployPatchGenerator::GeneratePatch(const std::vector<SimpleEntry>& entries
}
}
bool DeployPatchGenerator::CreatePatch(const char* localApkPath, const char* deviceApkMetadataPath,
borrowed_fd output) {
std::string content;
APKMetaData deviceApkMetadata;
if (android::base::ReadFileToString(deviceApkMetadataPath, &content)) {
deviceApkMetadata.ParsePartialFromString(content);
} else {
// TODO: What do we want to do if we don't find any metadata.
// The current fallback behavior is to build a patch with the contents of |localApkPath|.
}
bool DeployPatchGenerator::CreatePatch(const char* localApkPath, APKMetaData deviceApkMetadata,
android::base::borrowed_fd output) {
return CreatePatch(PatchUtils::GetHostAPKMetaData(localApkPath), std::move(deviceApkMetadata),
output);
}
APKMetaData localApkMetadata = PatchUtils::GetAPKMetaData(localApkPath);
// Log gathered metadata info.
APKMetaDataToLog(deviceApkMetadataPath, deviceApkMetadata);
APKMetaDataToLog(localApkPath, localApkMetadata);
bool DeployPatchGenerator::CreatePatch(APKMetaData localApkMetadata, APKMetaData deviceApkMetadata,
borrowed_fd output) {
// Log metadata info.
APKMetaDataToLog(deviceApkMetadata);
APKMetaDataToLog(localApkMetadata);
const std::string localApkPath = localApkMetadata.absolute_path();
const std::string deviceApkPath = deviceApkMetadata.absolute_path();
std::vector<SimpleEntry> identicalEntries;
uint64_t totalSize =
BuildIdenticalEntries(identicalEntries, localApkMetadata, deviceApkMetadata);
ReportSavings(identicalEntries, totalSize);
GeneratePatch(identicalEntries, localApkPath, output);
GeneratePatch(identicalEntries, localApkPath, deviceApkPath, output);
return true;
}
uint64_t DeployPatchGenerator::BuildIdenticalEntries(std::vector<SimpleEntry>& outIdenticalEntries,
const APKMetaData& localApkMetadata,
const APKMetaData& deviceApkMetadata) {
outIdenticalEntries.reserve(
std::min(localApkMetadata.entries_size(), deviceApkMetadata.entries_size()));
using md5Digest = std::pair<uint64_t, uint64_t>;
struct md5Hash {
size_t operator()(const md5Digest& digest) const {
std::hash<uint64_t> hasher;
size_t seed = 0;
seed ^= hasher(digest.first) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
seed ^= hasher(digest.second) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
return seed;
}
};
static_assert(sizeof(md5Digest) == MD5_DIGEST_LENGTH);
std::unordered_map<md5Digest, std::vector<const APKEntry*>, md5Hash> deviceEntries;
for (const auto& deviceEntry : deviceApkMetadata.entries()) {
md5Digest md5;
memcpy(&md5, deviceEntry.md5().data(), deviceEntry.md5().size());
deviceEntries[md5].push_back(&deviceEntry);
}
uint64_t totalSize = 0;
for (int i = 0; i < localApkMetadata.entries_size(); i++) {
const APKEntry& localEntry = localApkMetadata.entries(i);
totalSize += localEntry.compressedsize();
for (int j = 0; j < deviceApkMetadata.entries_size(); j++) {
const APKEntry& deviceEntry = deviceApkMetadata.entries(j);
if (deviceEntry.crc32() == localEntry.crc32() &&
deviceEntry.filename().compare(localEntry.filename()) == 0) {
for (const auto& localEntry : localApkMetadata.entries()) {
totalSize += localEntry.datasize();
md5Digest md5;
memcpy(&md5, localEntry.md5().data(), localEntry.md5().size());
auto deviceEntriesIt = deviceEntries.find(md5);
if (deviceEntriesIt == deviceEntries.end()) {
continue;
}
for (const auto* deviceEntry : deviceEntriesIt->second) {
if (deviceEntry->md5() == localEntry.md5()) {
SimpleEntry simpleEntry;
simpleEntry.localEntry = const_cast<APKEntry*>(&localEntry);
simpleEntry.deviceEntry = const_cast<APKEntry*>(&deviceEntry);
simpleEntry.localEntry = &localEntry;
simpleEntry.deviceEntry = deviceEntry;
APKEntryToLog(localEntry);
outIdenticalEntries.push_back(simpleEntry);
break;

View file

@ -27,12 +27,15 @@
*/
class DeployPatchGenerator {
public:
using APKEntry = com::android::fastdeploy::APKEntry;
using APKMetaData = com::android::fastdeploy::APKMetaData;
/**
* Simple struct to hold mapping between local metadata and device metadata.
*/
struct SimpleEntry {
com::android::fastdeploy::APKEntry* localEntry;
com::android::fastdeploy::APKEntry* deviceEntry;
const APKEntry* localEntry;
const APKEntry* deviceEntry;
};
/**
@ -41,10 +44,10 @@ class DeployPatchGenerator {
*/
explicit DeployPatchGenerator(bool is_verbose) : is_verbose_(is_verbose) {}
/**
* Given a |localApkPath|, and the |deviceApkMetadataPath| from an installed APK this function
* Given a |localApkPath|, and the |deviceApkMetadata| from an installed APK this function
* writes a patch to the given |output|.
*/
bool CreatePatch(const char* localApkPath, const char* deviceApkMetadataPath,
bool CreatePatch(const char* localApkPath, APKMetaData deviceApkMetadata,
android::base::borrowed_fd output);
private:
@ -57,14 +60,20 @@ class DeployPatchGenerator {
/**
* Helper function to log the APKMetaData structure. If |is_verbose_| is false this function
* early outs. |file| is the path to the file represented by |metadata|. This function is used
* for debugging / information.
* early outs. This function is used for debugging / information.
*/
void APKMetaDataToLog(const char* file, const com::android::fastdeploy::APKMetaData& metadata);
void APKMetaDataToLog(const APKMetaData& metadata);
/**
* Helper function to log APKEntry.
*/
void APKEntryToLog(const com::android::fastdeploy::APKEntry& entry);
void APKEntryToLog(const APKEntry& entry);
/**
* Given the |localApkMetadata| metadata, and the |deviceApkMetadata| from an installed APK this
* function writes a patch to the given |output|.
*/
bool CreatePatch(APKMetaData localApkMetadata, APKMetaData deviceApkMetadata,
android::base::borrowed_fd output);
/**
* Helper function to report savings by fastdeploy. This function prints out savings even with
@ -92,11 +101,11 @@ class DeployPatchGenerator {
* highest.
*/
void GeneratePatch(const std::vector<SimpleEntry>& entriesToUseOnDevice,
const char* localApkPath, android::base::borrowed_fd output);
const std::string& localApkPath, const std::string& deviceApkPath,
android::base::borrowed_fd output);
protected:
uint64_t BuildIdenticalEntries(
std::vector<SimpleEntry>& outIdenticalEntries,
const com::android::fastdeploy::APKMetaData& localApkMetadata,
const com::android::fastdeploy::APKMetaData& deviceApkMetadataPath);
};
uint64_t BuildIdenticalEntries(std::vector<SimpleEntry>& outIdenticalEntries,
const APKMetaData& localApkMetadata,
const APKMetaData& deviceApkMetadata);
};

View file

@ -15,6 +15,7 @@
*/
#include "deploy_patch_generator.h"
#include "apk_archive.h"
#include "patch_utils.h"
#include <android-base/file.h>
@ -31,21 +32,17 @@ static std::string GetTestFile(const std::string& name) {
return "fastdeploy/testdata/" + name;
}
class TestPatchGenerator : DeployPatchGenerator {
public:
TestPatchGenerator() : DeployPatchGenerator(false) {}
void GatherIdenticalEntries(std::vector<DeployPatchGenerator::SimpleEntry>& outIdenticalEntries,
const APKMetaData& metadataA, const APKMetaData& metadataB) {
BuildIdenticalEntries(outIdenticalEntries, metadataA, metadataB);
}
struct TestPatchGenerator : DeployPatchGenerator {
using DeployPatchGenerator::BuildIdenticalEntries;
using DeployPatchGenerator::DeployPatchGenerator;
};
TEST(DeployPatchGeneratorTest, IdenticalFileEntries) {
std::string apkPath = GetTestFile("rotating_cube-release.apk");
APKMetaData metadataA = PatchUtils::GetAPKMetaData(apkPath.c_str());
TestPatchGenerator generator;
APKMetaData metadataA = PatchUtils::GetHostAPKMetaData(apkPath.c_str());
TestPatchGenerator generator(false);
std::vector<DeployPatchGenerator::SimpleEntry> entries;
generator.GatherIdenticalEntries(entries, metadataA, metadataA);
generator.BuildIdenticalEntries(entries, metadataA, metadataA);
// Expect the entry count to match the number of entries in the metadata.
const uint32_t identicalCount = entries.size();
const uint32_t entriesCount = metadataA.entries_size();
@ -64,9 +61,28 @@ TEST(DeployPatchGeneratorTest, NoDeviceMetadata) {
// Create a patch that is 100% different.
TemporaryFile output;
DeployPatchGenerator generator(true);
generator.CreatePatch(apkPath.c_str(), "", output.fd);
generator.CreatePatch(apkPath.c_str(), {}, output.fd);
// Expect a patch file that has a size at least the size of our initial APK.
long patchSize = adb_lseek(output.fd, 0L, SEEK_END);
EXPECT_GT(patchSize, apkSize);
}
}
TEST(DeployPatchGeneratorTest, ZeroSizePatch) {
std::string apkPath = GetTestFile("rotating_cube-release.apk");
ApkArchive archive(apkPath);
auto dump = archive.ExtractMetadata();
EXPECT_NE(dump.cd().size(), 0u);
APKMetaData metadata = PatchUtils::GetDeviceAPKMetaData(dump);
// Create a patch that is 100% the same.
TemporaryFile output;
output.DoNotRemove();
DeployPatchGenerator generator(true);
generator.CreatePatch(apkPath.c_str(), metadata, output.fd);
// Expect a patch file that is smaller than 0.5K.
int64_t patchSize = adb_lseek(output.fd, 0L, SEEK_END);
EXPECT_LE(patchSize, 512);
}

View file

@ -16,72 +16,94 @@
#include "patch_utils.h"
#include <androidfw/ZipFileRO.h>
#include <stdio.h>
#include "adb_io.h"
#include "adb_utils.h"
#include "android-base/endian.h"
#include "sysdeps.h"
#include "apk_archive.h"
using namespace com::android;
using namespace com::android::fastdeploy;
using namespace android::base;
static constexpr char kSignature[] = "FASTDEPLOY";
APKMetaData PatchUtils::GetAPKMetaData(const char* apkPath) {
APKMetaData PatchUtils::GetDeviceAPKMetaData(const APKDump& apk_dump) {
APKMetaData apkMetaData;
#undef open
std::unique_ptr<android::ZipFileRO> zipFile(android::ZipFileRO::open(apkPath));
#define open ___xxx_unix_open
if (zipFile == nullptr) {
printf("Could not open %s", apkPath);
exit(1);
}
void* cookie;
if (zipFile->startIteration(&cookie)) {
android::ZipEntryRO entry;
while ((entry = zipFile->nextEntry(cookie)) != NULL) {
char fileName[256];
// Make sure we have a file name.
// TODO: Handle filenames longer than 256.
if (zipFile->getEntryFileName(entry, fileName, sizeof(fileName))) {
continue;
}
apkMetaData.set_absolute_path(apk_dump.absolute_path());
uint32_t uncompressedSize, compressedSize, crc32;
int64_t dataOffset;
zipFile->getEntryInfo(entry, nullptr, &uncompressedSize, &compressedSize, &dataOffset,
nullptr, &crc32);
APKEntry* apkEntry = apkMetaData.add_entries();
apkEntry->set_crc32(crc32);
apkEntry->set_filename(fileName);
apkEntry->set_compressedsize(compressedSize);
apkEntry->set_uncompressedsize(uncompressedSize);
apkEntry->set_dataoffset(dataOffset);
}
std::string md5Hash;
int64_t localFileHeaderOffset;
int64_t dataSize;
const auto& cd = apk_dump.cd();
auto cur = cd.data();
int64_t size = cd.size();
while (auto consumed = ApkArchive::ParseCentralDirectoryRecord(
cur, size, &md5Hash, &localFileHeaderOffset, &dataSize)) {
cur += consumed;
size -= consumed;
auto apkEntry = apkMetaData.add_entries();
apkEntry->set_md5(md5Hash);
apkEntry->set_dataoffset(localFileHeaderOffset);
apkEntry->set_datasize(dataSize);
}
return apkMetaData;
}
APKMetaData PatchUtils::GetHostAPKMetaData(const char* apkPath) {
ApkArchive archive(apkPath);
auto dump = archive.ExtractMetadata();
if (dump.cd().empty()) {
fprintf(stderr, "adb: Could not extract Central Directory from %s\n", apkPath);
error_exit("Aborting");
}
auto apkMetaData = GetDeviceAPKMetaData(dump);
// Now let's set data sizes.
for (auto& apkEntry : *apkMetaData.mutable_entries()) {
auto dataSize =
archive.CalculateLocalFileEntrySize(apkEntry.dataoffset(), apkEntry.datasize());
if (dataSize == 0) {
error_exit("Aborting");
}
apkEntry.set_datasize(dataSize);
}
return apkMetaData;
}
void PatchUtils::WriteSignature(borrowed_fd output) {
WriteFdExactly(output, kSignature, sizeof(kSignature) - 1);
}
void PatchUtils::WriteLong(int64_t value, borrowed_fd output) {
int64_t toLittleEndian = htole64(value);
WriteFdExactly(output, &toLittleEndian, sizeof(int64_t));
int64_t littleEndian = htole64(value);
WriteFdExactly(output, &littleEndian, sizeof(littleEndian));
}
void PatchUtils::WriteString(const std::string& value, android::base::borrowed_fd output) {
WriteLong(value.size(), output);
WriteFdExactly(output, value);
}
void PatchUtils::Pipe(borrowed_fd input, borrowed_fd output, size_t amount) {
constexpr static int BUFFER_SIZE = 128 * 1024;
constexpr static size_t BUFFER_SIZE = 128 * 1024;
char buffer[BUFFER_SIZE];
size_t transferAmount = 0;
while (transferAmount != amount) {
long chunkAmount =
amount - transferAmount > BUFFER_SIZE ? BUFFER_SIZE : amount - transferAmount;
long readAmount = adb_read(input, buffer, chunkAmount);
auto chunkAmount = std::min(amount - transferAmount, BUFFER_SIZE);
auto readAmount = adb_read(input, buffer, chunkAmount);
if (readAmount < 0) {
fprintf(stderr, "adb: failed to read from input: %s\n", strerror(errno));
error_exit("Aborting");
}
WriteFdExactly(output, buffer, readAmount);
transferAmount += readAmount;
}
}
}

View file

@ -24,12 +24,19 @@
*/
class PatchUtils {
public:
/**
* This function takes the dump of Central Directly and builds the APKMetaData required by the
* patching algorithm. The if this function has an error a string is printed to the terminal and
* exit(1) is called.
*/
static com::android::fastdeploy::APKMetaData GetDeviceAPKMetaData(
const com::android::fastdeploy::APKDump& apk_dump);
/**
* This function takes a local APK file and builds the APKMetaData required by the patching
* algorithm. The if this function has an error a string is printed to the terminal and exit(1)
* is called.
*/
static com::android::fastdeploy::APKMetaData GetAPKMetaData(const char* file);
static com::android::fastdeploy::APKMetaData GetHostAPKMetaData(const char* file);
/**
* Writes a fixed signature string to the header of the patch.
*/
@ -38,9 +45,13 @@ class PatchUtils {
* Writes an int64 to the |output| reversing the bytes.
*/
static void WriteLong(int64_t value, android::base::borrowed_fd output);
/**
* Writes string to the |output|.
*/
static void WriteString(const std::string& value, android::base::borrowed_fd output);
/**
* Copy |amount| of data from |input| to |output|.
*/
static void Pipe(android::base::borrowed_fd input, android::base::borrowed_fd output,
size_t amount);
};
};

View file

@ -23,10 +23,13 @@
#include <sstream>
#include <string>
#include <google/protobuf/util/message_differencer.h>
#include "adb_io.h"
#include "sysdeps.h"
using namespace com::android::fastdeploy;
using google::protobuf::util::MessageDifferencer;
static std::string GetTestFile(const std::string& name) {
return "fastdeploy/testdata/" + name;
@ -86,11 +89,56 @@ TEST(PatchUtilsTest, SignatureConstMatches) {
TEST(PatchUtilsTest, GatherMetadata) {
std::string apkFile = GetTestFile("rotating_cube-release.apk");
APKMetaData metadata = PatchUtils::GetAPKMetaData(apkFile.c_str());
APKMetaData actual = PatchUtils::GetHostAPKMetaData(apkFile.c_str());
std::string expectedMetadata;
android::base::ReadFileToString(GetTestFile("rotating_cube-metadata-release.data"),
&expectedMetadata);
APKMetaData expected;
EXPECT_TRUE(expected.ParseFromString(expectedMetadata));
// Test paths might vary.
expected.set_absolute_path(actual.absolute_path());
std::string actualMetadata;
metadata.SerializeToString(&actualMetadata);
actual.SerializeToString(&actualMetadata);
expected.SerializeToString(&expectedMetadata);
EXPECT_EQ(expectedMetadata, actualMetadata);
}
}
static inline void sanitize(APKMetaData& metadata) {
metadata.clear_absolute_path();
for (auto&& entry : *metadata.mutable_entries()) {
entry.clear_datasize();
}
}
TEST(PatchUtilsTest, GatherDumpMetadata) {
APKMetaData hostMetadata;
APKMetaData deviceMetadata;
hostMetadata = PatchUtils::GetHostAPKMetaData(GetTestFile("sample.apk").c_str());
{
std::string cd;
android::base::ReadFileToString(GetTestFile("sample.cd"), &cd);
APKDump dump;
dump.set_cd(std::move(cd));
deviceMetadata = PatchUtils::GetDeviceAPKMetaData(dump);
}
sanitize(hostMetadata);
sanitize(deviceMetadata);
std::string expectedMetadata;
hostMetadata.SerializeToString(&expectedMetadata);
std::string actualMetadata;
deviceMetadata.SerializeToString(&actualMetadata);
EXPECT_EQ(expectedMetadata, actualMetadata);
}

View file

@ -1,18 +1,26 @@
syntax = "proto2";
syntax = "proto3";
package com.android.fastdeploy;
option java_package = "com.android.fastdeploy";
option java_outer_classname = "ApkEntryProto";
option java_multiple_files = true;
option optimize_for = LITE_RUNTIME;
message APKDump {
string name = 1;
bytes cd = 2;
bytes signature = 3;
string absolute_path = 4;
}
message APKEntry {
required int64 crc32 = 1;
required string fileName = 2;
required int64 dataOffset = 3;
required int64 compressedSize = 4;
required int64 uncompressedSize = 5;
bytes md5 = 1;
int64 dataOffset = 2;
int64 dataSize = 3;
}
message APKMetaData {
repeated APKEntry entries = 1;
string absolute_path = 1;
repeated APKEntry entries = 2;
}

BIN
adb/fastdeploy/testdata/helloworld5.apk vendored Normal file

Binary file not shown.

BIN
adb/fastdeploy/testdata/helloworld7.apk vendored Normal file

Binary file not shown.

View file

@ -1,6 +1,8 @@
#ǂϫ META-INF/MANIFEST.MFÇ Q(W
#ƒ<>•ŽAndroidManifest.xml1 ä
6¦µ€>#lib/armeabi-v7a/libvulkan_sample.so<18> ÀÒQ(Œ²ì
—ã—‘resources.arscôàQ ´(´
‹œÂÉ classes.dexÁ ÿ
-fastdeploy/testdata/rotating_cube-release.apk
ìv4@ob<>O#&kýn
K 3Qcp^<Ð̽sF0ƒ
xB ™a­©–2áÒ_O'˜¨
¶ÅhsêÃDÍY
ª^"cvÀ <18>ÓQ
q<>žp¶Îó{2ÐÄÒÙ«ÂÁàQç

BIN
adb/fastdeploy/testdata/sample.apk vendored Normal file

Binary file not shown.

BIN
adb/fastdeploy/testdata/sample.cd vendored Normal file

Binary file not shown.

View file

@ -601,6 +601,10 @@ static __inline__ int adb_is_absolute_host_path(const char* path) {
return path[0] == '/';
}
static __inline__ int adb_get_os_handle(borrowed_fd fd) {
return fd.get();
}
#endif /* !_WIN32 */
static inline void disable_tcp_nagle(borrowed_fd fd) {