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
diff --git a/adb/fastdeploy/Android.bp b/adb/fastdeploy/Android.bp
index 95e1a28..245d52a 100644
--- a/adb/fastdeploy/Android.bp
+++ b/adb/fastdeploy/Android.bp
@@ -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,
}
-}
\ No newline at end of file
+}
+
+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",
+ ],
+}
diff --git a/adb/fastdeploy/AndroidManifest.xml b/adb/fastdeploy/AndroidManifest.xml
new file mode 100644
index 0000000..89dc745
--- /dev/null
+++ b/adb/fastdeploy/AndroidManifest.xml
@@ -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>
\ No newline at end of file
diff --git a/adb/fastdeploy/AndroidTest.xml b/adb/fastdeploy/AndroidTest.xml
new file mode 100644
index 0000000..24a72bc
--- /dev/null
+++ b/adb/fastdeploy/AndroidTest.xml
@@ -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>
diff --git a/adb/fastdeploy/deployagent/src/com/android/fastdeploy/ApkArchive.java b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/ApkArchive.java
new file mode 100644
index 0000000..31e0502
--- /dev/null
+++ b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/ApkArchive.java
@@ -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;
+ }
+}
diff --git a/adb/fastdeploy/deployagent/src/com/android/fastdeploy/DeployAgent.java b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/DeployAgent.java
index a8103c4..3812307 100644
--- a/adb/fastdeploy/deployagent/src/com/android/fastdeploy/DeployAgent.java
+++ b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/DeployAgent.java
@@ -24,18 +24,22 @@
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 @@
}
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 @@
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 @@
}
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 @@
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 @@
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 @@
}
}
- 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) {
diff --git a/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchFormatException.java b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/PatchFormatException.java
similarity index 100%
rename from adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchFormatException.java
rename to adb/fastdeploy/deployagent/src/com/android/fastdeploy/PatchFormatException.java
diff --git a/adb/fastdeploy/deployagent/src/com/android/fastdeploy/PatchUtils.java b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/PatchUtils.java
new file mode 100644
index 0000000..54be26f
--- /dev/null
+++ b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/PatchUtils.java
@@ -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;
+ }
+ }
+}
diff --git a/adb/fastdeploy/deployagent/test/com/android/fastdeploy/ApkArchiveTest.java b/adb/fastdeploy/deployagent/test/com/android/fastdeploy/ApkArchiveTest.java
new file mode 100644
index 0000000..7c2468f
--- /dev/null
+++ b/adb/fastdeploy/deployagent/test/com/android/fastdeploy/ApkArchiveTest.java
@@ -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();
+ }
+}
diff --git a/adb/fastdeploy/deployagent/test/com/android/fastdeploy/FastDeployTest.java b/adb/fastdeploy/deployagent/test/com/android/fastdeploy/FastDeployTest.java
new file mode 100644
index 0000000..ef6ccae
--- /dev/null
+++ b/adb/fastdeploy/deployagent/test/com/android/fastdeploy/FastDeployTest.java
@@ -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);
+ }
+}
+
+
diff --git a/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchUtils.java b/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchUtils.java
deleted file mode 100644
index c60f9a6..0000000
--- a/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchUtils.java
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/adb/fastdeploy/deploypatchgenerator/apk_archive.cpp b/adb/fastdeploy/deploypatchgenerator/apk_archive.cpp
new file mode 100644
index 0000000..3dc5e50
--- /dev/null
+++ b/adb/fastdeploy/deploypatchgenerator/apk_archive.cpp
@@ -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;
+}
diff --git a/adb/fastdeploy/deploypatchgenerator/apk_archive.h b/adb/fastdeploy/deploypatchgenerator/apk_archive.h
new file mode 100644
index 0000000..7127800
--- /dev/null
+++ b/adb/fastdeploy/deploypatchgenerator/apk_archive.h
@@ -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_;
+};
diff --git a/adb/fastdeploy/deploypatchgenerator/apk_archive_test.cpp b/adb/fastdeploy/deploypatchgenerator/apk_archive_test.cpp
new file mode 100644
index 0000000..554cb57
--- /dev/null
+++ b/adb/fastdeploy/deploypatchgenerator/apk_archive_test.cpp
@@ -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);
+}
diff --git a/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.cpp b/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.cpp
index 154c9b9..8aa7da7 100644
--- a/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.cpp
+++ b/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.cpp
@@ -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 @@
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::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 @@
}
}
-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;
diff --git a/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.h b/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.h
index 30e41a5..fd7eaee 100644
--- a/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.h
+++ b/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator.h
@@ -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 @@
*/
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 @@
/**
* 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 @@
* 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);
-};
\ No newline at end of file
+ uint64_t BuildIdenticalEntries(std::vector<SimpleEntry>& outIdenticalEntries,
+ const APKMetaData& localApkMetadata,
+ const APKMetaData& deviceApkMetadata);
+};
diff --git a/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator_test.cpp b/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator_test.cpp
index 9cdc44e..e4c96ea 100644
--- a/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator_test.cpp
+++ b/adb/fastdeploy/deploypatchgenerator/deploy_patch_generator_test.cpp
@@ -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 @@
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 @@
// 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);
-}
\ No newline at end of file
+}
+
+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);
+}
diff --git a/adb/fastdeploy/deploypatchgenerator/patch_utils.cpp b/adb/fastdeploy/deploypatchgenerator/patch_utils.cpp
index f11ddd1..2b00c80 100644
--- a/adb/fastdeploy/deploypatchgenerator/patch_utils.cpp
+++ b/adb/fastdeploy/deploypatchgenerator/patch_utils.cpp
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/adb/fastdeploy/deploypatchgenerator/patch_utils.h b/adb/fastdeploy/deploypatchgenerator/patch_utils.h
index 0ebfe8f..8dc9b9c 100644
--- a/adb/fastdeploy/deploypatchgenerator/patch_utils.h
+++ b/adb/fastdeploy/deploypatchgenerator/patch_utils.h
@@ -25,11 +25,18 @@
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.
*/
@@ -39,8 +46,12 @@
*/
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);
-};
\ No newline at end of file
+};
diff --git a/adb/fastdeploy/deploypatchgenerator/patch_utils_test.cpp b/adb/fastdeploy/deploypatchgenerator/patch_utils_test.cpp
index a7eeebf..3ec5ab3 100644
--- a/adb/fastdeploy/deploypatchgenerator/patch_utils_test.cpp
+++ b/adb/fastdeploy/deploypatchgenerator/patch_utils_test.cpp
@@ -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, 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);
-}
\ No newline at end of file
+}
+
+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);
+}
diff --git a/adb/fastdeploy/proto/ApkEntry.proto b/adb/fastdeploy/proto/ApkEntry.proto
index 9460d15..d84c5a5 100644
--- a/adb/fastdeploy/proto/ApkEntry.proto
+++ b/adb/fastdeploy/proto/ApkEntry.proto
@@ -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;
}
diff --git a/adb/fastdeploy/testdata/helloworld5.apk b/adb/fastdeploy/testdata/helloworld5.apk
new file mode 100644
index 0000000..4a1539e
--- /dev/null
+++ b/adb/fastdeploy/testdata/helloworld5.apk
Binary files differ
diff --git a/adb/fastdeploy/testdata/helloworld7.apk b/adb/fastdeploy/testdata/helloworld7.apk
new file mode 100644
index 0000000..82c46df
--- /dev/null
+++ b/adb/fastdeploy/testdata/helloworld7.apk
Binary files differ
diff --git a/adb/fastdeploy/testdata/rotating_cube-metadata-release.data b/adb/fastdeploy/testdata/rotating_cube-metadata-release.data
index 0671bf3..52352ff 100644
--- a/adb/fastdeploy/testdata/rotating_cube-metadata-release.data
+++ b/adb/fastdeploy/testdata/rotating_cube-metadata-release.data
Binary files differ
diff --git a/adb/fastdeploy/testdata/sample.apk b/adb/fastdeploy/testdata/sample.apk
new file mode 100644
index 0000000..c316205
--- /dev/null
+++ b/adb/fastdeploy/testdata/sample.apk
Binary files differ
diff --git a/adb/fastdeploy/testdata/sample.cd b/adb/fastdeploy/testdata/sample.cd
new file mode 100644
index 0000000..5e5b4d4
--- /dev/null
+++ b/adb/fastdeploy/testdata/sample.cd
Binary files differ