summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYohann Roussel <yroussel@google.com>2014-03-28 17:35:02 +0100
committerYohann Roussel <yroussel@google.com>2014-04-09 14:56:20 +0200
commit602c6ca8cae4718ba8ff9f65e53305d002479359 (patch)
treeea27d7ec0e1b77ea6471b4955c57ec71b38d5492
parent5516c9ffbddd16c90f17c45caea3f4b59ab360d8 (diff)
downloadmultidex-602c6ca8cae4718ba8ff9f65e53305d002479359.tar.gz
Change update detection to reduce load time.
Reduces load time if extraction was already made. It appeared that new ZipFile was really slow because it's preparing much things as soon as it's instanciated. The new criteria consist of the last modified time of the apk plus the crc of the apk's central directory, last modified time should be enough for nearly all modifications and the crc is here to try to handle an OTA mixing with dates. The transition from old criteria to new should be good: since there will be no stored values they would be detected as a new installation. Change-Id: Id390b77b03d794b8b7feb91eb0daae1126c6d691
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--NOTICE28
-rw-r--r--instrumentation/.classpath3
-rw-r--r--library/.classpath3
-rw-r--r--library/src/android/support/multidex/MultiDex.java7
-rw-r--r--library/src/android/support/multidex/MultiDexExtractor.java242
-rw-r--r--library/src/android/support/multidex/ZipUtil.java125
-rw-r--r--library/test/.classpath8
-rw-r--r--library/test/.project17
-rw-r--r--library/test/src/android/support/multidex/ZipEntryReader.java120
-rw-r--r--library/test/src/android/support/multidex/ZipUtilTest.java172
11 files changed, 609 insertions, 116 deletions
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..16611ea
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,28 @@
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the Android-specific code. ==
+ =========================================================================
+
+Android Code
+Copyright 2005-2014 The Android Open Source Project
+
+This product includes software developed as part of
+The Android Open Source Project (http://source.android.com).
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the Apache Harmony distribution. ==
+ =========================================================================
+
+Apache Harmony
+Copyright 2006 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+Portions of Harmony were originally developed by
+Intel Corporation and are licensed to the Apache Software
+Foundation under the "Software Grant and Corporate Contribution
+License Agreement", informally known as the "Intel Harmony CLA".
diff --git a/instrumentation/.classpath b/instrumentation/.classpath
index a4763d1..7bc01d9 100644
--- a/instrumentation/.classpath
+++ b/instrumentation/.classpath
@@ -3,6 +3,7 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
- <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
diff --git a/library/.classpath b/library/.classpath
index a4763d1..7bc01d9 100644
--- a/library/.classpath
+++ b/library/.classpath
@@ -3,6 +3,7 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
- <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
diff --git a/library/src/android/support/multidex/MultiDex.java b/library/src/android/support/multidex/MultiDex.java
index 6bfbf6f..6e650d0 100644
--- a/library/src/android/support/multidex/MultiDex.java
+++ b/library/src/android/support/multidex/MultiDex.java
@@ -16,14 +16,14 @@
package android.support.multidex;
-import dalvik.system.DexFile;
-
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
+import dalvik.system.DexFile;
+
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
@@ -78,6 +78,7 @@ public final class MultiDex {
* extension.
*/
public static void install(Context context) {
+ Log.i(TAG, "install");
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
@@ -158,6 +159,7 @@ public final class MultiDex {
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
// Try again, but this time force a reload of the zip file.
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
+
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
@@ -171,6 +173,7 @@ public final class MultiDex {
Log.e(TAG, "Multidex installation failure", e);
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
+ Log.i(TAG, "install done");
}
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
diff --git a/library/src/android/support/multidex/MultiDexExtractor.java b/library/src/android/support/multidex/MultiDexExtractor.java
index 010999d..99c5269 100644
--- a/library/src/android/support/multidex/MultiDexExtractor.java
+++ b/library/src/android/support/multidex/MultiDexExtractor.java
@@ -58,11 +58,17 @@ final class MultiDexExtractor {
private static final String EXTRACTED_SUFFIX = ".zip";
private static final int MAX_EXTRACT_ATTEMPTS = 3;
- private static final int BUFFER_SIZE = 0x4000;
-
private static final String PREFS_FILE = "multidex.version";
- private static final String KEY_NUM_DEX_FILES = "num_dex";
- private static final String KEY_PREFIX_DEX_CRC = "crc";
+ private static final String KEY_TIME_STAMP = "timestamp";
+ private static final String KEY_CRC = "crc";
+ private static final String KEY_DEX_NUMBER = "dex.number";
+
+ /**
+ * Size of reading buffers.
+ */
+ private static final int BUFFER_SIZE = 0x4000;
+ /* Keep value away from 0 because it is a too probable time stamp value */
+ private static final long NO_VALUE = -1L;
/**
* Extracts application secondary dexes into files in the application data
@@ -75,8 +81,87 @@ final class MultiDexExtractor {
*/
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
- Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")");
+ Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
final File sourceApk = new File(applicationInfo.sourceDir);
+
+ File archive = new File(applicationInfo.sourceDir);
+ long currentCrc = getZipCrc(archive);
+
+ List<File> files;
+ if (!forceReload && !isModified(context, archive, currentCrc)) {
+ try {
+ files = loadExistingExtractions(context, sourceApk, dexDir);
+ } catch (IOException ioe) {
+ Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ + " falling back to fresh extraction", ioe);
+ files = performExtractions(sourceApk, dexDir);
+ putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
+
+ }
+ } else {
+ Log.i(TAG, "Detected that extraction must be performed.");
+ files = performExtractions(sourceApk, dexDir);
+ putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
+ }
+
+ Log.i(TAG, "load found " + files.size() + " secondary dex files");
+ return files;
+ }
+
+ private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
+ throws IOException {
+ Log.i(TAG, "loading existing secondary dex files");
+
+ final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+ int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
+ final List<File> files = new ArrayList<File>(totalDexNumber);
+
+ for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
+ String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+ File extractedFile = new File(dexDir, fileName);
+ if (extractedFile.isFile()) {
+ files.add(extractedFile);
+ if (!verifyZipFile(extractedFile)) {
+ Log.i(TAG, "Invalid zip file: " + extractedFile);
+ throw new IOException("Invalid ZIP file.");
+ }
+ } else {
+ throw new IOException("Missing extracted secondary dex file '" +
+ extractedFile.getPath() + "'");
+ }
+ }
+
+ return files;
+ }
+
+ private static boolean isModified(Context context, File archive, long currentCrc) {
+ SharedPreferences prefs = getMultiDexPreferences(context);
+ return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
+ || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
+ }
+
+ private static long getTimeStamp(File archive) {
+ long timeStamp = archive.lastModified();
+ if (timeStamp == NO_VALUE) {
+ // never return NO_VALUE
+ timeStamp--;
+ }
+ return timeStamp;
+ }
+
+
+ private static long getZipCrc(File archive) throws IOException {
+ long computedValue = ZipUtil.getZipCrc(archive);
+ if (computedValue == NO_VALUE) {
+ // never return NO_VALUE
+ computedValue--;
+ }
+ return computedValue;
+ }
+
+ private static List<File> performExtractions(File sourceApk, File dexDir)
+ throws IOException {
+
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
@@ -85,15 +170,9 @@ final class MultiDexExtractor {
// while another had created it.
prepareDexDir(dexDir, extractedFilePrefix);
- final List<File> files = new ArrayList<File>();
- final ZipFile apk = new ZipFile(applicationInfo.sourceDir);
+ List<File> files = new ArrayList<File>();
- // If the CRC of any of the dex files is different than what we have stored or the number of
- // dex files are different, then force reload everything.
- ArrayList<Long> dexCrcs = getAllDexCrcs(apk);
- if (isAnyDexCrcDifferent(context, dexCrcs)) {
- forceReload = true;
- }
+ final ZipFile apk = new ZipFile(sourceApk);
try {
int secondaryNumber = 2;
@@ -104,41 +183,36 @@ final class MultiDexExtractor {
File extractedFile = new File(dexDir, fileName);
files.add(extractedFile);
- Log.i(TAG, "Need extracted file " + extractedFile);
- if (forceReload || !extractedFile.isFile()) {
- Log.i(TAG, "Extraction is needed for file " + extractedFile);
- int numAttempts = 0;
- boolean isExtractionSuccessful = false;
- while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
- numAttempts++;
-
- // Create a zip file (extractedFile) containing only the secondary dex file
- // (dexFile) from the apk.
- extract(apk, dexFile, extractedFile, extractedFilePrefix);
-
- // Verify that the extracted file is indeed a zip file.
- isExtractionSuccessful = verifyZipFile(extractedFile);
-
- // Log the sha1 of the extracted zip file
- Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
- " - length " + extractedFile.getAbsolutePath() + ": " +
- extractedFile.length());
- if (!isExtractionSuccessful) {
- // Delete the extracted file
- extractedFile.delete();
+ Log.i(TAG, "Extraction is needed for file " + extractedFile);
+ int numAttempts = 0;
+ boolean isExtractionSuccessful = false;
+ while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
+ numAttempts++;
+
+ // Create a zip file (extractedFile) containing only the secondary dex file
+ // (dexFile) from the apk.
+ extract(apk, dexFile, extractedFile, extractedFilePrefix);
+
+ // Verify that the extracted file is indeed a zip file.
+ isExtractionSuccessful = verifyZipFile(extractedFile);
+
+ // Log the sha1 of the extracted zip file
+ Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
+ " - length " + extractedFile.getAbsolutePath() + ": " +
+ extractedFile.length());
+ if (!isExtractionSuccessful) {
+ // Delete the extracted file
+ extractedFile.delete();
+ if (extractedFile.exists()) {
+ Log.w(TAG, "Failed to delete corrupted secondary dex '" +
+ extractedFile.getPath() + "'");
}
}
- if (isExtractionSuccessful) {
- // Write the dex crc's into the shared preferences
- putStoredDexCrcs(context, dexCrcs);
- } else {
- throw new IOException("Could not create zip file " +
- extractedFile.getAbsolutePath() + " for secondary dex (" +
- secondaryNumber + ")");
- }
- } else {
- Log.i(TAG, "No extraction needed for " + extractedFile + " of size " +
- extractedFile.length());
+ }
+ if (!isExtractionSuccessful) {
+ throw new IOException("Could not create zip file " +
+ extractedFile.getAbsolutePath() + " for secondary dex (" +
+ secondaryNumber + ")");
}
secondaryNumber++;
dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
@@ -154,69 +228,17 @@ final class MultiDexExtractor {
return files;
}
- /**
- * Iterate through the expected dex files, classes.dex, classes2.dex, classes3.dex, etc. and
- * return the CRC of each zip entry in a list.
- */
- private static ArrayList<Long> getAllDexCrcs(ZipFile apk) {
- ArrayList<Long> dexCrcs = new ArrayList<Long>();
-
- // Add the first one
- dexCrcs.add(apk.getEntry(DEX_PREFIX + DEX_SUFFIX).getCrc());
-
- // Get the number of dex files in the apk.
- int secondaryNumber = 2;
- while (true) {
- ZipEntry dexEntry = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
- if (dexEntry == null) {
- break;
- }
-
- dexCrcs.add(dexEntry.getCrc());
- secondaryNumber++;
- }
- return dexCrcs;
- }
-
- /**
- * Returns true if the number of dex files is different than what is stored in the shared
- * preferences file or if any dex CRC value is different.
- */
- private static boolean isAnyDexCrcDifferent(Context context, ArrayList<Long> dexCrcs) {
- final ArrayList<Long> storedDexCrcs = getStoredDexCrcs(context);
-
- if (dexCrcs.size() != storedDexCrcs.size()) {
- return true;
- }
-
- // We know the length of storedDexCrcs and dexCrcs are the same.
- for (int i = 0; i < storedDexCrcs.size(); i++) {
- if (storedDexCrcs.get(i).longValue() != dexCrcs.get(i).longValue()) {
- return true;
- }
- }
-
- // All the same
- return false;
- }
-
- private static ArrayList<Long> getStoredDexCrcs(Context context) {
- SharedPreferences prefs = getMultiDexPreferences(context);
- int numDexFiles = prefs.getInt(KEY_NUM_DEX_FILES, 0);
- ArrayList<Long> dexCrcs = new ArrayList<Long>(numDexFiles);
- for (int i = 0; i < numDexFiles; i++) {
- dexCrcs.add(prefs.getLong(makeDexCrcKey(i), 0));
- }
- return dexCrcs;
- }
-
- private static void putStoredDexCrcs(Context context, ArrayList<Long> dexCrcs) {
+ private static void putStoredApkInfo(Context context, long timeStamp, long crc,
+ int totalDexNumber) {
SharedPreferences prefs = getMultiDexPreferences(context);
SharedPreferences.Editor edit = prefs.edit();
- edit.putInt(KEY_NUM_DEX_FILES, dexCrcs.size());
- for (int i = 0; i < dexCrcs.size(); i++) {
- edit.putLong(makeDexCrcKey(i), dexCrcs.get(i));
- }
+ edit.putLong(KEY_TIME_STAMP, timeStamp);
+ edit.putLong(KEY_CRC, crc);
+ /* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the
+ * requested modifications" it should be OK to rely on saving the dex files number (getting
+ * old number value would go along with old crc and time stamp).
+ */
+ edit.putInt(KEY_DEX_NUMBER, totalDexNumber);
apply(edit);
}
@@ -227,10 +249,6 @@ final class MultiDexExtractor {
: Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
- private static String makeDexCrcKey(int i) {
- return KEY_PREFIX_DEX_CRC + Integer.toString(i);
- }
-
/**
* This removes any files that do not have the correct prefix.
*/
@@ -338,7 +356,7 @@ final class MultiDexExtractor {
private static Method sApplyMethod; // final
static {
try {
- Class cls = SharedPreferences.Editor.class;
+ Class<?> cls = SharedPreferences.Editor.class;
sApplyMethod = cls.getMethod("apply");
} catch (NoSuchMethodException unused) {
sApplyMethod = null;
diff --git a/library/src/android/support/multidex/ZipUtil.java b/library/src/android/support/multidex/ZipUtil.java
new file mode 100644
index 0000000..cd518cc
--- /dev/null
+++ b/library/src/android/support/multidex/ZipUtil.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/* Apache Harmony HEADER because the code in this class comes mostly from ZipFile, ZipEntry and
+ * ZipConstants from android libcore.
+ */
+
+package android.support.multidex;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.zip.CRC32;
+import java.util.zip.ZipException;
+
+/**
+ * Tools to build a quick partial crc of zip files.
+ */
+final class ZipUtil {
+ static class CentralDirectory {
+ long offset;
+ long size;
+ }
+
+ /* redefine those constant here because of bug 13721174 preventing to compile using the
+ * constants defined in ZipFile */
+ private static final int ENDHDR = 22;
+ private static final int ENDSIG = 0x6054b50;
+
+ /**
+ * Size of reading buffers.
+ */
+ private static final int BUFFER_SIZE = 0x4000;
+
+ /**
+ * Compute crc32 of the central directory of an apk. The central directory contains
+ * the crc32 of each entries in the zip so the computed result is considered valid for the whole
+ * zip file. Does not support zip64 nor multidisk but it should be OK for now since ZipFile does
+ * not either.
+ */
+ static long getZipCrc(File apk) throws IOException {
+ RandomAccessFile raf = new RandomAccessFile(apk, "r");
+ try {
+ CentralDirectory dir = findCentralDirectory(raf);
+
+ return computeCrcOfCentralDir(raf, dir);
+ } finally {
+ raf.close();
+ }
+ }
+
+ /* Package visible for testing */
+ static CentralDirectory findCentralDirectory(RandomAccessFile raf) throws IOException,
+ ZipException {
+ long scanOffset = raf.length() - ENDHDR;
+ if (scanOffset < 0) {
+ throw new ZipException("File too short to be a zip file: " + raf.length());
+ }
+
+ long stopOffset = scanOffset - 0x10000 /* ".ZIP file comment"'s max length */;
+ if (stopOffset < 0) {
+ stopOffset = 0;
+ }
+
+ int endSig = Integer.reverseBytes(ENDSIG);
+ while (true) {
+ raf.seek(scanOffset);
+ if (raf.readInt() == endSig) {
+ break;
+ }
+
+ scanOffset--;
+ if (scanOffset < stopOffset) {
+ throw new ZipException("End Of Central Directory signature not found");
+ }
+ }
+ // Read the End Of Central Directory. ENDHDR includes the signature
+ // bytes,
+ // which we've already read.
+
+ // Pull out the information we need.
+ raf.skipBytes(2); // diskNumber
+ raf.skipBytes(2); // diskWithCentralDir
+ raf.skipBytes(2); // numEntries
+ raf.skipBytes(2); // totalNumEntries
+ CentralDirectory dir = new CentralDirectory();
+ dir.size = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
+ dir.offset = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
+ return dir;
+ }
+
+ /* Package visible for testing */
+ static long computeCrcOfCentralDir(RandomAccessFile raf, CentralDirectory dir)
+ throws IOException {
+ CRC32 crc = new CRC32();
+ long stillToRead = dir.size;
+ raf.seek(dir.offset);
+ int length = (int) Math.min(BUFFER_SIZE, stillToRead);
+ byte[] buffer = new byte[BUFFER_SIZE];
+ length = raf.read(buffer, 0, length);
+ while (length != -1) {
+ crc.update(buffer, 0, length);
+ stillToRead -= length;
+ if (stillToRead == 0) {
+ break;
+ }
+ length = (int) Math.min(BUFFER_SIZE, stillToRead);
+ length = raf.read(buffer, 0, length);
+ }
+ return crc.getValue();
+ }
+}
diff --git a/library/test/.classpath b/library/test/.classpath
new file mode 100644
index 0000000..e3d0af0
--- /dev/null
+++ b/library/test/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/android-support-multidex"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/library/test/.project b/library/test/.project
new file mode 100644
index 0000000..29f077e
--- /dev/null
+++ b/library/test/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>android-support-multidex-tests</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/library/test/src/android/support/multidex/ZipEntryReader.java b/library/test/src/android/support/multidex/ZipEntryReader.java
new file mode 100644
index 0000000..c10bec5
--- /dev/null
+++ b/library/test/src/android/support/multidex/ZipEntryReader.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/* Apache Harmony HEADER because the code in this class comes mostly from ZipFile, ZipEntry and
+ * ZipConstants from android libcore.
+ */
+
+package android.support.multidex;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+
+class ZipEntryReader {
+ static final Charset UTF_8 = Charset.forName("UTF-8");
+ /**
+ * General Purpose Bit Flags, Bit 0.
+ * If set, indicates that the file is encrypted.
+ */
+ private static final int GPBF_ENCRYPTED_FLAG = 1 << 0;
+
+ /**
+ * Supported General Purpose Bit Flags Mask.
+ * Bit mask of bits not supported.
+ * Note: The only bit that we will enforce at this time
+ * is the encrypted bit. Although other bits are not supported,
+ * we must not enforce them as this could break some legitimate
+ * use cases (See http://b/8617715).
+ */
+ private static final int GPBF_UNSUPPORTED_MASK = GPBF_ENCRYPTED_FLAG;
+ private static final long CENSIG = 0x2014b50;
+
+ static ZipEntry readEntry(ByteBuffer in) throws IOException {
+
+ int sig = in.getInt();
+ if (sig != CENSIG) {
+ throw new ZipException("Central Directory Entry not found");
+ }
+
+ in.position(8);
+ int gpbf = in.getShort() & 0xffff;
+
+ if ((gpbf & GPBF_UNSUPPORTED_MASK) != 0) {
+ throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf);
+ }
+
+ int compressionMethod = in.getShort() & 0xffff;
+ int time = in.getShort() & 0xffff;
+ int modDate = in.getShort() & 0xffff;
+
+ // These are 32-bit values in the file, but 64-bit fields in this object.
+ long crc = ((long) in.getInt()) & 0xffffffffL;
+ long compressedSize = ((long) in.getInt()) & 0xffffffffL;
+ long size = ((long) in.getInt()) & 0xffffffffL;
+
+ int nameLength = in.getShort() & 0xffff;
+ int extraLength = in.getShort() & 0xffff;
+ int commentByteCount = in.getShort() & 0xffff;
+
+ // This is a 32-bit value in the file, but a 64-bit field in this object.
+ in.position(42);
+ long localHeaderRelOffset = ((long) in.getInt()) & 0xffffffffL;
+
+ byte[] nameBytes = new byte[nameLength];
+ in.get(nameBytes, 0, nameBytes.length);
+ String name = new String(nameBytes, 0, nameBytes.length, UTF_8);
+
+ ZipEntry entry = new ZipEntry(name);
+ entry.setMethod(compressionMethod);
+ entry.setTime(getTime(time, modDate));
+
+ entry.setCrc(crc);
+ entry.setCompressedSize(compressedSize);
+ entry.setSize(size);
+
+ // The RI has always assumed UTF-8. (If GPBF_UTF8_FLAG isn't set, the encoding is
+ // actually IBM-437.)
+ if (commentByteCount > 0) {
+ byte[] commentBytes = new byte[commentByteCount];
+ in.get(commentBytes, 0, commentByteCount);
+ entry.setComment(new String(commentBytes, 0, commentBytes.length, UTF_8));
+ }
+
+ if (extraLength > 0) {
+ byte[] extra = new byte[extraLength];
+ in.get(extra, 0, extraLength);
+ entry.setExtra(extra);
+ }
+
+ return entry;
+
+ }
+
+ private static long getTime(int time, int modDate) {
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(1980 + ((modDate >> 9) & 0x7f), ((modDate >> 5) & 0xf) - 1,
+ modDate & 0x1f, (time >> 11) & 0x1f, (time >> 5) & 0x3f,
+ (time & 0x1f) << 1);
+ return cal.getTime().getTime();
+ }
+
+}
diff --git a/library/test/src/android/support/multidex/ZipUtilTest.java b/library/test/src/android/support/multidex/ZipUtilTest.java
new file mode 100644
index 0000000..985d97f
--- /dev/null
+++ b/library/test/src/android/support/multidex/ZipUtilTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.multidex;
+
+import android.support.multidex.ZipUtil.CentralDirectory;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+/**
+ * Tests of ZipUtil class.
+ *
+ * The test assumes that ANDROID_BUILD_TOP environment variable is defined and point to the top of a
+ * built android tree. This is the case when the console used for running the tests is setup for
+ * android tree compilation.
+ */
+public class ZipUtilTest {
+ private static final File zipFile = new File(System.getenv("ANDROID_BUILD_TOP"),
+ "out/target/common/obj/JAVA_LIBRARIES/android-support-multidex_intermediates/javalib.jar");
+ @BeforeClass
+ public static void setupClass() throws ZipException, IOException {
+ // just verify the zip is valid
+ new ZipFile(zipFile).close();
+ }
+
+ @Test
+ public void testCrcDoNotCrash() throws IOException {
+
+ long crc =
+ ZipUtil.getZipCrc(zipFile);
+ System.out.println("crc is " + crc);
+
+ }
+
+ @Test
+ public void testCrcRange() throws IOException {
+ RandomAccessFile raf = new RandomAccessFile(zipFile, "r");
+ CentralDirectory dir = ZipUtil.findCentralDirectory(raf);
+ byte[] dirData = new byte[(int) dir.size];
+ int length = dirData.length;
+ int off = 0;
+ raf.seek(dir.offset);
+ while (length > 0) {
+ int read = raf.read(dirData, off, length);
+ if (length == -1) {
+ throw new EOFException();
+ }
+ length -= read;
+ off += read;
+ }
+ raf.close();
+ ByteBuffer buffer = ByteBuffer.wrap(dirData);
+ Map<String, ZipEntry> toCheck = new HashMap<String, ZipEntry>();
+ while (buffer.hasRemaining()) {
+ buffer = buffer.slice();
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ ZipEntry entry = ZipEntryReader.readEntry(buffer);
+ toCheck.put(entry.getName(), entry);
+ }
+
+ ZipFile zip = new ZipFile(zipFile);
+ Assert.assertEquals(zip.size(), toCheck.size());
+ Enumeration<? extends ZipEntry> ref = zip.entries();
+ while (ref.hasMoreElements()) {
+ ZipEntry refEntry = ref.nextElement();
+ ZipEntry checkEntry = toCheck.get(refEntry.getName());
+ Assert.assertNotNull(checkEntry);
+ Assert.assertEquals(refEntry.getName(), checkEntry.getName());
+ Assert.assertEquals(refEntry.getComment(), checkEntry.getComment());
+ Assert.assertEquals(refEntry.getTime(), checkEntry.getTime());
+ Assert.assertEquals(refEntry.getCrc(), checkEntry.getCrc());
+ Assert.assertEquals(refEntry.getCompressedSize(), checkEntry.getCompressedSize());
+ Assert.assertEquals(refEntry.getSize(), checkEntry.getSize());
+ Assert.assertEquals(refEntry.getMethod(), checkEntry.getMethod());
+ Assert.assertArrayEquals(refEntry.getExtra(), checkEntry.getExtra());
+ }
+ zip.close();
+ }
+
+ @Test
+ public void testCrcValue() throws IOException {
+ ZipFile zip = new ZipFile(zipFile);
+ Enumeration<? extends ZipEntry> ref = zip.entries();
+ byte[] buffer = new byte[0x2000];
+ while (ref.hasMoreElements()) {
+ ZipEntry refEntry = ref.nextElement();
+ if (refEntry.getSize() > 0) {
+ File tmp = File.createTempFile("ZipUtilTest", ".fakezip");
+ InputStream in = zip.getInputStream(refEntry);
+ OutputStream out = new FileOutputStream(tmp);
+ int read = in.read(buffer);
+ while (read != -1) {
+ out.write(buffer, 0, read);
+ read = in.read(buffer);
+ }
+ in.close();
+ out.close();
+ RandomAccessFile raf = new RandomAccessFile(tmp, "r");
+ CentralDirectory dir = new CentralDirectory();
+ dir.offset = 0;
+ dir.size = raf.length();
+ long crc = ZipUtil.computeCrcOfCentralDir(raf, dir);
+ Assert.assertEquals(refEntry.getCrc(), crc);
+ raf.close();
+ tmp.delete();
+ }
+ }
+ zip.close();
+ }
+ @Test
+ public void testInvalidCrcValue() throws IOException {
+ ZipFile zip = new ZipFile(zipFile);
+ Enumeration<? extends ZipEntry> ref = zip.entries();
+ byte[] buffer = new byte[0x2000];
+ while (ref.hasMoreElements()) {
+ ZipEntry refEntry = ref.nextElement();
+ if (refEntry.getSize() > 0) {
+ File tmp = File.createTempFile("ZipUtilTest", ".fakezip");
+ InputStream in = zip.getInputStream(refEntry);
+ OutputStream out = new FileOutputStream(tmp);
+ int read = in.read(buffer);
+ while (read != -1) {
+ out.write(buffer, 0, read);
+ read = in.read(buffer);
+ }
+ in.close();
+ out.close();
+ RandomAccessFile raf = new RandomAccessFile(tmp, "r");
+ CentralDirectory dir = new CentralDirectory();
+ dir.offset = 0;
+ dir.size = raf.length() - 1;
+ long crc = ZipUtil.computeCrcOfCentralDir(raf, dir);
+ Assert.assertNotEquals(refEntry.getCrc(), crc);
+ raf.close();
+ tmp.delete();
+ }
+ }
+ zip.close();
+ }
+
+}