Replace deprecated MoreExecutors.sameThreadExecutor(). am: a5c71db7e0 am: 07b7df8a83
am: 592d1eec01
Change-Id: Id38c59228712fa925e8ae1fa286111c8b37e245e
diff --git a/BUILD b/BUILD
index c334326..0094d45 100644
--- a/BUILD
+++ b/BUILD
@@ -1,5 +1,3 @@
-# Bazel (https://bazel.io/) BUILD file for apkzlib library.
-
licenses(["notice"]) # Apache License 2.0
java_library(
@@ -7,9 +5,10 @@
srcs = glob([
"src/main/java/**/*.java",
]),
- visibility = ["//visibility:public"],
+ visibility = ["//tools/base/build-system/builder:__pkg__"],
deps = [
- "//tools/base/annotations",
+ "//tools/apksig",
+ "//tools/base/third_party:com.google.code.findbugs_jsr305",
"//tools/base/third_party:com.google.guava_guava",
"//tools/base/third_party:org.bouncycastle_bcpkix-jdk15on",
"//tools/base/third_party:org.bouncycastle_bcprov-jdk15on",
@@ -18,21 +17,17 @@
java_test(
name = "apkzlib_tests",
- srcs = glob([
- "src/test/java/**/*.java",
- ]),
+ srcs = glob(["src/test/java/**/*.java"]),
jvm_flags = ["-Dtest.suite.jar=tests.jar"],
resources = glob(["src/test/resources/**"]),
- tags = ["manual"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":apkzlib",
- "//tools/base/annotations",
"//tools/base/testutils:tools.testutils",
"//tools/base/third_party:com.google.guava_guava",
"//tools/base/third_party:junit_junit",
"//tools/base/third_party:org.bouncycastle_bcpkix-jdk15on",
"//tools/base/third_party:org.bouncycastle_bcprov-jdk15on",
- "//tools/base/third_party:org.mockito_mockito-all",
+ "//tools/base/third_party:org.mockito_mockito-core",
],
)
diff --git a/apkzlib.iml b/apkzlib.iml
index 409bf1b..999fffa 100644
--- a/apkzlib.iml
+++ b/apkzlib.iml
@@ -9,10 +9,13 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
- <orderEntry type="module" module-name="android-annotations" />
+ <orderEntry type="library" name="jsr305" level="project" />
<orderEntry type="library" name="guava-tools" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="library" name="bouncy-castle" level="project" />
+ <orderEntry type="module" module-name="testutils" scope="TEST" />
+ <orderEntry type="module" module-name="apksig" />
+ <orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 48a1528..771d1b8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,14 +1,15 @@
-apply plugin: 'java'
+apply from: "$rootDir/buildSrc/base/baseJava.gradle"
dependencies {
- compile project(':base:annotations')
-
+ compile 'com.google.code.findbugs:jsr305:1.3.9'
compile 'com.google.guava:guava:18.0'
- compile 'org.bouncycastle:bcpkix-jdk15on:1.48'
- compile 'org.bouncycastle:bcprov-jdk15on:1.48'
+ compile 'org.bouncycastle:bcpkix-jdk15on:1.56'
+ compile 'org.bouncycastle:bcprov-jdk15on:1.56'
+ compile project(':apksig')
testCompile 'junit:junit:4.12'
- testCompile 'org.mockito:mockito-all:1.9.5'
+ testCompile 'org.mockito:mockito-core:2.7.1'
+ testCompile project(':base:testutils')
}
configurations {
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/DigestAlgorithm.java b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
similarity index 63%
rename from src/main/java/com/android/builder/internal/packaging/sign/DigestAlgorithm.java
rename to src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
index 13cbf6c..64427ae 100644
--- a/src/main/java/com/android/builder/internal/packaging/sign/DigestAlgorithm.java
+++ b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
-import com.android.annotations.NonNull;
+import javax.annotation.Nonnull;
/**
* Message digest algorithms.
@@ -40,33 +40,34 @@
SHA256("SHA-256", "SHA-256");
/**
- * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA}.
+ * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and
+ * {@link SignatureAlgorithm#ECDSA}.
*/
- public static final int API_SHA_256_RSA = 18;
+ public static final int API_SHA_256_RSA_AND_ECDSA = 18;
/**
* API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s.
*
- * <p>Before that, SHA256 can only be used with RSA.
+ * <p>Before that, SHA256 can only be used with RSA and ECDSA.
*/
public static final int API_SHA_256_ALL_ALGORITHMS = 21;
/**
* Name of algorithm for message digest.
*/
- @NonNull
+ @Nonnull
public final String messageDigestName;
/**
* Name of attribute in signature file with the manifest digest.
*/
- @NonNull
+ @Nonnull
public final String manifestAttributeName;
/**
* Name of attribute in entry (both manifest and signature file) with the entry's digest.
*/
- @NonNull
+ @Nonnull
public final String entryAttributeName;
/**
@@ -75,31 +76,9 @@
* @param attributeName attribute name in the signature file
* @param messageDigestName name of algorithm for message digest
*/
- DigestAlgorithm(@NonNull String attributeName, @NonNull String messageDigestName) {
+ DigestAlgorithm(@Nonnull String attributeName, @Nonnull String messageDigestName) {
this.messageDigestName = messageDigestName;
this.entryAttributeName = attributeName + "-Digest";
this.manifestAttributeName = attributeName + "-Digest-Manifest";
}
-
- /**
- * Finds the best digest algorithm applicable for a given SDK.
- *
- * @param minSdk the minimum SDK
- * @param signatureAlgorithm signature algorithm used
- * @return the best algorithm found
- */
- @NonNull
- public static DigestAlgorithm findBest(
- int minSdk,
- @NonNull SignatureAlgorithm signatureAlgorithm) {
- if (signatureAlgorithm == SignatureAlgorithm.RSA) {
- // PKCS #7 RSA signatures with SHA-256 are
- // supported only since API Level 18 (JB MR2).
- return minSdk >= API_SHA_256_RSA ? SHA256 : SHA1;
- } else {
- // PKCS #7 ECDSA and DSA signatures with SHA-256
- // are supported only since API Level 21 (Android L).
- return minSdk >= API_SHA_256_ALL_ALGORITHMS ? SHA256 : SHA1;
- }
- }
}
diff --git a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
new file mode 100644
index 0000000..33c47c6
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.sign;
+
+import com.android.apkzlib.utils.CachedSupplier;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zfile.ManifestAttributes;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileExtension;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Verify;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Extension to {@link ZFile} that will generate a manifest. The extension will register
+ * automatically with the {@link ZFile}.
+ *
+ * <p>Creating this extension will ensure a manifest for the zip exists.
+ * This extension will generate a manifest if one does not exist and will update an existing
+ * manifest, if one does exist. The extension will also provide access to the manifest so that
+ * others may update the manifest.
+ *
+ * <p>Apart from standard manifest elements, this extension does not handle any particular manifest
+ * features such as signing or adding custom attributes. It simply generates a plain manifest and
+ * provides infrastructure so that other extensions can add data in the manifest.
+ *
+ * <p>The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()}
+ * notification is received, meaning all manifest manipulation is done in-memory.
+ */
+public class ManifestGenerationExtension {
+
+ /**
+ * Name of META-INF directory.
+ */
+ private static final String META_INF_DIR = "META-INF";
+
+ /**
+ * Name of the manifest file.
+ */
+ static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
+
+ /**
+ * Who should be reported as the manifest builder.
+ */
+ @Nonnull
+ private final String builtBy;
+
+ /**
+ * Who should be reported as the manifest creator.
+ */
+ @Nonnull
+ private final String createdBy;
+
+ /**
+ * The file this extension is attached to. {@code null} if not yet registered.
+ */
+ @Nullable
+ private ZFile zFile;
+
+ /**
+ * The zip file's manifest.
+ */
+ @Nonnull
+ private final Manifest manifest;
+
+ /**
+ * Byte representation of the manifest. There is no guarantee that two writes of the java's
+ * {@code Manifest} object will yield the same byte array (there is no guaranteed order
+ * of entries in the manifest).
+ *
+ * <p>Because we need the byte representation of the manifest to be stable if there are
+ * no changes to the manifest, we cannot rely on {@code Manifest} to generate the byte
+ * representation every time we need the byte representation.
+ *
+ * <p>This cache will ensure that we will request one byte generation from the {@code Manifest}
+ * and will cache it. All further requests of the manifest's byte representation will
+ * receive the same byte array.
+ */
+ @Nonnull
+ private CachedSupplier<byte[]> manifestBytes;
+
+ /**
+ * Has the current manifest been changed and not yet flushed? If {@link #dirty} is
+ * {@code true}, then {@link #manifestBytes} should not be valid. This means that
+ * marking the manifest as dirty should also invalidate {@link #manifestBytes}. To avoid
+ * breaking the invariant, instead of setting {@link #dirty}, {@link #markDirty()} should
+ * be called.
+ */
+ private boolean dirty;
+
+ /**
+ * The extension to register with the {@link ZFile}. {@code null} if not registered.
+ */
+ @Nullable
+ private ZFileExtension extension;
+
+ /**
+ * Creates a new extension. This will not register the extension with the provided
+ * {@link ZFile}. Until {@link #register(ZFile)} is invoked, this extension is not used.
+ *
+ * @param builtBy who built the manifest?
+ * @param createdBy who created the manifest?
+ */
+ public ManifestGenerationExtension(@Nonnull String builtBy, @Nonnull String createdBy) {
+ this.builtBy = builtBy;
+ this.createdBy = createdBy;
+ manifest = new Manifest();
+ dirty = false;
+ manifestBytes = new CachedSupplier<>(() -> {
+ ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
+ try {
+ manifest.write(outBytes);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ return outBytes.toByteArray();
+ });
+ }
+
+ /**
+ * Marks the manifest as being dirty, <i>i.e.</i>, its data has changed since it was last
+ * read and/or written.
+ */
+ private void markDirty() {
+ dirty = true;
+ manifestBytes.reset();
+ }
+
+ /**
+ * Registers the extension with the {@link ZFile} provided in the constructor.
+ *
+ * @param zFile the zip file to add the extension to
+ * @throws IOException failed to analyze the zip
+ */
+ public void register(@Nonnull ZFile zFile) throws IOException {
+ Preconditions.checkState(extension == null, "register() has already been invoked.");
+ this.zFile = zFile;
+
+ rebuildManifest();
+
+ extension = new ZFileExtension() {
+ @Nullable
+ @Override
+ public IOExceptionRunnable beforeUpdate() {
+ return ManifestGenerationExtension.this::updateManifest;
+ }
+ };
+
+ this.zFile.addZFileExtension(extension);
+ }
+
+ /**
+ * Rebuilds the zip file's manifest, if it needs changes.
+ */
+ private void rebuildManifest() throws IOException {
+ Verify.verifyNotNull(zFile, "zFile == null");
+
+ StoredEntry manifestEntry = zFile.get(MANIFEST_NAME);
+
+ if (manifestEntry != null) {
+ /*
+ * Read the manifest entry in the zip file. Make sure we store these byte sequence
+ * because writing the manifest may not generate the same byte sequence, which may
+ * trigger an unnecessary re-sign of the jar.
+ */
+ manifest.clear();
+ byte[] manifestBytes = manifestEntry.read();
+ manifest.read(new ByteArrayInputStream(manifestBytes));
+ this.manifestBytes.precomputed(manifestBytes);
+ }
+
+ Attributes mainAttributes = manifest.getMainAttributes();
+ String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION);
+ if (currentVersion == null) {
+ setMainAttribute(
+ ManifestAttributes.MANIFEST_VERSION,
+ ManifestAttributes.CURRENT_MANIFEST_VERSION);
+ } else {
+ if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) {
+ throw new IOException("Unsupported manifest version: " + currentVersion + ".");
+ }
+ }
+
+ /*
+ * We "blindly" override all other main attributes.
+ */
+ setMainAttribute(ManifestAttributes.BUILT_BY, builtBy);
+ setMainAttribute(ManifestAttributes.CREATED_BY, createdBy);
+ }
+
+ /**
+ * Sets the value of a main attribute.
+ *
+ * @param attribute the attribute
+ * @param value the value
+ */
+ private void setMainAttribute(@Nonnull String attribute, @Nonnull String value) {
+ Attributes mainAttributes = manifest.getMainAttributes();
+ String current = mainAttributes.getValue(attribute);
+ if (!value.equals(current)) {
+ mainAttributes.putValue(attribute, value);
+ markDirty();
+ }
+ }
+
+ /**
+ * Updates the manifest in the zip file, if it has been changed.
+ *
+ * @throws IOException failed to update the manifest
+ */
+ private void updateManifest() throws IOException {
+ Verify.verifyNotNull(zFile, "zFile == null");
+
+ if (!dirty) {
+ return;
+ }
+
+ zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get()));
+ dirty = false;
+ }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/SignatureAlgorithm.java b/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
similarity index 80%
rename from src/main/java/com/android/builder/internal/packaging/sign/SignatureAlgorithm.java
rename to src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
index e171fa2..0667252 100644
--- a/src/main/java/com/android/builder/internal/packaging/sign/SignatureAlgorithm.java
+++ b/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,36 +14,26 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.sign;
import java.security.NoSuchAlgorithmException;
+import javax.annotation.Nonnull;
/**
* Signature algorithm.
*/
public enum SignatureAlgorithm {
- /**
- * RSA algorithm.
- */
- RSA("RSA", 0, "withRSA"),
+ /** RSA algorithm. */
+ RSA("RSA", 1, "withRSA"),
- /**
- * ECDSA algorithm.
- */
+ /** ECDSA algorithm. */
ECDSA("EC", 18, "withECDSA"),
- /**
- * DSA algorithm.
- */
- DSA("DSA", 0, "withDSA");
+ /** DSA algorithm. */
+ DSA("DSA", 1, "withDSA");
- /**
- * Name of the private key as reported by {@code PrivateKey}.
- */
- @NonNull
- public final String keyAlgorithm;
+ /** Name of the private key as reported by {@code PrivateKey}. */
+ @Nonnull public final String keyAlgorithm;
/**
* Minimum SDK version that allows this signature.
@@ -53,7 +43,7 @@
/**
* Suffix appended to digest algorithm to obtain signature algorithm.
*/
- @NonNull
+ @Nonnull
public final String signatureAlgorithmSuffix;
/**
@@ -64,7 +54,7 @@
* @param signatureAlgorithmSuffix suffix for signature name with used with a digest
*/
SignatureAlgorithm(
- @NonNull String keyAlgorithm, int minSdkVersion, @NonNull String signatureAlgorithmSuffix) {
+ @Nonnull String keyAlgorithm, int minSdkVersion, @Nonnull String signatureAlgorithmSuffix) {
this.keyAlgorithm = keyAlgorithm;
this.minSdkVersion = minSdkVersion;
this.signatureAlgorithmSuffix = signatureAlgorithmSuffix;
@@ -80,8 +70,8 @@
* @throws NoSuchAlgorithmException if no algorithm was found for the given private key; an
* algorithm was found but is not applicable to the given SDK version
*/
- @NonNull
- public static SignatureAlgorithm fromKeyAlgorithm(@NonNull String keyAlgorithm,
+ @Nonnull
+ public static SignatureAlgorithm fromKeyAlgorithm(@Nonnull String keyAlgorithm,
int minSdkVersion) throws NoSuchAlgorithmException {
for (SignatureAlgorithm alg : values()) {
if (alg.keyAlgorithm.equalsIgnoreCase(keyAlgorithm)) {
@@ -106,8 +96,8 @@
* @param digestAlgorithm the digest algorithm to use
* @return the name of the signature algorithm
*/
- @NonNull
- public String signatureAlgorithmName(@NonNull DigestAlgorithm digestAlgorithm) {
+ @Nonnull
+ public String signatureAlgorithmName(@Nonnull DigestAlgorithm digestAlgorithm) {
return digestAlgorithm.messageDigestName.replace("-", "") + signatureAlgorithmSuffix;
}
}
diff --git a/src/main/java/com/android/apkzlib/sign/SigningExtension.java b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
new file mode 100644
index 0000000..2685aa1
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apkzlib.sign;
+
+import com.android.apksig.ApkSignerEngine;
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.DefaultApkSignerEngine;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileExtension;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ZFile} extension which signs the APK.
+ *
+ * <p>
+ * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature
+ * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this
+ * extension's constructor.
+ */
+public class SigningExtension {
+ // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive
+ // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.
+ // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile
+ // extension events/callbacks.
+ //
+ // The main issue leading to additional complexity in this class is that the current build
+ // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that
+ // matter) for incremental builds. Thus:
+ // * ZFile extension receives no events for JAR entries already in the APK whereas
+ // ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this
+ // class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries
+ // already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR
+ // entries which the incremental build session did not touch.
+ // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed
+ // from it whereas ApkSignerEngine produces no output only if it has already produced a signed
+ // APK and no changes have since been made to it. This class addresses this issue by checking
+ // in its "register" method whether the APK is correctly signed and, only if that's the case,
+ // doesn't modify the APK unless a JAR entry is added to it or removed from it after
+ // "register".
+
+ /**
+ * Minimum API Level on which this APK is supposed to run.
+ */
+ private final int minSdkVersion;
+
+ /**
+ * Whether JAR signing (aka v1 signing) is enabled.
+ */
+ private final boolean v1SigningEnabled;
+
+ /**
+ * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled.
+ */
+ private final boolean v2SigningEnabled;
+
+ /**
+ * Certificate of the signer, to be embedded into the APK's signature.
+ */
+ @Nonnull
+ private final X509Certificate certificate;
+
+ /**
+ * APK signer which performs most of the heavy lifting.
+ */
+ @Nonnull
+ private final ApkSignerEngine signer;
+
+ /**
+ * Names of APK entries which have been processed by {@link #signer}.
+ */
+ private final Set<String> signerProcessedOutputEntryNames = new HashSet<>();
+
+ /**
+ * Cached contents of the most recently output APK Signing Block or {@code null} if the block
+ * hasn't yet been output.
+ */
+ @Nullable
+ private byte[] cachedApkSigningBlock;
+
+ /**
+ * {@code true} if signatures may need to be output, {@code false} if there's no need to output
+ * signatures. This is used in an optimization where we don't modify the APK if it's already
+ * signed and if no JAR entries have been added to or removed from the file.
+ */
+ private boolean dirty;
+
+ /**
+ * The extension registered with the {@link ZFile}. {@code null} if not registered.
+ */
+ @Nullable
+ private ZFileExtension extension;
+
+ /**
+ * The file this extension is attached to. {@code null} if not yet registered.
+ */
+ @Nullable
+ private ZFile zFile;
+
+ public SigningExtension(
+ int minSdkVersion,
+ @Nonnull X509Certificate certificate,
+ @Nonnull PrivateKey privateKey,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled) throws InvalidKeyException {
+ DefaultApkSignerEngine.SignerConfig signerConfig =
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ "CERT", privateKey, ImmutableList.of(certificate)).build();
+ signer =
+ new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion)
+ .setOtherSignersSignaturesPreserved(false)
+ .setV1SigningEnabled(v1SigningEnabled)
+ .setV2SigningEnabled(v2SigningEnabled)
+ .setCreatedBy("1.0 (Android)")
+ .build();
+ this.minSdkVersion = minSdkVersion;
+ this.v1SigningEnabled = v1SigningEnabled;
+ this.v2SigningEnabled = v2SigningEnabled;
+ this.certificate = certificate;
+ }
+
+ public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException {
+ Preconditions.checkState(extension == null, "register() already invoked");
+ this.zFile = zFile;
+ dirty = !isCurrentSignatureAsRequested();
+ extension = new ZFileExtension() {
+ @Override
+ public IOExceptionRunnable added(
+ @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
+ return () -> onZipEntryOutput(entry);
+ }
+
+ @Override
+ public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ return () -> onZipEntryRemovedFromOutput(entryName);
+ }
+
+ @Override
+ public IOExceptionRunnable beforeUpdate() throws IOException {
+ return () -> onOutputZipReadyForUpdate();
+ }
+
+ @Override
+ public void entriesWritten() throws IOException {
+ onOutputZipEntriesWritten();
+ }
+
+ @Override
+ public void closed() {
+ onOutputClosed();
+ }
+ };
+ this.zFile.addZFileExtension(extension);
+ }
+
+ /**
+ * Returns {@code true} if the APK's signatures are as requested by parameters to this signing
+ * extension.
+ */
+ private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {
+ ApkVerifier.Result result;
+ try {
+ result =
+ new ApkVerifier.Builder(new ZFileDataSource(zFile))
+ .setMinCheckedPlatformVersion(minSdkVersion)
+ .build()
+ .verify();
+ } catch (ApkFormatException e) {
+ // Malformed APK
+ return false;
+ }
+
+ if (!result.isVerified()) {
+ // Signature(s) did not verify
+ return false;
+ }
+
+ if ((result.isVerifiedUsingV1Scheme() != v1SigningEnabled)
+ || (result.isVerifiedUsingV2Scheme() != v2SigningEnabled)) {
+ // APK isn't signed with exactly the schemes we want it to be signed
+ return false;
+ }
+
+ List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();
+ if (verifiedSignerCerts.size() != 1) {
+ // APK is not signed by exactly one signer
+ return false;
+ }
+
+ byte[] expectedEncodedCert;
+ byte[] actualEncodedCert;
+ try {
+ expectedEncodedCert = certificate.getEncoded();
+ actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ // Failed to encode signing certificates
+ return false;
+ }
+
+ if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {
+ // APK is signed by a wrong signer
+ return false;
+ }
+
+ // APK is signed the way we want it to be signed
+ return true;
+ }
+
+ private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException {
+ setDirty();
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ // This event may arrive after the entry has already been deleted. In that case, we don't
+ // report the addition of the entry to ApkSignerEngine.
+ if (entry.isDeleted()) {
+ return;
+ }
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ signer.outputJarEntry(entryName);
+ signerProcessedOutputEntryNames.add(entryName);
+ if (inspectEntryRequest != null) {
+ byte[] entryContents = entry.read();
+ inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length);
+ inspectEntryRequest.done();
+ }
+ }
+
+ private void onZipEntryRemovedFromOutput(@Nonnull String entryName) {
+ setDirty();
+ signer.outputJarEntryRemoved(entryName);
+ signerProcessedOutputEntryNames.remove(entryName);
+ }
+
+ private void onOutputZipReadyForUpdate() throws IOException {
+ if (!dirty) {
+ return;
+ }
+
+ // Notify signer engine about ZIP entries that have appeared in the output without the
+ // engine knowing. Also identify ZIP entries which disappeared from the output without the
+ // engine knowing.
+ Set<String> unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames);
+ for (StoredEntry entry : zFile.entries()) {
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ unprocessedRemovedEntryNames.remove(entryName);
+ if (!signerProcessedOutputEntryNames.contains(entryName)) {
+ // Signer engine is not yet aware that this entry is in the output
+ onZipEntryOutput(entry);
+ }
+ }
+
+ // Notify signer engine about entries which disappeared from the output without the engine
+ // knowing
+ for (String entryName : unprocessedRemovedEntryNames) {
+ onZipEntryRemovedFromOutput(entryName);
+ }
+
+ // Check whether we need to output additional JAR entries which comprise the v1 signature
+ ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;
+ try {
+ addV1SignatureRequest = signer.outputJarEntries();
+ } catch (Exception e) {
+ throw new IOException("Failed to generate v1 signature", e);
+ }
+ if (addV1SignatureRequest == null) {
+ return;
+ }
+
+ // We need to output additional JAR entries which comprise the v1 signature
+ List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =
+ new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());
+
+ // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first
+ // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by
+ // ManifestGenerationExtension.
+ for (int i = 0; i < v1SignatureEntries.size(); i++) {
+ ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);
+ String name = entry.getName();
+ if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {
+ continue;
+ }
+ if (i != 0) {
+ v1SignatureEntries.remove(i);
+ v1SignatureEntries.add(0, entry);
+ }
+ break;
+ }
+
+ // Output the JAR entries comprising the v1 signature
+ for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {
+ String name = entry.getName();
+ byte[] data = entry.getData();
+ zFile.add(name, new ByteArrayInputStream(data));
+ }
+
+ addV1SignatureRequest.done();
+ }
+
+ private void onOutputZipEntriesWritten() throws IOException {
+ if (!dirty) {
+ return;
+ }
+
+ // Check whether we should output an APK Signing Block which contains v2 signatures
+ byte[] apkSigningBlock;
+ byte[] centralDirBytes = zFile.getCentralDirectoryBytes();
+ byte[] eocdBytes = zFile.getEocdBytes();
+ ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest;
+ // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we
+ // cache the block to speed things up. The cached block is invalidated by any changes to the
+ // file (as reported to this extension).
+ if (cachedApkSigningBlock != null) {
+ apkSigningBlock = cachedApkSigningBlock;
+ addV2SignatureRequest = null;
+ } else {
+ DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));
+ DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));
+ long zipEntriesSizeBytes =
+ zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset();
+ DataSource zipEntries = new ZFileDataSource(zFile, 0, zipEntriesSizeBytes);
+ try {
+ addV2SignatureRequest = signer.outputZipSections(zipEntries, centralDir, eocd);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException
+ | ApkFormatException | IOException e) {
+ throw new IOException("Failed to generate v2 signature", e);
+ }
+ apkSigningBlock =
+ (addV2SignatureRequest != null)
+ ? addV2SignatureRequest.getApkSigningBlock() : new byte[0];
+ cachedApkSigningBlock = apkSigningBlock;
+ }
+
+ // Insert the APK Signing Block into the output right before the ZIP Central Directory and
+ // accordingly update the start offset of ZIP Central Directory in ZIP End of Central
+ // Directory.
+ zFile.directWrite(
+ zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(),
+ apkSigningBlock);
+ zFile.setExtraDirectoryOffset(apkSigningBlock.length);
+
+ if (addV2SignatureRequest != null) {
+ addV2SignatureRequest.done();
+ }
+ }
+
+ private void onOutputClosed() {
+ if (!dirty) {
+ return;
+ }
+ signer.outputDone();
+ dirty = false;
+ }
+
+ private void setDirty() {
+ dirty = true;
+ cachedApkSigningBlock = null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
new file mode 100644
index 0000000..049cf35
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.sign;
+
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apkzlib.zip.ZFile;
+import com.google.common.base.Preconditions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
+
+/**
+ * {@link DataSource} backed by contents of {@link ZFile}.
+ */
+class ZFileDataSource implements DataSource {
+
+ private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+ @Nonnull
+ private final ZFile file;
+
+ /**
+ * Offset (in bytes) relative to the start of file where the region visible in this data source
+ * starts.
+ */
+ private final long offset;
+
+ /**
+ * Size (in bytes) of the file region visible in this data source or {@code -1} if the whole
+ * file is visible in this data source and thus its size may change if the file's size changes.
+ */
+ private final long size;
+
+ /**
+ * Constructs a new {@code ZFileDataSource} based on the data contained in the file. Changes to
+ * the contents of the file, including the size of the file, will be visible in this data
+ * source.
+ */
+ public ZFileDataSource(@Nonnull ZFile file) {
+ this.file = file;
+ offset = 0;
+ size = -1;
+ }
+
+ /**
+ * Constructs a new {@code ZFileDataSource} based on the data contained in the specified region
+ * of the provided file. Changes to the contents of this region of the file will be visible in
+ * this data source.
+ */
+ public ZFileDataSource(@Nonnull ZFile file, long offset, long size) {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+ Preconditions.checkArgument(size >= 0, "size < 0");
+ this.file = file;
+ this.offset = offset;
+ this.size = size;
+ }
+
+ @Override
+ public long size() {
+ if (size == -1) {
+ // Data source size is the current size of the file
+ try {
+ return file.directSize();
+ } catch (IOException e) {
+ return 0;
+ }
+ } else {
+ // Data source size is fixed
+ return size;
+ }
+ }
+
+ @Override
+ public DataSource slice(long offset, long size) {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if ((offset == 0) && (size == sourceSize)) {
+ return this;
+ }
+
+ return new ZFileDataSource(file, this.offset + offset, size);
+ }
+
+ @Override
+ public void feed(long offset, long size, @Nonnull DataSink sink) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ long chunkOffsetInFile = this.offset + offset;
+ long remaining = size;
+ byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
+ while (remaining > 0) {
+ int chunkSize = (int) Math.min(remaining, buf.length);
+ int readSize = file.directRead(chunkOffsetInFile, buf, 0, chunkSize);
+ if (readSize == -1) {
+ throw new EOFException("Premature EOF");
+ }
+ if (readSize > 0) {
+ sink.consume(buf, 0, readSize);
+ chunkOffsetInFile += readSize;
+ remaining -= readSize;
+ }
+ }
+ }
+
+ @Override
+ public void copyTo(long offset, int size, @Nonnull ByteBuffer dest) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ int prevLimit = dest.limit();
+ try {
+ file.directFullyRead(this.offset + offset, dest);
+ } finally {
+ dest.limit(prevLimit);
+ }
+ }
+
+ @Override
+ public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+ ByteBuffer result = ByteBuffer.allocate(size);
+ copyTo(offset, size, result);
+ result.flip();
+ return result;
+ }
+
+ private static void checkChunkValid(long offset, long size, long sourceSize) {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+ Preconditions.checkArgument(size >= 0, "size < 0");
+ Preconditions.checkArgument(offset <= sourceSize, "offset > sourceSize");
+ long endOffset = offset + size;
+ Preconditions.checkArgument(offset <= endOffset, "offset > endOffset");
+ Preconditions.checkArgument(endOffset <= sourceSize, "endOffset > sourceSize");
+ }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/package-info.java b/src/main/java/com/android/apkzlib/sign/package-info.java
similarity index 81%
rename from src/main/java/com/android/builder/internal/packaging/sign/package-info.java
rename to src/main/java/com/android/apkzlib/sign/package-info.java
index e8bf6e1..6bb692c 100644
--- a/src/main/java/com/android/builder/internal/packaging/sign/package-info.java
+++ b/src/main/java/com/android/apkzlib/sign/package-info.java
@@ -28,14 +28,14 @@
extensions with the {@code zip} package. These extensions are notified in changes made in the zip
and will change the zip file itself.
<p>
-The {@link com.android.builder.internal.packaging.sign.ManifestGenerationExtension} extension will
+The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will
ensure the zip has a manifest file and is, therefore, a valid jar.
-The {@link com.android.builder.internal.packaging.sign.SignatureExtension} extension will
+The {@link com.android.apkzlib.sign.SigningExtension} extension will
ensure the jar is signed.
<p>
The extension mechanism used is the one provided in the {@code zip} package (see
-{@link com.android.builder.internal.packaging.zip.ZFile}
-and {@link com.android.builder.internal.packaging.zip.ZFileExtension}. Building the zip and then
+{@link com.android.apkzlib.zip.ZFile}
+and {@link com.android.apkzlib.zip.ZFileExtension}. Building the zip and then
operating the extensions is not done sequentially, as we don't want to build a zip and then sign it.
We want to build a zip that is automatically signed. Extension are basically observers that
register on the zip and are notified when things happen in the zip. They will then modify the zip
@@ -72,7 +72,7 @@
<li>The zip is finally written with an updated manifest.</li>
</ol>
<p>
-To generate a signed apk (v1), we need to add a second extension, the {@code SignatureExtension}.
+To generate a signed apk, we need to add a second extension, the {@code SigningExtension}.
This extension will also register listeners with the {@code ZFile}.
<p>
In this case the flow would be (starting a bit earlier for clarity and assuming a package task
@@ -85,9 +85,9 @@
<li>Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.</li>
<li>The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid
manifest. No changes are done to the {@code ZFile}.</li>
- <li>Package task creates a {@code SignatureExtension}.</li>
- <li>Package task registers the {@code SignatureExtension} with the {@code ZFile}.</li>
- <li>The {@code SignatureExtension} registers a {@code ZFileExtension} with the {@code ZFile}
+ <li>Package task creates a {@code SigningExtension}.</li>
+ <li>Package task registers the {@code SigningExtension} with the {@code ZFile}.</li>
+ <li>The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile}
and look at the {@code ZFile} to see if there is a valid signature file.</li>
<li>If there are changes to the digital signature file needed, these are marked internally in
the extension. If there are changes needed to the digests, the manifest is updated (by calling
@@ -100,7 +100,7 @@
<li>For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added}
method of all registered extensions.</li>
<li>The {@code ManifestGenerationExtension} ignores added invocations.</li>
- <li>The {@code SignatureExtension} computes the digest for the added file and stores them in
+ <li>The {@code SigningExtension} computes the digest for the added file and stores them in
the manifest.<br>
<em>(when all files are added to the apk, all digests are computed and the manifest is updated
but only in memory; the apk file has not been touched; also note that {@code ZFile} has not
@@ -108,15 +108,15 @@
<li>Package task calls {@code ZFile.update()} to update the apk.</li>
<li>{@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is
done before anything is written. In this case both the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} are invoked.</li>
+ {@code SigningExtension} are invoked.</li>
<li>The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new manifest,
unless nothing has changed, in which case it does nothing.</li>
- <li>The {@code SignatureExtension} will add the SF file (unless nothing has changed), will
+ <li>The {@code SigningExtension} will add the SF file (unless nothing has changed), will
compute the digital signature of the SF file and write it to the {@code ZFile}.<br>
<em>(note that the order by which the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} are called is non-deterministic; however, this is not a problem
+ {@code SigningExtension} are called is non-deterministic; however, this is not a problem
because the manifest is already computed by the {@code ManifestGenerationExtension} at this
- time and the {@code SignatureExtension} will obtain the manifest data from the
+ time and the {@code SigningExtension} will obtain the manifest data from the
{@code ManifestGenerationExtension} and not from the {@code ZFile}; this means that the
{@code SF} file may be added to the {@code ZFile} before the {@code MF} file, but that is
irrelevant.)</em></li>
@@ -124,9 +124,8 @@
{@code ZFile.update()} method continues.</li>
<li>{@code ZFile.update()} writes all changes and new entries to the zip file.</li>
<li>{@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all
- registered extensions. Both the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} ignore this notification -- but the {@code FullApkSignExtension} will
- kick in at this point, if it has been created.</li>
+ registered extensions. {@code SigningExtension} will kick in at this point, if v2 signature
+ has changed.</li>
<li>{@code ZFile} writes the central directory and EOCD.</li>
<li>{@code ZFile.update()} returns control to the package task.</li>
<li>The package task finishes.</li>
@@ -139,18 +138,16 @@
<p>
If there are no changes to the {@code ZFile} made by the package task and the file's manifest and v1
signatures are correct, neither the {@code ManifestGenerationExtension} nor the
-{@code SignatureExtension} will not do anything on the {@code beforeUpdate()} and the
+{@code SigningExtension} will not do anything on the {@code beforeUpdate()} and the
{@code ZFile} won't even be open for writing.
<p>
This implementation provides perfect incremental updates.
<p>
Additionally, by adding/removing extensions we can configure what type of apk we want:
<ul>
- <li>No SignatureExtension & No FullApkSignExtension ⇒ Aligned, unsigned apk.</li>
- <li>Signature Extension & No FullApkSignExtension ⇒ Aligned, v1 only signed apk.</li>
- <li>Signature Extension & FullApkSignExtension ⇒ Aligned, v1 & v2 signed apk.</li>
- <li>No Signature Extension & FullApkSignExtension ⇒ Aligned, v2 only signed apk.</li>
+ <li>No SigningExtension ⇒ Aligned, unsigned apk.</li>
+ <li>SigningExtension ⇒ Aligned, signed apk.
</ul>
So, by configuring which extensions to add, the package task can decide what type of apk we want.
*/
-package com.android.builder.internal.packaging.sign;
\ No newline at end of file
+package com.android.apkzlib.sign;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/utils/ApkZLibPair.java b/src/main/java/com/android/apkzlib/utils/ApkZLibPair.java
similarity index 95%
rename from src/main/java/com/android/builder/utils/ApkZLibPair.java
rename to src/main/java/com/android/apkzlib/utils/ApkZLibPair.java
index 04c1ecf..8cf79ee 100644
--- a/src/main/java/com/android/builder/utils/ApkZLibPair.java
+++ b/src/main/java/com/android/apkzlib/utils/ApkZLibPair.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
/**
* Pair implementation to use with the {@code apkzlib} library.
diff --git a/src/main/java/com/android/builder/utils/CachedFileContents.java b/src/main/java/com/android/apkzlib/utils/CachedFileContents.java
similarity index 69%
rename from src/main/java/com/android/builder/utils/CachedFileContents.java
rename to src/main/java/com/android/apkzlib/utils/CachedFileContents.java
index 4e75aa4..f2a3331 100644
--- a/src/main/java/com/android/builder/utils/CachedFileContents.java
+++ b/src/main/java/com/android/apkzlib/utils/CachedFileContents.java
@@ -14,38 +14,39 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import com.google.common.base.Objects;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
-
import java.io.File;
import java.io.IOException;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
- * A cache for file contents. The cache allows closing a file and saving in memory its contents
- * (or some related information). It can then be used to check if the contents are still valid
- * at some later time. Typical usage flow is:
+ * A cache for file contents. The cache allows closing a file and saving in memory its contents (or
+ * some related information). It can then be used to check if the contents are still valid at some
+ * later time. Typical usage flow is:
*
* <p>
- * <pre>
- * Object fileRepresentation = // ...
- * File toWrite = // ...
- * // Write file contents and update in memory representation
- * CachedFileContents<Object> contents = new CachedFileContents<Object>(toWrite);
- * contents.closed(fileRepresentation);
*
- * // Later, when data is needed:
- * if (contents.isValid()) {
- * fileRepresentation = contents.getCache();
- * } else {
- * // Re-read the file and recreate the file representation
- * }
- * </pre>
+ * <pre>{@code
+ * Object fileRepresentation = // ...
+ * File toWrite = // ...
+ * // Write file contents and update in memory representation
+ * CachedFileContents<Object> contents = new CachedFileContents<Object>(toWrite);
+ * contents.closed(fileRepresentation);
+ *
+ * // Later, when data is needed:
+ * if (contents.isValid()) {
+ * fileRepresentation = contents.getCache();
+ * } else {
+ * // Re-read the file and recreate the file representation
+ * }
+ * }</pre>
+ *
* @param <T> the type of cached contents
*/
public class CachedFileContents<T> {
@@ -53,30 +54,30 @@
/**
* The file.
*/
- @NonNull
- private File mFile;
+ @Nonnull
+ private File file;
/**
* Time when last closed (time when {@link #closed(Object)} was invoked).
*/
- private long mLastClosed;
+ private long lastClosed;
/**
* Size of the file when last closed.
*/
- private long mSize;
+ private long size;
/**
* Hash of the file when closed. {@code null} if hashing failed for some reason.
*/
@Nullable
- private HashCode mHash;
+ private HashCode hash;
/**
* Cached data associated with the file.
*/
@Nullable
- private T mCache;
+ private T cache;
/**
* Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked
@@ -84,8 +85,8 @@
*
* @param file the file
*/
- public CachedFileContents(@NonNull File file) {
- mFile = file;
+ public CachedFileContents(@Nonnull File file) {
+ this.file = file;
}
/**
@@ -97,10 +98,10 @@
* @param cache an optional cache to save
*/
public void closed(@Nullable T cache) {
- mCache = cache;
- mLastClosed = mFile.lastModified();
- mSize = mFile.length();
- mHash = hashFile();
+ this.cache = cache;
+ lastClosed = file.lastModified();
+ size = file.length();
+ hash = hashFile();
}
/**
@@ -113,24 +114,24 @@
public boolean isValid() {
boolean valid = true;
- if (!mFile.exists()) {
+ if (!file.exists()) {
valid = false;
}
- if (valid && mFile.lastModified() != mLastClosed) {
+ if (valid && file.lastModified() != lastClosed) {
valid = false;
}
- if (valid && mFile.length() != mSize) {
+ if (valid && file.length() != size) {
valid = false;
}
- if (valid && !Objects.equal(mHash, hashFile())) {
+ if (valid && !Objects.equal(hash, hashFile())) {
valid = false;
}
if (!valid) {
- mCache = null;
+ cache = null;
}
return valid;
@@ -145,7 +146,7 @@
*/
@Nullable
public T getCache() {
- return mCache;
+ return cache;
}
/**
@@ -156,7 +157,7 @@
@Nullable
private HashCode hashFile() {
try {
- return Files.hash(mFile, Hashing.crc32());
+ return Files.hash(file, Hashing.crc32());
} catch (IOException e) {
return null;
}
@@ -168,8 +169,8 @@
* @return the file; this file always exists and contains the old (cached) contents of the
* file
*/
- @NonNull
+ @Nonnull
public File getFile() {
- return mFile;
+ return file;
}
}
diff --git a/src/main/java/com/android/builder/utils/CachedSupplier.java b/src/main/java/com/android/apkzlib/utils/CachedSupplier.java
similarity index 89%
rename from src/main/java/com/android/builder/utils/CachedSupplier.java
rename to src/main/java/com/android/apkzlib/utils/CachedSupplier.java
index ead563a..bc5eb69 100644
--- a/src/main/java/com/android/builder/utils/CachedSupplier.java
+++ b/src/main/java/com/android/apkzlib/utils/CachedSupplier.java
@@ -14,17 +14,17 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.utils;
import java.util.function.Supplier;
+import javax.annotation.Nonnull;
/**
* Supplier that will cache a computed value and always supply the same value. It can be used to
* lazily compute data. For example:
- * <pre>
- * CachedSupplier<Integer> value = new CachedSupplier<>(() -> {
+ *
+ * <pre>{@code
+ * CachedSupplier<Integer> value = new CachedSupplier<>(() -> {
* Integer result;
* // Do some expensive computation.
* return result;
@@ -41,12 +41,13 @@
* }
*
* // If neither a nor b are true, we avoid doing the computation at all.
- * </pre>
+ * }</pre>
*/
public class CachedSupplier<T> {
/**
- * The cached data, {@code null} if computation resulted in {@code null}.
+ * The cached data, {@code null} if computation resulted in {@code null}. It is also
+ * {@code null} if the cached data has not yet been computed.
*/
private T cached;
@@ -58,13 +59,13 @@
/**
* Actual supplier of data, if computation is needed.
*/
- @NonNull
+ @Nonnull
private final Supplier<T> supplier;
/**
* Creates a new supplier.
*/
- public CachedSupplier(@NonNull Supplier<T> supplier) {
+ public CachedSupplier(@Nonnull Supplier<T> supplier) {
valid = false;
this.supplier = supplier;
}
diff --git a/src/main/java/com/android/builder/utils/IOExceptionConsumer.java b/src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java
similarity index 86%
rename from src/main/java/com/android/builder/utils/IOExceptionConsumer.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java
index 89b430b..fcf3ab0 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionConsumer.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.function.Consumer;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Consumer that can throw an {@link IOException}.
@@ -40,8 +40,8 @@
*
* @param c the consumer
*/
- @NonNull
- static <T> Consumer<T> asConsumer(@NonNull IOExceptionConsumer<T> c) {
+ @Nonnull
+ static <T> Consumer<T> asConsumer(@Nonnull IOExceptionConsumer<T> c) {
return i -> {
try {
c.accept(i);
diff --git a/src/main/java/com/android/builder/utils/IOExceptionFunction.java b/src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java
similarity index 83%
rename from src/main/java/com/android/builder/utils/IOExceptionFunction.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java
index a13cc69..6d84b5b 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionFunction.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
+package com.android.apkzlib.utils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.function.Function;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Function that can throw an I/O Exception
@@ -41,8 +40,8 @@
*
* @param f the function
*/
- @NonNull
- static <F, T> Function<F, T> asFunction(@NonNull IOExceptionFunction<F, T> f) {
+ @Nonnull
+ static <F, T> Function<F, T> asFunction(@Nonnull IOExceptionFunction<F, T> f) {
return i -> {
try {
return f.apply(i);
diff --git a/src/main/java/com/android/builder/utils/IOExceptionRunnable.java b/src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java
similarity index 84%
rename from src/main/java/com/android/builder/utils/IOExceptionRunnable.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java
index 7dbcea3..67ed75c 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionRunnable.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
-import com.android.annotations.NonNull;
import java.io.IOException;
import java.io.UncheckedIOException;
+import javax.annotation.Nonnull;
/**
* Runnable that can throw I/O exceptions.
@@ -28,6 +28,7 @@
/**
* Runs the runnable.
+ *
* @throws IOException failed to run
*/
void run() throws IOException;
@@ -37,8 +38,8 @@
*
* @param r the runnable
*/
- @NonNull
- public static Runnable asRunnable(@NonNull IOExceptionRunnable r) {
+ @Nonnull
+ public static Runnable asRunnable(@Nonnull IOExceptionRunnable r) {
return () -> {
try {
r.run();
diff --git a/src/main/java/com/android/builder/utils/IOExceptionWrapper.java b/src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java
similarity index 87%
rename from src/main/java/com/android/builder/utils/IOExceptionWrapper.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java
index bb0423e..067b260 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionWrapper.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.utils;
import java.io.IOException;
+import javax.annotation.Nonnull;
/**
* Runtime exception used to encapsulate an IO Exception. This is used to allow throwing I/O
@@ -31,12 +30,12 @@
*
* @param e the I/O exception to encapsulate
*/
- public IOExceptionWrapper(@NonNull IOException e) {
+ public IOExceptionWrapper(@Nonnull IOException e) {
super(e);
}
@Override
- @NonNull
+ @Nonnull
public IOException getCause() {
return (IOException) super.getCause();
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/utils/package-info.java
similarity index 67%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/utils/package-info.java
index 22983ab..2f17b60 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/utils/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
-
/**
- * Type of stored entry.
+ * Utilities to work with {@code apkzlib}.
*/
-public enum StoredEntryType {
- /**
- * Entry is a file.
- */
- FILE,
-
- /**
- * Entry is a directory.
- */
- DIRECTORY
-}
+package com.android.apkzlib.utils;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/packaging/ApkCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkCreator.java
similarity index 72%
rename from src/main/java/com/android/builder/packaging/ApkCreator.java
rename to src/main/java/com/android/apkzlib/zfile/ApkCreator.java
index d62d730..3cac7dc 100644
--- a/src/main/java/com/android/builder/packaging/ApkCreator.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkCreator.java
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-package com.android.builder.packaging;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
+package com.android.apkzlib.zfile;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.function.Function;
import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Creates or updates APKs based on provided entries.
@@ -32,9 +31,9 @@
/**
* Copies the content of a Jar/Zip archive into the receiver archive.
- * <p>
- * An optional {@link ZipEntryFilter} allows to selectively choose which files
- * to copy over.
+ *
+ * <p>An optional predicate allows to selectively choose which files to copy over and an
+ * option function allows renaming the files as they are copied.
*
* @param zip the zip to copy data from
* @param transform an optional transform to apply to file names before copying them
@@ -43,8 +42,11 @@
* predicate applies after transformation
* @throws IOException I/O error
*/
- void writeZip(@NonNull File zip, @Nullable Function<String, String> transform,
- @Nullable Predicate<String> isIgnored) throws IOException;
+ void writeZip(
+ @Nonnull File zip,
+ @Nullable Function<String, String> transform,
+ @Nullable Predicate<String> isIgnored)
+ throws IOException;
/**
* Writes a new {@link File} into the archive. If a file already existed with the given
@@ -54,7 +56,7 @@
* @param apkPath the filepath inside the archive.
* @throws IOException I/O error
*/
- void writeFile(@NonNull File inputFile, @NonNull String apkPath) throws IOException;
+ void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException;
/**
* Deletes a file in a given path.
@@ -62,5 +64,8 @@
* @param apkPath the path to remove
* @throws IOException failed to remove the entry
*/
- void deleteFile(@NonNull String apkPath) throws IOException;
+ void deleteFile(@Nonnull String apkPath) throws IOException;
+
+ /** Returns true if the APK will be rewritten on close. */
+ boolean hasPendingChangesWithWait() throws IOException;
}
diff --git a/src/main/java/com/android/builder/packaging/ApkCreatorFactory.java b/src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java
similarity index 75%
rename from src/main/java/com/android/builder/packaging/ApkCreatorFactory.java
rename to src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java
index 3501c33..503eaf1 100644
--- a/src/main/java/com/android/builder/packaging/ApkCreatorFactory.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-package com.android.builder.packaging;
+package com.android.apkzlib.zfile;
import static com.google.common.base.Preconditions.checkNotNull;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import com.google.common.base.Preconditions;
-
import java.io.File;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Factory that creates instances of {@link ApkCreator}.
@@ -37,7 +36,7 @@
*
* @param creationData the information to create the APK
*/
- ApkCreator make(@NonNull CreationData creationData);
+ ApkCreator make(@Nonnull CreationData creationData);
/**
* Data structure with the required information to initiate the creation of an APK. See
@@ -49,54 +48,60 @@
* The path where the APK should be located. May already exist or not (if it does, then
* the APK may be updated instead of created).
*/
- @NonNull
- private final File mApkPath;
+ @Nonnull
+ private final File apkPath;
/**
* Key used to sign the APK. May be {@code null}.
*/
@Nullable
- private final PrivateKey mKey;
+ private final PrivateKey key;
/**
- * Certificate used to sign the APK. Is {@code null} if and only if {@link #mKey} is
+ * Certificate used to sign the APK. Is {@code null} if and only if {@link #key} is
* {@code null}.
*/
@Nullable
- private final X509Certificate mCertificate;
+ private final X509Certificate certificate;
/**
* Whether signing the APK with JAR Signing Scheme (aka v1 signing) is enabled.
*/
- private final boolean mV1SigningEnabled;
+ private final boolean v1SigningEnabled;
/**
* Whether signing the APK with APK Signature Scheme v2 (aka v2 signing) is enabled.
*/
- private final boolean mV2SigningEnabled;
+ private final boolean v2SigningEnabled;
/**
* Built-by information for the APK, if any.
*/
@Nullable
- private final String mBuiltBy;
+ private final String builtBy;
/**
* Created-by information for the APK, if any.
*/
@Nullable
- private final String mCreatedBy;
+ private final String createdBy;
/**
* Minimum SDk version that will run the APK.
*/
- private final int mMinSdkVersion;
+ private final int minSdkVersion;
- @NonNull
- private final NativeLibrariesPackagingMode mNativeLibrariesPackagingMode;
+ /**
+ * How should native libraries be packaged?
+ */
+ @Nonnull
+ private final NativeLibrariesPackagingMode nativeLibrariesPackagingMode;
- @NonNull
- private final Predicate<String> mNoCompressPredicate;
+ /**
+ * Predicate identifying paths that should not be compressed.
+ */
+ @Nonnull
+ private final Predicate<String> noCompressPredicate;
/**
*
@@ -115,10 +120,11 @@
* default should be used
* @param minSdkVersion minimum SDK version that will run the APK
* @param nativeLibrariesPackagingMode packaging mode for native libraries
- * @param noCompressPredicate predicate to decide which file paths should be uncompressed
+ * @param noCompressPredicate predicate to decide which file paths should be uncompressed;
+ * returns {@code true} for files that should not be compressed
*/
public CreationData(
- @NonNull File apkPath,
+ @Nonnull File apkPath,
@Nullable PrivateKey key,
@Nullable X509Certificate certificate,
boolean v1SigningEnabled,
@@ -126,22 +132,22 @@
@Nullable String builtBy,
@Nullable String createdBy,
int minSdkVersion,
- @NonNull NativeLibrariesPackagingMode nativeLibrariesPackagingMode,
- @NonNull Predicate<String> noCompressPredicate) {
+ @Nonnull NativeLibrariesPackagingMode nativeLibrariesPackagingMode,
+ @Nonnull Predicate<String> noCompressPredicate) {
Preconditions.checkArgument((key == null) == (certificate == null),
"(key == null) != (certificate == null)");
Preconditions.checkArgument(minSdkVersion >= 0, "minSdkVersion < 0");
- mApkPath = apkPath;
- mKey = key;
- mCertificate = certificate;
- mV1SigningEnabled = v1SigningEnabled;
- mV2SigningEnabled = v2SigningEnabled;
- mBuiltBy = builtBy;
- mCreatedBy = createdBy;
- mMinSdkVersion = minSdkVersion;
- mNativeLibrariesPackagingMode = checkNotNull(nativeLibrariesPackagingMode);
- mNoCompressPredicate = checkNotNull(noCompressPredicate);
+ this.apkPath = apkPath;
+ this.key = key;
+ this.certificate = certificate;
+ this.v1SigningEnabled = v1SigningEnabled;
+ this.v2SigningEnabled = v2SigningEnabled;
+ this.builtBy = builtBy;
+ this.createdBy = createdBy;
+ this.minSdkVersion = minSdkVersion;
+ this.nativeLibrariesPackagingMode = checkNotNull(nativeLibrariesPackagingMode);
+ this.noCompressPredicate = checkNotNull(noCompressPredicate);
}
/**
@@ -150,9 +156,9 @@
*
* @return the path that may already exist or not
*/
- @NonNull
+ @Nonnull
public File getApkPath() {
- return mApkPath;
+ return apkPath;
}
/**
@@ -162,7 +168,7 @@
*/
@Nullable
public PrivateKey getPrivateKey() {
- return mKey;
+ return key;
}
/**
@@ -173,7 +179,7 @@
*/
@Nullable
public X509Certificate getCertificate() {
- return mCertificate;
+ return certificate;
}
/**
@@ -181,7 +187,7 @@
* scheme).
*/
public boolean isV1SigningEnabled() {
- return mV1SigningEnabled;
+ return v1SigningEnabled;
}
/**
@@ -189,7 +195,7 @@
* scheme).
*/
public boolean isV2SigningEnabled() {
- return mV2SigningEnabled;
+ return v2SigningEnabled;
}
/**
@@ -199,7 +205,7 @@
*/
@Nullable
public String getBuiltBy() {
- return mBuiltBy;
+ return builtBy;
}
/**
@@ -209,7 +215,7 @@
*/
@Nullable
public String getCreatedBy() {
- return mCreatedBy;
+ return createdBy;
}
/**
@@ -218,23 +224,23 @@
* @return the minimum SDK version
*/
public int getMinSdkVersion() {
- return mMinSdkVersion;
+ return minSdkVersion;
}
/**
* Returns the packaging policy that the {@link ApkCreator} should use for native libraries.
*/
- @NonNull
+ @Nonnull
public NativeLibrariesPackagingMode getNativeLibrariesPackagingMode() {
- return mNativeLibrariesPackagingMode;
+ return nativeLibrariesPackagingMode;
}
/**
* Returns the predicate to decide which file paths should be uncompressed.
*/
- @NonNull
+ @Nonnull
public Predicate<String> getNoCompressPredicate() {
- return mNoCompressPredicate;
+ return noCompressPredicate;
}
}
}
diff --git a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
new file mode 100644
index 0000000..c85ad44
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zfile;
+
+import com.android.apkzlib.zip.AlignmentRule;
+import com.android.apkzlib.zip.AlignmentRules;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import com.google.common.base.Preconditions;
+import com.google.common.io.Closer;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK.
+ */
+class ApkZFileCreator implements ApkCreator {
+
+ /**
+ * Suffix for native libraries.
+ */
+ private static final String NATIVE_LIBRARIES_SUFFIX = ".so";
+
+ /**
+ * Shared libraries are alignment at 4096 boundaries.
+ */
+ private static final AlignmentRule SO_RULE =
+ AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096);
+
+ /**
+ * The zip file.
+ */
+ @Nonnull
+ private final ZFile zip;
+
+ /**
+ * Has the zip file been closed?
+ */
+ private boolean closed;
+
+ /**
+ * Predicate defining which files should not be compressed.
+ */
+ @Nonnull
+ private final Predicate<String> noCompressPredicate;
+
+ /**
+ * Creates a new creator.
+ *
+ * @param creationData the data needed to create the APK
+ * @param options zip file options
+ * @throws IOException failed to create the zip
+ */
+ ApkZFileCreator(
+ @Nonnull ApkCreatorFactory.CreationData creationData,
+ @Nonnull ZFileOptions options)
+ throws IOException {
+
+ switch (creationData.getNativeLibrariesPackagingMode()) {
+ case COMPRESSED:
+ noCompressPredicate = creationData.getNoCompressPredicate();
+ break;
+ case UNCOMPRESSED_AND_ALIGNED:
+ noCompressPredicate =
+ creationData.getNoCompressPredicate().or(
+ name -> name.endsWith(NATIVE_LIBRARIES_SUFFIX));
+ options.setAlignmentRule(
+ AlignmentRules.compose(SO_RULE, options.getAlignmentRule()));
+ break;
+ default:
+ throw new AssertionError();
+ }
+
+ zip = ZFiles.apk(
+ creationData.getApkPath(),
+ options,
+ creationData.getPrivateKey(),
+ creationData.getCertificate(),
+ creationData.isV1SigningEnabled(),
+ creationData.isV2SigningEnabled(),
+ creationData.getBuiltBy(),
+ creationData.getCreatedBy(),
+ creationData.getMinSdkVersion());
+ closed = false;
+ }
+
+ @Override
+ public void writeZip(@Nonnull File zip, @Nullable Function<String, String> transform,
+ @Nullable Predicate<String> isIgnored) throws IOException {
+ Preconditions.checkState(!closed, "closed == true");
+ Preconditions.checkArgument(zip.isFile(), "!zip.isFile()");
+
+ Closer closer = Closer.create();
+ try {
+ ZFile toMerge = closer.register(new ZFile(zip));
+
+ Predicate<String> ignorePredicate;
+ if (isIgnored == null) {
+ ignorePredicate = s -> false;
+ } else {
+ ignorePredicate = isIgnored;
+ }
+
+ // Files that *must* be uncompressed in the result should not be merged and should be
+ // added after. This is just very slightly less efficient than ignoring just the ones
+ // that were compressed and must be uncompressed, but it is a lot simpler :)
+ Predicate<String> noMergePredicate = ignorePredicate.or(noCompressPredicate);
+
+ this.zip.mergeFrom(toMerge, noMergePredicate);
+
+ for (StoredEntry toMergeEntry : toMerge.entries()) {
+ String path = toMergeEntry.getCentralDirectoryHeader().getName();
+ if (noCompressPredicate.test(path) && !ignorePredicate.test(path)) {
+ // This entry *must* be uncompressed so it was ignored in the merge and should
+ // now be added to the apk.
+ try (InputStream ignoredData = toMergeEntry.open()) {
+ this.zip.add(path, ignoredData, false);
+ }
+ }
+ }
+ } catch (Throwable t) {
+ throw closer.rethrow(t);
+ } finally {
+ closer.close();
+ }
+ }
+
+ @Override
+ public void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException {
+ Preconditions.checkState(!closed, "closed == true");
+
+ boolean mayCompress = !noCompressPredicate.test(apkPath);
+
+ Closer closer = Closer.create();
+ try {
+ FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile));
+ zip.add(apkPath, inputFileStream, mayCompress);
+ } catch (IOException e) {
+ throw closer.rethrow(e, IOException.class);
+ } catch (Throwable t) {
+ throw closer.rethrow(t);
+ } finally {
+ closer.close();
+ }
+ }
+
+ @Override
+ public void deleteFile(@Nonnull String apkPath) throws IOException {
+ Preconditions.checkState(!closed, "closed == true");
+
+ StoredEntry entry = zip.get(apkPath);
+ if (entry != null) {
+ entry.delete();
+ }
+ }
+
+ @Override
+ public boolean hasPendingChangesWithWait() throws IOException {
+ return zip.hasPendingChangesWithWait();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (closed) {
+ return;
+ }
+
+ zip.close();
+ closed = true;
+ }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreatorFactory.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java
similarity index 67%
rename from src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreatorFactory.java
rename to src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java
index feb8f25..2e4c7c9 100644
--- a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreatorFactory.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java
@@ -14,14 +14,12 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zfile;
+package com.android.apkzlib.zfile;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.packaging.ApkCreator;
-import com.android.builder.packaging.ApkCreatorFactory;
+import com.android.apkzlib.zip.ZFileOptions;
import java.io.IOException;
import java.io.UncheckedIOException;
+import javax.annotation.Nonnull;
/**
* Creates instances of {@link ApkZFileCreator}.
@@ -31,24 +29,24 @@
/**
* Options for the {@link ZFileOptions} to use in all APKs.
*/
- @NonNull
- private final ZFileOptions mOptions;
+ @Nonnull
+ private final ZFileOptions options;
/**
* Creates a new factory.
*
* @param options the options to use for all instances created
*/
- public ApkZFileCreatorFactory(@NonNull ZFileOptions options) {
- mOptions = options;
+ public ApkZFileCreatorFactory(@Nonnull ZFileOptions options) {
+ this.options = options;
}
@Override
- @NonNull
- public ApkCreator make(@NonNull CreationData creationData) {
+ @Nonnull
+ public ApkCreator make(@Nonnull CreationData creationData) {
try {
- return new ApkZFileCreator(creationData, mOptions);
+ return new ApkZFileCreator(creationData, options);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
diff --git a/src/main/java/com/android/builder/packaging/ManifestAttributes.java b/src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java
similarity index 96%
rename from src/main/java/com/android/builder/packaging/ManifestAttributes.java
rename to src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java
index dc6bdf3..d8a7d2d 100644
--- a/src/main/java/com/android/builder/packaging/ManifestAttributes.java
+++ b/src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.packaging;
+package com.android.apkzlib.zfile;
/**
* Java manifest attributes and some default values.
diff --git a/src/main/java/com/android/builder/packaging/NativeLibrariesPackagingMode.java b/src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java
similarity index 96%
rename from src/main/java/com/android/builder/packaging/NativeLibrariesPackagingMode.java
rename to src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java
index df28ef5..8916abe 100644
--- a/src/main/java/com/android/builder/packaging/NativeLibrariesPackagingMode.java
+++ b/src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.packaging;
+package com.android.apkzlib.zfile;
/**
* Describes how native libs should be packaged.
diff --git a/src/main/java/com/android/builder/internal/packaging/zfile/ZFiles.java b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
similarity index 63%
rename from src/main/java/com/android/builder/internal/packaging/zfile/ZFiles.java
rename to src/main/java/com/android/apkzlib/zfile/ZFiles.java
index 39fb279..d5102a6 100644
--- a/src/main/java/com/android/builder/internal/packaging/zfile/ZFiles.java
+++ b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
@@ -14,25 +14,22 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zfile;
+package com.android.apkzlib.zfile;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.sign.FullApkSignExtension;
-import com.android.builder.internal.packaging.sign.ManifestGenerationExtension;
-import com.android.builder.internal.packaging.sign.SignatureExtension;
-import com.android.builder.internal.packaging.zip.AlignmentRule;
-import com.android.builder.internal.packaging.zip.AlignmentRules;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-
+import com.android.apkzlib.sign.ManifestGenerationExtension;
+import com.android.apkzlib.sign.SigningExtension;
+import com.android.apkzlib.zip.AlignmentRule;
+import com.android.apkzlib.zip.AlignmentRules;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Factory for {@link ZFile}s that are specifically configured to be APKs, AARs, ...
@@ -64,8 +61,8 @@
* @return the zip file
* @throws IOException failed to create the zip file
*/
- @NonNull
- public static ZFile apk(@NonNull File f, @NonNull ZFileOptions options) throws IOException {
+ @Nonnull
+ public static ZFile apk(@Nonnull File f, @Nonnull ZFileOptions options) throws IOException {
options.setAlignmentRule(
AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE));
return new ZFile(f, options);
@@ -91,10 +88,10 @@
* @return the zip file
* @throws IOException failed to create the zip file
*/
- @NonNull
+ @Nonnull
public static ZFile apk(
- @NonNull File f,
- @NonNull ZFileOptions options,
+ @Nonnull File f,
+ @Nonnull ZFileOptions options,
@Nullable PrivateKey key,
@Nullable X509Certificate certificate,
boolean v1SigningEnabled,
@@ -119,35 +116,12 @@
if (key != null && certificate != null) {
try {
- if (v1SigningEnabled) {
- String apkSignedHeaderValue =
- (v2SigningEnabled)
- ? SignatureExtension
- .SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED
- : null;
- SignatureExtension jarSignatureSchemeExt = new SignatureExtension(manifestExt,
- minSdkVersion, certificate, key,
- apkSignedHeaderValue);
- jarSignatureSchemeExt.register();
- }
- if (v2SigningEnabled) {
- FullApkSignExtension apkSignatureSchemeV2Ext =
- new FullApkSignExtension(
- zfile,
- minSdkVersion,
- certificate,
- key);
- apkSignatureSchemeV2Ext.register();
- }
- if (!v1SigningEnabled) {
- // Remove v1 signature files from the APK
- for (StoredEntry entry : zfile.entries()) {
- if (SignatureExtension.isIgnoredFile(
- entry.getCentralDirectoryHeader().getName())) {
- entry.delete();
- }
- }
- }
+ new SigningExtension(
+ minSdkVersion,
+ certificate,
+ key,
+ v1SigningEnabled,
+ v2SigningEnabled).register(zfile);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IOException("Failed to create signature extensions", e);
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zfile/package-info.java
similarity index 67%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/zfile/package-info.java
index 22983ab..0c5ab6d 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zfile/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
-
/**
- * Type of stored entry.
+ * The {@code zfile} package contains
*/
-public enum StoredEntryType {
- /**
- * Entry is a file.
- */
- FILE,
-
- /**
- * Entry is a directory.
- */
- DIRECTORY
-}
+package com.android.apkzlib.zfile;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRule.java b/src/main/java/com/android/apkzlib/zip/AlignmentRule.java
similarity index 89%
rename from src/main/java/com/android/builder/internal/packaging/zip/AlignmentRule.java
rename to src/main/java/com/android/apkzlib/zip/AlignmentRule.java
index 5fcd0a6..4ee6963 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRule.java
+++ b/src/main/java/com/android/apkzlib/zip/AlignmentRule.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
+import javax.annotation.Nonnull;
/**
* An alignment rule defines how to a file should be aligned in a zip, based on its name.
@@ -35,7 +35,5 @@
* @return the alignment value, always greater than {@code 0}; if this rule places no
* restrictions on the provided path, then {@link AlignmentRule#NO_ALIGNMENT} is returned
*/
- int alignment(@NonNull String path);
+ int alignment(@Nonnull String path);
}
-
-
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRules.java b/src/main/java/com/android/apkzlib/zip/AlignmentRules.java
similarity index 91%
rename from src/main/java/com/android/builder/internal/packaging/zip/AlignmentRules.java
rename to src/main/java/com/android/apkzlib/zip/AlignmentRules.java
index 01e472f..b06a596 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRules.java
+++ b/src/main/java/com/android/apkzlib/zip/AlignmentRules.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
import com.google.common.base.Preconditions;
+import javax.annotation.Nonnull;
/**
* Factory for instances of {@link AlignmentRule}.
@@ -46,7 +46,7 @@
* @param alignment the alignment for paths that match the provided suffix
* @return the rule
*/
- public static AlignmentRule constantForSuffix(@NonNull String suffix, int alignment) {
+ public static AlignmentRule constantForSuffix(@Nonnull String suffix, int alignment) {
Preconditions.checkArgument(!suffix.isEmpty(), "suffix.isEmpty()");
Preconditions.checkArgument(alignment > 0, "alignment <= 0");
@@ -62,7 +62,7 @@
* {@link AlignmentRule#NO_ALIGNMENT} is returned
* @return the composition rule
*/
- public static AlignmentRule compose(@NonNull AlignmentRule... rules) {
+ public static AlignmentRule compose(@Nonnull AlignmentRule... rules) {
return (String path) -> {
for (AlignmentRule r : rules) {
int align = r.alignment(path);
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectory.java b/src/main/java/com/android/apkzlib/zip/CentralDirectory.java
similarity index 80%
rename from src/main/java/com/android/builder/internal/packaging/zip/CentralDirectory.java
rename to src/main/java/com/android/apkzlib/zip/CentralDirectory.java
index 92392cf..44389c1 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectory.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectory.java
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.CachedSupplier;
-import com.android.builder.internal.packaging.zip.utils.MsDosDateTimeUtils;
+import com.android.apkzlib.utils.CachedSupplier;
+import com.android.apkzlib.zip.utils.MsDosDateTimeUtils;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
@@ -26,14 +26,13 @@
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
-
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import javax.annotation.Nonnull;
/**
* Representation of the central directory of a zip archive.
@@ -54,7 +53,8 @@
/**
* Field in the central directory with the minimum version required to extract the entry.
*/
- private static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(),
+ @VisibleForTesting
+ static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(),
"Version to extract", new ZipFieldInvariantNonNegative());
/**
@@ -174,30 +174,37 @@
/**
* Contains all entries in the directory mapped from their names.
*/
- @NonNull
- private final Map<String, StoredEntry> mEntries;
+ @Nonnull
+ private final Map<String, StoredEntry> entries;
/**
* The file where this directory belongs to.
*/
- @NonNull
- private final ZFile mFile;
+ @Nonnull
+ private final ZFile file;
/**
* Supplier that provides a byte representation of the central directory.
*/
- @NonNull
- private final CachedSupplier<byte[]> mBytesSupplier;
+ @Nonnull
+ private final CachedSupplier<byte[]> bytesSupplier;
+
+ /**
+ * Verify log for the central directory.
+ */
+ @Nonnull
+ private final VerifyLog verifyLog;
/**
* Creates a new, empty, central directory, for a given zip file.
*
* @param file the file
*/
- CentralDirectory(@NonNull ZFile file) {
- mEntries = Maps.newHashMap();
- mFile = file;
- mBytesSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+ CentralDirectory(@Nonnull ZFile file) {
+ entries = Maps.newHashMap();
+ this.file = file;
+ bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+ verifyLog = file.getVerifyLog();
}
/**
@@ -214,7 +221,7 @@
* @throws IOException failed to read data from the zip, or the central directory is corrupted
* or has unsupported features
*/
- static CentralDirectory makeFromData(@NonNull ByteBuffer bytes, int count, @NonNull ZFile file)
+ static CentralDirectory makeFromData(@Nonnull ByteBuffer bytes, int count, @Nonnull ZFile file)
throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
Preconditions.checkArgument(count >= 0, "count < 0");
@@ -223,7 +230,7 @@
for (int i = 0; i < count; i++) {
try {
- directory.readEntry(bytes, file);
+ directory.readEntry(bytes);
} catch (IOException e) {
throw new IOException(
"Failed to read directory entry index "
@@ -247,52 +254,50 @@
* @param file the zip file itself
* @return the created central directory
*/
- static CentralDirectory makeFromEntries(@NonNull Set<StoredEntry> entries,
- @NonNull ZFile file) {
+ static CentralDirectory makeFromEntries(
+ @Nonnull Set<StoredEntry> entries,
+ @Nonnull ZFile file) {
CentralDirectory directory = new CentralDirectory(file);
for (StoredEntry entry : entries) {
CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader();
Preconditions.checkArgument(
- !directory.mEntries.containsKey(cdr.getName()),
+ !directory.entries.containsKey(cdr.getName()),
"Duplicate filename");
- directory.mEntries.put(cdr.getName(), entry);
+ directory.entries.put(cdr.getName(), entry);
}
return directory;
}
/**
- * Reads the next entry from the central directory and adds it to {@link #mEntries}.
+ * Reads the next entry from the central directory and adds it to {@link #entries}.
*
* @param bytes the central directory's data, positioned starting at the beginning of the next
* entry to read; when finished, the buffer's position will be at the first byte after the
* entry
- * @param file the file this entry belongs to
* @throws IOException failed to read the directory entry, either because of an I/O error,
* because it is corrupt or contains unsupported features
*/
- private void readEntry(@NonNull ByteBuffer bytes, @NonNull ZFile file) throws IOException {
+ private void readEntry(@Nonnull ByteBuffer bytes) throws IOException {
F_SIGNATURE.verify(bytes);
long madeBy = F_MADE_BY.read(bytes);
long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes);
- if (versionNeededToExtract > MAX_VERSION_TO_EXTRACT) {
- throw new IOException("Unknown version needed to extract in zip directory entry: "
- + versionNeededToExtract + ".");
- }
+ verifyLog.verify(
+ versionNeededToExtract <= MAX_VERSION_TO_EXTRACT,
+ "Ignored unknown version needed to extract in zip directory entry: %s.",
+ versionNeededToExtract);
long gpBit = F_GP_BIT.read(bytes);
GPFlags flags = GPFlags.from(gpBit);
long methodCode = F_METHOD.read(bytes);
CompressionMethod method = CompressionMethod.fromCode(methodCode);
- if (method == null) {
- throw new IOException("Unknown method in zip directory entry: " + methodCode + ".");
- }
+ verifyLog.verify(method != null, "Unknown method in zip directory entry: %s.", methodCode);
long lastModTime;
long lastModDate;
- if (mFile.areTimestampsIgnored()) {
+ if (file.areTimestampsIgnored()) {
lastModTime = 0;
lastModDate = 0;
F_LAST_MOD_TIME.skip(bytes);
@@ -309,11 +314,12 @@
int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes));
int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes));
- F_DISK_NUMBER_START.verify(bytes);
+ F_DISK_NUMBER_START.verify(bytes, verifyLog);
long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes);
- if ((internalAttributes & ~ASCII_BIT) != 0) {
- throw new IOException("Invalid internal attributes: " + internalAttributes + ".");
- }
+ verifyLog.verify(
+ (internalAttributes & ~ASCII_BIT) == 0,
+ "Ignored invalid internal attributes: %s.",
+ internalAttributes);
long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes);
long entryOffset = F_OFFSET.read(bytes);
@@ -321,13 +327,23 @@
long remainingSize = fileNameLength + extraFieldLength + fileCommentLength;
if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) {
- throw new IOException("Directory entry should have " + remainingSize
- + " bytes remaining (name = " + fileNameLength + ", extra = "
- + extraFieldLength + ", comment = " + fileCommentLength + "), but it has "
- + bytes.remaining() + ".");
+ throw new IOException(
+ "Directory entry should have "
+ + remainingSize
+ + " bytes remaining (name = "
+ + fileNameLength
+ + ", extra = "
+ + extraFieldLength
+ + ", comment = "
+ + fileCommentLength
+ + "), but it has "
+ + bytes.remaining()
+ + ".");
}
- String fileName = EncodeUtils.decode(bytes, fileNameLength, flags);
+ byte[] encodedFileName = new byte[fileNameLength];
+ bytes.get(encodedFileName);
+ String fileName = EncodeUtils.decode(encodedFileName, flags);
byte[] extraField = new byte[extraFieldLength];
bytes.get(extraField);
@@ -341,15 +357,14 @@
* information we need the CentralDirectoryHeader
*/
ListenableFuture<CentralDirectoryHeaderCompressInfo> compressInfo =
- Futures.immediateFuture(new CentralDirectoryHeaderCompressInfo(method,
- compressedSize, versionNeededToExtract));
+ Futures.immediateFuture(
+ new CentralDirectoryHeaderCompressInfo(
+ method,
+ compressedSize,
+ versionNeededToExtract));
CentralDirectoryHeader centralDirectoryHeader =
new CentralDirectoryHeader(
- fileName,
- uncompressedSize,
- compressInfo,
- flags,
- file);
+ fileName, encodedFileName, uncompressedSize, compressInfo, flags, file);
centralDirectoryHeader.setMadeBy(madeBy);
centralDirectoryHeader.setLastModTime(lastModTime);
centralDirectoryHeader.setLastModDate(lastModDate);
@@ -363,16 +378,16 @@
StoredEntry entry;
try {
- entry = new StoredEntry(centralDirectoryHeader, mFile, null);
+ entry = new StoredEntry(centralDirectoryHeader, file, null);
} catch (IOException e) {
throw new IOException("Failed to read stored entry '" + fileName + "'.", e);
}
- if (mEntries.containsKey(fileName)) {
- throw new IOException("File file contains duplicate file '" + fileName + "'.");
+ if (entries.containsKey(fileName)) {
+ verifyLog.log("File file contains duplicate file '" + fileName + "'.");
}
- mEntries.put(fileName, entry);
+ entries.put(fileName, entry);
}
/**
@@ -380,9 +395,9 @@
*
* @return all entries on a non-modifiable map
*/
- @NonNull
+ @Nonnull
Map<String, StoredEntry> getEntries() {
- return ImmutableMap.copyOf(mEntries);
+ return ImmutableMap.copyOf(entries);
}
/**
@@ -392,7 +407,7 @@
* @throws IOException failed to write the byte array
*/
byte[] toBytes() throws IOException {
- return mBytesSupplier.get();
+ return bytesSupplier.get();
}
/**
@@ -403,15 +418,15 @@
*/
private byte[] computeByteRepresentation() {
- List<StoredEntry> sorted = Lists.newArrayList(mEntries.values());
- Collections.sort(sorted, StoredEntry.COMPARE_BY_NAME);
+ List<StoredEntry> sorted = Lists.newArrayList(entries.values());
+ sorted.sort(StoredEntry.COMPARE_BY_NAME);
- CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[mEntries.size()];
+ CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()];
CentralDirectoryHeaderCompressInfo[] compressInfos =
- new CentralDirectoryHeaderCompressInfo[mEntries.size()];
- byte[][] encodedFileNames = new byte[mEntries.size()][];
- byte[][] extraFields = new byte[mEntries.size()][];
- byte[][] comments = new byte[mEntries.size()][];
+ new CentralDirectoryHeaderCompressInfo[entries.size()];
+ byte[][] encodedFileNames = new byte[entries.size()][];
+ byte[][] extraFields = new byte[entries.size()][];
+ byte[][] comments = new byte[entries.size()][];
try {
/*
@@ -434,14 +449,14 @@
ByteBuffer out = ByteBuffer.allocate(total);
- for (idx = 0; idx < mEntries.size(); idx++) {
+ for (idx = 0; idx < entries.size(); idx++) {
F_SIGNATURE.write(out);
F_MADE_BY.write(out, cdhs[idx].getMadeBy());
F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract());
F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue());
F_METHOD.write(out, compressInfos[idx].getMethod().methodCode);
- if (mFile.areTimestampsIgnored()) {
+ if (file.areTimestampsIgnored()) {
F_LAST_MOD_TIME.write(out, 0);
F_LAST_MOD_DATE.write(out, 0);
} else {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeader.java b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
similarity index 76%
rename from src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeader.java
rename to src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
index f846b9d..f10477f 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeader.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.MsDosDateTimeUtils;
+import com.android.apkzlib.zip.utils.MsDosDateTimeUtils;
import com.google.common.base.Verify;
-
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
/**
* The Central Directory Header contains information about files stored in the zip. Instances of
@@ -45,121 +44,123 @@
/**
* Name of the file.
*/
- @NonNull
- private String mName;
+ @Nonnull
+ private String name;
/**
* CRC32 of the data. 0 if not yet computed.
*/
- private long mCrc32;
+ private long crc32;
/**
* Size of the file uncompressed. 0 if the file has no data.
*/
- private long mUncompressedSize;
+ private long uncompressedSize;
/**
* Code of the program that made the zip. We actually don't care about this.
*/
- private long mMadeBy;
+ private long madeBy;
/**
* General-purpose bit flag.
*/
- @NonNull
- private GPFlags mGpBit;
+ @Nonnull
+ private GPFlags gpBit;
/**
* Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packTime(long)}).
*/
- private long mLastModTime;
+ private long lastModTime;
/**
* Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packDate(long)}).
*/
- private long mLastModDate;
+ private long lastModDate;
/**
* Extra data field contents. This field follows a specific structure according to the
* specification.
*/
- @NonNull
- private ExtraField mExtraField;
+ @Nonnull
+ private ExtraField extraField;
/**
* File comment.
*/
- @NonNull
- private byte[] mComment;
+ @Nonnull
+ private byte[] comment;
/**
* File internal attributes.
*/
- private long mInternalAttributes;
+ private long internalAttributes;
/**
* File external attributes.
*/
- private long mExternalAttributes;
+ private long externalAttributes;
/**
* Offset in the file where the data is located. This will be -1 if the header corresponds to
* a new file that is not yet written in the zip and, therefore, has no written data.
*/
- private long mOffset;
+ private long offset;
/**
* Encoded file name.
*/
- private byte[] mEncodedFileName;
+ private byte[] encodedFileName;
/**
* Compress information that may not have been computed yet due to lazy compression.
*/
- @NonNull
- private Future<CentralDirectoryHeaderCompressInfo> mCompressInfo;
+ @Nonnull
+ private Future<CentralDirectoryHeaderCompressInfo> compressInfo;
/**
* The file this header belongs to.
*/
- @NonNull
- private final ZFile mFile;
+ @Nonnull
+ private final ZFile file;
/**
* Creates data for a file.
*
* @param name the file name
+ * @param encodedFileName the encoded file name, this array will be owned by the header
* @param uncompressedSize the uncompressed file size
* @param compressInfo computation that defines the compression information
* @param flags flags used in the entry
* @param zFile the file this header belongs to
*/
CentralDirectoryHeader(
- @NonNull String name,
+ @Nonnull String name,
+ @Nonnull byte[] encodedFileName,
long uncompressedSize,
- @NonNull Future<CentralDirectoryHeaderCompressInfo> compressInfo,
- @NonNull GPFlags flags,
- @NonNull ZFile zFile) {
- mName = name;
- mUncompressedSize = uncompressedSize;
- mCrc32 = 0;
+ @Nonnull Future<CentralDirectoryHeaderCompressInfo> compressInfo,
+ @Nonnull GPFlags flags,
+ @Nonnull ZFile zFile) {
+ this.name = name;
+ this.uncompressedSize = uncompressedSize;
+ crc32 = 0;
/*
* Set sensible defaults for the rest.
*/
- mMadeBy = DEFAULT_VERSION_MADE_BY;
+ madeBy = DEFAULT_VERSION_MADE_BY;
- mGpBit = flags;
- mLastModTime = MsDosDateTimeUtils.packCurrentTime();
- mLastModDate = MsDosDateTimeUtils.packCurrentDate();
- mExtraField = new ExtraField();
- mComment = new byte[0];
- mInternalAttributes = 0;
- mExternalAttributes = 0;
- mOffset = -1;
- mEncodedFileName = EncodeUtils.encode(name, mGpBit);
- mCompressInfo = compressInfo;
- mFile = zFile;
+ gpBit = flags;
+ lastModTime = MsDosDateTimeUtils.packCurrentTime();
+ lastModDate = MsDosDateTimeUtils.packCurrentDate();
+ extraField = new ExtraField();
+ comment = new byte[0];
+ internalAttributes = 0;
+ externalAttributes = 0;
+ offset = -1;
+ this.encodedFileName = encodedFileName;
+ this.compressInfo = compressInfo;
+ file = zFile;
}
/**
@@ -167,9 +168,9 @@
*
* @return the name
*/
- @NonNull
+ @Nonnull
public String getName() {
- return mName;
+ return name;
}
/**
@@ -178,7 +179,7 @@
* @return the size of the file
*/
public long getUncompressedSize() {
- return mUncompressedSize;
+ return uncompressedSize;
}
/**
@@ -187,7 +188,7 @@
* @return the CRC32, 0 if not yet computed
*/
public long getCrc32() {
- return mCrc32;
+ return crc32;
}
/**
@@ -196,7 +197,7 @@
* @param crc32 the CRC 32
*/
void setCrc32(long crc32) {
- mCrc32 = crc32;
+ this.crc32 = crc32;
}
/**
@@ -205,7 +206,7 @@
* @return the code
*/
public long getMadeBy() {
- return mMadeBy;
+ return madeBy;
}
/**
@@ -214,7 +215,7 @@
* @param madeBy the code
*/
void setMadeBy(long madeBy) {
- mMadeBy = madeBy;
+ this.madeBy = madeBy;
}
/**
@@ -222,9 +223,9 @@
*
* @return the bit flag
*/
- @NonNull
+ @Nonnull
public GPFlags getGpBit() {
- return mGpBit;
+ return gpBit;
}
/**
@@ -234,7 +235,7 @@
* {@link MsDosDateTimeUtils#packTime(long)})
*/
public long getLastModTime() {
- return mLastModTime;
+ return lastModTime;
}
/**
@@ -244,7 +245,7 @@
* {@link MsDosDateTimeUtils#packTime(long)})
*/
void setLastModTime(long lastModTime) {
- mLastModTime = lastModTime;
+ this.lastModTime = lastModTime;
}
/**
@@ -254,7 +255,7 @@
* {@link MsDosDateTimeUtils#packDate(long)})
*/
public long getLastModDate() {
- return mLastModDate;
+ return lastModDate;
}
/**
@@ -264,7 +265,7 @@
* {@link MsDosDateTimeUtils#packDate(long)})
*/
void setLastModDate(long lastModDate) {
- mLastModDate = lastModDate;
+ this.lastModDate = lastModDate;
}
/**
@@ -272,9 +273,9 @@
*
* @return the data (returns an empty array if there is none)
*/
- @NonNull
+ @Nonnull
public ExtraField getExtraField() {
- return mExtraField;
+ return extraField;
}
/**
@@ -282,9 +283,9 @@
*
* @param extraField the data to set
*/
- public void setExtraField(@NonNull ExtraField extraField) {
+ public void setExtraField(@Nonnull ExtraField extraField) {
setExtraFieldNoNotify(extraField);
- mFile.centralDirectoryChanged();
+ file.centralDirectoryChanged();
}
/**
@@ -293,8 +294,8 @@
*
* @param extraField the data to set
*/
- void setExtraFieldNoNotify(@NonNull ExtraField extraField) {
- mExtraField = extraField;
+ void setExtraFieldNoNotify(@Nonnull ExtraField extraField) {
+ this.extraField = extraField;
}
/**
@@ -302,9 +303,9 @@
*
* @return the comment (returns an empty array if there is no comment)
*/
- @NonNull
+ @Nonnull
public byte[] getComment() {
- return mComment;
+ return comment;
}
/**
@@ -312,8 +313,8 @@
*
* @param comment the comment
*/
- void setComment(@NonNull byte[] comment) {
- mComment = comment;
+ void setComment(@Nonnull byte[] comment) {
+ this.comment = comment;
}
/**
@@ -322,7 +323,7 @@
* @return the entry's internal attributes
*/
public long getInternalAttributes() {
- return mInternalAttributes;
+ return internalAttributes;
}
/**
@@ -331,7 +332,7 @@
* @param internalAttributes the entry's internal attributes
*/
void setInternalAttributes(long internalAttributes) {
- mInternalAttributes = internalAttributes;
+ this.internalAttributes = internalAttributes;
}
/**
@@ -340,7 +341,7 @@
* @return the entry's external attributes
*/
public long getExternalAttributes() {
- return mExternalAttributes;
+ return externalAttributes;
}
/**
@@ -349,7 +350,7 @@
* @param externalAttributes the entry's external attributes
*/
void setExternalAttributes(long externalAttributes) {
- mExternalAttributes = externalAttributes;
+ this.externalAttributes = externalAttributes;
}
/**
@@ -359,7 +360,7 @@
* is stored in memory
*/
public long getOffset() {
- return mOffset;
+ return offset;
}
/**
@@ -368,7 +369,7 @@
* @param offset the offset or {@code -1} if the file is new and has no data in the zip yet
*/
void setOffset(long offset) {
- mOffset = offset;
+ this.offset = offset;
}
/**
@@ -377,7 +378,7 @@
* @return the encoded file name
*/
public byte[] getEncodedFileName() {
- return mEncodedFileName;
+ return encodedFileName;
}
/**
@@ -388,15 +389,15 @@
* We actually create a new set of flags. Since the only information we care about is the
* UTF-8 encoding, we'll just create a brand new object.
*/
- mGpBit = GPFlags.make(mGpBit.isUtf8FileName());
+ gpBit = GPFlags.make(gpBit.isUtf8FileName());
}
@Override
protected CentralDirectoryHeader clone() throws CloneNotSupportedException {
CentralDirectoryHeader cdr = (CentralDirectoryHeader) super.clone();
- cdr.mExtraField = mExtraField;
- cdr.mComment = Arrays.copyOf(mComment, mComment.length);
- cdr.mEncodedFileName = Arrays.copyOf(mEncodedFileName, mEncodedFileName.length);
+ cdr.extraField = extraField;
+ cdr.comment = Arrays.copyOf(comment, comment.length);
+ cdr.encodedFileName = Arrays.copyOf(encodedFileName, encodedFileName.length);
return cdr;
}
@@ -405,9 +406,9 @@
*
* @return the information
*/
- @NonNull
+ @Nonnull
public Future<CentralDirectoryHeaderCompressInfo> getCompressionInfo() {
- return mCompressInfo;
+ return compressInfo;
}
/**
@@ -417,7 +418,7 @@
* @return the result of the future
* @throws IOException failed to get the information
*/
- @NonNull
+ @Nonnull
public CentralDirectoryHeaderCompressInfo getCompressionInfoWithWait()
throws IOException {
try {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeaderCompressInfo.java b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java
similarity index 80%
rename from src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeaderCompressInfo.java
rename to src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java
index ada9f2b..7c3ad63 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeaderCompressInfo.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
+import javax.annotation.Nonnull;
/**
* Information stored in the {@link CentralDirectoryHeader} that is related to compression and may
@@ -37,18 +37,18 @@
/**
* The compression method.
*/
- @NonNull
+ @Nonnull
private final CompressionMethod mMethod;
/**
* Size of the file compressed. 0 if the file has no data.
*/
- private final long mCompressedSize;
+ private final long compressedSize;
/**
* Version needed to extract the zip.
*/
- private final long mVersionExtract;
+ private final long versionExtract;
/**
* Creates new compression information for the central directory header.
@@ -59,12 +59,12 @@
* {@link #VERSION_WITH_STORE_FILES_ONLY} or {@link #VERSION_WITH_DIRECTORIES_AND_DEFLATE})
*/
public CentralDirectoryHeaderCompressInfo(
- @NonNull CompressionMethod method,
+ @Nonnull CompressionMethod method,
long compressedSize,
long versionToExtract) {
mMethod = method;
- mCompressedSize = compressedSize;
- mVersionExtract = versionToExtract;
+ this.compressedSize = compressedSize;
+ versionExtract = versionToExtract;
}
/**
@@ -74,18 +74,18 @@
* @param method the compression method
* @param compressedSize the compressed size
*/
- public CentralDirectoryHeaderCompressInfo(@NonNull CentralDirectoryHeader header,
- @NonNull CompressionMethod method, long compressedSize) {
+ public CentralDirectoryHeaderCompressInfo(@Nonnull CentralDirectoryHeader header,
+ @Nonnull CompressionMethod method, long compressedSize) {
mMethod = method;
- mCompressedSize = compressedSize;
+ this.compressedSize = compressedSize;
if (header.getName().endsWith("/") || method == CompressionMethod.DEFLATE) {
/*
* Directories and compressed files only in version 2.0.
*/
- mVersionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE;
+ versionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE;
} else {
- mVersionExtract = VERSION_WITH_STORE_FILES_ONLY;
+ versionExtract = VERSION_WITH_STORE_FILES_ONLY;
}
}
@@ -95,7 +95,7 @@
* @return the compressed data size
*/
public long getCompressedSize() {
- return mCompressedSize;
+ return compressedSize;
}
/**
@@ -103,7 +103,7 @@
*
* @return the compression method
*/
- @NonNull
+ @Nonnull
public CompressionMethod getMethod() {
return mMethod;
}
@@ -114,6 +114,6 @@
* @return the minimum version
*/
public long getVersionExtract() {
- return mVersionExtract;
+ return versionExtract;
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CompressionMethod.java b/src/main/java/com/android/apkzlib/zip/CompressionMethod.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/CompressionMethod.java
rename to src/main/java/com/android/apkzlib/zip/CompressionMethod.java
index 9984d3e..dd2ee8d 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CompressionMethod.java
+++ b/src/main/java/com/android/apkzlib/zip/CompressionMethod.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.Nullable;
+import javax.annotation.Nullable;
/**
* Enumeration with all known compression methods.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CompressionResult.java b/src/main/java/com/android/apkzlib/zip/CompressionResult.java
similarity index 70%
rename from src/main/java/com/android/builder/internal/packaging/zip/CompressionResult.java
rename to src/main/java/com/android/apkzlib/zip/CompressionResult.java
index ab9fc75..34f5d72 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CompressionResult.java
+++ b/src/main/java/com/android/apkzlib/zip/CompressionResult.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import javax.annotation.Nonnull;
/**
* Result of compressing data.
@@ -27,53 +27,57 @@
/**
* The compression method used.
*/
- @NonNull
- private final CompressionMethod mCompressionMethod;
+ @Nonnull
+ private final CompressionMethod compressionMethod;
/**
* The resulting data.
*/
- @NonNull
- private final CloseableByteSource mSource;
+ @Nonnull
+ private final CloseableByteSource source;
/**
- * Size of the compressed source. Kept because {@code mSource.size()} can throw
+ * Size of the compressed source. Kept because {@code source.size()} can throw
* {@code IOException}.
*/
private final long mSize;
/**
* Creates a new compression result.
+ *
* @param source the data source
* @param method the compression method
*/
- public CompressionResult(@NonNull CloseableByteSource source, @NonNull CompressionMethod method,
+ public CompressionResult(@Nonnull CloseableByteSource source, @Nonnull CompressionMethod method,
long size) {
- mCompressionMethod = method;
- mSource = source;
+ compressionMethod = method;
+ this.source = source;
mSize = size;
}
/**
* Obtains the compression method.
+ *
* @return the compression method
*/
- @NonNull
+ @Nonnull
public CompressionMethod getCompressionMethod() {
- return mCompressionMethod;
+ return compressionMethod;
}
/**
* Obtains the compressed data.
+ *
* @return the data, the resulting array should not be modified
*/
- @NonNull
+ @Nonnull
public CloseableByteSource getSource() {
- return mSource;
+ return source;
}
/**
* Obtains the size of the compression result.
+ *
* @return the size
*/
public long getSize() {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/Compressor.java b/src/main/java/com/android/apkzlib/zip/Compressor.java
similarity index 82%
rename from src/main/java/com/android/builder/internal/packaging/zip/Compressor.java
rename to src/main/java/com/android/apkzlib/zip/Compressor.java
index 8084e25..a94242e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/Compressor.java
+++ b/src/main/java/com/android/apkzlib/zip/Compressor.java
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.util.concurrent.ListenableFuture;
+import javax.annotation.Nonnull;
/**
* A compressor is capable of, well, compressing data. Data is read from an {@code ByteSource}.
@@ -29,9 +29,10 @@
/**
* Compresses an entry source.
+ *
* @param source the source to compress
* @return a future that will eventually contain the compression result
*/
- @NonNull
- ListenableFuture<CompressionResult> compress(@NonNull CloseableByteSource source);
+ @Nonnull
+ ListenableFuture<CompressionResult> compress(@Nonnull CloseableByteSource source);
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/DataDescriptorType.java b/src/main/java/com/android/apkzlib/zip/DataDescriptorType.java
similarity index 96%
rename from src/main/java/com/android/builder/internal/packaging/zip/DataDescriptorType.java
rename to src/main/java/com/android/apkzlib/zip/DataDescriptorType.java
index f762852..9b7425e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/DataDescriptorType.java
+++ b/src/main/java/com/android/apkzlib/zip/DataDescriptorType.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
/**
* Type of data descriptor that an entry has. Data descriptors are used if the CRC and sizing data
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/EncodeUtils.java b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
similarity index 64%
rename from src/main/java/com/android/builder/internal/packaging/zip/EncodeUtils.java
rename to src/main/java/com/android/apkzlib/zip/EncodeUtils.java
index 35ebcf6..259f64e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/EncodeUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
@@ -14,14 +14,15 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
import com.google.common.base.Charsets;
-
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
+import javax.annotation.Nonnull;
/**
* Utilities to encode and decode file names in zips.
@@ -45,18 +46,17 @@
* @param flags the zip entry flags
* @return the decode file name
*/
- @NonNull
- public static String decode(@NonNull ByteBuffer bytes, int length, @NonNull GPFlags flags)
+ @Nonnull
+ public static String decode(@Nonnull ByteBuffer bytes, int length, @Nonnull GPFlags flags)
throws IOException {
if (bytes.remaining() < length) {
throw new IOException("Only " + bytes.remaining() + " bytes exist in the buffer, but "
+ "length is " + length + ".");
}
- Charset charset = flagsCharset(flags);
byte[] stringBytes = new byte[length];
bytes.get(stringBytes);
- return charset.decode(ByteBuffer.wrap(stringBytes)).toString();
+ return decode(stringBytes, flags);
}
/**
@@ -66,10 +66,34 @@
* @param flags the zip entry flags
* @return the decode file name
*/
- @NonNull
- public static String decode(@NonNull byte[] data, @NonNull GPFlags flags) {
- Charset charset = flagsCharset(flags);
- return charset.decode(ByteBuffer.wrap(data)).toString();
+ @Nonnull
+ public static String decode(@Nonnull byte[] data, @Nonnull GPFlags flags) {
+ return decode(data, flagsCharset(flags));
+ }
+
+ /**
+ * Decodes a file name.
+ *
+ * @param data the raw data
+ * @param charset the charset to use
+ * @return the decode file name
+ */
+ @Nonnull
+ private static String decode(@Nonnull byte[] data, @Nonnull Charset charset) {
+ try {
+ return charset.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .decode(ByteBuffer.wrap(data))
+ .toString();
+ } catch (CharacterCodingException e) {
+ // If we're trying to decode ASCII, try UTF-8. Otherwise, revert to the default
+ // behavior (usually replacing invalid characters).
+ if (charset.equals(Charsets.US_ASCII)) {
+ return decode(data, Charsets.UTF_8);
+ } else {
+ return charset.decode(ByteBuffer.wrap(data)).toString();
+ }
+ }
}
/**
@@ -79,8 +103,8 @@
* @param flags the zip entry flags
* @return the encoded file name
*/
- @NonNull
- public static byte[] encode(@NonNull String name, @NonNull GPFlags flags) {
+ @Nonnull
+ public static byte[] encode(@Nonnull String name, @Nonnull GPFlags flags) {
Charset charset = flagsCharset(flags);
ByteBuffer bytes = charset.encode(name);
byte[] result = new byte[bytes.remaining()];
@@ -94,8 +118,8 @@
* @param flags the flags
* @return the charset to use
*/
- @NonNull
- private static Charset flagsCharset(@NonNull GPFlags flags) {
+ @Nonnull
+ private static Charset flagsCharset(@Nonnull GPFlags flags) {
if (flags.isUtf8FileName()) {
return Charsets.UTF_8;
} else {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/Eocd.java b/src/main/java/com/android/apkzlib/zip/Eocd.java
similarity index 79%
rename from src/main/java/com/android/builder/internal/packaging/zip/Eocd.java
rename to src/main/java/com/android/apkzlib/zip/Eocd.java
index 5d3bf33..1568840 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/Eocd.java
+++ b/src/main/java/com/android/apkzlib/zip/Eocd.java
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.CachedSupplier;
+import com.android.apkzlib.utils.CachedSupplier;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.primitives.Ints;
-
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
/**
* End Of Central Directory record in a zip file.
@@ -90,29 +89,29 @@
/**
* Number of entries in the central directory.
*/
- private final int mTotalRecords;
+ private final int totalRecords;
/**
* Offset from the beginning of the archive where the Central Directory is located.
*/
- private final long mDirectoryOffset;
+ private final long directoryOffset;
/**
* Number of bytes of the Central Directory.
*/
- private final long mDirectorySize;
+ private final long directorySize;
/**
* Contents of the EOCD comment.
*/
- @NonNull
- private final byte[] mComment;
+ @Nonnull
+ private final byte[] comment;
/**
* Supplier of the byte representation of the EOCD.
*/
- @NonNull
- private final CachedSupplier<byte[]> mByteSupplier;
+ @Nonnull
+ private final CachedSupplier<byte[]> byteSupplier;
/**
* Creates a new EOCD, reading it from a byte source. This method will parse the byte source
@@ -122,7 +121,7 @@
* buffer's position will have moved to the end of the EOCD
* @throws IOException failed to read information or the EOCD data is corrupt or invalid
*/
- Eocd(@NonNull ByteBuffer bytes) throws IOException {
+ Eocd(@Nonnull ByteBuffer bytes) throws IOException {
/*
* Read the EOCD record.
@@ -146,18 +145,18 @@
Verify.verify(totalRecords1 <= Integer.MAX_VALUE);
- mTotalRecords = Ints.checkedCast(totalRecords1);
- mDirectorySize = directorySize;
- mDirectoryOffset = directoryOffset;
+ totalRecords = Ints.checkedCast(totalRecords1);
+ this.directorySize = directorySize;
+ this.directoryOffset = directoryOffset;
if (bytes.remaining() < commentSize) {
throw new IOException("Corrupt EOCD record: not enough data for comment (comment "
+ "size is " + commentSize + ").");
}
- mComment = new byte[commentSize];
- bytes.get(mComment);
- mByteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+ comment = new byte[commentSize];
+ bytes.get(comment);
+ byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
@@ -168,17 +167,18 @@
* @param directoryOffset offset, since beginning of archive, where the Central Directory is
* located
* @param directorySize number of bytes of the Central Directory
+ * @param comment the EOCD comment
*/
- Eocd(int totalRecords, long directoryOffset, long directorySize) {
+ Eocd(int totalRecords, long directoryOffset, long directorySize, @Nonnull byte[] comment) {
Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0");
Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0");
Preconditions.checkArgument(directorySize >= 0, "directorySize < 0");
- mTotalRecords = totalRecords;
- mDirectoryOffset = directoryOffset;
- mDirectorySize = directorySize;
- mComment = new byte[0];
- mByteSupplier = new CachedSupplier<byte[]>(this::computeByteRepresentation);
+ this.totalRecords = totalRecords;
+ this.directoryOffset = directoryOffset;
+ this.directorySize = directorySize;
+ this.comment = comment;
+ byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
@@ -187,7 +187,7 @@
* @return the number of records
*/
int getTotalRecords() {
- return mTotalRecords;
+ return totalRecords;
}
/**
@@ -197,7 +197,7 @@
* @return the offset where the Central Directory is located
*/
long getDirectoryOffset() {
- return mDirectoryOffset;
+ return directoryOffset;
}
/**
@@ -206,7 +206,7 @@
* @return the number of bytes that make up the Central Directory
*/
long getDirectorySize() {
- return mDirectorySize;
+ return directorySize;
}
/**
@@ -215,7 +215,7 @@
* @return the size, in bytes, of the EOCD
*/
long getEocdSize() {
- return F_COMMENT_SIZE.endOffset() + mComment.length;
+ return (long) F_COMMENT_SIZE.endOffset() + comment.length;
}
/**
@@ -224,9 +224,22 @@
* @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
* @throws IOException failed to generate the EOCD data
*/
- @NonNull
+ @Nonnull
byte[] toBytes() throws IOException {
- return mByteSupplier.get();
+ return byteSupplier.get();
+ }
+
+ /*
+ * Obtains the comment in the EOCD.
+ *
+ * @return the comment exactly as it is represented in the file (no encoding conversion is
+ * done)
+ */
+ @Nonnull
+ byte[] getComment() {
+ byte[] commentCopy = new byte[comment.length];
+ System.arraycopy(comment, 0, commentCopy, 0, comment.length);
+ return commentCopy;
}
/**
@@ -235,20 +248,20 @@
* @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
* @throws UncheckedIOException failed to generate the EOCD data
*/
- @NonNull
+ @Nonnull
private byte[] computeByteRepresentation() {
- ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + mComment.length);
+ ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length);
try {
F_SIGNATURE.write(out);
F_NUMBER_OF_DISK.write(out);
F_DISK_CD_START.write(out);
- F_RECORDS_DISK.write(out, mTotalRecords);
- F_RECORDS_TOTAL.write(out, mTotalRecords);
- F_CD_SIZE.write(out, mDirectorySize);
- F_CD_OFFSET.write(out, mDirectoryOffset);
- F_COMMENT_SIZE.write(out, mComment.length);
- out.put(mComment);
+ F_RECORDS_DISK.write(out, totalRecords);
+ F_RECORDS_TOTAL.write(out, totalRecords);
+ F_CD_SIZE.write(out, directorySize);
+ F_CD_OFFSET.write(out, directoryOffset);
+ F_COMMENT_SIZE.write(out, comment.length);
+ out.put(comment);
return out.array();
} catch (IOException e) {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ExtraField.java b/src/main/java/com/android/apkzlib/zip/ExtraField.java
similarity index 74%
rename from src/main/java/com/android/builder/internal/packaging/zip/ExtraField.java
rename to src/main/java/com/android/apkzlib/zip/ExtraField.java
index 27721a9..d70fa7f 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ExtraField.java
+++ b/src/main/java/com/android/apkzlib/zip/ExtraField.java
@@ -14,20 +14,18 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.LittleEndianUtils;
+import com.android.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
-
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
-
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Contains an extra field.
@@ -57,11 +55,11 @@
static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935;
/**
- * The field's raw data, if it is known. Either this variable or {@link #mSegments} must be
+ * The field's raw data, if it is known. Either this variable or {@link #segments} must be
* non-{@code null}.
*/
@Nullable
- private final byte[] mRawData;
+ private final byte[] rawData;
/**
* The list of field's segments. Will be populated if the extra field is created based on a
@@ -69,24 +67,24 @@
* on the raw bytes.
*/
@Nullable
- private ImmutableList<Segment> mSegments;
+ private ImmutableList<Segment> segments;
/**
* Creates an extra field based on existing raw data.
*
* @param rawData the raw data; will not be parsed unless needed
*/
- public ExtraField(@NonNull byte[] rawData) {
- mRawData = rawData;
- mSegments = null;
+ public ExtraField(@Nonnull byte[] rawData) {
+ this.rawData = rawData;
+ segments = null;
}
/**
* Creates a new extra field with no segments.
*/
public ExtraField() {
- mRawData = null;
- mSegments = ImmutableList.of();
+ rawData = null;
+ segments = ImmutableList.of();
}
/**
@@ -94,9 +92,9 @@
*
* @param segments the segments
*/
- public ExtraField(@NonNull ImmutableList<Segment> segments) {
- mRawData = null;
- mSegments = segments;
+ public ExtraField(@Nonnull ImmutableList<Segment> segments) {
+ rawData = null;
+ this.segments = segments;
}
/**
@@ -106,12 +104,12 @@
* @throws IOException failed to parse the extra field
*/
public ImmutableList<Segment> getSegments() throws IOException {
- if (mSegments == null) {
+ if (segments == null) {
parseSegments();
}
- Preconditions.checkNotNull(mSegments);
- return mSegments;
+ Preconditions.checkNotNull(segments);
+ return segments;
}
/**
@@ -137,16 +135,16 @@
}
/**
- * Parses the raw data and generates all segments in {@link #mSegments}.
+ * Parses the raw data and generates all segments in {@link #segments}.
*
* @throws IOException failed to parse the data
*/
private void parseSegments() throws IOException {
- Preconditions.checkNotNull(mRawData);
- Preconditions.checkState(mSegments == null);
+ Preconditions.checkNotNull(rawData);
+ Preconditions.checkState(segments == null);
List<Segment> segments = new ArrayList<>();
- ByteBuffer buffer = ByteBuffer.wrap(mRawData);
+ ByteBuffer buffer = ByteBuffer.wrap(rawData);
while (buffer.remaining() > 0) {
int headerId = LittleEndianUtils.readUnsigned2Le(buffer);
@@ -160,6 +158,16 @@
}
byte[] data = new byte[dataSize];
+ if (buffer.remaining() < dataSize) {
+ throw new IOException(
+ "Invalid data size for extra field segment with header ID "
+ + headerId
+ + ": "
+ + dataSize
+ + " (only "
+ + buffer.remaining()
+ + " bytes are available)");
+ }
buffer.get(data);
SegmentFactory factory = identifySegmentFactory(headerId);
@@ -167,7 +175,7 @@
segments.add(seg);
}
- mSegments = ImmutableList.copyOf(segments);
+ this.segments = ImmutableList.copyOf(segments);
}
/**
@@ -176,12 +184,12 @@
* @return the size
*/
public int size() {
- if (mRawData != null) {
- return mRawData.length;
+ if (rawData != null) {
+ return rawData.length;
} else {
- Preconditions.checkNotNull(mSegments);
+ Preconditions.checkNotNull(segments);
int sz = 0;
- for (Segment s : mSegments) {
+ for (Segment s : segments) {
sz += s.size();
}
@@ -196,12 +204,12 @@
* written
* @throws IOException failed to write the extra fields
*/
- public void write(@NonNull ByteBuffer out) throws IOException {
- if (mRawData != null) {
- out.put(mRawData);
+ public void write(@Nonnull ByteBuffer out) throws IOException {
+ if (rawData != null) {
+ out.put(rawData);
} else {
- Preconditions.checkNotNull(mSegments);
- for (Segment s : mSegments) {
+ Preconditions.checkNotNull(segments);
+ for (Segment s : segments) {
s.write(out);
}
}
@@ -213,7 +221,7 @@
* @param headerId the header ID
* @return the segmnet factory that creates segments with the given header
*/
- @NonNull
+ @Nonnull
private static SegmentFactory identifySegmentFactory(int headerId) {
if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
return AlignmentSegment::new;
@@ -249,7 +257,7 @@
* be written
* @throws IOException failed to write segment data
*/
- void write(@NonNull ByteBuffer out) throws IOException;
+ void write(@Nonnull ByteBuffer out) throws IOException;
}
/**
@@ -266,8 +274,8 @@
* @return the created segment
* @throws IOException failed to create the segment from the data
*/
- @NonNull
- Segment make(int headerId, @NonNull byte[] data) throws IOException;
+ @Nonnull
+ Segment make(int headerId, @Nonnull byte[] data) throws IOException;
}
/**
@@ -279,13 +287,13 @@
/**
* Header ID.
*/
- private final int mHeaderId;
+ private final int headerId;
/**
* Data in the segment.
*/
- @NonNull
- private final byte[] mData;
+ @Nonnull
+ private final byte[] data;
/**
* Creates a new raw data segment.
@@ -293,26 +301,26 @@
* @param headerId the header ID
* @param data the segment data
*/
- RawDataSegment(int headerId, @NonNull byte[] data) {
- mHeaderId = headerId;
- mData = data;
+ RawDataSegment(int headerId, @Nonnull byte[] data) {
+ this.headerId = headerId;
+ this.data = data;
}
@Override
public int getHeaderId() {
- return mHeaderId;
+ return headerId;
}
@Override
- public void write(@NonNull ByteBuffer out) throws IOException {
- LittleEndianUtils.writeUnsigned2Le(out, mHeaderId);
- LittleEndianUtils.writeUnsigned2Le(out, mData.length);
- out.put(mData);
+ public void write(@Nonnull ByteBuffer out) throws IOException {
+ LittleEndianUtils.writeUnsigned2Le(out, headerId);
+ LittleEndianUtils.writeUnsigned2Le(out, data.length);
+ out.put(data);
}
@Override
public int size() {
- return 4 + mData.length;
+ return 4 + data.length;
}
}
@@ -326,14 +334,19 @@
public static class AlignmentSegment implements Segment {
/**
+ * Minimum size for an alignment segment.
+ */
+ public static final int MINIMUM_SIZE = 6;
+
+ /**
* The alignment value.
*/
- private int mAlignment;
+ private int alignment;
/**
* How many bytes of padding are in this segment?
*/
- private int mPadding;
+ private int padding;
/**
* Creates a new alignment segment.
@@ -343,14 +356,14 @@
*/
public AlignmentSegment(int alignment, int totalSize) {
Preconditions.checkArgument(alignment > 0, "alignment <= 0");
- Preconditions.checkArgument(totalSize >= 6, "totalSize < 6");
+ Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, "totalSize < MINIMUM_SIZE");
/*
* We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment
* value (2 bytes).
*/
- mAlignment = alignment;
- mPadding = totalSize - 6;
+ this.alignment = alignment;
+ padding = totalSize - MINIMUM_SIZE;
}
/**
@@ -360,29 +373,29 @@
* @param data the segment data
* @throws IOException failed to create the segment from the data
*/
- public AlignmentSegment(int headerId, @NonNull byte[] data) throws IOException {
+ public AlignmentSegment(int headerId, @Nonnull byte[] data) throws IOException {
Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
ByteBuffer dataBuffer = ByteBuffer.wrap(data);
- mAlignment = LittleEndianUtils.readUnsigned2Le(dataBuffer);
- if (mAlignment <= 0) {
- throw new IOException("Invalid alignment in alignment field: " + mAlignment);
+ alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer);
+ if (alignment <= 0) {
+ throw new IOException("Invalid alignment in alignment field: " + alignment);
}
- mPadding = data.length - 2;
+ padding = data.length - 2;
}
@Override
- public void write(@NonNull ByteBuffer out) throws IOException {
+ public void write(@Nonnull ByteBuffer out) throws IOException {
LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
- LittleEndianUtils.writeUnsigned2Le(out, mPadding + 2);
- LittleEndianUtils.writeUnsigned2Le(out, mAlignment);
- out.put(new byte[mPadding]);
+ LittleEndianUtils.writeUnsigned2Le(out, padding + 2);
+ LittleEndianUtils.writeUnsigned2Le(out, alignment);
+ out.put(new byte[padding]);
}
@Override
public int size() {
- return mPadding + 6;
+ return padding + 6;
}
@Override
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMap.java b/src/main/java/com/android/apkzlib/zip/FileUseMap.java
similarity index 79%
rename from src/main/java/com/android/builder/internal/packaging/zip/FileUseMap.java
rename to src/main/java/com/android/apkzlib/zip/FileUseMap.java
index ab2df9b..8a76878 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMap.java
+++ b/src/main/java/com/android/apkzlib/zip/FileUseMap.java
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.Lists;
@@ -28,6 +26,8 @@
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* The file use map keeps track of which parts of the zip file are used which parts are not.
@@ -50,24 +50,24 @@
class FileUseMap {
/**
* Size of the file according to the map. This should always match the last entry in
- * {@code #mMap}.
+ * {@code #map}.
*/
- private long mSize;
+ private long size;
/**
- * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #mSize}.
- * If {@link #mSize} is zero then this set is empty. This is the only situation in which the map
+ * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #size}.
+ * If {@link #size} is zero then this set is empty. This is the only situation in which the map
* will be empty.
*/
- @NonNull
- private TreeSet<FileUseMapEntry<?>> mMap;
+ @Nonnull
+ private TreeSet<FileUseMapEntry<?>> map;
/**
- * Tree with all free blocks ordered by size. This is essentially a view over {@link #mMap}
+ * Tree with all free blocks ordered by size. This is essentially a view over {@link #map}
* containing only the free blocks, but in a different order.
*/
- @NonNull
- private TreeSet<FileUseMapEntry<?>> mFree;
+ @Nonnull
+ private TreeSet<FileUseMapEntry<?>> free;
/**
* If defined, defines the minimum size for a free entry.
@@ -84,9 +84,9 @@
Preconditions.checkArgument(size >= 0, "size < 0");
Preconditions.checkArgument(minFreeSize >= 0, "minFreeSize < 0");
- mSize = size;
- mMap = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
- mFree = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE);
+ this.size = size;
+ map = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
+ free = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE);
mMinFreeSize = minFreeSize;
if (size > 0) {
@@ -99,11 +99,11 @@
*
* @param entry the entry to add
*/
- private void internalAdd(@NonNull FileUseMapEntry<?> entry) {
- mMap.add(entry);
+ private void internalAdd(@Nonnull FileUseMapEntry<?> entry) {
+ map.add(entry);
if (entry.isFree()) {
- mFree.add(entry);
+ free.add(entry);
}
}
@@ -112,12 +112,12 @@
*
* @param entry the entry to remove
*/
- private void internalRemove(@NonNull FileUseMapEntry<?> entry) {
- boolean wasRemoved = mMap.remove(entry);
- Preconditions.checkState(wasRemoved, "entry not in mMap");
+ private void internalRemove(@Nonnull FileUseMapEntry<?> entry) {
+ boolean wasRemoved = map.remove(entry);
+ Preconditions.checkState(wasRemoved, "entry not in map");
if (entry.isFree()) {
- mFree.remove(entry);
+ free.remove(entry);
}
}
@@ -128,9 +128,9 @@
*
* @param entry the entry to add
*/
- private void add(@NonNull FileUseMapEntry<?> entry) {
- Preconditions.checkArgument(entry.getStart() < mSize, "entry.getStart() >= mSize");
- Preconditions.checkArgument(entry.getEnd() <= mSize, "entry.getEnd() > mSize");
+ private void add(@Nonnull FileUseMapEntry<?> entry) {
+ Preconditions.checkArgument(entry.getStart() < size, "entry.getStart() >= size");
+ Preconditions.checkArgument(entry.getEnd() <= size, "entry.getEnd() > size");
Preconditions.checkArgument(!entry.isFree(), "entry.isFree()");
FileUseMapEntry<?> container = findContainer(entry);
@@ -149,8 +149,8 @@
*
* @param entry the entry
*/
- void remove(@NonNull FileUseMapEntry<?> entry) {
- Preconditions.checkState(mMap.contains(entry), "!mMap.contains(entry)");
+ void remove(@Nonnull FileUseMapEntry<?> entry) {
+ Preconditions.checkState(map.contains(entry), "!map.contains(entry)");
Preconditions.checkArgument(!entry.isFree(), "entry.isFree()");
internalRemove(entry);
@@ -174,7 +174,7 @@
* @param <T> the type of data to store in the entry
* @return the new entry
*/
- <T> FileUseMapEntry<T> add(long start, long end, @NonNull T store) {
+ <T> FileUseMapEntry<T> add(long start, long end, @Nonnull T store) {
Preconditions.checkArgument(start >= 0, "start < 0");
Preconditions.checkArgument(end > start, "end < start");
@@ -189,9 +189,9 @@
* @param entry the entry whose container we're looking for
* @return the container
*/
- @NonNull
- private FileUseMapEntry<?> findContainer(@NonNull FileUseMapEntry<?> entry) {
- FileUseMapEntry container = mMap.floor(entry);
+ @Nonnull
+ private FileUseMapEntry<?> findContainer(@Nonnull FileUseMapEntry<?> entry) {
+ FileUseMapEntry container = map.floor(entry);
Verify.verifyNotNull(container);
Verify.verify(container.getStart() <= entry.getStart());
Verify.verify(container.getEnd() >= entry.getEnd());
@@ -203,15 +203,15 @@
* Splits a container to add an entry, adding new free entries before and after the provided
* entry if needed.
*
- * @param container the container entry, a free entry that is in {@link #mMap} that that
+ * @param container the container entry, a free entry that is in {@link #map} that that
* encloses {@code entry}
* @param entry the entry that will be used to split {@code container}
* @return a set of non-overlapping entries that completely covers {@code container} and that
* includes {@code entry}
*/
- @NonNull
- private static Set<FileUseMapEntry<?>> split(@NonNull FileUseMapEntry<?> container,
- @NonNull FileUseMapEntry<?> entry) {
+ @Nonnull
+ private static Set<FileUseMapEntry<?>> split(@Nonnull FileUseMapEntry<?> container,
+ @Nonnull FileUseMapEntry<?> entry) {
Preconditions.checkArgument(container.isFree(), "!container.isFree()");
long farStart = container.getStart();
@@ -243,7 +243,7 @@
*
* @param entry the free entry to coalesce with neighbors
*/
- private void coalesce(@NonNull FileUseMapEntry<?> entry) {
+ private void coalesce(@Nonnull FileUseMapEntry<?> entry) {
Preconditions.checkArgument(entry.isFree(), "!entry.isFree()");
FileUseMapEntry<?> prevToMerge = null;
@@ -252,7 +252,7 @@
/*
* See if we have a previous entry to merge with this one.
*/
- prevToMerge = mMap.floor(FileUseMapEntry.makeFree(start - 1, start));
+ prevToMerge = map.floor(FileUseMapEntry.makeFree(start - 1, start));
Verify.verifyNotNull(prevToMerge);
if (!prevToMerge.isFree()) {
prevToMerge = null;
@@ -261,11 +261,11 @@
FileUseMapEntry<?> nextToMerge = null;
long end = entry.getEnd();
- if (end < mSize) {
+ if (end < size) {
/*
* See if we have a next entry to merge with this one.
*/
- nextToMerge = mMap.ceiling(FileUseMapEntry.makeFree(end, end + 1));
+ nextToMerge = map.ceiling(FileUseMapEntry.makeFree(end, end + 1));
Verify.verifyNotNull(nextToMerge);
if (!nextToMerge.isFree()) {
nextToMerge = null;
@@ -296,18 +296,18 @@
* Truncates map removing the top entry if it is free and reducing the map's size.
*/
void truncate() {
- if (mSize == 0) {
+ if (size == 0) {
return;
}
/*
* Find the last entry.
*/
- FileUseMapEntry<?> last = mMap.last();
+ FileUseMapEntry<?> last = map.last();
Verify.verifyNotNull(last, "last == null");
if (last.isFree()) {
internalRemove(last);
- mSize = last.getStart();
+ size = last.getStart();
}
}
@@ -317,7 +317,7 @@
* @return the size
*/
long size() {
- return mSize;
+ return size;
}
/**
@@ -326,7 +326,7 @@
* @return the size of the file discounting the last block if it is empty
*/
long usedSize() {
- if (mSize == 0) {
+ if (size == 0) {
return 0;
}
@@ -334,13 +334,13 @@
* Find the last entry to see if it is an empty entry. If it is, we need to remove its size
* from the returned value.
*/
- FileUseMapEntry<?> last = mMap.last();
+ FileUseMapEntry<?> last = map.last();
Verify.verifyNotNull(last, "last == null");
if (last.isFree()) {
return last.getStart();
} else {
- Verify.verify(last.getEnd() == mSize);
- return mSize;
+ Verify.verify(last.getEnd() == size);
+ return size;
}
}
@@ -351,16 +351,16 @@
* @param size the new size of the map that cannot be smaller that the current size
*/
void extend(long size) {
- Preconditions.checkArgument(size >= mSize, "size < mSize");
+ Preconditions.checkArgument(size >= this.size, "size < size");
- if (mSize == size) {
+ if (this.size == size) {
return;
}
- FileUseMapEntry<?> newBlock = FileUseMapEntry.makeFree(mSize, size);
+ FileUseMapEntry<?> newBlock = FileUseMapEntry.makeFree(this.size, size);
internalAdd(newBlock);
- mSize = size;
+ this.size = size;
coalesce(newBlock);
}
@@ -381,7 +381,7 @@
* @param alg which algorithm to use
* @return the location of the contiguous area; this may be located at the end of the map
*/
- long locateFree(long size, long alignOffset, long align, @NonNull PositionAlgorithm alg) {
+ long locateFree(long size, long alignOffset, long align, @Nonnull PositionAlgorithm alg) {
Preconditions.checkArgument(size > 0, "size <= 0");
FileUseMapEntry<?> minimumSizedEntry = FileUseMapEntry.makeFree(0, size);
@@ -389,10 +389,10 @@
switch (alg) {
case BEST_FIT:
- matches = mFree.tailSet(minimumSizedEntry);
+ matches = free.tailSet(minimumSizedEntry);
break;
case FIRST_FIT:
- matches = mMap;
+ matches = map;
break;
default:
throw new AssertionError();
@@ -445,7 +445,7 @@
*/
long emptySpaceLeft = curr.getSize() - (size + extraSize);
if (emptySpaceLeft > 0 && emptySpaceLeft < mMinFreeSize) {
- FileUseMapEntry<?> next = mMap.higher(curr);
+ FileUseMapEntry<?> next = map.higher(curr);
if (next != null && !next.isFree()) {
continue;
}
@@ -473,9 +473,9 @@
/*
* If no entry that could hold size is found, get the first free byte.
*/
- long firstFree = mSize;
- if (best == null && !mMap.isEmpty()) {
- FileUseMapEntry<?> last = mMap.last();
+ long firstFree = this.size;
+ if (best == null && !map.isEmpty()) {
+ FileUseMapEntry<?> last = map.last();
if (last.isFree()) {
firstFree = last.getStart();
}
@@ -511,12 +511,12 @@
* in file order, that is, if area {@code x} starts before area {@code y}, then area {@code x}
* will be stored before area {@code y} in the list
*/
- @NonNull
+ @Nonnull
List<FileUseMapEntry<?>> getFreeAreas() {
List<FileUseMapEntry<?>> freeAreas = Lists.newArrayList();
- for (FileUseMapEntry<?> area : mMap) {
- if (area.isFree() && area.getEnd() != mSize) {
+ for (FileUseMapEntry<?> area : map) {
+ if (area.isFree() && area.getEnd() != size) {
freeAreas.add(area);
}
}
@@ -532,16 +532,53 @@
* in the map
*/
@Nullable
- FileUseMapEntry<?> before(@NonNull FileUseMapEntry<?> entry) {
+ FileUseMapEntry<?> before(@Nonnull FileUseMapEntry<?> entry) {
Preconditions.checkNotNull(entry, "entry == null");
- return mMap.lower(entry);
+ return map.lower(entry);
+ }
+
+ /**
+ * Obtains the entry that is located after the one provided.
+ *
+ * @param entry the map entry to get the next one for; must belong to the map
+ * @return the entry after the provided one, {@code null} if {@code entry} is the last entry in
+ * the map
+ */
+ @Nullable
+ FileUseMapEntry<?> after(@Nonnull FileUseMapEntry<?> entry) {
+ Preconditions.checkNotNull(entry, "entry == null");
+
+ return map.higher(entry);
+ }
+
+ /**
+ * Obtains the entry at the given offset.
+ *
+ * @param offset the offset to look for
+ * @return the entry found or {@code null} if there is no entry (not even a free one) at the
+ * given offset
+ */
+ @Nullable
+ FileUseMapEntry<?> at(long offset) {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+ Preconditions.checkArgument(offset < size, "offset >= size");
+
+ FileUseMapEntry<?> entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1));
+ if (entry == null) {
+ return null;
+ }
+
+ Verify.verify(entry.getStart() <= offset);
+ Verify.verify(entry.getEnd() > offset);
+
+ return entry;
}
@Override
public String toString() {
StringJoiner j = new StringJoiner(", ");
- mMap.stream()
+ map.stream()
.map(e -> e.getStart() + " - " + e.getEnd() + ": " + e.getStore())
.forEach(j::add);
return "FileUseMap[" + j.toString() + "]";
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMapEntry.java b/src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java
similarity index 87%
rename from src/main/java/com/android/builder/internal/packaging/zip/FileUseMapEntry.java
rename to src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java
index c0529c8..3e5aaba 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMapEntry.java
+++ b/src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java
@@ -14,15 +14,14 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.primitives.Ints;
-
import java.util.Comparator;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Represents an entry in the {@link FileUseMap}. Each entry contains an interval of bytes. The
@@ -51,18 +50,18 @@
/**
* The first byte in the entry.
*/
- private final long mStart;
+ private final long start;
/**
* The first byte no longer in the entry.
*/
- private final long mEnd;
+ private final long end;
/**
* The stored data. If {@code null} then this entry represents a free entry.
*/
@Nullable
- private final T mStore;
+ private final T store;
/**
* Creates a new map entry.
@@ -75,9 +74,9 @@
Preconditions.checkArgument(start >= 0, "start < 0");
Preconditions.checkArgument(end > start, "end <= start");
- mStart = start;
- mEnd = end;
- mStore = store;
+ this.start = start;
+ this.end = end;
+ this.store = store;
}
/**
@@ -100,7 +99,7 @@
* @param <T> the type of data to store in the entry
* @return the entry
*/
- public static <T> FileUseMapEntry<T> makeUsed(long start, long end, @NonNull T store) {
+ public static <T> FileUseMapEntry<T> makeUsed(long start, long end, @Nonnull T store) {
Preconditions.checkNotNull(store, "store == null");
return new FileUseMapEntry<>(start, end, store);
}
@@ -112,7 +111,7 @@
* is empty and contains no data)
*/
long getStart() {
- return mStart;
+ return start;
}
/**
@@ -121,7 +120,7 @@
* @return the first byte no longer in the entry
*/
long getEnd() {
- return mEnd;
+ return end;
}
/**
@@ -130,7 +129,7 @@
* @return the number of bytes contained in the entry
*/
long getSize() {
- return mEnd - mStart;
+ return end - start;
}
/**
@@ -139,7 +138,7 @@
* @return is this entry free?
*/
boolean isFree() {
- return mStore == null;
+ return store == null;
}
/**
@@ -149,15 +148,15 @@
*/
@Nullable
T getStore() {
- return mStore;
+ return store;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
- .add("start", mStart)
- .add("end", mEnd)
- .add("store", mStore)
+ .add("start", start)
+ .add("end", end)
+ .add("store", store)
.toString();
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/GPFlags.java b/src/main/java/com/android/apkzlib/zip/GPFlags.java
similarity index 90%
rename from src/main/java/com/android/builder/internal/packaging/zip/GPFlags.java
rename to src/main/java/com/android/apkzlib/zip/GPFlags.java
index 440718a..fc27c5d 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/GPFlags.java
+++ b/src/main/java/com/android/apkzlib/zip/GPFlags.java
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.zip;
import java.io.IOException;
+import javax.annotation.Nonnull;
/**
* General purpose bit flags. Contains the encoding of the zip's general purpose bits.
@@ -68,20 +67,21 @@
*/
private static final int BIT_UNUSED = (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10)
| (1 << 14) | (1 << 15);
+
/**
* Bit flag value.
*/
- private final long mValue;
+ private final long value;
/**
* Has the CRC computation beeen deferred?
*/
- private boolean mDeferredCrc;
+ private boolean deferredCrc;
/**
* Is the file name encoded in UTF-8?
*/
- private boolean mUtf8FileName;
+ private boolean utf8FileName;
/**
* Creates a new flags object.
@@ -89,10 +89,10 @@
* @param value the value of the bit mask
*/
private GPFlags(long value) {
- mValue = value;
+ this.value = value;
- mDeferredCrc = ((value & BIT_DEFERRED_CRC) != 0);
- mUtf8FileName = ((value & BIT_EFS) != 0);
+ deferredCrc = ((value & BIT_DEFERRED_CRC) != 0);
+ utf8FileName = ((value & BIT_EFS) != 0);
}
/**
@@ -101,7 +101,7 @@
* @return the value of the bit mask
*/
public long getValue() {
- return mValue;
+ return value;
}
/**
@@ -110,7 +110,7 @@
* @return is the CRC computation deferred?
*/
public boolean isDeferredCrc() {
- return mDeferredCrc;
+ return deferredCrc;
}
/**
@@ -119,7 +119,7 @@
* @return is the file name encoded in UTF-8?
*/
public boolean isUtf8FileName() {
- return mUtf8FileName;
+ return utf8FileName;
}
/**
@@ -128,7 +128,7 @@
* @param utf8Encoding should UTF-8 encoding be used?
* @return the new bit mask
*/
- @NonNull
+ @Nonnull
static GPFlags make(boolean utf8Encoding) {
long flags = 0;
@@ -147,7 +147,7 @@
* @return the created flag information
* @throws IOException unsupported options are used in the bit mask
*/
- @NonNull
+ @Nonnull
static GPFlags from(long bits) throws IOException {
if ((bits & BIT_ENCRYPTION) != 0) {
throw new IOException("Zip files with encrypted of entries not supported.");
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/InflaterByteSource.java b/src/main/java/com/android/apkzlib/zip/InflaterByteSource.java
similarity index 78%
rename from src/main/java/com/android/builder/internal/packaging/zip/InflaterByteSource.java
rename to src/main/java/com/android/apkzlib/zip/InflaterByteSource.java
index 45154cc..974d4ac 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/InflaterByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/InflaterByteSource.java
@@ -14,17 +14,16 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-
+import com.android.apkzlib.zip.utils.CloseableByteSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
+import javax.annotation.Nonnull;
/**
* Byte source that inflates another byte source. It assumed the inner byte source has deflated
@@ -35,15 +34,15 @@
/**
* The stream factory for the deflated data.
*/
- @NonNull
- private final CloseableByteSource mDeflatedSource;
+ @Nonnull
+ private final CloseableByteSource deflatedSource;
/**
* Creates a new source.
* @param byteSource the factory for deflated data
*/
- public InflaterByteSource(@NonNull CloseableByteSource byteSource) {
- mDeflatedSource = byteSource;
+ public InflaterByteSource(@Nonnull CloseableByteSource byteSource) {
+ deflatedSource = byteSource;
}
@Override
@@ -54,12 +53,12 @@
* "Oh, I need an extra dummy byte to allow for some... err... optimizations..."
*/
ByteArrayInputStream hackByte = new ByteArrayInputStream(new byte[] { 0 });
- return new InflaterInputStream(new SequenceInputStream(mDeflatedSource.openStream(),
+ return new InflaterInputStream(new SequenceInputStream(deflatedSource.openStream(),
hackByte), new Inflater(true));
}
@Override
public void innerClose() throws IOException {
- mDeflatedSource.close();
+ deflatedSource.close();
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/LazyDelegateByteSource.java b/src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java
similarity index 84%
rename from src/main/java/com/android/builder/internal/packaging/zip/LazyDelegateByteSource.java
rename to src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java
index 1620d10..bdd3e4c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/LazyDelegateByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.io.ByteProcessor;
@@ -26,12 +25,12 @@
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import com.google.common.util.concurrent.ListenableFuture;
-
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutionException;
+import javax.annotation.Nonnull;
/**
* {@code ByteSource} that delegates all operations to another {@code ByteSource}. The other
@@ -42,24 +41,24 @@
/**
* Byte source where we delegate operations to.
*/
- @NonNull
- private final ListenableFuture<CloseableByteSource> mDelegate;
+ @Nonnull
+ private final ListenableFuture<CloseableByteSource> delegate;
/**
* Creates a new byte source that delegates operations to the provided source.
* @param delegate the source that will receive all operations
*/
- public LazyDelegateByteSource(@NonNull ListenableFuture<CloseableByteSource> delegate) {
- mDelegate = delegate;
+ public LazyDelegateByteSource(@Nonnull ListenableFuture<CloseableByteSource> delegate) {
+ this.delegate = delegate;
}
/**
* Obtains the delegate future.
* @return the delegate future, that may be computed or not
*/
- @NonNull
+ @Nonnull
public ListenableFuture<CloseableByteSource> getDelegate() {
- return mDelegate;
+ return delegate;
}
/**
@@ -67,10 +66,10 @@
* @return the byte source
* @throws IOException failed to compute the future :)
*/
- @NonNull
+ @Nonnull
private CloseableByteSource get() throws IOException {
try {
- CloseableByteSource r = mDelegate.get();
+ CloseableByteSource r = delegate.get();
if (r == null) {
throw new IOException("Delegate byte source computation resulted in null.");
}
@@ -117,12 +116,12 @@
}
@Override
- public long copyTo(@NonNull OutputStream output) throws IOException {
+ public long copyTo(@Nonnull OutputStream output) throws IOException {
return get().copyTo(output);
}
@Override
- public long copyTo(@NonNull ByteSink sink) throws IOException {
+ public long copyTo(@Nonnull ByteSink sink) throws IOException {
return get().copyTo(sink);
}
@@ -132,7 +131,7 @@
}
@Override
- public <T> T read(@NonNull ByteProcessor<T> processor) throws IOException {
+ public <T> T read(@Nonnull ByteProcessor<T> processor) throws IOException {
return get().read(processor);
}
@@ -142,7 +141,7 @@
}
@Override
- public boolean contentEquals(@NonNull ByteSource other) throws IOException {
+ public boolean contentEquals(@Nonnull ByteSource other) throws IOException {
return get().contentEquals(other);
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ProcessedAndRawByteSources.java b/src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java
similarity index 75%
rename from src/main/java/com/android/builder/internal/packaging/zip/ProcessedAndRawByteSources.java
rename to src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java
index 1bda379..86c6382 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ProcessedAndRawByteSources.java
+++ b/src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.io.Closer;
-
import java.io.Closeable;
import java.io.IOException;
+import javax.annotation.Nonnull;
/**
* Container that has two bytes sources: one representing raw data and another processed data.
@@ -34,51 +33,54 @@
/**
* The processed byte source.
*/
- @NonNull
- private final CloseableByteSource mProcessedSource;
+ @Nonnull
+ private final CloseableByteSource processedSource;
/**
* The processed raw source.
*/
- @NonNull
- private final CloseableByteSource mRawSource;
+ @Nonnull
+ private final CloseableByteSource rawSource;
/**
* Creates a new container.
+ *
* @param processedSource the processed source
* @param rawSource the raw source
*/
- public ProcessedAndRawByteSources(@NonNull CloseableByteSource processedSource,
- @NonNull CloseableByteSource rawSource) {
- mProcessedSource = processedSource;
- mRawSource = rawSource;
+ public ProcessedAndRawByteSources(@Nonnull CloseableByteSource processedSource,
+ @Nonnull CloseableByteSource rawSource) {
+ this.processedSource = processedSource;
+ this.rawSource = rawSource;
}
/**
* Obtains a byte source that read the processed contents of the entry.
+ *
* @return a byte source
*/
- @NonNull
+ @Nonnull
public CloseableByteSource getProcessedByteSource() {
- return mProcessedSource;
+ return processedSource;
}
/**
* Obtains a byte source that reads the raw contents of an entry. This is the data that is
* ultimately stored in the file and, in the case of compressed files, is the same data in the
* source returned by {@link #getProcessedByteSource()}.
+ *
* @return a byte source
*/
- @NonNull
+ @Nonnull
public CloseableByteSource getRawByteSource() {
- return mRawSource;
+ return rawSource;
}
@Override
public void close() throws IOException {
Closer closer = Closer.create();
- closer.register(mProcessedSource);
- closer.register(mRawSource);
+ closer.register(processedSource);
+ closer.register(rawSource);
closer.close();
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntry.java b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
similarity index 68%
rename from src/main/java/com/android/builder/internal/packaging/zip/StoredEntry.java
rename to src/main/java/com/android/apkzlib/zip/StoredEntry.java
index a67c6b2..854bf3a 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntry.java
+++ b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
@@ -14,22 +14,23 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-import com.android.builder.internal.packaging.zip.utils.CloseableDelegateByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableDelegateByteSource;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Ints;
-
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Comparator;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* A stored entry represents a file in the zip. The entry may or may not be written to the zip
@@ -85,7 +86,8 @@
/**
* Local header field: version to extract, should match the CDH's.
*/
- private static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(
+ @VisibleForTesting
+ static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(
F_LOCAL_SIGNATURE.endOffset(), "Version to extract",
new ZipFieldInvariantNonNegative());
@@ -152,44 +154,50 @@
/**
* Type of entry.
*/
- @NonNull
- private StoredEntryType mType;
+ @Nonnull
+ private StoredEntryType type;
/**
* The central directory header with information about the file.
*/
- @NonNull
- private CentralDirectoryHeader mCdh;
+ @Nonnull
+ private CentralDirectoryHeader cdh;
/**
* The file this entry is associated with
*/
- @NonNull
- private ZFile mFile;
+ @Nonnull
+ private ZFile file;
/**
* Has this entry been deleted?
*/
- private boolean mDeleted;
+ private boolean deleted;
/**
* Extra field specified in the local directory.
*/
- @NonNull
- private ExtraField mLocalExtra;
+ @Nonnull
+ private ExtraField localExtra;
/**
* Type of data descriptor associated with the entry.
*/
- @NonNull
- private DataDescriptorType mDataDescriptorType;
+ @Nonnull
+ private DataDescriptorType dataDescriptorType;
/**
* Source for this entry's data. If this entry is a directory, this source has to have zero
* size.
*/
- @NonNull
- private ProcessedAndRawByteSources mSource;
+ @Nonnull
+ private ProcessedAndRawByteSources source;
+
+ /**
+ * Verify log for the entry.
+ */
+ @Nonnull
+ private final VerifyLog verifyLog;
/**
* Creates a new stored entry.
@@ -201,39 +209,45 @@
* read from the zip file, that is, if {@code header.getOffset()} is non-negative
* @throws IOException failed to create the entry
*/
- StoredEntry(@NonNull CentralDirectoryHeader header, @NonNull ZFile file,
- @Nullable ProcessedAndRawByteSources source) throws IOException {
- mCdh = header;
- mFile = file;
- mDeleted = false;
+ StoredEntry(
+ @Nonnull CentralDirectoryHeader header,
+ @Nonnull ZFile file,
+ @Nullable ProcessedAndRawByteSources source)
+ throws IOException {
+ cdh = header;
+ this.file = file;
+ deleted = false;
+ verifyLog = file.makeVerifyLog();
if (header.getOffset() >= 0) {
/*
* This will be overwritten during readLocalHeader. However, IJ complains if we don't
- * assign a value to mLocalExtra because of the @NonNull annotation.
+ * assign a value to localExtra because of the @Nonnull annotation.
*/
- mLocalExtra = new ExtraField();
+ localExtra = new ExtraField();
readLocalHeader();
- Preconditions.checkArgument(source == null, "Source was defined but contents already "
- + "exist on file.");
+ Preconditions.checkArgument(
+ source == null,
+ "Source was defined but contents already exist on file.");
/*
* Since the file is already in the zip, dynamically create a source that will read
* the file from the zip when needed. The assignment is not really needed, but we
* would get a warning because of the @NotNull otherwise.
*/
- mSource = createSourceFromZip(mCdh.getOffset());
+ this.source = createSourceFromZip(cdh.getOffset());
} else {
/*
* There is no local extra data for new files.
*/
- mLocalExtra = new ExtraField();
+ localExtra = new ExtraField();
- Preconditions.checkNotNull(source, "Source was not defined, but contents are not "
- + "on file.");
- mSource = source;
+ Preconditions.checkNotNull(
+ source,
+ "Source was not defined, but contents are not on file.");
+ this.source = source;
}
/*
@@ -241,31 +255,35 @@
* This seems to be respected by all zip utilities although I could not find there anywhere
* in the specification.
*/
- if (mCdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) {
- mType = StoredEntryType.DIRECTORY;
- Verify.verify(mSource.getProcessedByteSource().isEmpty(),
+ if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) {
+ type = StoredEntryType.DIRECTORY;
+ verifyLog.verify(
+ this.source.getProcessedByteSource().isEmpty(),
"Directory source is not empty.");
- Verify.verify(mCdh.getCrc32() == 0, "Directory has CRC32 = %s.", mCdh.getCrc32());
- Verify.verify(mCdh.getUncompressedSize() == 0, "Directory has uncompressed size = %s.",
- mCdh.getUncompressedSize());
+ verifyLog.verify(cdh.getCrc32() == 0, "Directory has CRC32 = %s.", cdh.getCrc32());
+ verifyLog.verify(
+ cdh.getUncompressedSize() == 0,
+ "Directory has uncompressed size = %s.",
+ cdh.getUncompressedSize());
/*
* Some clever (OMG!) tools, like jar will actually try to compress the directory
* contents and generate a 2 byte compressed data. Of course, the uncompressed size is
* zero and we're just wasting space.
*/
- long compressedSize = mCdh.getCompressionInfoWithWait().getCompressedSize();
- Verify.verify(compressedSize == 0 || compressedSize == 2,
+ long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize();
+ verifyLog.verify(
+ compressedSize == 0 || compressedSize == 2,
"Directory has compressed size = %s.", compressedSize);
} else {
- mType = StoredEntryType.FILE;
+ type = StoredEntryType.FILE;
}
/*
* By default we assume there is no data descriptor unless the CRC is marked as deferred
* in the header's GP Bit.
*/
- mDataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
+ dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
if (header.getGpBit().isDeferredCrc()) {
/*
* If the deferred CRC bit exists, then we have an extra descriptor field. This extra
@@ -288,8 +306,8 @@
* @return the local header size in bytes
*/
public int getLocalHeaderSize() {
- Preconditions.checkState(!mDeleted, "mDeleted");
- return FIXED_LOCAL_FILE_HEADER_SIZE + mCdh.getEncodedFileName().length + mLocalExtra.size();
+ Preconditions.checkState(!deleted, "deleted");
+ return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size();
}
/**
@@ -300,9 +318,9 @@
* @throws IOException failed to get compression information
*/
long getInFileSize() throws IOException {
- Preconditions.checkState(!mDeleted, "mDeleted");
- return mCdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize()
- + mDataDescriptorType.size;
+ Preconditions.checkState(!deleted, "deleted");
+ return cdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize()
+ + dataDescriptorType.size;
}
/**
@@ -311,9 +329,9 @@
* @return a stream that will return as many bytes as the uncompressed entry size
* @throws IOException failed to open the stream
*/
- @NonNull
+ @Nonnull
public InputStream open() throws IOException {
- return mSource.getProcessedByteSource().openStream();
+ return source.getProcessedByteSource().openStream();
}
/**
@@ -322,7 +340,7 @@
* @return a byte array with the contents of the file (uncompressed if the file was compressed)
* @throws IOException failed to read the file
*/
- @NonNull
+ @Nonnull
public byte[] read() throws IOException {
try (InputStream is = open()) {
return ByteStreams.toByteArray(is);
@@ -330,14 +348,31 @@
}
/**
+ * Obtains the contents of the file in an existing buffer.
+ *
+ * @param bytes buffer to read the file contents in.
+ * @return the number of bytes read
+ * @throws IOException failed to read the file.
+ */
+ public int read(byte[] bytes) throws IOException {
+ if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) {
+ throw new RuntimeException(
+ "Buffer to small while reading {}" + getCentralDirectoryHeader().getName());
+ }
+ try (InputStream is = new BufferedInputStream(open())) {
+ return ByteStreams.read(is, bytes, 0, bytes.length);
+ }
+ }
+
+ /**
* Obtains the type of entry.
*
* @return the type of entry
*/
- @NonNull
+ @Nonnull
public StoredEntryType getType() {
- Preconditions.checkState(!mDeleted, "mDeleted");
- return mType;
+ Preconditions.checkState(!deleted, "deleted");
+ return type;
}
/**
@@ -345,6 +380,7 @@
* To eventually write updates to disk, {@link ZFile#update()} must be called.
*
* @throws IOException failed to delete the entry
+ * @throws IllegalStateException if the zip file was open in read-only mode
*/
public void delete() throws IOException {
delete(true);
@@ -357,12 +393,20 @@
* @param notify should listeners be notified of the deletion? This will only be
* {@code false} if the entry is being removed as part of a replacement
* @throws IOException failed to delete the entry
+ * @throws IllegalStateException if the zip file was open in read-only mode
*/
void delete(boolean notify) throws IOException {
- Preconditions.checkState(!mDeleted, "mDeleted");
- mFile.delete(this, notify);
- mDeleted = true;
- mSource.close();
+ Preconditions.checkState(!deleted, "deleted");
+ file.delete(this, notify);
+ deleted = true;
+ source.close();
+ }
+
+ /**
+ * Returns {@code true} if this entry has been deleted/replaced.
+ */
+ public boolean isDeleted() {
+ return deleted;
}
/**
@@ -370,9 +414,9 @@
*
* @return the CDH
*/
- @NonNull
+ @Nonnull
public CentralDirectoryHeader getCentralDirectoryHeader() {
- return mCdh;
+ return cdh;
}
/**
@@ -380,29 +424,29 @@
* Header provided in the constructor. This method should only be called if the entry already
* exists on disk; new entries do not have local headers.
* <p>
- * This method will define the {@link #mLocalExtra} field that is only defined in the
+ * This method will define the {@link #localExtra} field that is only defined in the
* local descriptor.
*
* @throws IOException failed to read the local header
*/
private void readLocalHeader() throws IOException {
byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE];
- mFile.directFullyRead(mCdh.getOffset(), localHeader);
+ file.directFullyRead(cdh.getOffset(), localHeader);
- CentralDirectoryHeaderCompressInfo compressInfo = mCdh.getCompressionInfoWithWait();
+ CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
ByteBuffer bytes = ByteBuffer.wrap(localHeader);
F_LOCAL_SIGNATURE.verify(bytes);
- F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract());
- F_GP_BIT.verify(bytes, mCdh.getGpBit().getValue());
- F_METHOD.verify(bytes, compressInfo.getMethod().methodCode);
+ F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog);
+ F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog);
+ F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog);
- if (mFile.areTimestampsIgnored()) {
+ if (file.areTimestampsIgnored()) {
F_LAST_MOD_TIME.skip(bytes);
F_LAST_MOD_DATE.skip(bytes);
} else {
- F_LAST_MOD_TIME.verify(bytes, mCdh.getLastModTime());
- F_LAST_MOD_DATE.verify(bytes, mCdh.getLastModDate());
+ F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog);
+ F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog);
}
/*
@@ -410,32 +454,36 @@
* File Header must be ignored and their actual values must be read from the Data
* Descriptor following the contents of this entry. See readDataDescriptorRecord().
*/
- if (mCdh.getGpBit().isDeferredCrc()) {
+ if (cdh.getGpBit().isDeferredCrc()) {
F_CRC32.skip(bytes);
F_COMPRESSED_SIZE.skip(bytes);
F_UNCOMPRESSED_SIZE.skip(bytes);
} else {
- F_CRC32.verify(bytes, mCdh.getCrc32());
- F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize());
- F_UNCOMPRESSED_SIZE.verify(bytes, mCdh.getUncompressedSize());
+ F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog);
+ F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog);
+ F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog);
}
- F_FILE_NAME_LENGTH.verify(bytes, mCdh.getEncodedFileName().length);
+ F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length);
long extraLength = F_EXTRA_LENGTH.read(bytes);
- long fileNameStart = mCdh.getOffset() + F_EXTRA_LENGTH.endOffset();
- byte[] fileNameData = new byte[mCdh.getEncodedFileName().length];
- mFile.directFullyRead(fileNameStart, fileNameData);
+ long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset();
+ byte[] fileNameData = new byte[cdh.getEncodedFileName().length];
+ file.directFullyRead(fileNameStart, fileNameData);
- String fileName = EncodeUtils.decode(fileNameData, mCdh.getGpBit());
- if (!fileName.equals(mCdh.getName())) {
- throw new IOException("Central directory reports file as being named '" + mCdh.getName()
- + "' but local header reports file being named '" + fileName + "'.");
+ String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit());
+ if (!fileName.equals(cdh.getName())) {
+ verifyLog.log(
+ String.format(
+ "Central directory reports file as being named '%s' but local header"
+ + "reports file being named '%s'.",
+ cdh.getName(),
+ fileName));
}
- long localExtraStart = fileNameStart + mCdh.getEncodedFileName().length;
+ long localExtraStart = fileNameStart + cdh.getEncodedFileName().length;
byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)];
- mFile.directFullyRead(localExtraStart, localExtraRaw);
- mLocalExtra = new ExtraField(localExtraRaw);
+ file.directFullyRead(localExtraStart, localExtraRaw);
+ localExtra = new ExtraField(localExtraRaw);
}
/**
@@ -443,18 +491,18 @@
* that a data descriptor does exist. It will read the data descriptor and check that the data
* described there matches the data provided in the Central Directory.
* <p>
- * This method will set the {@link #mDataDescriptorType} field to the appropriate type of
+ * This method will set the {@link #dataDescriptorType} field to the appropriate type of
* data descriptor record.
*
* @throws IOException failed to read the data descriptor record
*/
private void readDataDescriptorRecord() throws IOException {
- CentralDirectoryHeaderCompressInfo compressInfo = mCdh.getCompressionInfoWithWait();
+ CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
- long ddStart = mCdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE
- + mCdh.getName().length() + mLocalExtra.size() + compressInfo.getCompressedSize();
- byte ddData[] = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];
- mFile.directFullyRead(ddStart, ddData);
+ long ddStart = cdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE
+ + cdh.getName().length() + localExtra.size() + compressInfo.getCompressedSize();
+ byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];
+ file.directFullyRead(ddStart, ddData);
ByteBuffer ddBytes = ByteBuffer.wrap(ddData);
@@ -462,9 +510,9 @@
int cpos = ddBytes.position();
long sig = signatureField.read(ddBytes);
if (sig == DATA_DESC_SIGNATURE) {
- mDataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE;
+ dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE;
} else {
- mDataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE;
+ dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE;
ddBytes.position(cpos);
}
@@ -473,11 +521,9 @@
ZipField.F4 uncompressedField = new ZipField.F4(compressedField.endOffset(),
"Uncompressed size");
- if (!mFile.getSkipDataDescriptorVerification()) {
- crc32Field.verify(ddBytes, mCdh.getCrc32());
- compressedField.verify(ddBytes, compressInfo.getCompressedSize());
- uncompressedField.verify(ddBytes, mCdh.getUncompressedSize());
- }
+ crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog);
+ compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog);
+ uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog);
}
/**
@@ -487,14 +533,14 @@
* @throws IOException failed to close the old source
* @return the created source
*/
- @NonNull
+ @Nonnull
private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset)
throws IOException {
Preconditions.checkArgument(zipOffset >= 0, "zipOffset < 0");
final CentralDirectoryHeaderCompressInfo compressInfo;
try {
- compressInfo = mCdh.getCompressionInfoWithWait();
+ compressInfo = cdh.getCompressionInfoWithWait();
} catch (IOException e) {
throw new RuntimeException("IOException should never occur here because compression "
+ "information should be immediately available if reading from zip.", e);
@@ -509,16 +555,16 @@
return compressInfo.getCompressedSize();
}
- @NonNull
+ @Nonnull
@Override
public InputStream openStream() throws IOException {
- Preconditions.checkState(!mDeleted, "mDeleted");
+ Preconditions.checkState(!deleted, "deleted");
long dataStart = zipOffset + getLocalHeaderSize();
long dataEnd = dataStart + compressInfo.getCompressedSize();
- mFile.openReadOnly();
- return mFile.directOpen(dataStart, dataEnd);
+ file.openReadOnly();
+ return file.directOpen(dataStart, dataEnd);
}
@Override
@@ -540,12 +586,12 @@
* @param rawContents the raw data to create the source from
* @return the sources for this entry
*/
- @NonNull
+ @Nonnull
private ProcessedAndRawByteSources createSourcesFromRawContents(
- @NonNull CloseableByteSource rawContents) {
+ @Nonnull CloseableByteSource rawContents) {
CentralDirectoryHeaderCompressInfo compressInfo;
try {
- compressInfo = mCdh.getCompressionInfoWithWait();
+ compressInfo = cdh.getCompressionInfoWithWait();
} catch (IOException e) {
throw new RuntimeException("IOException should never occur here because compression "
+ "information should be immediately available if creating from raw "
@@ -568,7 +614,7 @@
}
/**
- * Replaces {@link #mSource} with one that reads file data from the zip file.
+ * Replaces {@link #source} with one that reads file data from the zip file.
*
* @param zipFileOffset the offset in the zip file where data is written; must be non-negative
* @throws IOException failed to replace the source
@@ -576,14 +622,14 @@
void replaceSourceFromZip(long zipFileOffset) throws IOException {
Preconditions.checkArgument(zipFileOffset >= 0, "zipFileOffset < 0");
- ProcessedAndRawByteSources oldSource = mSource;
- mSource = createSourceFromZip(zipFileOffset);
- mCdh.setOffset(zipFileOffset);
+ ProcessedAndRawByteSources oldSource = source;
+ source = createSourceFromZip(zipFileOffset);
+ cdh.setOffset(zipFileOffset);
oldSource.close();
}
/**
- * Loads all data in memory and replaces {@link #mSource} with one that contains all the data
+ * Loads all data in memory and replaces {@link #source} with one that contains all the data
* in memory.
*
* <p>If the entry's contents are already in memory, this call does nothing.
@@ -591,7 +637,7 @@
* @throws IOException failed to replace the source
*/
void loadSourceIntoMemory() throws IOException {
- if (mCdh.getOffset() == -1) {
+ if (cdh.getOffset() == -1) {
/*
* No offset in the CDR means data has not been written to disk which, in turn,
* means data is already loaded into memory.
@@ -599,11 +645,11 @@
return;
}
- ProcessedAndRawByteSources oldSource = mSource;
+ ProcessedAndRawByteSources oldSource = source;
byte[] rawContents = oldSource.getRawByteSource().read();
- mSource = createSourcesFromRawContents(new CloseableDelegateByteSource(
+ source = createSourcesFromRawContents(new CloseableDelegateByteSource(
ByteSource.wrap(rawContents), rawContents.length));
- mCdh.setOffset(-1);
+ cdh.setOffset(-1);
oldSource.close();
}
@@ -613,9 +659,9 @@
*
* @return the entry source
*/
- @NonNull
+ @Nonnull
ProcessedAndRawByteSources getSource() {
- return mSource;
+ return source;
}
/**
@@ -623,9 +669,25 @@
*
* @return the type of data descriptor
*/
- @NonNull
+ @Nonnull
public DataDescriptorType getDataDescriptorType() {
- return mDataDescriptorType;
+ return dataDescriptorType;
+ }
+
+ /**
+ * Removes the data descriptor, if it has one and resets the data descriptor bit in the
+ * central directory header.
+ *
+ * @return was the data descriptor remove?
+ */
+ boolean removeDataDescriptor() {
+ if (dataDescriptorType == DataDescriptorType.NO_DATA_DESCRIPTOR) {
+ return false;
+ }
+
+ dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
+ cdh.resetDeferredCrc();
+ return true;
}
/**
@@ -634,38 +696,38 @@
* @return the header data
* @throws IOException failed to get header byte data
*/
- @NonNull
+ @Nonnull
byte[] toHeaderData() throws IOException {
- byte[] encodedFileName = mCdh.getEncodedFileName();
+ byte[] encodedFileName = cdh.getEncodedFileName();
ByteBuffer out =
ByteBuffer.allocate(
- F_EXTRA_LENGTH.endOffset() + encodedFileName.length + mLocalExtra.size());
+ F_EXTRA_LENGTH.endOffset() + encodedFileName.length + localExtra.size());
- CentralDirectoryHeaderCompressInfo compressInfo = mCdh.getCompressionInfoWithWait();
+ CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
F_LOCAL_SIGNATURE.write(out);
F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract());
- F_GP_BIT.write(out, mCdh.getGpBit().getValue());
+ F_GP_BIT.write(out, cdh.getGpBit().getValue());
F_METHOD.write(out, compressInfo.getMethod().methodCode);
- if (mFile.areTimestampsIgnored()) {
+ if (file.areTimestampsIgnored()) {
F_LAST_MOD_TIME.write(out, 0);
F_LAST_MOD_DATE.write(out, 0);
} else {
- F_LAST_MOD_TIME.write(out, mCdh.getLastModTime());
- F_LAST_MOD_DATE.write(out, mCdh.getLastModDate());
+ F_LAST_MOD_TIME.write(out, cdh.getLastModTime());
+ F_LAST_MOD_DATE.write(out, cdh.getLastModDate());
}
- F_CRC32.write(out, mCdh.getCrc32());
+ F_CRC32.write(out, cdh.getCrc32());
F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize());
- F_UNCOMPRESSED_SIZE.write(out, mCdh.getUncompressedSize());
- F_FILE_NAME_LENGTH.write(out, mCdh.getEncodedFileName().length);
- F_EXTRA_LENGTH.write(out, mLocalExtra.size());
+ F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize());
+ F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length);
+ F_EXTRA_LENGTH.write(out, localExtra.size());
- out.put(mCdh.getEncodedFileName());
- mLocalExtra.write(out);
+ out.put(cdh.getEncodedFileName());
+ localExtra.write(out);
return out.array();
}
@@ -683,9 +745,9 @@
* file
*/
public boolean realign() throws IOException {
- Preconditions.checkState(!mDeleted, "Entry has been deleted.");
+ Preconditions.checkState(!deleted, "Entry has been deleted.");
- return mFile.realign(this);
+ return file.realign(this);
}
/**
@@ -693,9 +755,9 @@
*
* @return the contents of the local extra field
*/
- @NonNull
+ @Nonnull
public ExtraField getLocalExtra() {
- return mLocalExtra;
+ return localExtra;
}
/**
@@ -704,9 +766,9 @@
* @param localExtra the contents of the local extra field
* @throws IOException failed to update the zip file
*/
- public void setLocalExtra(@NonNull ExtraField localExtra) throws IOException {
+ public void setLocalExtra(@Nonnull ExtraField localExtra) throws IOException {
boolean resized = setLocalExtraNoNotify(localExtra);
- mFile.localHeaderChanged(this, resized);
+ file.localHeaderChanged(this, resized);
}
/**
@@ -718,7 +780,7 @@
* @return has the local header size changed?
* @throws IOException failed to load the file
*/
- boolean setLocalExtraNoNotify(@NonNull ExtraField localExtra) throws IOException {
+ boolean setLocalExtraNoNotify(@Nonnull ExtraField localExtra) throws IOException {
boolean sizeChanged;
/*
@@ -734,13 +796,23 @@
*/
loadSourceIntoMemory();
- if (mLocalExtra.size() != localExtra.size()) {
+ if (this.localExtra.size() != localExtra.size()) {
sizeChanged = true;
} else {
sizeChanged = false;
}
- mLocalExtra = localExtra;
+ this.localExtra = localExtra;
return sizeChanged;
}
+
+ /**
+ * Obtains the verify log for the entry.
+ *
+ * @return the verify log
+ */
+ @Nonnull
+ public VerifyLog getVerifyLog() {
+ return verifyLog;
+ }
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zip/StoredEntryType.java
similarity index 93%
rename from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
rename to src/main/java/com/android/apkzlib/zip/StoredEntryType.java
index 22983ab..9ce9252 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zip/StoredEntryType.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
/**
* Type of stored entry.
diff --git a/src/main/java/com/android/apkzlib/zip/VerifyLog.java b/src/main/java/com/android/apkzlib/zip/VerifyLog.java
new file mode 100644
index 0000000..2a7db7c
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zip/VerifyLog.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import com.google.common.collect.ImmutableList;
+import javax.annotation.Nonnull;
+
+/**
+ * The verify log contains verification messages. It is used to capture validation issues with a
+ * zip file or with parts of a zip file.
+ */
+public interface VerifyLog {
+
+ /**
+ * Logs a message.
+ *
+ * @param message the message to verify
+ */
+ void log(@Nonnull String message);
+
+ /**
+ * Obtains all save logged messages.
+ *
+ * @return the logged messages
+ */
+ @Nonnull
+ ImmutableList<String> getLogs();
+
+ /**
+ * Performs verification of a non-critical condition, logging a message if the condition is
+ * not verified.
+ *
+ * @param condition the condition
+ * @param message the message to write if {@code condition} is {@code false}.
+ * @param args arguments for formatting {@code message} using {@code String.format}
+ */
+ default void verify(boolean condition, @Nonnull String message, @Nonnull Object... args) {
+ if (!condition) {
+ log(String.format(message, args));
+ }
+ }
+}
diff --git a/src/main/java/com/android/apkzlib/zip/VerifyLogs.java b/src/main/java/com/android/apkzlib/zip/VerifyLogs.java
new file mode 100644
index 0000000..b7acb83
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zip/VerifyLogs.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nonnull;
+
+/**
+ * Factory for verification logs.
+ */
+final class VerifyLogs {
+
+ private VerifyLogs() {}
+
+ /**
+ * Creates a {@link VerifyLog} that ignores all messages logged.
+ *
+ * @return the log
+ */
+ @Nonnull
+ static VerifyLog devNull() {
+ return new VerifyLog() {
+ @Override
+ public void log(@Nonnull String message) {}
+
+ @Nonnull
+ @Override
+ public ImmutableList<String> getLogs() {
+ return ImmutableList.of();
+ }
+ };
+ }
+
+ /**
+ * Creates a {@link VerifyLog} that stores all log messages.
+ *
+ * @return the log
+ */
+ @Nonnull
+ static VerifyLog unlimited() {
+ return new VerifyLog() {
+
+ /**
+ * All saved messages.
+ */
+ @Nonnull
+ private final List<String> messages = new ArrayList<>();
+
+ @Override
+ public void log(@Nonnull String message) {
+ messages.add(message);
+ }
+
+ @Nonnull
+ @Override
+ public ImmutableList<String> getLogs() {
+ return ImmutableList.copyOf(messages);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZFile.java b/src/main/java/com/android/apkzlib/zip/ZFile.java
similarity index 67%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZFile.java
rename to src/main/java/com/android/apkzlib/zip/ZFile.java
index 7048e46..9034f4c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZFile.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFile.java
@@ -14,19 +14,18 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-import com.android.builder.internal.packaging.zip.utils.LittleEndianUtils;
-import com.android.builder.internal.packaging.zip.utils.RandomAccessFileUtils;
-import com.android.builder.internal.utils.CachedFileContents;
-import com.android.builder.internal.utils.IOExceptionFunction;
-import com.android.builder.internal.utils.IOExceptionRunnable;
+import com.android.apkzlib.utils.CachedFileContents;
+import com.android.apkzlib.utils.IOExceptionFunction;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zip.compress.Zip64NotSupportedException;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
+import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -39,15 +38,18 @@
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
+import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -60,6 +62,9 @@
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.function.Predicate;
+import java.util.function.Supplier;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* The {@code ZFile} provides the main interface for interacting with zip files. A {@code ZFile}
@@ -196,9 +201,14 @@
private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
/**
+ * Maximum size for the EOCD.
+ */
+ private static final int MAX_EOCD_COMMENT_SIZE = 65535;
+
+ /**
* How many bytes to look back from the end of the file to look for the EOCD signature.
*/
- private static final int LAST_BYTES_TO_READ = 65535 + MIN_EOCD_SIZE;
+ private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE;
/**
* Signature of the Zip64 EOCD locator record.
@@ -206,6 +216,11 @@
private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
/**
+ * Signature of the EOCD record.
+ */
+ private static final byte[] EOCD_SIGNATURE = new byte[] { 0x06, 0x05, 0x4b, 0x50 };
+
+ /**
* Size of buffer for I/O operations.
*/
private static final int IO_BUFFER_SIZE = 1024 * 1024;
@@ -217,9 +232,11 @@
private static final int MAXIMUM_EXTENSION_CYCLE_COUNT = 10;
/**
- * Minimum size for the extra field.
+ * Minimum size for the extra field when we have to add one. We rely on the alignment segment
+ * to do that so the minimum size for the extra field is the minimum size of an alignment
+ * segment.
*/
- private static final int MINIMUM_EXTRA_FIELD_SIZE = 6;
+ private static final int MINIMUM_EXTRA_FIELD_SIZE = ExtraField.AlignmentSegment.MINIMUM_SIZE;
/**
* Maximum size of the extra field.
@@ -232,29 +249,32 @@
/**
* File zip file.
*/
- @NonNull
- private final File mFile;
+ @Nonnull
+ private final File file;
/**
* The random access file used to access the zip file. This will be {@code null} if and only
- * if {@link #mState} is {@link ZipFileState#CLOSED}.
+ * if {@link #state} is {@link ZipFileState#CLOSED}.
*/
@Nullable
- private RandomAccessFile mRaf;
+ private RandomAccessFile raf;
/**
* The map containing the in-memory contents of the zip file. It keeps track of which parts of
* the zip file are used and which are not.
*/
- @NonNull
- private final FileUseMap mMap;
+ @Nonnull
+ private final FileUseMap map;
/**
* The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the
* one that exists on disk is no longer valid (because the zip has been changed).
+ *
+ * <p>If the EOCD is deleted because the zip has been changed and the old EOCD was no longer
+ * valid, then {@link #eocdComment} will contain the comment saved from the EOCD.
*/
@Nullable
- private FileUseMapEntry<Eocd> mEocdEntry;
+ private FileUseMapEntry<Eocd> eocdEntry;
/**
* The Central Directory entry. Will be {@code null} if there is no Central Directory (because
@@ -262,42 +282,45 @@
* has been changed).
*/
@Nullable
- private FileUseMapEntry<CentralDirectory> mDirectoryEntry;
+ private FileUseMapEntry<CentralDirectory> directoryEntry;
/**
* All entries in the zip file. It includes in-memory changes and may not reflect what is
* written on disk. Only entries that have been compressed are in this list.
*/
- @NonNull
- private final Map<String, FileUseMapEntry<StoredEntry>> mEntries;
+ @Nonnull
+ private final Map<String, FileUseMapEntry<StoredEntry>> entries;
/**
* Entries added to the zip file, but that are not yet compressed. When compression is done,
- * these entries are eventually moved to {@link #mEntries}. mUncompressedEntries is a list
+ * these entries are eventually moved to {@link #entries}. uncompressedEntries is a list
* because entries need to be kept in the order by which they were added. It allows adding
* multiple files with the same name and getting the right notifications on which files replaced
* which.
*
* <p>Files are placed in this list in {@link #add(StoredEntry)} method. This method will
- * keep files here temporarily and move then to {@link #mEntries} when the data is
+ * keep files here temporarily and move then to {@link #entries} when the data is
* available.
*
- * <p>Moving files out of this list to {@link #mEntries} is done by
+ * <p>Moving files out of this list to {@link #entries} is done by
* {@link #processAllReadyEntries()}.
*/
- @NonNull
- private final List<StoredEntry> mUncompressedEntries;
+ @Nonnull
+ private final List<StoredEntry> uncompressedEntries;
/**
* Current state of the zip file.
*/
- @NonNull
- private ZipFileState mState;
+ @Nonnull
+ private ZipFileState state;
/**
* Are the in-memory changes that have not been written to the zip file?
+ *
+ * <p>This might be false, but will become true after {@link #processAllReadyEntriesWithWait()}
+ * is called if there are {@link #uncompressedEntries} compressing in the background.
*/
- private boolean mDirty;
+ private boolean dirty;
/**
* Non-{@code null} only if the file is currently closed. Used to detect if the zip is
@@ -305,72 +328,96 @@
* be {@code null} even if it is closed.
*/
@Nullable
- private CachedFileContents<Object> mClosedControl;
+ private CachedFileContents<Object> closedControl;
/**
* The alignment rule.
*/
- @NonNull
- private final AlignmentRule mAlignmentRule;
+ @Nonnull
+ private final AlignmentRule alignmentRule;
/**
* Extensions registered with the file.
*/
- @NonNull
- private final List<ZFileExtension> mExtensions;
+ @Nonnull
+ private final List<ZFileExtension> extensions;
/**
* When notifying extensions, extensions may request that some runnables are executed. This
* list collects all runnables by the order they were requested. Together with
- * {@link #mIsNotifying}, it is used to avoid reordering notifications.
+ * {@link #isNotifying}, it is used to avoid reordering notifications.
*/
- @NonNull
- private final List<IOExceptionRunnable> mToRun;
+ @Nonnull
+ private final List<IOExceptionRunnable> toRun;
/**
- * {@code true} when {@link #notify(IOExceptionFunction)} is notifying extensions. Used
- * to avoid reordering notifications.
+ * {@code true} when {@link #notify(com.android.apkzlib.utils.IOExceptionFunction)} is
+ * notifying extensions. Used to avoid reordering notifications.
*/
- private boolean mIsNotifying;
+ private boolean isNotifying;
/**
* An extra offset for the central directory location. {@code 0} if the central directory
* should be written in its standard location.
*/
- private long mExtraDirectoryOffset;
+ private long extraDirectoryOffset;
/**
* Should all timestamps be zeroed when reading / writing the zip?
*/
- private boolean mNoTimestamps;
+ private boolean noTimestamps;
/**
* Compressor to use.
*/
- @NonNull
- private Compressor mCompressor;
+ @Nonnull
+ private Compressor compressor;
/**
* Byte tracker to use.
*/
- @NonNull
- private final ByteTracker mTracker;
+ @Nonnull
+ private final ByteTracker tracker;
/**
* Use the zip entry's "extra field" field to cover empty space in the zip file?
*/
- private boolean mCoverEmptySpaceUsingExtraField;
+ private boolean coverEmptySpaceUsingExtraField;
/**
* Should files be automatically sorted when updating?
*/
- private boolean mAutoSortFiles;
+ private boolean autoSortFiles;
/**
- * Should data descriptor verification be skipped? See
- * {@link ZFileOptions#getSkipDataDescriptorValidation()}.
+ * Verify log factory to use.
*/
- private boolean mSkipDataDescriptorVerification;
+ @Nonnull
+ private final Supplier<VerifyLog> verifyLogFactory;
+
+ /**
+ * Verify log to use.
+ */
+ @Nonnull
+ private final VerifyLog verifyLog;
+
+ /**
+ * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure.
+ * This may happen, for example, if the zip has been changed and the Central Directory and
+ * EOCD have been deleted (in-memory). In that case, this field will save the comment to place
+ * on the EOCD once it is created.
+ *
+ * <p>This field will only be non-{@code null} if there is no in-memory EOCD structure
+ * (<i>i.e.</i>, {@link #eocdEntry} is {@code null}). If there is an {@link #eocdEntry}, then
+ * the comment will be there instead of being in this field.
+ */
+ @Nullable
+ private byte[] eocdComment;
+
+ /**
+ * Is the file in read-only mode? In read-only mode no changes are allowed.
+ */
+ private boolean readOnly;
/**
@@ -382,7 +429,7 @@
* @param file the zip file
* @throws IOException some file exists but could not be read
*/
- public ZFile(@NonNull File file) throws IOException {
+ public ZFile(@Nonnull File file) throws IOException {
this(file, new ZFileOptions());
}
@@ -396,55 +443,90 @@
* @param options configuration options
* @throws IOException some file exists but could not be read
*/
- public ZFile(@NonNull File file, @NonNull ZFileOptions options) throws IOException {
- mFile = file;
- mMap = new FileUseMap(
+ public ZFile(@Nonnull File file, @Nonnull ZFileOptions options) throws IOException {
+ this(file, options, false);
+ }
+
+ /**
+ * Creates a new zip file. If the zip file does not exist, then no file is created at this
+ * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will
+ * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists,
+ * it will be parsed and read.
+ *
+ * @param file the zip file
+ * @param options configuration options
+ * @param readOnly should the file be open in read-only mode? If {@code true} then the file must
+ * exist and no methods can be invoked that could potentially change the file
+ * @throws IOException some file exists but could not be read
+ */
+ public ZFile(@Nonnull File file, @Nonnull ZFileOptions options, boolean readOnly)
+ throws IOException {
+ this.file = file;
+ map = new FileUseMap(
0,
options.getCoverEmptySpaceUsingExtraField()
? MINIMUM_EXTRA_FIELD_SIZE
: 0);
- mDirty = false;
- mClosedControl = null;
- mAlignmentRule = options.getAlignmentRule();
- mExtensions = Lists.newArrayList();
- mToRun = Lists.newArrayList();
- mNoTimestamps = options.getNoTimestamps();
- mTracker = options.getTracker();
- mCompressor = options.getCompressor();
- mCoverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField();
- mAutoSortFiles = options.getAutoSortFiles();
- mSkipDataDescriptorVerification = options.getSkipDataDescriptorValidation();
+ this.readOnly = readOnly;
+ dirty = false;
+ closedControl = null;
+ alignmentRule = options.getAlignmentRule();
+ extensions = Lists.newArrayList();
+ toRun = Lists.newArrayList();
+ noTimestamps = options.getNoTimestamps();
+ tracker = options.getTracker();
+ compressor = options.getCompressor();
+ coverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField();
+ autoSortFiles = options.getAutoSortFiles();
+ verifyLogFactory = options.getVerifyLogFactory();
+ verifyLog = verifyLogFactory.get();
/*
* These two values will be overwritten by openReadOnly() below if the file exists.
*/
- mState = ZipFileState.CLOSED;
- mRaf = null;
+ state = ZipFileState.CLOSED;
+ raf = null;
if (file.exists()) {
openReadOnly();
+ } else if (readOnly) {
+ throw new IOException("File does not exist but read-only mode requested");
} else {
- mDirty = true;
+ dirty = true;
}
- mEntries = Maps.newHashMap();
- mUncompressedEntries = Lists.newArrayList();
- mExtraDirectoryOffset = 0;
+ entries = Maps.newHashMap();
+ uncompressedEntries = Lists.newArrayList();
+ extraDirectoryOffset = 0;
try {
- if (mState != ZipFileState.CLOSED) {
- long rafSize = mRaf.length();
+ if (state != ZipFileState.CLOSED) {
+ long rafSize = raf.length();
if (rafSize > Integer.MAX_VALUE) {
throw new IOException("File exceeds size limit of " + Integer.MAX_VALUE + ".");
}
- mMap.extend(Ints.checkedCast(rafSize));
+ map.extend(Ints.checkedCast(rafSize));
readData();
+ }
+ // If we don't have an EOCD entry, set the comment to empty.
+ if (eocdEntry == null) {
+ eocdComment = new byte[0];
+ }
+
+ // Notify the extensions if the zip file has been open.
+ if (state != ZipFileState.CLOSED) {
notify(ZFileExtension::open);
}
+ } catch (Zip64NotSupportedException e) {
+ throw e;
} catch (IOException e) {
throw new IOException("Failed to read zip file '" + file.getAbsolutePath() + "'.", e);
+ } catch (IllegalStateException | IllegalArgumentException | VerifyException e) {
+ throw new RuntimeException(
+ "Internal error when trying to read zip file '" + file.getAbsolutePath() + "'.",
+ e);
}
}
@@ -454,11 +536,11 @@
*
* @return all entries in the zip
*/
- @NonNull
+ @Nonnull
public Set<StoredEntry> entries() {
Map<String, StoredEntry> entries = Maps.newHashMap();
- for (FileUseMapEntry<StoredEntry> mapEntry : mEntries.values()) {
+ for (FileUseMapEntry<StoredEntry> mapEntry : this.entries.values()) {
StoredEntry entry = mapEntry.getStore();
assert entry != null;
entries.put(entry.getCentralDirectoryHeader().getName(), entry);
@@ -468,7 +550,7 @@
* mUncompressed may override mEntriesReady as we may not have yet processed all
* entries.
*/
- for (StoredEntry uncompressed : mUncompressedEntries) {
+ for (StoredEntry uncompressed : uncompressedEntries) {
entries.put(uncompressed.getCentralDirectoryHeader().getName(), uncompressed);
}
@@ -482,18 +564,18 @@
* @return the entry at the path or {@code null} if none exists
*/
@Nullable
- public StoredEntry get(@NonNull String path) {
+ public StoredEntry get(@Nonnull String path) {
/*
* The latest entries are the last ones in uncompressed and they may eventually override
- * files in mEntries.
+ * files in entries.
*/
- for (StoredEntry stillUncompressed : Lists.reverse(mUncompressedEntries)) {
+ for (StoredEntry stillUncompressed : Lists.reverse(uncompressedEntries)) {
if (stillUncompressed.getCentralDirectoryHeader().getName().equals(path)) {
return stillUncompressed;
}
}
- FileUseMapEntry<StoredEntry> found = mEntries.get(path);
+ FileUseMapEntry<StoredEntry> found = entries.get(path);
if (found == null) {
return null;
}
@@ -508,20 +590,20 @@
* @throws IOException failed to read the zip file
*/
private void readData() throws IOException {
- Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
- Preconditions.checkState(mRaf != null, "mRaf == null");
+ Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+ Preconditions.checkState(raf != null, "raf == null");
readEocd();
readCentralDirectory();
/*
- * Compute where the last file ends. We will need this to compute thee extra offset.
+ * Go over all files and create the usage map, verifying there is no overlap in the files.
*/
long entryEndOffset;
long directoryStartOffset;
- if (mDirectoryEntry != null) {
- CentralDirectory directory = mDirectoryEntry.getStore();
+ if (directoryEntry != null) {
+ CentralDirectory directory = directoryEntry.getStore();
assert directory != null;
entryEndOffset = 0;
@@ -543,53 +625,101 @@
* file.
*/
- FileUseMapEntry<StoredEntry> mapEntry = mMap.add(start, end, entry);
- mEntries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
+ Verify.verify(start >= 0, "start < 0");
+ Verify.verify(end < map.size(), "end >= map.size()");
+
+ FileUseMapEntry<?> found = map.at(start);
+ Verify.verifyNotNull(found);
+
+ // We've got a problem if the found entry is not free or is a free entry but
+ // doesn't cover the whole file.
+ if (!found.isFree() || found.getEnd() < end) {
+ if (found.isFree()) {
+ found = map.after(found);
+ Verify.verify(found != null && !found.isFree());
+ }
+
+ Object foundEntry = found.getStore();
+ Verify.verify(foundEntry != null);
+
+ // Obtains a custom description of an entry.
+ IOExceptionFunction<StoredEntry, String> describe =
+ e ->
+ String.format(
+ "'%s' (offset: %d, size: %d)",
+ e.getCentralDirectoryHeader().getName(),
+ e.getCentralDirectoryHeader().getOffset(),
+ e.getInFileSize());
+
+ String overlappingEntryDescription;
+ if (foundEntry instanceof StoredEntry) {
+ StoredEntry foundStored = (StoredEntry) foundEntry;
+ overlappingEntryDescription = describe.apply((StoredEntry) foundEntry);
+ } else {
+ overlappingEntryDescription =
+ "Central Directory / EOCD: "
+ + found.getStart()
+ + " - "
+ + found.getEnd();
+ }
+
+ throw new IOException(
+ "Cannot read entry "
+ + describe.apply(entry)
+ + " because it overlaps with "
+ + overlappingEntryDescription);
+ }
+
+ FileUseMapEntry<StoredEntry> mapEntry = map.add(start, end, entry);
+ entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
if (end > entryEndOffset) {
entryEndOffset = end;
}
}
- directoryStartOffset = mDirectoryEntry.getStart();
+ directoryStartOffset = directoryEntry.getStart();
} else {
/*
* No directory means an empty zip file. Use the start of the EOCD to compute
* an existing offset.
*/
- Verify.verifyNotNull(mEocdEntry);
- assert mEocdEntry != null;
- directoryStartOffset = mEocdEntry.getStart();
+ Verify.verifyNotNull(eocdEntry);
+ assert eocdEntry != null;
+ directoryStartOffset = eocdEntry.getStart();
entryEndOffset = 0;
}
+ /*
+ * Check if there is an extra central directory offset. If there is, save it. Note that
+ * we can't call extraDirectoryOffset() because that would mark the file as dirty.
+ */
long extraOffset = directoryStartOffset - entryEndOffset;
Verify.verify(extraOffset >= 0, "extraOffset (%s) < 0", extraOffset);
- setExtraDirectoryOffset(extraOffset);
+ extraDirectoryOffset = extraOffset;
}
/**
- * Finds the EOCD marker and reads it. It will populate the {@link #mEocdEntry} variable.
+ * Finds the EOCD marker and reads it. It will populate the {@link #eocdEntry} variable.
*
* @throws IOException failed to read the EOCD
*/
private void readEocd() throws IOException {
- Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
- Preconditions.checkState(mRaf != null, "mRaf == null");
+ Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+ Preconditions.checkState(raf != null, "raf == null");
/*
* Read the last part of the zip into memory. If we don't find the EOCD signature by then,
* the file is corrupt.
*/
int lastToRead = LAST_BYTES_TO_READ;
- if (lastToRead > mRaf.length()) {
- lastToRead = Ints.checkedCast(mRaf.length());
+ if (lastToRead > raf.length()) {
+ lastToRead = Ints.checkedCast(raf.length());
}
byte[] last = new byte[lastToRead];
- directFullyRead(mRaf.length() - lastToRead, last);
+ directFullyRead(raf.length() - lastToRead, last);
- byte[] eocdSignature = new byte[] { 0x06, 0x05, 0x4b, 0x50 };
/*
* Start endIdx at the first possible location where the signature can be located and then
@@ -610,10 +740,10 @@
/*
* Remember: little endian...
*/
- if (last[endIdx] == eocdSignature[3]
- && last[endIdx + 1] == eocdSignature[2]
- && last[endIdx + 2] == eocdSignature[1]
- && last[endIdx + 3] == eocdSignature[0]) {
+ if (last[endIdx] == EOCD_SIGNATURE[3]
+ && last[endIdx + 1] == EOCD_SIGNATURE[2]
+ && last[endIdx + 2] == EOCD_SIGNATURE[1]
+ && last[endIdx + 3] == EOCD_SIGNATURE[0]) {
/*
* We found a signature. Try to read the EOCD record.
@@ -625,15 +755,20 @@
try {
eocd = new Eocd(eocdBytes);
- eocdStart = Ints.checkedCast(mRaf.length() - lastToRead + foundEocdSignature);
+ eocdStart = Ints.checkedCast(raf.length() - lastToRead + foundEocdSignature);
/*
- * Make sure the EOCD takes the whole file up to the end.
+ * Make sure the EOCD takes the whole file up to the end. Log an error if it
+ * doesn't.
*/
- if (eocdStart + eocd.getEocdSize() != mRaf.length()) {
- throw new IOException("EOCD starts at " + eocdStart + " and has "
- + eocd.getEocdSize() + " bytes, but file ends at " + mRaf.length()
- + ".");
+ if (eocdStart + eocd.getEocdSize() != raf.length()) {
+ verifyLog.log("EOCD starts at "
+ + eocdStart
+ + " and has "
+ + eocd.getEocdSize()
+ + " bytes, but file ends at "
+ + raf.length()
+ + ".");
}
} catch (IOException e) {
if (errorFindingSignature != null) {
@@ -660,44 +795,60 @@
*/
int zip64LocatorStart = eocdStart - ZIP64_EOCD_LOCATOR_SIZE;
if (zip64LocatorStart >= 0) {
- byte possibleZip64Locator[] = new byte[4];
+ byte[] possibleZip64Locator = new byte[4];
directFullyRead(zip64LocatorStart, possibleZip64Locator);
if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator)) ==
ZIP64_EOCD_LOCATOR_SIGNATURE) {
- throw new IOException("Zip64 EOCD locator found but Zip64 format is not "
- + "supported.");
+ throw new Zip64NotSupportedException(
+ "Zip64 EOCD locator found but Zip64 format is not supported.");
}
}
- mEocdEntry = mMap.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd);
+ eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd);
}
/**
- * Reads the zip's central directory and populates the {@link #mDirectoryEntry} variable. This
+ * Reads the zip's central directory and populates the {@link #directoryEntry} variable. This
* method can only be called after the EOCD has been read. If the central directory is empty
- * (if there are no files on the zip archive), then {@link #mDirectoryEntry} will be set to
+ * (if there are no files on the zip archive), then {@link #directoryEntry} will be set to
* {@code null}.
*
* @throws IOException failed to read the central directory
*/
private void readCentralDirectory() throws IOException {
- Preconditions.checkNotNull(mEocdEntry, "mEocdEntry == null");
- Preconditions.checkNotNull(mEocdEntry.getStore(), "mEocdEntry.getStore() == null");
- Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
- Preconditions.checkState(mRaf != null, "mRaf == null");
- Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry != null");
+ Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
+ Preconditions.checkNotNull(eocdEntry.getStore(), "eocdEntry.getStore() == null");
+ Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+ Preconditions.checkState(raf != null, "raf == null");
+ Preconditions.checkState(directoryEntry == null, "directoryEntry != null");
- Eocd eocd = mEocdEntry.getStore();
+ Eocd eocd = eocdEntry.getStore();
long dirSize = eocd.getDirectorySize();
if (dirSize > Integer.MAX_VALUE) {
throw new IOException("Cannot read central directory with size " + dirSize + ".");
}
- if (eocd.getDirectoryOffset() + dirSize != mEocdEntry.getStart()) {
- throw new IOException("Central directory is stored in [" + eocd.getDirectoryOffset()
- + " - " + (eocd.getDirectoryOffset() + dirSize) + "] and EOCD starts at "
- + mEocdEntry.getStart() + ".");
+ long centralDirectoryEnd = eocd.getDirectoryOffset() + dirSize;
+ if (centralDirectoryEnd != eocdEntry.getStart()) {
+ String msg = "Central directory is stored in ["
+ + eocd.getDirectoryOffset()
+ + " - "
+ + (centralDirectoryEnd - 1)
+ + "] and EOCD starts at "
+ + eocdEntry.getStart()
+ + ".";
+
+ /*
+ * If there is an empty space between the central directory and the EOCD, we proceed
+ * logging an error. If the central directory ends after the start of the EOCD (and
+ * therefore, they overlap), throw an exception.
+ */
+ if (centralDirectoryEnd > eocdEntry.getSize()) {
+ throw new IOException(msg);
+ } else {
+ verifyLog.log(msg);
+ }
}
byte[] directoryData = new byte[Ints.checkedCast(dirSize)];
@@ -709,8 +860,10 @@
eocd.getTotalRecords(),
this);
if (eocd.getDirectorySize() > 0) {
- mDirectoryEntry = mMap.add(eocd.getDirectoryOffset(), eocd.getDirectoryOffset()
- + eocd.getDirectorySize(), directory);
+ directoryEntry = map.add(
+ eocd.getDirectoryOffset(),
+ eocd.getDirectoryOffset() + eocd.getDirectorySize(),
+ directory);
}
}
@@ -726,13 +879,13 @@
* returned <em>as is</em>
* @throws IOException failed to open the zip file
*/
- @NonNull
+ @Nonnull
public InputStream directOpen(final long start, final long end) throws IOException {
- Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
- Preconditions.checkState(mRaf != null, "mRaf == null");
+ Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+ Preconditions.checkState(raf != null, "raf == null");
Preconditions.checkArgument(start >= 0, "start < 0");
Preconditions.checkArgument(end >= start, "end < start");
- Preconditions.checkArgument(end <= mRaf.length(), "end > mRaf.length()");
+ Preconditions.checkArgument(end <= raf.length(), "end > raf.length()");
return new InputStream() {
private long mCurr = start;
@@ -743,7 +896,7 @@
return -1;
}
- byte b[] = new byte[1];
+ byte[] b = new byte[1];
int r = directRead(mCurr, b);
if (r > 0) {
mCurr++;
@@ -754,7 +907,7 @@
}
@Override
- public int read(@NonNull byte[] b, int off, int len) throws IOException {
+ public int read(@Nonnull byte[] b, int off, int len) throws IOException {
Preconditions.checkNotNull(b, "b == null");
Preconditions.checkArgument(off >= 0, "off < 0");
Preconditions.checkArgument(off <= b.length, "off > b.length");
@@ -790,17 +943,20 @@
* @param notify should listeners be notified of the deletion? This will only be
* {@code false} if the entry is being removed as part of a replacement
* @throws IOException failed to delete the entry
+ * @throws IllegalStateException if open in read-only mode
*/
- void delete(@NonNull final StoredEntry entry, boolean notify) throws IOException {
+ void delete(@Nonnull final StoredEntry entry, boolean notify) throws IOException {
+ checkNotInReadOnlyMode();
+
String path = entry.getCentralDirectoryHeader().getName();
- FileUseMapEntry<StoredEntry> mapEntry = mEntries.get(path);
+ FileUseMapEntry<StoredEntry> mapEntry = entries.get(path);
Preconditions.checkNotNull(mapEntry, "mapEntry == null");
Preconditions.checkArgument(entry == mapEntry.getStore(), "entry != mapEntry.getStore()");
- mDirty = true;
+ dirty = true;
- mMap.remove(mapEntry);
- mEntries.remove(path);
+ map.remove(mapEntry);
+ entries.remove(path);
if (notify) {
notify(ext -> ext.removed(entry));
@@ -808,13 +964,26 @@
}
/**
+ * Checks that the file is not in read-only mode.
+ *
+ * @throws IllegalStateException if the file is in read-only mode
+ */
+ private void checkNotInReadOnlyMode() {
+ if (readOnly) {
+ throw new IllegalStateException("Illegal operation in read only model");
+ }
+ }
+
+ /**
* Updates the file writing new entries and removing deleted entries. This will force
* reopening the file as read/write if the file wasn't open in read/write mode.
- *
+ *
* @throws IOException failed to update the file; this exception may have been thrown by
* the compressor but only reported here
*/
public void update() throws IOException {
+ checkNotInReadOnlyMode();
+
/*
* Process all background stuff before calling in the extensions.
*/
@@ -828,7 +997,7 @@
processAllReadyEntriesWithWait();
- if (!mDirty) {
+ if (!dirty) {
return;
}
@@ -839,7 +1008,7 @@
* empty spaces or sort. If we sort, we don't need to repack as sorting forces the
* zip file to be as compact as possible.
*/
- if (mAutoSortFiles) {
+ if (autoSortFiles) {
sortZipContents();
} else {
packIfNecessary();
@@ -850,22 +1019,22 @@
* will have to be rewritten.
*/
deleteDirectoryAndEocd();
- mMap.truncate();
+ map.truncate();
/*
* If we need to use the extra field to cover empty spaces, we do the processing here.
*/
- if (mCoverEmptySpaceUsingExtraField) {
+ if (coverEmptySpaceUsingExtraField) {
/* We will go over all files in the zip and check whether there is empty space before
* them. If there is, then we will move the entry to the beginning of the empty space
* (covering it) and extend the extra field with the size of the empty space.
*/
- for (FileUseMapEntry<StoredEntry> entry : new HashSet<>(mEntries.values())) {
+ for (FileUseMapEntry<StoredEntry> entry : new HashSet<>(entries.values())) {
StoredEntry storedEntry = entry.getStore();
assert storedEntry != null;
- FileUseMapEntry<?> before = mMap.before(entry);
+ FileUseMapEntry<?> before = map.before(entry);
if (before == null || !before.isFree()) {
continue;
}
@@ -891,17 +1060,27 @@
* Remove the entry.
*/
String name = storedEntry.getCentralDirectoryHeader().getName();
- mMap.remove(entry);
- Verify.verify(entry == mEntries.remove(name));
+ map.remove(entry);
+ Verify.verify(entry == entries.remove(name));
/*
* Make a list will all existing segments in the entry's extra field, but remove
* the alignment field, if it exists. Also, sum the size of all kept extra field
* segments.
*/
+ ImmutableList<ExtraField.Segment> currentSegments;
+ try {
+ currentSegments = storedEntry.getLocalExtra().getSegments();
+ } catch (IOException e) {
+ /*
+ * Parsing current segments has failed. This means the contents of the extra
+ * field are not valid. We'll continue discarding the existing segments.
+ */
+ currentSegments = ImmutableList.of();
+ }
+
List<ExtraField.Segment> extraFieldSegments = new ArrayList<>();
- int newExtraFieldSize =
- storedEntry.getLocalExtra().getSegments().stream()
+ int newExtraFieldSize = currentSegments.stream()
.filter(s -> s.getHeaderId()
!= ExtraField.ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)
.peek(extraFieldSegments::add)
@@ -919,7 +1098,7 @@
storedEntry.setLocalExtraNoNotify(
new ExtraField(ImmutableList.copyOf(extraFieldSegments)));
- mEntries.put(name, mMap.add(newStart, newStart + newSize, storedEntry));
+ entries.put(name, map.add(newStart, newStart + newSize, storedEntry));
/*
* Reset the offset to force the file to be rewritten.
@@ -941,7 +1120,7 @@
TreeMap<FileUseMapEntry<?>, StoredEntry> toWriteToStore =
new TreeMap<>(FileUseMapEntry.COMPARE_BY_START);
- for (FileUseMapEntry<StoredEntry> entry : mEntries.values()) {
+ for (FileUseMapEntry<StoredEntry> entry : entries.values()) {
StoredEntry entryStore = entry.getStore();
assert entryStore != null;
if (entryStore.getCentralDirectoryHeader().getOffset() == -1) {
@@ -952,7 +1131,7 @@
/*
* Add all free entries to the set.
*/
- for(FileUseMapEntry<?> freeArea : mMap.getFreeAreas()) {
+ for(FileUseMapEntry<?> freeArea : map.getFreeAreas()) {
toWriteToStore.put(freeArea, null);
}
@@ -975,7 +1154,7 @@
computeCentralDirectory();
computeEocd();
- hasCentralDirectory = (mDirectoryEntry != null);
+ hasCentralDirectory = (directoryEntry != null);
notify(ext -> {
ext.entriesWritten();
@@ -986,15 +1165,15 @@
throw new IOException("Extensions keep resetting the central directory. This is "
+ "probably a bug.");
}
- } while (hasCentralDirectory && mDirectoryEntry == null);
+ } while (hasCentralDirectory && directoryEntry == null);
appendCentralDirectory();
appendEocd();
- Verify.verifyNotNull(mRaf);
- mRaf.setLength(mMap.size());
+ Verify.verifyNotNull(raf);
+ raf.setLength(map.size());
- mDirty = false;
+ dirty = false;
notify(ext -> {
ext.updated();
@@ -1004,7 +1183,7 @@
/**
* Reorganizes the zip so that there are no gaps between files bigger than
- * {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #mCoverEmptySpaceUsingExtraField}
+ * {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #coverEmptySpaceUsingExtraField}
* is set to {@code true}.
*
* <p>Essentially, this makes sure we can cover any empty space with the extra field, given
@@ -1014,19 +1193,19 @@
* @throws IOException failed to repack
*/
private void packIfNecessary() throws IOException {
- if (!mCoverEmptySpaceUsingExtraField) {
+ if (!coverEmptySpaceUsingExtraField) {
return;
}
SortedSet<FileUseMapEntry<StoredEntry>> entriesByLocation =
new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
- entriesByLocation.addAll(mEntries.values());
+ entriesByLocation.addAll(entries.values());
for (FileUseMapEntry<StoredEntry> entry : entriesByLocation) {
StoredEntry storedEntry = entry.getStore();
assert storedEntry != null;
- FileUseMapEntry<?> before = mMap.before(entry);
+ FileUseMapEntry<?> before = map.before(entry);
if (before == null || !before.isFree()) {
continue;
}
@@ -1052,20 +1231,20 @@
* @param positionHint hint to where the file should be positioned when re-adding
* @throws IOException failed to load the entry into memory
*/
- private void reAdd(@NonNull StoredEntry entry, @NonNull PositionHint positionHint)
+ private void reAdd(@Nonnull StoredEntry entry, @Nonnull PositionHint positionHint)
throws IOException {
String name = entry.getCentralDirectoryHeader().getName();
- FileUseMapEntry<StoredEntry> mapEntry = mEntries.get(name);
+ FileUseMapEntry<StoredEntry> mapEntry = entries.get(name);
Preconditions.checkNotNull(mapEntry);
Preconditions.checkState(mapEntry.getStore() == entry);
entry.loadSourceIntoMemory();
- mMap.remove(mapEntry);
- mEntries.remove(name);
+ map.remove(mapEntry);
+ entries.remove(name);
FileUseMapEntry<StoredEntry> positioned = positionInFile(entry, positionHint);
- mEntries.put(name, positioned);
- mDirty = true;
+ entries.put(name, positioned);
+ dirty = true;
}
/**
@@ -1076,8 +1255,8 @@
* @param resized was the local header resized?
* @throws IOException failed to load the entry into memory
*/
- void localHeaderChanged(@NonNull StoredEntry entry, boolean resized) throws IOException {
- mDirty = true;
+ void localHeaderChanged(@Nonnull StoredEntry entry, boolean resized) throws IOException {
+ dirty = true;
if (resized) {
reAdd(entry, PositionHint.ANYWHERE);
@@ -1088,7 +1267,7 @@
* Invoked when the central directory has changed and needs to be rewritten.
*/
void centralDirectoryChanged() {
- mDirty = true;
+ dirty = true;
deleteDirectoryAndEocd();
}
@@ -1097,10 +1276,12 @@
*/
@Override
public void close() throws IOException {
- // We need to make sure to release mRaf, otherwise we end up locking the file on
+ // We need to make sure to release raf, otherwise we end up locking the file on
// Windows. Use try-with-resources to handle exception suppressing.
try (Closeable ignored = this::innerClose) {
- update();
+ if (!readOnly) {
+ update();
+ }
}
notify(ext -> {
@@ -1112,16 +1293,22 @@
/**
* Removes the Central Directory and EOCD from the file. This will free space for new entries
* as well as allowing the zip file to be truncated if files have been removed.
+ *
+ * <p>This method does not mark the zip as dirty.
*/
private void deleteDirectoryAndEocd() {
- if (mDirectoryEntry != null) {
- mMap.remove(mDirectoryEntry);
- mDirectoryEntry = null;
+ if (directoryEntry != null) {
+ map.remove(directoryEntry);
+ directoryEntry = null;
}
- if (mEocdEntry != null) {
- mMap.remove(mEocdEntry);
- mEocdEntry = null;
+ if (eocdEntry != null) {
+ map.remove(eocdEntry);
+
+ Eocd eocd = eocdEntry.getStore();
+ Verify.verify(eocd != null);
+ eocdComment = eocd.getComment();
+ eocdEntry = null;
}
}
@@ -1134,12 +1321,12 @@
* @param offset the offset at which the entry should be written
* @throws IOException failed to write the entry
*/
- private void writeEntry(@NonNull StoredEntry entry, long offset) throws IOException {
+ private void writeEntry(@Nonnull StoredEntry entry, long offset) throws IOException {
Preconditions.checkArgument(entry.getDataDescriptorType()
== DataDescriptorType. NO_DATA_DESCRIPTOR, "Cannot write entries with a data "
+ "descriptor.");
- Preconditions.checkNotNull(mRaf, "mRaf == null");
- Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
+ Preconditions.checkNotNull(raf, "raf == null");
+ Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
/*
* Place the cursor and write the local header.
@@ -1175,19 +1362,19 @@
/**
* Computes the central directory. The central directory must not have been computed yet. When
- * this method finishes, the central directory has been computed {@link #mDirectoryEntry},
- * unless the directory is empty in which case {@link #mDirectoryEntry}
+ * this method finishes, the central directory has been computed {@link #directoryEntry},
+ * unless the directory is empty in which case {@link #directoryEntry}
* is left as {@code null}. Nothing is written to disk as a result of this method's invocation.
*
* @throws IOException failed to append the central directory
*/
private void computeCentralDirectory() throws IOException {
- Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
- Preconditions.checkNotNull(mRaf, "mRaf == null");
- Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry == null");
+ Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+ Preconditions.checkNotNull(raf, "raf == null");
+ Preconditions.checkState(directoryEntry == null, "directoryEntry == null");
Set<StoredEntry> newStored = Sets.newHashSet();
- for (FileUseMapEntry<StoredEntry> mapEntry : mEntries.values()) {
+ for (FileUseMapEntry<StoredEntry> mapEntry : entries.values()) {
newStored.add(mapEntry.getStore());
}
@@ -1195,47 +1382,47 @@
* Make sure we truncate the map before computing the central directory's location since
* the central directory is the last part of the file.
*/
- mMap.truncate();
+ map.truncate();
CentralDirectory newDirectory = CentralDirectory.makeFromEntries(newStored, this);
byte[] newDirectoryBytes = newDirectory.toBytes();
- long directoryOffset = mMap.size() + mExtraDirectoryOffset;
+ long directoryOffset = map.size() + extraDirectoryOffset;
- mMap.extend(directoryOffset + newDirectoryBytes.length);
+ map.extend(directoryOffset + newDirectoryBytes.length);
if (newDirectoryBytes.length > 0) {
- mDirectoryEntry = mMap.add(directoryOffset, directoryOffset + newDirectoryBytes.length,
+ directoryEntry = map.add(directoryOffset, directoryOffset + newDirectoryBytes.length,
newDirectory);
}
}
/**
- * Writes the central directory to the end of the zip file. {@link #mDirectoryEntry} may be
+ * Writes the central directory to the end of the zip file. {@link #directoryEntry} may be
* {@code null} only if there are no files in the archive.
*
* @throws IOException failed to append the central directory
*/
private void appendCentralDirectory() throws IOException {
- Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
- Preconditions.checkNotNull(mRaf, "mRaf == null");
+ Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+ Preconditions.checkNotNull(raf, "raf == null");
- if (mEntries.isEmpty()) {
- Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry != null");
+ if (entries.isEmpty()) {
+ Preconditions.checkState(directoryEntry == null, "directoryEntry != null");
return;
}
- Preconditions.checkNotNull(mDirectoryEntry, "mDirectoryEntry != null");
+ Preconditions.checkNotNull(directoryEntry, "directoryEntry != null");
- CentralDirectory newDirectory = mDirectoryEntry.getStore();
+ CentralDirectory newDirectory = directoryEntry.getStore();
Preconditions.checkNotNull(newDirectory, "newDirectory != null");
byte[] newDirectoryBytes = newDirectory.toBytes();
- long directoryOffset = mDirectoryEntry.getStart();
+ long directoryOffset = directoryEntry.getStart();
/*
* It is fine to seek beyond the end of file. Seeking beyond the end of file will not extend
* the file. Even if we do not have any directory data to write, the extend() call below
- * will force the file to be extended leaving exactly mExtraDirectoryOffset bytes empty at
+ * will force the file to be extended leaving exactly extraDirectoryOffset bytes empty at
* the beginning.
*/
directWrite(directoryOffset, newDirectoryBytes);
@@ -1249,79 +1436,81 @@
* @return the byte representation, or an empty array if there are no entries in the zip
* @throws IOException failed to compute the central directory byte representation
*/
- @NonNull
+ @Nonnull
public byte[] getCentralDirectoryBytes() throws IOException {
- if (mEntries.isEmpty()) {
- Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry != null");
+ if (entries.isEmpty()) {
+ Preconditions.checkState(directoryEntry == null, "directoryEntry != null");
return new byte[0];
}
- Preconditions.checkNotNull(mDirectoryEntry, "mDirectoryEntry == null");
+ Preconditions.checkNotNull(directoryEntry, "directoryEntry == null");
- CentralDirectory cd = mDirectoryEntry.getStore();
+ CentralDirectory cd = directoryEntry.getStore();
Preconditions.checkNotNull(cd, "cd == null");
return cd.toBytes();
}
/**
- * Computes the EOCD. This creates a new {@link #mEocdEntry}. The
- * central directory must already be written. If {@link #mDirectoryEntry} is {@code null}, then
+ * Computes the EOCD. This creates a new {@link #eocdEntry}. The
+ * central directory must already be written. If {@link #directoryEntry} is {@code null}, then
* the zip file must not have any entries.
*
* @throws IOException failed to write the EOCD
*/
private void computeEocd() throws IOException {
- Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
- Preconditions.checkNotNull(mRaf, "mRaf == null");
- if (mDirectoryEntry == null) {
- Preconditions.checkState(mEntries.isEmpty(),
- "mDirectoryEntry == null && !mEntries.isEmpty()");
+ Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+ Preconditions.checkNotNull(raf, "raf == null");
+ if (directoryEntry == null) {
+ Preconditions.checkState(entries.isEmpty(),
+ "directoryEntry == null && !entries.isEmpty()");
}
long dirStart;
long dirSize = 0;
- if (mDirectoryEntry != null) {
- CentralDirectory directory = mDirectoryEntry.getStore();
+ if (directoryEntry != null) {
+ CentralDirectory directory = directoryEntry.getStore();
assert directory != null;
- dirStart = mDirectoryEntry.getStart();
- dirSize = mDirectoryEntry.getSize();
- Verify.verify(directory.getEntries().size() == mEntries.size());
+ dirStart = directoryEntry.getStart();
+ dirSize = directoryEntry.getSize();
+ Verify.verify(directory.getEntries().size() == entries.size());
} else {
/*
* If we do not have a directory, then we must leave any requested offset empty.
*/
- dirStart = mExtraDirectoryOffset;
+ dirStart = extraDirectoryOffset;
}
- Eocd eocd = new Eocd(mEntries.size(), dirStart, dirSize);
+ Verify.verify(eocdComment != null);
+ Eocd eocd = new Eocd(entries.size(), dirStart, dirSize, eocdComment);
+ eocdComment = null;
byte[] eocdBytes = eocd.toBytes();
- long eocdOffset = mMap.size();
+ long eocdOffset = map.size();
- mMap.extend(eocdOffset + eocdBytes.length);
+ map.extend(eocdOffset + eocdBytes.length);
- mEocdEntry = mMap.add(eocdOffset, eocdOffset + eocdBytes.length, eocd);
+ eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd);
}
/**
- * Writes the EOCD to the end of the zip file. This creates a new {@link #mEocdEntry}. The
- * central directory must already be written. If {@link #mDirectoryEntry} is {@code null}, then
+ * Writes the EOCD to the end of the zip file. This creates a new {@link #eocdEntry}. The
+ * central directory must already be written. If {@link #directoryEntry} is {@code null}, then
* the zip file must not have any entries.
*
* @throws IOException failed to write the EOCD
*/
private void appendEocd() throws IOException {
- Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
- Preconditions.checkNotNull(mRaf, "mRaf == null");
- Preconditions.checkNotNull(mEocdEntry, "mEocdEntry == null");
+ Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+ Preconditions.checkNotNull(raf, "raf == null");
+ Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
- Eocd eocd = mEocdEntry.getStore();
+ Eocd eocd = eocdEntry.getStore();
Preconditions.checkNotNull(eocd, "eocd == null");
byte[] eocdBytes = eocd.toBytes();
- long eocdOffset = mEocdEntry.getStart();
+ long eocdOffset = eocdEntry.getStart();
directWrite(eocdOffset, eocdBytes);
}
@@ -1333,11 +1522,11 @@
* @return the byte representation of the EOCD
* @throws IOException failed to obtain the byte representation of the EOCD
*/
- @NonNull
+ @Nonnull
public byte[] getEocdBytes() throws IOException {
- Preconditions.checkNotNull(mEocdEntry, "mEocdEntry == null");
+ Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
- Eocd eocd = mEocdEntry.getStore();
+ Eocd eocd = eocdEntry.getStore();
Preconditions.checkNotNull(eocd, "eocd == null");
return eocd.toBytes();
}
@@ -1348,20 +1537,20 @@
* @throws IOException failed to close the file
*/
private void innerClose() throws IOException {
- if (mState == ZipFileState.CLOSED) {
+ if (state == ZipFileState.CLOSED) {
return;
}
- Verify.verifyNotNull(mRaf, "mRaf == null");
+ Verify.verifyNotNull(raf, "raf == null");
- mRaf.close();
- mRaf = null;
- mState = ZipFileState.CLOSED;
- if (mClosedControl == null) {
- mClosedControl = new CachedFileContents<>(mFile);
+ raf.close();
+ raf = null;
+ state = ZipFileState.CLOSED;
+ if (closedControl == null) {
+ closedControl = new CachedFileContents<>(file);
}
- mClosedControl.closed(null);
+ closedControl.closed(null);
}
/**
@@ -1372,28 +1561,31 @@
* @throws IOException failed to open the file
*/
public void openReadOnly() throws IOException {
- if (mState != ZipFileState.CLOSED) {
+ if (state != ZipFileState.CLOSED) {
return;
}
- mState = ZipFileState.OPEN_RO;
- mRaf = new RandomAccessFile(mFile, "r");
+ state = ZipFileState.OPEN_RO;
+ raf = new RandomAccessFile(file, "r");
}
/**
* Opens (or reopens) the zip file as read-write. This method will ensure that
- * {@link #mRaf} is not null and open for writing.
+ * {@link #raf} is not null and open for writing.
*
* @throws IOException failed to open the file, failed to close it or the file was closed and
* has been modified outside the control of this object
*/
private void reopenRw() throws IOException {
- if (mState == ZipFileState.OPEN_RW) {
+ // We an never open a file RW in read-only mode. We should never get this far, though.
+ Verify.verify(!readOnly);
+
+ if (state == ZipFileState.OPEN_RW) {
return;
}
boolean wasClosed;
- if (mState == ZipFileState.OPEN_RO) {
+ if (state == ZipFileState.OPEN_RO) {
/*
* ReadAccessFile does not have a way to reopen as RW so we have to close it and
* open it again.
@@ -1404,16 +1596,24 @@
wasClosed = true;
}
- Verify.verify(mState == ZipFileState.CLOSED, "mState != ZpiFileState.CLOSED");
- Verify.verify(mRaf == null, "mRaf != null");
+ Verify.verify(state == ZipFileState.CLOSED, "state != ZpiFileState.CLOSED");
+ Verify.verify(raf == null, "raf != null");
- if (mClosedControl != null && !mClosedControl.isValid()) {
- throw new IOException("File '" + mFile.getAbsolutePath() + "' has been modified "
+ if (closedControl != null && !closedControl.isValid()) {
+ throw new IOException("File '" + file.getAbsolutePath() + "' has been modified "
+ "by an external application.");
}
- mRaf = new RandomAccessFile(mFile, "rw");
- mState = ZipFileState.OPEN_RW;
+ raf = new RandomAccessFile(file, "rw");
+ state = ZipFileState.OPEN_RW;
+
+ /*
+ * Now that we've open the zip and are ready to write, clear out any data descriptors
+ * in the zip since we don't need them and they take space in the archive.
+ */
+ for (StoredEntry entry : entries()) {
+ dirty |= entry.removeDataDescriptor();
+ }
if (wasClosed) {
notify(ZFileExtension::open);
@@ -1428,8 +1628,10 @@
* and the name should not end in slash
* @param stream the source for the file's data
* @throws IOException failed to read the source data
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void add(@NonNull String name, @NonNull InputStream stream) throws IOException {
+ public void add(@Nonnull String name, @Nonnull InputStream stream) throws IOException {
+ checkNotInReadOnlyMode();
add(name, stream, true);
}
@@ -1443,25 +1645,27 @@
* @return the created entry
* @throws IOException failed to create the entry
*/
- @NonNull
+ @Nonnull
private StoredEntry makeStoredEntry(
- @NonNull String name,
- @NonNull InputStream stream,
+ @Nonnull String name,
+ @Nonnull InputStream stream,
boolean mayCompress)
throws IOException {
- CloseableByteSource source = mTracker.fromStream(stream);
+ CloseableByteSource source = tracker.fromStream(stream);
long crc32 = source.hash(Hashing.crc32()).padToLong();
boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name);
SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo =
SettableFuture.create();
+ GPFlags flags = GPFlags.make(encodeWithUtf8);
CentralDirectoryHeader newFileData =
new CentralDirectoryHeader(
name,
+ EncodeUtils.encode(name, flags),
source.size(),
compressInfo,
- GPFlags.make(encodeWithUtf8),
+ flags,
this);
newFileData.setCrc32(crc32);
@@ -1489,30 +1693,37 @@
* @return the sources whose data may or may not be already defined
* @throws IOException failed to create the raw sources
*/
- @NonNull
+ @Nonnull
private ProcessedAndRawByteSources createSources(
boolean mayCompress,
- @NonNull CloseableByteSource source,
- @NonNull SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo,
- @NonNull CentralDirectoryHeader newFileData)
+ @Nonnull CloseableByteSource source,
+ @Nonnull SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo,
+ @Nonnull CentralDirectoryHeader newFileData)
throws IOException {
if (mayCompress) {
- ListenableFuture<CompressionResult> result = mCompressor.compress(source);
- Futures.addCallback(result, new FutureCallback<CompressionResult>() {
- @Override
- public void onSuccess(CompressionResult result) {
- compressInfo.set(new CentralDirectoryHeaderCompressInfo(newFileData,
- result.getCompressionMethod(), result.getSize()));
- }
+ ListenableFuture<CompressionResult> result = compressor.compress(source);
+ Futures.addCallback(
+ result,
+ new FutureCallback<CompressionResult>() {
+ @Override
+ public void onSuccess(CompressionResult result) {
+ compressInfo.set(
+ new CentralDirectoryHeaderCompressInfo(
+ newFileData,
+ result.getCompressionMethod(),
+ result.getSize()));
+ }
- @Override
- public void onFailure(@NonNull Throwable t) {
- compressInfo.setException(t);
- }
- });
+ @Override
+ public void onFailure(@Nonnull Throwable t) {
+ compressInfo.setException(t);
+ }
+ },
+ MoreExecutors.directExecutor());
ListenableFuture<CloseableByteSource> compressedByteSourceFuture =
- Futures.transform(result, CompressionResult::getSource);
+ Futures.transform(
+ result, CompressionResult::getSource, MoreExecutors.directExecutor());
LazyDelegateByteSource compressedByteSource = new LazyDelegateByteSource(
compressedByteSourceFuture);
return new ProcessedAndRawByteSources(source, compressedByteSource);
@@ -1538,9 +1749,11 @@
* @param mayCompress can the file be compressed? This flag will be ignored if the alignment
* rules force the file to be aligned, in which case the file will not be compressed.
* @throws IOException failed to read the source data
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void add(@NonNull String name, @NonNull InputStream stream, boolean mayCompress)
+ public void add(@Nonnull String name, @Nonnull InputStream stream, boolean mayCompress)
throws IOException {
+ checkNotInReadOnlyMode();
/*
* Clean pending background work, if needed.
@@ -1552,25 +1765,25 @@
/**
* Adds a {@link StoredEntry} to the zip. The entry is not immediately added to
- * {@link #mEntries} because data may not yet be available. Instead, it is placed under
- * {@link #mUncompressedEntries} and later moved to {@link #processAllReadyEntries()} when
+ * {@link #entries} because data may not yet be available. Instead, it is placed under
+ * {@link #uncompressedEntries} and later moved to {@link #processAllReadyEntries()} when
* done.
*
* <p>This method invokes {@link #processAllReadyEntries()} to move the entry if it has already
* been computed so, if there is no delay in compression, and no more files are in waiting
- * queue, then the entry is added to {@link #mEntries} immediately.
+ * queue, then the entry is added to {@link #entries} immediately.
*
* @param newEntry the entry to add
* @throws IOException failed to process this entry (or a previous one whose future only
* completed now)
*/
- private void add(@NonNull final StoredEntry newEntry) throws IOException {
- mUncompressedEntries.add(newEntry);
+ private void add(@Nonnull final StoredEntry newEntry) throws IOException {
+ uncompressedEntries.add(newEntry);
processAllReadyEntries();
}
/**
- * Moves all ready entries from {@link #mUncompressedEntries} to {@link #mEntries}. It will
+ * Moves all ready entries from {@link #uncompressedEntries} to {@link #entries}. It will
* stop as soon as entry whose future has not been completed is found.
*
* @throws IOException the exception reported in the future computation, if any, or failed
@@ -1580,15 +1793,15 @@
/*
* Many things can happen during addToEntries(). Because addToEntries() fires
* notifications to extensions, other files can be added, removed, etc. Ee are *not*
- * guaranteed that new stuff does not get into mUncompressedEntries: add() will still work
+ * guaranteed that new stuff does not get into uncompressedEntries: add() will still work
* and will add new entries in there.
*
* However -- important -- processReadyEntries() may be invoked during addToEntries()
* because of the extension mechanism. This means that stuff *can* be removed from
- * mUncompressedEntries and moved to mEntries during addToEntries().
+ * uncompressedEntries and moved to entries during addToEntries().
*/
- while (!mUncompressedEntries.isEmpty()) {
- StoredEntry next = mUncompressedEntries.get(0);
+ while (!uncompressedEntries.isEmpty()) {
+ StoredEntry next = uncompressedEntries.get(0);
CentralDirectoryHeader cdh = next.getCentralDirectoryHeader();
Future<CentralDirectoryHeaderCompressInfo> compressionInfo = cdh.getCompressionInfo();
if (!compressionInfo.isDone()) {
@@ -1598,7 +1811,7 @@
return;
}
- mUncompressedEntries.remove(0);
+ uncompressedEntries.remove(0);
try {
compressionInfo.get();
@@ -1614,19 +1827,19 @@
}
/**
- * Waits until {@link #mUncompressedEntries} is empty.
+ * Waits until {@link #uncompressedEntries} is empty.
*
* @throws IOException the exception reported in the future computation, if any, or failed
* to add a file to the archive
*/
private void processAllReadyEntriesWithWait() throws IOException {
processAllReadyEntries();
- while (!mUncompressedEntries.isEmpty()) {
+ while (!uncompressedEntries.isEmpty()) {
/*
* Wait for the first future to complete and then try again. Keep looping until we're
* done.
*/
- StoredEntry first = mUncompressedEntries.get(0);
+ StoredEntry first = uncompressedEntries.get(0);
CentralDirectoryHeader cdh = first.getCentralDirectoryHeader();
cdh.getCompressionInfoWithWait();
@@ -1635,13 +1848,13 @@
}
/**
- * Adds a new file to {@link #mEntries}. This is actually added to the zip and its space
- * allocated in the {@link #mMap}.
+ * Adds a new file to {@link #entries}. This is actually added to the zip and its space
+ * allocated in the {@link #map}.
*
* @param newEntry the new entry to add
* @throws IOException failed to add the file
*/
- private void addToEntries(@NonNull final StoredEntry newEntry) throws IOException {
+ private void addToEntries(@Nonnull final StoredEntry newEntry) throws IOException {
Preconditions.checkArgument(newEntry.getDataDescriptorType() ==
DataDescriptorType.NO_DATA_DESCRIPTOR, "newEntry has data descriptor");
@@ -1651,7 +1864,7 @@
* StoredEntry.delete() will call {@link ZFile#delete(StoredEntry, boolean)} to perform
* data structure cleanup.
*/
- FileUseMapEntry<StoredEntry> toReplace = mEntries.get(
+ FileUseMapEntry<StoredEntry> toReplace = entries.get(
newEntry.getCentralDirectoryHeader().getName());
final StoredEntry replaceStore;
if (toReplace != null) {
@@ -1664,9 +1877,9 @@
FileUseMapEntry<StoredEntry> fileUseMapEntry =
positionInFile(newEntry, PositionHint.ANYWHERE);
- mEntries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry);
+ entries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry);
- mDirty = true;
+ dirty = true;
notify(ext -> ext.added(newEntry, replaceStore));
}
@@ -1685,10 +1898,10 @@
* @param positionHint hint to where the file should be positioned
* @return the position in the file where the entry should be placed
*/
- @NonNull
+ @Nonnull
private FileUseMapEntry<StoredEntry> positionInFile(
- @NonNull StoredEntry entry,
- @NonNull PositionHint positionHint)
+ @Nonnull StoredEntry entry,
+ @Nonnull PositionHint positionHint)
throws IOException {
deleteDirectoryAndEocd();
long size = entry.getInFileSize();
@@ -1708,13 +1921,13 @@
throw new AssertionError();
}
- long newOffset = mMap.locateFree(size, localHeaderSize, alignment, algorithm);
+ long newOffset = map.locateFree(size, localHeaderSize, alignment, algorithm);
long newEnd = newOffset + entry.getInFileSize();
- if (newEnd > mMap.size()) {
- mMap.extend(newEnd);
+ if (newEnd > map.size()) {
+ map.extend(newEnd);
}
- return mMap.add(newOffset, newEnd, entry);
+ return map.add(newOffset, newEnd, entry);
}
/**
@@ -1725,7 +1938,7 @@
* required for the entry
* @throws IOException failed to determine the alignment
*/
- private int chooseAlignment(@NonNull StoredEntry entry) throws IOException {
+ private int chooseAlignment(@Nonnull StoredEntry entry) throws IOException {
CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader();
CentralDirectoryHeaderCompressInfo compressionInfo = cdh.getCompressionInfoWithWait();
@@ -1733,7 +1946,7 @@
if (isCompressed) {
return AlignmentRule.NO_ALIGNMENT;
} else {
- return mAlignmentRule.alignment(cdh.getName());
+ return alignmentRule.alignment(cdh.getName());
}
}
@@ -1750,9 +1963,12 @@
* @param ignoreFilter predicate that, if {@code true}, identifies files in <em>src</em> that
* should be ignored by merging; merging will behave as if these files were not there
* @throws IOException failed to read from <em>src</em> or write on the output
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void mergeFrom(@NonNull ZFile src, @NonNull Predicate<String> ignoreFilter)
+ public void mergeFrom(@Nonnull ZFile src, @Nonnull Predicate<String> ignoreFilter)
throws IOException {
+ checkNotInReadOnlyMode();
+
for (StoredEntry fromEntry : src.entries()) {
if (ignoreFilter.test(fromEntry.getCentralDirectoryHeader().getName())) {
continue;
@@ -1760,7 +1976,7 @@
boolean replaceCurrent = true;
String path = fromEntry.getCentralDirectoryHeader().getName();
- FileUseMapEntry<StoredEntry> currentEntry = mEntries.get(path);
+ FileUseMapEntry<StoredEntry> currentEntry = entries.get(path);
if (currentEntry != null) {
long fromSize = fromEntry.getCentralDirectoryHeader().getUncompressedSize();
@@ -1806,7 +2022,7 @@
throw new IOException("Cannot read source with " + sourceSize + " bytes.");
}
- byte data[] = new byte[Ints.checkedCast(sourceSize)];
+ byte[] data = new byte[Ints.checkedCast(sourceSize)];
int read = 0;
while (read < data.length) {
int r = fromInput.read(data, read, data.length - read);
@@ -1818,7 +2034,7 @@
* Build the new source and wrap it around an inflater source if data came from
* a compressed source.
*/
- CloseableByteSource rawContents = mTracker.fromSource(fromSource.getRawByteSource());
+ CloseableByteSource rawContents = tracker.fromSource(fromSource.getRawByteSource());
CloseableByteSource processedContents;
if (fromCompressInfo.getMethod() == CompressionMethod.DEFLATE) {
//noinspection IOResourceOpenedButNotSafelyClosed
@@ -1842,9 +2058,12 @@
/**
* Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()}
* or {@link #close()} are invoked.
+ *
+ * @throws IllegalStateException if the file is in read-only mode
*/
public void touch() {
- mDirty = true;
+ checkNotInReadOnlyMode();
+ dirty = true;
}
/**
@@ -1870,13 +2089,20 @@
* of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid
* @throws IOException failed to realign the zip; some entries in the zip may have been lost
* due to the I/O error
+ * @throws IllegalStateException if the file is in read-only mode
*/
public boolean realign() throws IOException {
+ checkNotInReadOnlyMode();
+
boolean anyChanges = false;
for (StoredEntry entry : entries()) {
anyChanges |= entry.realign();
}
+ if (anyChanges) {
+ dirty = true;
+ }
+
return anyChanges;
}
@@ -1890,9 +2116,9 @@
* @throws IOException failed to read/write an entry; the entry may no longer exist in the
* file
*/
- boolean realign(@NonNull StoredEntry entry) throws IOException {
+ boolean realign(@Nonnull StoredEntry entry) throws IOException {
FileUseMapEntry<StoredEntry> mapEntry =
- mEntries.get(entry.getCentralDirectoryHeader().getName());
+ entries.get(entry.getCentralDirectoryHeader().getName());
Verify.verify(entry == mapEntry.getStore());
long currentDataOffset = mapEntry.getStart() + entry.getLocalHeaderSize();
@@ -1910,21 +2136,21 @@
* File is not aligned but it is not written. We do not really need to do much other
* than find another place in the map.
*/
- mMap.remove(mapEntry);
+ map.remove(mapEntry);
long newStart =
- mMap.locateFree(
+ map.locateFree(
mapEntry.getSize(),
entry.getLocalHeaderSize(),
expectedAlignment,
FileUseMap.PositionAlgorithm.BEST_FIT);
- mapEntry = mMap.add(newStart, newStart + entry.getInFileSize(), entry);
- mEntries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
+ mapEntry = map.add(newStart, newStart + entry.getInFileSize(), entry);
+ entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
/*
* Just for safety. We're modifying the in-memory structures but the file should
* already be marked as dirty.
*/
- Verify.verify(mDirty);
+ Verify.verify(dirty);
return false;
@@ -1956,7 +2182,7 @@
clonedCdh.setOffset(-1);
clonedCdh.resetDeferredCrc();
- CloseableByteSource rawContents = mTracker.fromSource(source.getRawByteSource());
+ CloseableByteSource rawContents = tracker.fromSource(source.getRawByteSource());
CloseableByteSource processedContents;
if (compressInfo.getMethod() == CompressionMethod.DEFLATE) {
@@ -1981,18 +2207,22 @@
* Adds an extension to this zip file.
*
* @param extension the listener to add
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void addZFileExtension(@NonNull ZFileExtension extension) {
- mExtensions.add(extension);
+ public void addZFileExtension(@Nonnull ZFileExtension extension) {
+ checkNotInReadOnlyMode();
+ extensions.add(extension);
}
/**
* Removes an extension from this zip file.
*
* @param extension the listener to remove
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void removeZFileExtension(@NonNull ZFileExtension extension) {
- mExtensions.remove(extension);
+ public void removeZFileExtension(@Nonnull ZFileExtension extension) {
+ checkNotInReadOnlyMode();
+ extensions.remove(extension);
}
/**
@@ -2002,25 +2232,25 @@
* notification method on the listener and return the result of that invocation
* @throws IOException failed to process some extensions
*/
- private void notify(@NonNull IOExceptionFunction<ZFileExtension, IOExceptionRunnable> function)
+ private void notify(@Nonnull IOExceptionFunction<ZFileExtension, IOExceptionRunnable> function)
throws IOException {
- for (ZFileExtension fl : Lists.newArrayList(mExtensions)) {
+ for (ZFileExtension fl : Lists.newArrayList(extensions)) {
IOExceptionRunnable r = function.apply(fl);
if (r != null) {
- mToRun.add(r);
+ toRun.add(r);
}
}
- if (!mIsNotifying) {
- mIsNotifying = true;
+ if (!isNotifying) {
+ isNotifying = true;
try {
- while (!mToRun.isEmpty()) {
- IOExceptionRunnable r = mToRun.remove(0);
+ while (!toRun.isEmpty()) {
+ IOExceptionRunnable r = toRun.remove(0);
r.run();
}
} finally {
- mIsNotifying = false;
+ isNotifying = false;
}
}
}
@@ -2035,9 +2265,12 @@
* @param start start offset in {@code data} where data to write is located
* @param count number of bytes of data to write
* @throws IOException failed to write the data
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void directWrite(long offset, @NonNull byte[] data, int start, int count)
+ public void directWrite(long offset, @Nonnull byte[] data, int start, int count)
throws IOException {
+ checkNotInReadOnlyMode();
+
Preconditions.checkArgument(offset >= 0, "offset < 0");
Preconditions.checkArgument(start >= 0, "start >= 0");
Preconditions.checkArgument(count >= 0, "count >= 0");
@@ -2050,10 +2283,10 @@
Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
reopenRw();
- assert mRaf != null;
+ assert raf != null;
- mRaf.seek(offset);
- mRaf.write(data, start, count);
+ raf.seek(offset);
+ raf.write(data, start, count);
}
/**
@@ -2062,12 +2295,30 @@
* @param offset the offset at which data should be written
* @param data the data to write, may be an empty array
* @throws IOException failed to write the data
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void directWrite(long offset, @NonNull byte[] data) throws IOException {
+ public void directWrite(long offset, @Nonnull byte[] data) throws IOException {
+ checkNotInReadOnlyMode();
directWrite(offset, data, 0, data.length);
}
/**
+ * Returns the current size (in bytes) of the underlying file.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public long directSize() throws IOException {
+ /*
+ * Only force a reopen if the file is closed.
+ */
+ if (raf == null) {
+ reopenRw();
+ assert raf != null;
+ }
+ return raf.length();
+ }
+
+ /**
* Directly reads data from the zip file. Invoking this method may force the zip to be reopened
* in read/write mode.
*
@@ -2079,29 +2330,42 @@
* to be read
* @throws IOException failed to write the data
*/
- public int directRead(long offset, @NonNull byte[] data, int start, int count)
+ public int directRead(long offset, @Nonnull byte[] data, int start, int count)
throws IOException {
- Preconditions.checkArgument(offset >= 0, "offset < 0");
Preconditions.checkArgument(start >= 0, "start >= 0");
Preconditions.checkArgument(count >= 0, "count >= 0");
-
- if (data.length == 0) {
- return 0;
- }
-
Preconditions.checkArgument(start <= data.length, "start > data.length");
Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
+ return directRead(offset, ByteBuffer.wrap(data, start, count));
+ }
+
+ /**
+ * Directly reads data from the zip file. Invoking this method may force the zip to be reopened
+ * in read/write mode.
+ *
+ * @param offset the offset from which data should be read
+ * @param dest the output buffer to fill with data from the {@code offset}.
+ * @return how many bytes of data have been written or {@code -1} if there are no more bytes
+ * to be read
+ * @throws IOException failed to write the data
+ */
+ public int directRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+
+ if (!dest.hasRemaining()) {
+ return 0;
+ }
/*
* Only force a reopen if the file is closed.
*/
- if (mRaf == null) {
+ if (raf == null) {
reopenRw();
- assert mRaf != null;
+ assert raf != null;
}
- mRaf.seek(offset);
- return mRaf.read(data, start, count);
+ raf.seek(offset);
+ return raf.getChannel().read(dest);
}
/**
@@ -2111,24 +2375,55 @@
* @param data receives the read data, may be an empty array
* @throws IOException failed to read the data
*/
- public int directRead(long offset, @NonNull byte[] data) throws IOException {
+ public int directRead(long offset, @Nonnull byte[] data) throws IOException {
return directRead(offset, data, 0, data.length);
}
/**
- * Reads exactly @code data.length} bytes of data, failing if it was not possible to read all
+ * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all
* the requested data.
*
* @param offset the offset at which to start reading
* @param data the array that receives the data read
* @throws IOException failed to read some data or there is not enough data to read
*/
- public void directFullyRead(long offset, @NonNull byte[] data) throws IOException {
- Preconditions.checkArgument(offset >= 0, "offset < 0");
- Preconditions.checkNotNull(mRaf, "File is closed");
+ public void directFullyRead(long offset, @Nonnull byte[] data) throws IOException {
+ directFullyRead(offset, ByteBuffer.wrap(data));
+ }
- mRaf.seek(offset);
- RandomAccessFileUtils.fullyRead(mRaf, data);
+ /**
+ * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read
+ * all the requested data.
+ *
+ * @param offset the offset at which to start reading
+ * @param dest the output buffer to fill with data
+ * @throws IOException failed to read some data or there is not enough data to read
+ */
+ public void directFullyRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+
+ if (!dest.hasRemaining()) {
+ return;
+ }
+
+ /*
+ * Only force a reopen if the file is closed.
+ */
+ if (raf == null) {
+ reopenRw();
+ assert raf != null;
+ }
+
+ FileChannel fileChannel = raf.getChannel();
+ while (dest.hasRemaining()) {
+ fileChannel.position(offset);
+ int chunkSize = fileChannel.read(dest);
+ if (chunkSize == -1) {
+ throw new EOFException(
+ "Failed to read " + dest.remaining() + " more bytes: premature EOF");
+ }
+ offset += chunkSize;
+ }
}
/**
@@ -2140,8 +2435,10 @@
* @param file a file or directory; if it is a directory, all files and directories will be
* added recursively
* @throws IOException failed to some (or all ) of the files
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void addAllRecursively(@NonNull File file) throws IOException {
+ public void addAllRecursively(@Nonnull File file) throws IOException {
+ checkNotInReadOnlyMode();
addAllRecursively(file, f -> true);
}
@@ -2152,9 +2449,13 @@
* added recursively
* @param mayCompress a function that decides whether files may be compressed
* @throws IOException failed to some (or all ) of the files
+ * @throws IllegalStateException if the file is in read-only mode
*/
- public void addAllRecursively(@NonNull File file,
- @NonNull Function<? super File, Boolean> mayCompress) throws IOException {
+ public void addAllRecursively(
+ @Nonnull File file,
+ @Nonnull Function<? super File, Boolean> mayCompress) throws IOException {
+ checkNotInReadOnlyMode();
+
/*
* The case of file.isFile() is different because if file.isFile() we will add it to the
* zip in the root. However, if file.isDirectory() we won't add it and add its children.
@@ -2199,22 +2500,22 @@
* includes any extra offset for the central directory
*/
public long getCentralDirectoryOffset() {
- if (mDirectoryEntry != null) {
- return mDirectoryEntry.getStart();
+ if (directoryEntry != null) {
+ return directoryEntry.getStart();
}
/*
* If there are no entries, the central directory is written at the start of the file.
*/
- if (mEntries.isEmpty()) {
- return mExtraDirectoryOffset;
+ if (entries.isEmpty()) {
+ return extraDirectoryOffset;
}
/*
* The Central Directory is written after all entries. This will be at the end of the file
* if the
*/
- return mMap.usedSize() + mExtraDirectoryOffset;
+ return map.usedSize() + extraDirectoryOffset;
}
/**
@@ -2225,11 +2526,11 @@
* been computed
*/
public long getCentralDirectorySize() {
- if (mDirectoryEntry != null) {
- return mDirectoryEntry.getSize();
+ if (directoryEntry != null) {
+ return directoryEntry.getSize();
}
- if (mEntries.isEmpty()) {
+ if (entries.isEmpty()) {
return 0;
}
@@ -2242,11 +2543,11 @@
* @return the offset of the EOCD or {@code -1} if none exists yet
*/
public long getEocdOffset() {
- if (mEocdEntry == null) {
+ if (eocdEntry == null) {
return -1;
}
- return mEocdEntry.getStart();
+ return eocdEntry.getStart();
}
/**
@@ -2255,11 +2556,77 @@
* @return the size of the EOCD of {@code -1} it none exists yet
*/
public long getEocdSize() {
- if (mEocdEntry == null) {
+ if (eocdEntry == null) {
return -1;
}
- return mEocdEntry.getSize();
+ return eocdEntry.getSize();
+ }
+
+ /**
+ * Obtains the comment in the EOCD.
+ *
+ * @return the comment exactly as it was encoded in the EOCD, no encoding conversion is done
+ */
+ @Nonnull
+ public byte[] getEocdComment() {
+ if (eocdEntry == null) {
+ Verify.verify(eocdComment != null);
+ byte[] eocdCommentCopy = new byte[eocdComment.length];
+ System.arraycopy(eocdComment, 0, eocdCommentCopy, 0, eocdComment.length);
+ return eocdCommentCopy;
+ }
+
+ Eocd eocd = eocdEntry.getStore();
+ Verify.verify(eocd != null);
+ return eocd.getComment();
+ }
+
+ /**
+ * Sets the comment in the EOCD.
+ *
+ * @param comment the new comment; no conversion is done, these exact bytes will be placed in
+ * the EOCD comment
+ * @throws IllegalStateException if file is in read-only mode
+ */
+ public void setEocdComment(@Nonnull byte[] comment) {
+ checkNotInReadOnlyMode();
+
+ if (comment.length > MAX_EOCD_COMMENT_SIZE) {
+ throw new IllegalArgumentException(
+ "EOCD comment size ("
+ + comment.length
+ + ") is larger than the maximum allowed ("
+ + MAX_EOCD_COMMENT_SIZE
+ + ")");
+ }
+
+ // Check if the EOCD signature appears anywhere in the comment we need to check if it
+ // is valid.
+ for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) {
+ // Remember: little endian...
+ if (comment[i] == EOCD_SIGNATURE[3]
+ && comment[i + 1] == EOCD_SIGNATURE[2]
+ && comment[i + 2] == EOCD_SIGNATURE[1]
+ && comment[i + 3] == EOCD_SIGNATURE[0]) {
+ // We found a possible EOCD signature at position i. Try to read it.
+ ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i);
+ try {
+ new Eocd(bytes);
+ throw new IllegalArgumentException(
+ "Position "
+ + i
+ + " of the comment contains a valid EOCD record.");
+ } catch (IOException e) {
+ // Fine, this is an invalid record. Move along...
+ }
+ }
+ }
+
+ deleteDirectoryAndEocd();
+ eocdComment = new byte[comment.length];
+ System.arraycopy(comment, 0, eocdComment, 0, comment.length);
+ dirty = true;
}
/**
@@ -2268,14 +2635,16 @@
* updated.
*
* @param offset the offset or {@code 0} to write the central directory at its current location
+ * @throws IllegalStateException if file is in read-only mode
*/
public void setExtraDirectoryOffset(long offset) {
+ checkNotInReadOnlyMode();
Preconditions.checkArgument(offset >= 0, "offset < 0");
- if (mExtraDirectoryOffset != offset) {
- mExtraDirectoryOffset = offset;
+ if (extraDirectoryOffset != offset) {
+ extraDirectoryOffset = offset;
deleteDirectoryAndEocd();
- mDirty = true;
+ dirty = true;
}
}
@@ -2285,7 +2654,7 @@
* @return the offset or {@code 0} if no offset is set
*/
public long getExtraDirectoryOffset() {
- return mExtraDirectoryOffset;
+ return extraDirectoryOffset;
}
/**
@@ -2294,7 +2663,7 @@
* @return are the timestamps being ignored?
*/
public boolean areTimestampsIgnored() {
- return mNoTimestamps;
+ return noTimestamps;
}
/**
@@ -2305,33 +2674,36 @@
* written to disk.
*
* @throws IOException failed to load or move a file in the zip
+ * @throws IllegalStateException if file is in read-only mode
*/
public void sortZipContents() throws IOException {
+ checkNotInReadOnlyMode();
reopenRw();
processAllReadyEntriesWithWait();
- Verify.verify(mUncompressedEntries.isEmpty());
+ Verify.verify(uncompressedEntries.isEmpty());
SortedSet<StoredEntry> sortedEntries = Sets.newTreeSet(StoredEntry.COMPARE_BY_NAME);
- for (FileUseMapEntry<StoredEntry> fmEntry : mEntries.values()) {
+ for (FileUseMapEntry<StoredEntry> fmEntry : entries.values()) {
StoredEntry entry = fmEntry.getStore();
Preconditions.checkNotNull(entry);
sortedEntries.add(entry);
entry.loadSourceIntoMemory();
- mMap.remove(fmEntry);
+ map.remove(fmEntry);
}
- mEntries.clear();
+ entries.clear();
for (StoredEntry entry : sortedEntries) {
String name = entry.getCentralDirectoryHeader().getName();
FileUseMapEntry<StoredEntry> positioned =
positionInFile(entry, PositionHint.LOWEST_OFFSET);
- mEntries.put(name, positioned);
+
+ entries.put(name, positioned);
}
- mDirty = true;
+ dirty = true;
}
/**
@@ -2340,23 +2712,44 @@
* @return the file that may or may not exist (depending on whether something existed there
* before the zip was created and on whether the zip has been updated or not)
*/
- @NonNull
+ @Nonnull
public File getFile() {
- return mFile;
+ return file;
}
/**
- * Checks whether data description verification should be skipped.
+ * Creates a new verify log.
*
- * @return should it be skipped?
+ * @return the new verify log
*/
- boolean getSkipDataDescriptorVerification() {
- return mSkipDataDescriptorVerification;
+ @Nonnull
+ VerifyLog makeVerifyLog() {
+ VerifyLog log = verifyLogFactory.get();
+ assert log != null;
+ return log;
}
/**
- * Hint to where files should be positioned.
+ * Obtains the zip file's verify log.
+ *
+ * @return the verify log
*/
+ @Nonnull
+ VerifyLog getVerifyLog() {
+ return verifyLog;
+ }
+
+ /**
+ * Are there in-memory changes that have not been written to the zip file?
+ *
+ * <p>Waits for all pending processing which may make changes.
+ */
+ public boolean hasPendingChangesWithWait() throws IOException {
+ processAllReadyEntriesWithWait();
+ return dirty;
+ }
+
+ /** Hint to where files should be positioned. */
enum PositionHint {
/**
* File may be positioned anywhere, caller doesn't care.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZFileExtension.java b/src/main/java/com/android/apkzlib/zip/ZFileExtension.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZFileExtension.java
rename to src/main/java/com/android/apkzlib/zip/ZFileExtension.java
index 2a26caf..fdb6ca4 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZFileExtension.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFileExtension.java
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-
+import com.android.apkzlib.utils.IOExceptionRunnable;
import java.io.IOException;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* An extension of a {@link ZFile}. Extensions are notified when files are open, updated, closed and
@@ -127,7 +126,7 @@
* @return an optional runnable to run when notification of all listeners has ended
*/
@Nullable
- public IOExceptionRunnable added(@NonNull StoredEntry entry, @Nullable StoredEntry replaced) {
+ public IOExceptionRunnable added(@Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
return null;
}
@@ -141,7 +140,7 @@
* @return an optional runnable to run when notification of all listeners has ended
*/
@Nullable
- public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
+ public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
return null;
}
}
diff --git a/src/main/java/com/android/apkzlib/zip/ZFileOptions.java b/src/main/java/com/android/apkzlib/zip/ZFileOptions.java
new file mode 100644
index 0000000..08a1d83
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zip/ZFileOptions.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import com.android.apkzlib.zip.compress.DeflateExecutionCompressor;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import java.util.function.Supplier;
+import java.util.zip.Deflater;
+import javax.annotation.Nonnull;
+
+/**
+ * Options to create a {@link ZFile}.
+ */
+public class ZFileOptions {
+
+ /**
+ * The byte tracker.
+ */
+ @Nonnull
+ private ByteTracker tracker;
+
+ /**
+ * The compressor to use.
+ */
+ @Nonnull
+ private Compressor compressor;
+
+ /**
+ * Should timestamps be zeroed?
+ */
+ private boolean noTimestamps;
+
+ /**
+ * The alignment rule to use.
+ */
+ @Nonnull
+ private AlignmentRule alignmentRule;
+
+ /**
+ * Should the extra field be used to cover empty space?
+ */
+ private boolean coverEmptySpaceUsingExtraField;
+
+ /**
+ * Should files be automatically sorted before update?
+ */
+ private boolean autoSortFiles;
+
+ /**
+ * Factory creating verification logs to use.
+ */
+ @Nonnull
+ private Supplier<VerifyLog> verifyLogFactory;
+
+ /**
+ * Creates a new options object. All options are set to their defaults.
+ */
+ public ZFileOptions() {
+ tracker = new ByteTracker();
+ compressor =
+ new DeflateExecutionCompressor(
+ Runnable::run,
+ tracker,
+ Deflater.DEFAULT_COMPRESSION);
+ alignmentRule = AlignmentRules.compose();
+ verifyLogFactory = VerifyLogs::devNull;
+ }
+
+ /**
+ * Obtains the ZFile's byte tracker.
+ *
+ * @return the byte tracker
+ */
+ @Nonnull
+ public ByteTracker getTracker() {
+ return tracker;
+ }
+
+ /**
+ * Obtains the compressor to use.
+ *
+ * @return the compressor
+ */
+ @Nonnull
+ public Compressor getCompressor() {
+ return compressor;
+ }
+
+ /**
+ * Sets the compressor to use.
+ *
+ * @param compressor the compressor
+ */
+ public ZFileOptions setCompressor(@Nonnull Compressor compressor) {
+ this.compressor = compressor;
+ return this;
+ }
+
+ /**
+ * Obtains whether timestamps should be zeroed.
+ *
+ * @return should timestamps be zeroed?
+ */
+ public boolean getNoTimestamps() {
+ return noTimestamps;
+ }
+
+ /**
+ * Sets whether timestamps should be zeroed.
+ *
+ * @param noTimestamps should timestamps be zeroed?
+ */
+ public ZFileOptions setNoTimestamps(boolean noTimestamps) {
+ this.noTimestamps = noTimestamps;
+ return this;
+ }
+
+ /**
+ * Obtains the alignment rule.
+ *
+ * @return the alignment rule
+ */
+ @Nonnull
+ public AlignmentRule getAlignmentRule() {
+ return alignmentRule;
+ }
+
+ /**
+ * Sets the alignment rule.
+ *
+ * @param alignmentRule the alignment rule
+ */
+ public ZFileOptions setAlignmentRule(@Nonnull AlignmentRule alignmentRule) {
+ this.alignmentRule = alignmentRule;
+ return this;
+ }
+
+ /**
+ * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for
+ * an explanation on using the extra field for covering empty spaces.
+ *
+ * @return should the extra field be used to cover empty spaces?
+ */
+ public boolean getCoverEmptySpaceUsingExtraField() {
+ return coverEmptySpaceUsingExtraField;
+ }
+
+ /**
+ * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an
+ * explanation on using the extra field for covering empty spaces.
+ *
+ * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces?
+ */
+ public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {
+ this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField;
+ return this;
+ }
+
+ /**
+ * Obtains whether files should be automatically sorted before updating the zip file. See
+ * {@link ZFile} for an explanation on automatic sorting.
+ *
+ * @return should the file be automatically sorted?
+ */
+ public boolean getAutoSortFiles() {
+ return autoSortFiles;
+ }
+
+ /**
+ * Sets whether files should be automatically sorted before updating the zip file. See {@link
+ * ZFile} for an explanation on automatic sorting.
+ *
+ * @param autoSortFiles should the file be automatically sorted?
+ */
+ public ZFileOptions setAutoSortFiles(boolean autoSortFiles) {
+ this.autoSortFiles = autoSortFiles;
+ return this;
+ }
+
+ /**
+ * Sets the verification log factory.
+ *
+ * @param verifyLogFactory verification log factory
+ */
+ public ZFileOptions setVerifyLogFactory(@Nonnull Supplier<VerifyLog> verifyLogFactory) {
+ this.verifyLogFactory = verifyLogFactory;
+ return this;
+ }
+
+ /**
+ * Obtains the verification log factory. By default, the verification log doesn't store
+ * anything and will always return an empty log.
+ *
+ * @return the verification log factory
+ */
+ @Nonnull
+ public Supplier<VerifyLog> getVerifyLogFactory() {
+ return verifyLogFactory;
+ }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipField.java b/src/main/java/com/android/apkzlib/zip/ZipField.java
similarity index 68%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipField.java
rename to src/main/java/com/android/apkzlib/zip/ZipField.java
index 1169f69..4b0b675 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipField.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipField.java
@@ -14,20 +14,19 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.LittleEndianUtils;
+import com.android.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
-
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* The ZipField class represents a field in a record in a zip file. Zip files are made with records
@@ -58,30 +57,30 @@
/**
* Field name. Used for providing (more) useful error messages.
*/
- @NonNull
- private final String mName;
+ @Nonnull
+ private final String name;
/**
* Offset of the file in the record.
*/
- protected final int mOffset;
+ protected final int offset;
/**
* Size of the field. Only 2 or 4 allowed.
*/
- private final int mSize;
+ private final int size;
/**
* If a fixed value exists for the field, then this attribute will contain that value.
*/
@Nullable
- private final Long mExpected;
+ private final Long expected;
/**
* All invariants that this field must verify.
*/
- @NonNull
- private Set<ZipFieldInvariant> mInvariants;
+ @Nonnull
+ private Set<ZipFieldInvariant> invariants;
/**
* Creates a new field that does not contain a fixed value.
@@ -91,15 +90,15 @@
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
- ZipField(int offset, int size, @NonNull String name, ZipFieldInvariant... invariants) {
+ ZipField(int offset, int size, @Nonnull String name, ZipFieldInvariant... invariants) {
Preconditions.checkArgument(offset >= 0, "offset >= 0");
Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4");
- mName = name;
- mOffset = offset;
- mSize = size;
- mExpected = null;
- mInvariants = Sets.newHashSet(invariants);
+ this.name = name;
+ this.offset = offset;
+ this.size = size;
+ expected = null;
+ this.invariants = Sets.newHashSet(invariants);
}
/**
@@ -110,15 +109,15 @@
* @param expected the expected field value
* @param name the field's name
*/
- ZipField(int offset, int size, long expected, @NonNull String name) {
+ ZipField(int offset, int size, long expected, @Nonnull String name) {
Preconditions.checkArgument(offset >= 0, "offset >= 0");
Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4");
- mName = name;
- mOffset = offset;
- mSize = size;
- mExpected = expected;
- mInvariants = Sets.newHashSet();
+ this.name = name;
+ this.offset = offset;
+ this.size = size;
+ this.expected = expected;
+ invariants = Sets.newHashSet();
}
/**
@@ -129,9 +128,9 @@
* @throws IOException the invariants are not verified
*/
private void checkVerifiesInvariants(long value) throws IOException {
- for (ZipFieldInvariant invariant : mInvariants) {
+ for (ZipFieldInvariant invariant : invariants) {
if (!invariant.isValid(value)) {
- throw new IOException("Value " + value + " of field " + mName + " is invalid "
+ throw new IOException("Value " + value + " of field " + name + " is invalid "
+ "(fails '" + invariant.getName() + "').");
}
}
@@ -144,13 +143,13 @@
* the size of this field
* @throws IOException failed to advance the buffer
*/
- void skip(@NonNull ByteBuffer bytes) throws IOException {
- if (bytes.remaining() < mSize) {
- throw new IOException("Cannot skip field " + mName + " because only "
+ void skip(@Nonnull ByteBuffer bytes) throws IOException {
+ if (bytes.remaining() < size) {
+ throw new IOException("Cannot skip field " + name + " because only "
+ bytes.remaining() + " remain in the buffer.");
}
- bytes.position(bytes.position() + mSize);
+ bytes.position(bytes.position() + size);
}
/**
@@ -161,16 +160,16 @@
* @return the value of the field
* @throws IOException failed to read the field
*/
- long read(@NonNull ByteBuffer bytes) throws IOException {
- if (bytes.remaining() < mSize) {
- throw new IOException("Cannot skip field " + mName + " because only "
+ long read(@Nonnull ByteBuffer bytes) throws IOException {
+ if (bytes.remaining() < size) {
+ throw new IOException("Cannot skip field " + name + " because only "
+ bytes.remaining() + " remain in the buffer.");
}
bytes.order(ByteOrder.LITTLE_ENDIAN);
long r;
- if (mSize == 2) {
+ if (size == 2) {
r = LittleEndianUtils.readUnsigned2Le(bytes);
} else {
r = LittleEndianUtils.readUnsigned4Le(bytes);
@@ -188,9 +187,23 @@
* will be positioned at the first byte after the field
* @throws IOException failed to read the field or the field does not have the expected value
*/
- void verify(@NonNull ByteBuffer bytes) throws IOException {
- Preconditions.checkState(mExpected != null, "mExpected == null");
- verify(bytes, mExpected);
+ void verify(@Nonnull ByteBuffer bytes) throws IOException {
+ verify(bytes, null);
+ }
+
+ /**
+ * Verifies that the field at the current buffer position has the expected value. The field
+ * must have been created with the constructor that defines the expected value.
+ *
+ * @param bytes the byte buffer with the record data; after this method finishes, the buffer
+ * will be positioned at the first byte after the field
+ * @param verifyLog if non-{@code null}, will log the verification error
+ * @throws IOException failed to read the data or the field does not have the expected value;
+ * only thrown if {@code verifyLog} is {@code null}
+ */
+ void verify(@Nonnull ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException {
+ Preconditions.checkState(expected != null, "expected == null");
+ verify(bytes, expected, verifyLog);
}
/**
@@ -202,12 +215,40 @@
* value must verify them
* @throws IOException failed to read the data or the field does not have the expected value
*/
- void verify(@NonNull ByteBuffer bytes, long expected) throws IOException {
+ void verify(@Nonnull ByteBuffer bytes, long expected) throws IOException {
+ verify(bytes, expected, null);
+ }
+
+ /**
+ * Verifies that the field has an expected value.
+ *
+ * @param bytes the byte buffer with the record data; after this method finishes, the buffer
+ * will be positioned at the first byte after the field
+ * @param expected the value we expect the field to have; if this field has invariants, the
+ * value must verify them
+ * @param verifyLog if non-{@code null}, will log the verification error
+ * @throws IOException failed to read the data or the field does not have the expected value;
+ * only thrown if {@code verifyLog} is {@code null}
+ */
+ void verify(
+ @Nonnull ByteBuffer bytes,
+ long expected,
+ @Nullable VerifyLog verifyLog) throws IOException {
checkVerifiesInvariants(expected);
long r = read(bytes);
if (r != expected) {
- throw new IOException("Incorrect value for field '" + mName + "': value is " +
- r + " but " + expected + " expected.");
+ String error =
+ String.format(
+ "Incorrect value for field '%s': value is %s but %s expected.",
+ name,
+ r,
+ expected);
+
+ if (verifyLog == null) {
+ throw new IOException(error);
+ } else {
+ verifyLog.log(error);
+ }
}
}
@@ -219,16 +260,16 @@
* @param value the value to write
* @throws IOException failed to write the value in the stream
*/
- void write(@NonNull ByteBuffer output, long value) throws IOException {
+ void write(@Nonnull ByteBuffer output, long value) throws IOException {
checkVerifiesInvariants(value);
Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
- if (mSize == 2) {
+ if (size == 2) {
Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value);
LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value));
} else {
- Verify.verify(mSize == 4);
+ Verify.verify(size == 4);
Preconditions.checkArgument(value <= 0x00000000ffffffffL,
"value (%s) > 0x00000000ffffffffL", value);
LittleEndianUtils.writeUnsigned4Le(output, value);
@@ -242,9 +283,18 @@
* of the buffer
* @throws IOException failed to write the value in the stream
*/
- void write(@NonNull ByteBuffer output) throws IOException {
- Preconditions.checkState(mExpected != null, "mExpected == null");
- write(output, mExpected);
+ void write(@Nonnull ByteBuffer output) throws IOException {
+ Preconditions.checkState(expected != null, "expected == null");
+ write(output, expected);
+ }
+
+ /**
+ * Obtains the offset at which the field starts.
+ *
+ * @return the start offset
+ */
+ int offset() {
+ return offset;
}
/**
@@ -254,7 +304,7 @@
* @return the end offset
*/
int endOffset() {
- return mOffset + mSize;
+ return offset + size;
}
/**
@@ -269,7 +319,7 @@
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
- F2(int offset, @NonNull String name, ZipFieldInvariant... invariants) {
+ F2(int offset, @Nonnull String name, ZipFieldInvariant... invariants) {
super(offset, 2, name, invariants);
}
@@ -280,7 +330,7 @@
* @param expected the expected field value
* @param name the field's name
*/
- F2(int offset, long expected, @NonNull String name) {
+ F2(int offset, long expected, @Nonnull String name) {
super(offset, 2, expected, name);
}
}
@@ -296,7 +346,7 @@
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
- F4(int offset, @NonNull String name, ZipFieldInvariant... invariants) {
+ F4(int offset, @Nonnull String name, ZipFieldInvariant... invariants) {
super(offset, 4, name, invariants);
}
@@ -307,7 +357,7 @@
* @param expected the expected field value
* @param name the field's name
*/
- F4(int offset, long expected, @NonNull String name) {
+ F4(int offset, long expected, @Nonnull String name) {
super(offset, 4, expected, name);
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariant.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java
similarity index 95%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariant.java
rename to src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java
index 0ae76cd..87fc46c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariant.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
/**
* A field rule defines an invariant (<em>i.e.</em>, a constraint) that has to be verified by a
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantMaxValue.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java
similarity index 86%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantMaxValue.java
rename to src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java
index 437e2bb..5905e1a 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantMaxValue.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
/**
* Invariant checking a zip field does not exceed a threshold.
@@ -24,7 +24,7 @@
/**
* The maximum value allowed.
*/
- private long mMax;
+ private long max;
/**
* Creates a new invariant.
@@ -32,16 +32,16 @@
* @param max the maximum value allowed for the field
*/
ZipFieldInvariantMaxValue(int max) {
- mMax = max;
+ this.max = max;
}
@Override
public boolean isValid(long value) {
- return value <= mMax;
+ return value <= max;
}
@Override
public String getName() {
- return "Maximum value " + mMax;
+ return "Maximum value " + max;
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantNonNegative.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantNonNegative.java
rename to src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java
index e4d68d8..4d1770b 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantNonNegative.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
/**
* Invariant that verifies a field's value is not negative.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFileState.java b/src/main/java/com/android/apkzlib/zip/ZipFileState.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFileState.java
rename to src/main/java/com/android/apkzlib/zip/ZipFileState.java
index b3b9990..7ecf2d5 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFileState.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFileState.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
/**
* The {@code ZipFileState} enumeration holds the state of a {@link ZFile}.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/compress/BestAndDefaultDeflateExecutorCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
similarity index 65%
rename from src/main/java/com/android/builder/internal/packaging/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
rename to src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
index 5e132d2..8948a1c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.CompressionResult;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.CompressionResult;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.base.Preconditions;
-
import java.util.concurrent.Executor;
import java.util.zip.Deflater;
+import javax.annotation.Nonnull;
/**
* Compressor that tries both the best and default compression algorithms and picks the default
@@ -34,20 +33,20 @@
/**
* Deflater using the default compression level.
*/
- @NonNull
- private final DeflateExecutionCompressor mDefaultDeflater;
+ @Nonnull
+ private final DeflateExecutionCompressor defaultDeflater;
/**
* Deflater using the best compression level.
*/
- @NonNull
- private final DeflateExecutionCompressor mBestDeflater;
+ @Nonnull
+ private final DeflateExecutionCompressor bestDeflater;
/**
* Minimum best compression size / default compression size ratio needed to pick the default
* compression size.
*/
- private final double mMinRatio;
+ private final double minRatio;
/**
* Creates a new compressor.
@@ -59,29 +58,29 @@
* if {@code 1.0} then the best compression is always picked unless it produces the exact same
* size as the default compression.
*/
- public BestAndDefaultDeflateExecutorCompressor(@NonNull Executor executor,
- @NonNull ByteTracker tracker, double minRatio) {
+ public BestAndDefaultDeflateExecutorCompressor(@Nonnull Executor executor,
+ @Nonnull ByteTracker tracker, double minRatio) {
super(executor);
Preconditions.checkArgument(minRatio >= 0.0, "minRatio < 0.0");
Preconditions.checkArgument(minRatio <= 1.0, "minRatio > 1.0");
- mDefaultDeflater = new DeflateExecutionCompressor(executor, tracker,
- Deflater.DEFAULT_COMPRESSION);
- mBestDeflater = new DeflateExecutionCompressor(executor, tracker,
- Deflater.BEST_COMPRESSION);
- mMinRatio = minRatio;
+ defaultDeflater =
+ new DeflateExecutionCompressor(executor, tracker, Deflater.DEFAULT_COMPRESSION);
+ bestDeflater =
+ new DeflateExecutionCompressor(executor, tracker, Deflater.BEST_COMPRESSION);
+ this.minRatio = minRatio;
}
- @NonNull
+ @Nonnull
@Override
- protected CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+ protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
throws Exception {
- CompressionResult defaultResult = mDefaultDeflater.immediateCompress(source);
- CompressionResult bestResult = mBestDeflater.immediateCompress(source);
+ CompressionResult defaultResult = defaultDeflater.immediateCompress(source);
+ CompressionResult bestResult = bestDeflater.immediateCompress(source);
double sizeRatio = bestResult.getSize() / (double) defaultResult.getSize();
- if (sizeRatio >= mMinRatio) {
+ if (sizeRatio >= minRatio) {
return defaultResult;
} else {
return bestResult;
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/compress/DeflateExecutionCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java
similarity index 69%
rename from src/main/java/com/android/builder/internal/packaging/zip/compress/DeflateExecutionCompressor.java
rename to src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java
index d8c6ab9..309e356 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/compress/DeflateExecutionCompressor.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.CompressionMethod;
-import com.android.builder.internal.packaging.zip.CompressionResult;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-
+import com.android.apkzlib.zip.CompressionMethod;
+import com.android.apkzlib.zip.CompressionResult;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.Executor;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
+import javax.annotation.Nonnull;
/**
* Compressor that uses deflate with an executor.
@@ -36,13 +35,13 @@
/**
* Deflate compression level.
*/
- private final int mLevel;
+ private final int level;
/**
* Byte tracker to use to create byte sources.
*/
- @NonNull
- private final ByteTracker mTracker;
+ @Nonnull
+ private final ByteTracker tracker;
/**
* Creates a new compressor.
@@ -51,26 +50,28 @@
* @param tracker the byte tracker to use to keep track of memory usage
* @param level the compression level
*/
- public DeflateExecutionCompressor(@NonNull Executor executor, @NonNull ByteTracker tracker,
+ public DeflateExecutionCompressor(
+ @Nonnull Executor executor,
+ @Nonnull ByteTracker tracker,
int level) {
super(executor);
- mLevel = level;
- mTracker = tracker;
+ this.level = level;
+ this.tracker = tracker;
}
- @NonNull
+ @Nonnull
@Override
- protected CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+ protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
- Deflater deflater = new Deflater(mLevel, true);
+ Deflater deflater = new Deflater(level, true);
try (DeflaterOutputStream dos = new DeflaterOutputStream(output, deflater)) {
dos.write(source.read());
}
- CloseableByteSource result = mTracker.fromStream(output);
+ CloseableByteSource result = tracker.fromStream(output);
if (result.size() >= source.size()) {
return new CompressionResult(source, CompressionMethod.STORE, source.size());
} else {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/compress/ExecutorCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
similarity index 72%
rename from src/main/java/com/android/builder/internal/packaging/zip/compress/ExecutorCompressor.java
rename to src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
index f52c542..54be20c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/compress/ExecutorCompressor.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.CompressionResult;
-import com.android.builder.internal.packaging.zip.Compressor;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.CompressionResult;
+import com.android.apkzlib.zip.Compressor;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
-
import java.util.concurrent.Executor;
+import javax.annotation.Nonnull;
/**
* A synchronous compressor is a compressor that computes the result of compression immediately
@@ -34,26 +33,26 @@
/**
* The executor that does the work.
*/
- @NonNull
- private final Executor mExecutor;
+ @Nonnull
+ private final Executor executor;
/**
* Compressor that delegates execution into the given executor.
* @param executor the executor that will do the compress
*/
- public ExecutorCompressor(@NonNull Executor executor) {
- mExecutor = executor;
+ public ExecutorCompressor(@Nonnull Executor executor) {
+ this.executor = executor;
}
- @NonNull
+ @Nonnull
@Override
public ListenableFuture<CompressionResult> compress(
- @NonNull final CloseableByteSource source) {
+ @Nonnull final CloseableByteSource source) {
final SettableFuture<CompressionResult> future = SettableFuture.create();
- mExecutor.execute(() -> {
+ executor.execute(() -> {
try {
future.set(immediateCompress(source));
- } catch (Exception e) {
+ } catch (Throwable e) {
future.setException(e);
}
});
@@ -67,7 +66,7 @@
* @return the result of compression
* @throws Exception failed to compress
*/
- @NonNull
- protected abstract CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+ @Nonnull
+ protected abstract CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
throws Exception;
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java
similarity index 60%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java
index 22983ab..3b7411e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,14 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip.compress;
-/**
- * Type of stored entry.
- */
-public enum StoredEntryType {
- /**
- * Entry is a file.
- */
- FILE,
+import java.io.IOException;
- /**
- * Entry is a directory.
- */
- DIRECTORY
+/** Exception raised by ZFile when encountering unsupported Zip64 format jar files. */
+public class Zip64NotSupportedException extends IOException {
+
+ public Zip64NotSupportedException(String message) {
+ super(message);
+ }
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zip/compress/package-info.java
similarity index 67%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/zip/compress/package-info.java
index 22983ab..e2fcbd6 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
-
/**
- * Type of stored entry.
+ * Compressors to use with the {@code zip} package.
*/
-public enum StoredEntryType {
- /**
- * Entry is a file.
- */
- FILE,
-
- /**
- * Entry is a directory.
- */
- DIRECTORY
-}
+package com.android.apkzlib.zip.compress;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/ByteTracker.java b/src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java
similarity index 82%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/ByteTracker.java
rename to src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java
index 9c7a719..88ed939 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/ByteTracker.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java
@@ -14,35 +14,38 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
-import com.android.annotations.NonNull;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
-
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import javax.annotation.Nonnull;
+/**
+ * Keeps track of used bytes allowing gauging memory usage.
+ */
public class ByteTracker {
/**
* Number of bytes currently in use.
*/
- private long mBytesUsed;
+ private long bytesUsed;
/**
* Maximum number of bytes used.
*/
- private long mMaxBytesUsed;
+ private long maxBytesUsed;
/**
* Creates a new byte source by fully reading an input stream.
+ *
* @param stream the input stream
* @return a byte source containing the cached data from the given stream
* @throws IOException failed to read the stream
*/
- public CloseableDelegateByteSource fromStream(@NonNull InputStream stream) throws IOException {
+ public CloseableDelegateByteSource fromStream(@Nonnull InputStream stream) throws IOException {
byte[] data = ByteStreams.toByteArray(stream);
updateUsage(data.length);
return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {
@@ -56,11 +59,13 @@
/**
* Creates a new byte source by snapshotting the provided stream.
+ *
* @param stream the stream with the data
* @return a byte source containing the cached data from the given stream
* @throws IOException failed to read the stream
*/
- public CloseableDelegateByteSource fromStream(@NonNull ByteArrayOutputStream stream) throws IOException {
+ public CloseableDelegateByteSource fromStream(@Nonnull ByteArrayOutputStream stream)
+ throws IOException {
byte[] data = stream.toByteArray();
updateUsage(data.length);
return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {
@@ -74,38 +79,42 @@
/**
* Creates a new byte source from another byte source.
+ *
* @param source the byte source to copy data from
* @return the tracked byte source
* @throws IOException failed to read data from the byte source
*/
- public CloseableDelegateByteSource fromSource(@NonNull ByteSource source) throws IOException {
+ public CloseableDelegateByteSource fromSource(@Nonnull ByteSource source) throws IOException {
return fromStream(source.openStream());
}
/**
* Updates the memory used by this tracker.
+ *
* @param delta the number of bytes to add or remove, if negative
*/
private synchronized void updateUsage(long delta) {
- mBytesUsed += delta;
- if (mMaxBytesUsed < mBytesUsed) {
- mMaxBytesUsed = mBytesUsed;
+ bytesUsed += delta;
+ if (maxBytesUsed < bytesUsed) {
+ maxBytesUsed = bytesUsed;
}
}
/**
* Obtains the number of bytes currently used.
+ *
* @return the number of bytes
*/
public synchronized long getBytesUsed() {
- return mBytesUsed;
+ return bytesUsed;
}
/**
* Obtains the maximum number of bytes ever used by this tracker.
+ *
* @return the number of bytes
*/
public synchronized long getMaxBytesUsed() {
- return mMaxBytesUsed;
+ return maxBytesUsed;
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableByteSource.java b/src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java
similarity index 89%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableByteSource.java
rename to src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java
index 28afd71..1479a03 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
import com.google.common.io.ByteSource;
@@ -32,31 +32,32 @@
/**
* Has the source been closed?
*/
- private boolean mClosed;
+ private boolean closed;
/**
* Creates a new byte source.
*/
public CloseableByteSource() {
- mClosed = false;
+ closed = false;
}
@Override
public final synchronized void close() throws IOException {
- if (mClosed) {
+ if (closed) {
return;
}
try {
innerClose();
} finally {
- mClosed = true;
+ closed = true;
}
}
/**
* Closes the by source. This method is only invoked once, even if {@link #close()} is
- * called multiple times
+ * called multiple times.
+ *
* @throws IOException failed to close
*/
protected abstract void innerClose() throws IOException;
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableDelegateByteSource.java b/src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java
similarity index 83%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableDelegateByteSource.java
rename to src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java
index 7788b9d..aebb29a 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableDelegateByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java
@@ -14,21 +14,20 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.io.ByteProcessor;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
-
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Closeable byte source that delegates to another byte source.
@@ -39,11 +38,11 @@
* The byte source we delegate all operations to. {@code null} if disposed.
*/
@Nullable
- private ByteSource mInner;
+ private ByteSource inner;
/**
- * Size of the byte source. This is the same as {@code mInner.size()} (when {@code mInner}
- * is not {@code null}), but we keep it separate to avoid calling {@code mInner.size()}
+ * Size of the byte source. This is the same as {@code inner.size()} (when {@code inner}
+ * is not {@code null}), but we keep it separate to avoid calling {@code inner.size()}
* because it might throw {@code IOException}.
*/
private final long mSize;
@@ -54,8 +53,8 @@
* @param inner the inner byte source
* @param size the size of the source
*/
- public CloseableDelegateByteSource(@NonNull ByteSource inner, long size) {
- mInner = inner;
+ public CloseableDelegateByteSource(@Nonnull ByteSource inner, long size) {
+ this.inner = inner;
mSize = size;
}
@@ -65,13 +64,13 @@
*
* @return the inner byte source
*/
- @NonNull
+ @Nonnull
private synchronized ByteSource get() {
- if (mInner == null) {
+ if (inner == null) {
throw new ByteSourceDisposedException();
}
- return mInner;
+ return inner;
}
/**
@@ -79,11 +78,11 @@
*/
@Override
protected synchronized void innerClose() throws IOException {
- if (mInner == null) {
+ if (inner == null) {
return;
}
- mInner = null;
+ inner = null;
}
/**
@@ -122,12 +121,12 @@
}
@Override
- public long copyTo(@NonNull OutputStream output) throws IOException {
+ public long copyTo(@Nonnull OutputStream output) throws IOException {
return get().copyTo(output);
}
@Override
- public long copyTo(@NonNull ByteSink sink) throws IOException {
+ public long copyTo(@Nonnull ByteSink sink) throws IOException {
return get().copyTo(sink);
}
@@ -137,7 +136,7 @@
}
@Override
- public <T> T read(@NonNull ByteProcessor<T> processor) throws IOException {
+ public <T> T read(@Nonnull ByteProcessor<T> processor) throws IOException {
return get().read(processor);
}
@@ -147,7 +146,7 @@
}
@Override
- public boolean contentEquals(@NonNull ByteSource other) throws IOException {
+ public boolean contentEquals(@Nonnull ByteSource other) throws IOException {
return get().contentEquals(other);
}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtils.java b/src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java
similarity index 85%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtils.java
rename to src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java
index 51885e2..c257d39 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java
@@ -14,17 +14,14 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
-import com.android.annotations.NonNull;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
-import com.google.common.io.ByteSource;
-
import java.io.EOFException;
import java.io.IOException;
-import java.io.OutputStream;
import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
/**
* Utilities to read and write 16 and 32 bit integers with support for little-endian
@@ -46,7 +43,7 @@
* @return the 32-bit value
* @throws IOException failed to read the value
*/
- public static long readUnsigned4Le(@NonNull ByteBuffer bytes) throws IOException {
+ public static long readUnsigned4Le(@Nonnull ByteBuffer bytes) throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
if (bytes.remaining() < 4) {
@@ -72,12 +69,14 @@
* @return the 16-bit value
* @throws IOException failed to read the value
*/
- public static int readUnsigned2Le(@NonNull ByteBuffer bytes) throws IOException {
+ public static int readUnsigned2Le(@Nonnull ByteBuffer bytes) throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
if (bytes.remaining() < 2) {
- throw new EOFException("Not enough data: 2 bytes expected, " + bytes.remaining()
- + " available.");
+ throw new EOFException(
+ "Not enough data: 2 bytes expected, "
+ + bytes.remaining()
+ + " available.");
}
byte b0 = bytes.get();
@@ -96,12 +95,14 @@
* @param value the 32-bit value to convert
* @throws IOException failed to write the value data
*/
- public static void writeUnsigned4Le(@NonNull ByteBuffer output, long value)
+ public static void writeUnsigned4Le(@Nonnull ByteBuffer output, long value)
throws IOException {
Preconditions.checkNotNull(output, "output == null");
Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
- Preconditions.checkArgument(value <= 0x00000000ffffffffL,
- "value (%s) > 0x00000000ffffffffL", value);
+ Preconditions.checkArgument(
+ value <= 0x00000000ffffffffL,
+ "value (%s) > 0x00000000ffffffffL",
+ value);
output.put((byte) (value & 0xff));
output.put((byte) ((value >> 8) & 0xff));
@@ -116,7 +117,7 @@
* @param value the 16-bit value to convert
* @throws IOException failed to write the value data
*/
- public static void writeUnsigned2Le(@NonNull ByteBuffer output, int value)
+ public static void writeUnsigned2Le(@Nonnull ByteBuffer output, int value)
throws IOException {
Preconditions.checkNotNull(output, "output == null");
Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtils.java b/src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java
similarity index 97%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtils.java
rename to src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java
index 43c5793..2b9f365 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
import com.google.common.base.Verify;
@@ -33,6 +33,7 @@
/**
* Packs java time value into an MS-DOS time value.
+ *
* @param time the time value
* @return the MS-DOS packed time
*/
@@ -57,6 +58,7 @@
/**
* Packs the current time value into an MS-DOS time value.
+ *
* @return the MS-DOS packed time
*/
public static int packCurrentTime() {
@@ -65,6 +67,7 @@
/**
* Packs java time value into an MS-DOS date value.
+ *
* @param time the time value
* @return the MS-DOS packed date
*/
@@ -99,6 +102,7 @@
/**
* Packs the current time value into an MS-DOS date value.
+ *
* @return the MS-DOS packed date
*/
public static int packCurrentDate() {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/RandomAccessFileUtils.java b/src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java
similarity index 70%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/RandomAccessFileUtils.java
rename to src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java
index c1ee5b6..3bfb55c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/RandomAccessFileUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java
@@ -14,22 +14,18 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.zip.utils;
import java.io.IOException;
import java.io.RandomAccessFile;
+import javax.annotation.Nonnull;
/**
* Utility class with utility methods for random access files.
*/
-public class RandomAccessFileUtils {
- /**
- * Utility class: no constructor.
- */
- private RandomAccessFileUtils() {
- }
+public final class RandomAccessFileUtils {
+
+ private RandomAccessFileUtils() {}
/**
* Reads from an random access file until the provided array is filled. Data is read from the
@@ -37,9 +33,9 @@
*
* @param raf the file to read data from
* @param data the array that will receive the data
- * @throws IOException
+ * @throws IOException failed to read the data
*/
- public static void fullyRead(@NonNull RandomAccessFile raf, @NonNull byte[] data)
+ public static void fullyRead(@Nonnull RandomAccessFile raf, @Nonnull byte[] data)
throws IOException {
int r;
int p = 0;
@@ -52,8 +48,12 @@
}
if (p < data.length) {
- throw new IOException("Failed to read " + data.length + " bytes from file. Only "
- + p + " bytes could be read.");
+ throw new IOException(
+ "Failed to read "
+ + data.length
+ + " bytes from file. Only "
+ + p
+ + " bytes could be read.");
}
}
}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/FullApkSignExtension.java b/src/main/java/com/android/builder/internal/packaging/sign/FullApkSignExtension.java
deleted file mode 100644
index 8f9dd6a..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/FullApkSignExtension.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.sign.v2.ApkSignerV2;
-import com.android.builder.internal.packaging.sign.v2.ByteArrayDigestSource;
-import com.android.builder.internal.packaging.sign.v2.DigestSource;
-import com.android.builder.internal.packaging.sign.v2.SignatureAlgorithm;
-import com.android.builder.internal.packaging.sign.v2.ZFileDigestSource;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileExtension;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
-import com.google.common.collect.ImmutableList;
-
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.PrivateKey;
-import java.security.SignatureException;
-import java.security.cert.X509Certificate;
-import java.util.List;
-
-/**
- * Extension that adds full APK signing. This extension will:
- * <ul>
- * <li>Generate a new signature if the zip is modified after the extension is added.</li>
- * <li>Generate a new signature even if the zip is not modified after the extension is
- * added, but a valid signature is not found.</li>
- * </ul>
- * <p>
- * The extension computes the signature, if needed, at the {@link ZFileExtension#entriesWritten()}
- * event, after all zip entries have been written, but before the central directory or EOCD have.
- * This allows the extension to set the central directory offset parameter in the zip file
- * using {@link ZFile#setExtraDirectoryOffset(long)} adding enough space in the zip file for the
- * signature block.
- * <p>
- * The signature block is written before the central directory, allowing the zip file to be written
- * in sequential order.
- */
-public class FullApkSignExtension {
-
- /**
- * The zip file this extension is registered with.
- */
- @NonNull
- private final ZFile mFile;
-
- /**
- * Signer certificate.
- */
- @NonNull
- private final X509Certificate mCertificate;
-
- /**
- * Signer private key.
- */
- @NonNull
- private final PrivateKey mPrivateKey;
-
- /**
- * APK Signature Scheme v2 algorithms to use for signing the APK.
- */
- private final List<SignatureAlgorithm> mV2SignatureAlgorithms;
-
- /**
- * {@code true} if the zip needs its signature to be updated.
- */
- private boolean mNeedsSignatureUpdate = true;
-
- /**
- * The extension to register with the {@link ZFile}. {@code null} if not registered.
- */
- @Nullable
- private ZFileExtension mExtension;
-
- /**
- * Creates a new extension. This will not register the extension with the provided
- * {@link ZFile}. Until {@link #register()} is invoked, this extension is not used.
- *
- * @param file the zip file to register the extension with
- * @param minSdkVersion minSdkVersion of the package
- * @param certificate sign certificate
- * @param privateKey the private key to sign the jar
- *
- * @throws InvalidKeyException if the signing key is not suitable for signing this APK.
- */
- public FullApkSignExtension(@NonNull ZFile file,
- int minSdkVersion,
- @NonNull X509Certificate certificate,
- @NonNull PrivateKey privateKey) throws InvalidKeyException {
- mFile = file;
- mCertificate = certificate;
- mPrivateKey = privateKey;
- mV2SignatureAlgorithms =
- ApkSignerV2.getSuggestedSignatureAlgorithms(
- certificate.getPublicKey(), minSdkVersion);
- }
-
- /**
- * Registers the extension with the {@link ZFile} provided in the constructor.
- */
- public void register() {
- Preconditions.checkState(mExtension == null, "register() has already been invoked.");
-
- mExtension = new ZFileExtension() {
- @Nullable
- @Override
- public IOExceptionRunnable beforeUpdate() throws IOException {
- mFile.setExtraDirectoryOffset(0);
- return null;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable added(@NonNull StoredEntry entry,
- @Nullable StoredEntry replaced) {
- onZipChanged();
- return null;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
- onZipChanged();
- return null;
- }
-
- @Override
- public void entriesWritten() throws IOException {
- onEntriesWritten();
- }
- };
-
- mFile.addZFileExtension(mExtension);
- }
-
- /**
- * Invoked when the zip file has been changed.
- */
- private void onZipChanged() {
- mNeedsSignatureUpdate = true;
- }
-
- /**
- * Invoked before the zip file has been updated.
- *
- * @throws IOException failed to perform the update
- */
- private void onEntriesWritten() throws IOException {
- if (!mNeedsSignatureUpdate) {
- return;
- }
- mNeedsSignatureUpdate = false;
-
- byte[] apkSigningBlock = generateApkSigningBlock();
- Verify.verify(apkSigningBlock.length > 0, "apkSigningBlock.length == 0");
- mFile.setExtraDirectoryOffset(apkSigningBlock.length);
- long apkSigningBlockOffset =
- mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset();
- mFile.directWrite(apkSigningBlockOffset, apkSigningBlock);
- }
-
- /**
- * Generates a signature for the APK.
- *
- * @return the signature data block
- * @throws IOException failed to generate a signature
- */
- @NonNull
- private byte[] generateApkSigningBlock() throws IOException {
- byte[] centralDirectoryData = mFile.getCentralDirectoryBytes();
- byte[] eocdData = mFile.getEocdBytes();
-
- ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
- signerConfig.privateKey = mPrivateKey;
- signerConfig.certificates = ImmutableList.of(mCertificate);
- signerConfig.signatureAlgorithms = mV2SignatureAlgorithms;
- DigestSource centralDir = new ByteArrayDigestSource(centralDirectoryData);
- DigestSource eocd = new ByteArrayDigestSource(eocdData);
- DigestSource zipEntries =
- new ZFileDigestSource(
- mFile,
- 0,
- mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset());
- try {
- return ApkSignerV2.generateApkSigningBlock(
- zipEntries,
- centralDir,
- eocd,
- ImmutableList.of(signerConfig));
- } catch (InvalidKeyException | SignatureException e) {
- throw new IOException("Failed to sign APK using APK Signature Scheme v2", e);
- }
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/ManifestGenerationExtension.java b/src/main/java/com/android/builder/internal/packaging/sign/ManifestGenerationExtension.java
deleted file mode 100644
index 8db3d6c..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/ManifestGenerationExtension.java
+++ /dev/null
@@ -1,347 +0,0 @@
-/*
- * Copyright (C) 2015 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.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileExtension;
-import com.android.builder.internal.utils.CachedSupplier;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-import com.android.builder.packaging.ManifestAttributes;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
-import com.google.common.collect.Maps;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.Map;
-import java.util.jar.Attributes;
-import java.util.jar.Manifest;
-
-/**
- * Extension to {@link ZFile} that will generate a manifest. The extension will register
- * automatically with the {@link ZFile}.
- *
- * <p>Creating this extension will ensure a manifest for the zip exists.
- * This extension will generate a manifest if one does not exist and will update an existing
- * manifest, if one does exist. The extension will also provide access to the manifest so that
- * others may update the manifest.
- *
- * <p>Apart from standard manifest elements, this extension does not handle any particular manifest
- * features such as signing or adding custom attributes. It simply generates a plain manifest and
- * provides infrastructure so that other extensions can add data in the manifest.
- *
- * <p>The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()}
- * notification is received, meaning all manifest manipulation is done in-memory.
- */
-public class ManifestGenerationExtension {
-
- /**
- * Name of META-INF directory.
- */
- public static final String META_INF_DIR = "META-INF";
-
- /**
- * Name of the manifest file.
- */
- public static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
-
- /**
- * Who should be reported as the manifest builder.
- */
- @NonNull
- private final String mBuiltBy;
-
- /**
- * Who should be reported as the manifest creator.
- */
- @NonNull
- private final String mCreatedBy;
-
- /**
- * The file this extension is attached to. {@code null} if not yet registered.
- */
- @Nullable
- private ZFile mZFile;
-
- /**
- * The zip file's manifest.
- */
- @NonNull
- private final Manifest mManifest;
-
- /**
- * Byte representation of the manifest. There is no guarantee that two writes of the java's
- * {@code Manifest} object will yield the same byte array (there is no guaranteed order
- * of entries in the manifest).
- *
- * <p>Because we need the byte representation of the manifest to be stable if there are
- * no changes to the manifest, we cannot rely on {@code Manifest} to generate the byte
- * representation every time we need the byte representation.
- *
- * <p>This cache will ensure that we will request one byte generation from the {@code Manifest}
- * and will cache it. All further requests of the manifest's byte representation will
- * receive the same byte array.
- */
- @NonNull
- private CachedSupplier<byte[]> mManifestBytes;
-
- /**
- * Has the current manifest been changed and not yet flushed? If {@link #mDirty} is
- * {@code true}, then {@link #mManifestBytes} should not be valid. This means that
- * marking the manifest as dirty should also invalidate {@link #mManifestBytes}. To avoid
- * breaking the invariant, instead of setting {@link #mDirty}, {@link #markDirty()} should
- * be called.
- */
- private boolean mDirty;
-
- /**
- * The extension to register with the {@link ZFile}. {@code null} if not registered.
- */
- @Nullable
- private ZFileExtension mExtension;
-
- /**
- * Creates a new extension. This will not register the extension with the provided
- * {@link ZFile}. Until {@link #register(ZFile)} is invoked, this extension is not used.
- *
- * @param builtBy who built the manifest?
- * @param createdBy who created the manifest?
- */
- public ManifestGenerationExtension(@NonNull String builtBy, @NonNull String createdBy) {
- mBuiltBy = builtBy;
- mCreatedBy = createdBy;
- mManifest = new Manifest();
- mDirty = false;
- mManifestBytes = new CachedSupplier<>(() -> {
- ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
- try {
- mManifest.write(outBytes);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
-
- return outBytes.toByteArray();
- });
- }
-
- /**
- * Marks the manifest as being dirty, <i>i.e.</i>, its data has changed since it was last
- * read and/or written.
- */
- private void markDirty() {
- mDirty = true;
- mManifestBytes.reset();
- }
-
- /**
- * Registers the extension with the {@link ZFile} provided in the constructor.
- *
- * @param zFile the zip file to add the extension to
- * @throws IOException failed to analyze the zip
- */
- public void register(@NonNull ZFile zFile) throws IOException {
- Preconditions.checkState(mExtension == null, "register() has already been invoked.");
- mZFile = zFile;
-
- rebuildManifest();
-
- mExtension = new ZFileExtension() {
- @Nullable
- @Override
- public IOExceptionRunnable beforeUpdate() {
- return ManifestGenerationExtension.this::updateManifest;
- }
- };
-
- mZFile.addZFileExtension(mExtension);
- }
-
- /**
- * Rebuilds the zip file's manifest, if it needs changes.
- */
- private void rebuildManifest() throws IOException {
- Verify.verifyNotNull(mZFile, "mZFile == null");
-
- StoredEntry manifestEntry = mZFile.get(MANIFEST_NAME);
-
- if (manifestEntry != null) {
- /*
- * Read the manifest entry in the zip file. Make sure we store these byte sequence
- * because writing the manifest may not generate the same byte sequence, which may
- * trigger an unnecessary re-sign of the jar.
- */
- mManifest.clear();
- byte[] manifestBytes = manifestEntry.read();
- mManifest.read(new ByteArrayInputStream(manifestBytes));
- mManifestBytes.precomputed(manifestBytes);
- }
-
- Attributes mainAttributes = mManifest.getMainAttributes();
- String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION);
- if (currentVersion == null) {
- setMainAttribute(
- ManifestAttributes.MANIFEST_VERSION,
- ManifestAttributes.CURRENT_MANIFEST_VERSION);
- } else {
- if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) {
- throw new IOException("Unsupported manifest version: " + currentVersion + ".");
- }
- }
-
- /*
- * We "blindly" override all other main attributes.
- */
- setMainAttribute(ManifestAttributes.BUILT_BY, mBuiltBy);
- setMainAttribute(ManifestAttributes.CREATED_BY, mCreatedBy);
- }
-
- /**
- * Sets the value of a main attribute.
- *
- * @param attribute the attribute
- * @param value the value
- */
- private void setMainAttribute(@NonNull String attribute, @NonNull String value) {
- Attributes mainAttributes = mManifest.getMainAttributes();
- String current = mainAttributes.getValue(attribute);
- if (!value.equals(current)) {
- mainAttributes.putValue(attribute, value);
- markDirty();
- }
- }
-
- /**
- * Updates the manifest in the zip file, if it has been changed.
- *
- * @throws IOException failed to update the manifest
- */
- private void updateManifest() throws IOException {
- Verify.verifyNotNull(mZFile, "mZFile == null");
-
- if (!mDirty) {
- return;
- }
-
- mZFile.add(MANIFEST_NAME, new ByteArrayInputStream(mManifestBytes.get()));
- mDirty = false;
- }
-
- /**
- * Obtains the {@link ZFile} this extension is associated with. This method can only be invoked
- * after {@link #register(ZFile)} has been invoked.
- *
- * @return the {@link ZFile}
- */
- @NonNull
- public ZFile zFile() {
- Preconditions.checkNotNull(mZFile, "mZFile == null");
- return mZFile;
- }
-
- /**
- * Obtains the stored entry in the {@link ZFile} that contains the manifest. This method can
- * only be invoked after {@link #register(ZFile)} has been invoked.
- *
- * @return the entry, {@code null} if none
- */
- @Nullable
- public StoredEntry manifestEntry() {
- Preconditions.checkNotNull(mZFile, "mZFile == null");
- return mZFile.get(MANIFEST_NAME);
- }
-
- /**
- * Obtains an attribute of an entry.
- *
- * @param entryName the name of the entry
- * @param attr the name of the attribute
- * @return the attribute value or {@code null} if the entry does not have any attributes or
- * if it doesn't have the specified attribute
- */
- @Nullable
- public String getAttribute(@NonNull String entryName, @NonNull String attr) {
- Attributes attrs = mManifest.getAttributes(entryName);
- if (attrs == null) {
- return null;
- }
-
- return attrs.getValue(attr);
- }
-
- /**
- * Sets the value of an attribute of an entry. If this entry's attribute already has the given
- * value, this method does nothing.
- *
- * @param entryName the name of the entry
- * @param attr the name of the attribute
- * @param value the attribute value
- */
- public void setAttribute(@NonNull String entryName, @NonNull String attr,
- @NonNull String value) {
- Attributes attrs = mManifest.getAttributes(entryName);
- if (attrs == null) {
- attrs = new Attributes();
- markDirty();
- mManifest.getEntries().put(entryName, attrs);
- }
-
- String current = attrs.getValue(attr);
- if (!value.equals(current)) {
- attrs.putValue(attr, value);
- markDirty();
- }
- }
-
- /**
- * Obtains the current manifest.
- *
- * @return a byte sequence representation of the manifest that is guaranteed not to change if
- * the manifest is not modified
- * @throws IOException failed to compute the manifest's byte representation
- */
- @NonNull
- public byte[] getManifestBytes() throws IOException {
- return mManifestBytes.get();
- }
-
- /**
- * Obtains all entries and all attributes they have in the manifest.
- *
- * @return a map that relates entry names to entry attributes
- */
- @NonNull
- public Map<String, Attributes> allEntries() {
- return Maps.newHashMap(mManifest.getEntries());
- }
-
- /**
- * Removes an entry from the manifest. If no entry exists with the given name, this operation
- * does nothing.
- *
- * @param name the entry's name
- */
- public void removeEntry(@NonNull String name) {
- if (mManifest.getEntries().remove(name) != null) {
- markDirty();
- }
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/SignatureExtension.java b/src/main/java/com/android/builder/internal/packaging/sign/SignatureExtension.java
deleted file mode 100644
index b8d526b..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/SignatureExtension.java
+++ /dev/null
@@ -1,630 +0,0 @@
-/*
- * Copyright (C) 2015 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.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileExtension;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Sets;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Locale;
-import java.util.Set;
-import java.util.jar.Attributes;
-import java.util.jar.Manifest;
-import java.util.stream.Collectors;
-import org.bouncycastle.asn1.ASN1InputStream;
-import org.bouncycastle.asn1.DEROutputStream;
-import org.bouncycastle.cert.jcajce.JcaCertStore;
-import org.bouncycastle.cms.CMSException;
-import org.bouncycastle.cms.CMSProcessableByteArray;
-import org.bouncycastle.cms.CMSSignedData;
-import org.bouncycastle.cms.CMSSignedDataGenerator;
-import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
-
-/**
- * {@link ZFile} extension that signs all files in the APK and generates a signature file and a
- * digital signature of the signature file. The extension registers itself automatically with the
- * {@link ZFile} upon creation.
- * <p>
- * The signature extension will recompute signatures of files already in the zip file but won't
- * update the manifest if these signatures match the ones in the manifest.
- * <p>
- * This extension does 4 main tasks: maintaining the digests of all files in the zip in the manifest
- * file, maintaining the digests of all files in the zip in the signature file, maintaining the
- * digest of the manifest in the signature file and maintaining the digital signature file. For
- * performance, the digests and signatures are only computed when needed.
- * <p>
- * These tasks are done at three different moments: when the extension
- * is created, when files are added to the zip and before the zip is updated.
- * When the extension is created: (Note that the manifest's digest is <em>not</em> checked when
- * the extension is created.)
- * <ul>
- * <li>The signature file is read, if one exists.
- * <li>The signature "administrative" info is read and updated if not up-to-date.
- * <li>The digests for entries in the manifest and signature file that do not correspond to
- * any file in the zip are removed.
- * <li>The digests for all entries in the zip are recomputed and updated in the signature file
- * and in the manifest, if needed.
- * </ul>
- * <p>
- * When files are added or removed:
- * <ul>
- * <li>The signature file and manifest are updated to reflect the changes.
- * <li>If the file was added, its digest is computed.
- * </ul>
- * <p>
- * Before updating the zip file:
- * <ul>
- * <li>If a signature file already exists, checks the digest of the manifest and updates the
- * signature file if needed.
- * <li>Creates the signature file if it did not already exist.
- * <li>Recreates the digital signature of the signature file if the signature file was created
- * or updated.
- * </ul>
- */
-public class SignatureExtension {
-
- /**
- * Base of signature files.
- */
- private static final String SIGNATURE_BASE = ManifestGenerationExtension.META_INF_DIR + "/CERT";
-
- /**
- * Path of the signature file.
- */
- private static final String SIGNATURE_FILE = SIGNATURE_BASE + ".SF";
-
- /**
- * Name of attribute with the signature version.
- */
- private static final String SIGNATURE_VERSION_NAME = "Signature-Version";
-
- /**
- * Version of the signature version.
- */
- private static final String SIGNATURE_VERSION_VALUE = "1.0";
-
- /**
- * Name of attribute with the "created by" attribute.
- */
- private static final String SIGNATURE_CREATED_BY_NAME = "Created-By";
-
- /**
- * Value of the "created by" attribute.
- */
- private static final String SIGNATURE_CREATED_BY_VALUE = "1.0 (Android)";
-
- /**
- * Name of the {@code X-Android-APK-Signer} attribute.
- */
- private static final String SIGNATURE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
-
- /**
- * Value of the {@code X-Android-APK-Signer} attribute when the APK is signed with the v2
- * scheme.
- */
- public static final String SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED = "2";
-
- /**
- * Files to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_FILES = Sets.newHashSet(
- ManifestGenerationExtension.MANIFEST_NAME, SIGNATURE_FILE);
-
- /**
- * Same as {@link #IGNORED_FILES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_FILES_LC = Sets.newHashSet(
- IGNORED_FILES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
-
- /**
- * Prefix of files in META-INF to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_PREFIXES = Sets.newHashSet(
- "SIG-");
-
- /**
- * Same as {@link #IGNORED_PREFIXES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_PREFIXES_LC = Sets.newHashSet(
- IGNORED_PREFIXES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
- /**
- * Suffixes of files in META-INF to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_SUFFIXES = Sets.newHashSet(
- ".SF", ".DSA", ".RSA", ".EC");
-
- /**
- * Same as {@link #IGNORED_SUFFIXES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_SUFFIXES_LC = Sets.newHashSet(
- IGNORED_SUFFIXES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
- /**
- * Extension maintaining the manifest.
- */
- @NonNull
- private final ManifestGenerationExtension mManifestExtension;
-
- /**
- * Message digest to use.
- */
- @NonNull
- private final MessageDigest mMessageDigest;
-
- /**
- * Signature file. Note that the signature file is itself a manifest file but it is
- * a different one from the "standard" MANIFEST.MF.
- */
- @NonNull
- private final Manifest mSignatureFile;
-
- /**
- * Has the signature manifest been changed?
- */
- private boolean mDirty;
-
- /**
- * Signer certificate.
- */
- @NonNull
- private final X509Certificate mCertificate;
-
- /**
- * The private key used to sign the jar.
- */
- @NonNull
- private final PrivateKey mPrivateKey;
-
- /**
- * Algorithm with which .SF file is signed.
- */
- @NonNull
- private final SignatureAlgorithm mSignatureAlgorithm;
-
- /**
- * Digest algorithm to use for MANIFEST.MF and contents of APK entries.
- */
- @NonNull
- private final DigestAlgorithm mDigestAlgorithm;
-
- /**
- * Value to output for the {@code X-Android-APK-Signed} header or {@code null} if the header
- * should not be output.
- */
- @Nullable
- private final String mApkSignedHeaderValue;
-
- /**
- * The extension registered with the {@link ZFile}. {@code null} if not registered.
- */
- @Nullable
- private ZFileExtension mExtension;
-
- /**
- * Creates a new signature extension.
- *
- * @param manifestExtension the extension maintaining the manifest
- * @param minSdkVersion minSdkVersion of the package
- * @param certificate sign certificate
- * @param privateKey the private key to sign the jar
- * @param apkSignedHeaderValue value of the {@code X-Android-APK-Signed} header to output into
- * the {@code .SF} file or {@code null} if the header should not be output.
- *
- * @throws NoSuchAlgorithmException failed to obtain the digest algorithm.
- */
- public SignatureExtension(@NonNull ManifestGenerationExtension manifestExtension,
- int minSdkVersion, @NonNull X509Certificate certificate, @NonNull PrivateKey privateKey,
- @Nullable String apkSignedHeaderValue)
- throws NoSuchAlgorithmException {
- mManifestExtension = manifestExtension;
- mSignatureFile = new Manifest();
- mDirty = false;
- mCertificate = certificate;
- mPrivateKey = privateKey;
- mApkSignedHeaderValue = apkSignedHeaderValue;
-
- mSignatureAlgorithm =
- SignatureAlgorithm.fromKeyAlgorithm(privateKey.getAlgorithm(), minSdkVersion);
- mDigestAlgorithm = DigestAlgorithm.findBest(minSdkVersion, mSignatureAlgorithm);
- mMessageDigest = MessageDigest.getInstance(mDigestAlgorithm.messageDigestName);
- }
-
- /**
- * Registers the extension with the {@link ZFile} provided in the
- * {@link ManifestGenerationExtension}. Note that the {@code ManifestGenerationExtension}
- * needs to be registered as a precondition for this method.
- *
- * @throws IOException failed to analyze the zip
- */
- public void register() throws IOException {
- Preconditions.checkState(mExtension == null, "register() already invoked");
-
- mExtension = new ZFileExtension() {
- @Nullable
- @Override
- public IOExceptionRunnable beforeUpdate() {
- return SignatureExtension.this::updateSignatureIfNeeded;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable added(@NonNull final StoredEntry entry,
- @Nullable final StoredEntry replaced) {
- if (replaced != null) {
- Preconditions.checkArgument(entry.getCentralDirectoryHeader().getName().equals(
- replaced.getCentralDirectoryHeader().getName()));
- }
-
- if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
- return null;
- }
-
- return () -> {
- if (replaced != null) {
- SignatureExtension.this.removed(replaced);
- }
-
- SignatureExtension.this.added(entry);
- };
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable removed(@NonNull final StoredEntry entry) {
- if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
- return null;
- }
-
- return () -> SignatureExtension.this.removed(entry);
- }
- };
-
- mManifestExtension.zFile().addZFileExtension(mExtension);
- readSignatureFile();
- }
-
- /**
- * Reads the signature file (if any) on the zip file.
- * <p>
- * When this method terminates, we have the following guarantees:
- * <ul>
- * <li>An internal signature manifest exists.</li>
- * <li>All entries in the in-memory signature file exist in the zip file.</li>
- * <li>All entries in the zip file (with the exception of the signature-related files,
- * as specified by https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html)
- * exist in the in-memory signature file.</li>
- * <li>All entries in the in-memory signature file have digests that match their
- * contents in the zip.</li>
- * <li>All entries in the in-memory signature manifest exist also in the manifest file
- * and the digests are the same.</li>
- * <li>The main attributes of the in-memory signature manifest are valid. The manifest's
- * digest has not been verified and may not even exist.</li>
- * <li>If the internal in-memory signature manifest differs in any way from the one
- * written in the file, {@link #mDirty} will be set to {@code true}. Otherwise,
- * {@link #mDirty} will be set to {@code false}.</li>
- * </ul>
- *
- * @throws IOException failed to read the signature file
- */
- private void readSignatureFile() throws IOException {
- boolean needsNewSignature = false;
-
- StoredEntry signatureEntry = mManifestExtension.zFile().get(SIGNATURE_FILE);
- if (signatureEntry != null) {
- byte[] signatureData = signatureEntry.read();
- mSignatureFile.read(new ByteArrayInputStream(signatureData));
-
- Attributes mainAttrs = mSignatureFile.getMainAttributes();
- String versionName = mainAttrs.getValue(SIGNATURE_VERSION_NAME);
- String createdBy = mainAttrs.getValue(SIGNATURE_CREATED_BY_NAME);
- String apkSigned = mainAttrs.getValue(SIGNATURE_ANDROID_APK_SIGNED_NAME);
-
- if (!SIGNATURE_VERSION_VALUE.equals(versionName)
- || !SIGNATURE_CREATED_BY_VALUE.equals(createdBy)
- || mainAttrs.getValue(mDigestAlgorithm.manifestAttributeName) == null
- || !Objects.equal(mApkSignedHeaderValue, apkSigned)) {
- needsNewSignature = true;
- }
- } else {
- needsNewSignature = true;
- }
-
- if (needsNewSignature) {
- Attributes mainAttrs = mSignatureFile.getMainAttributes();
-
- mainAttrs.putValue(SIGNATURE_CREATED_BY_NAME, SIGNATURE_CREATED_BY_VALUE);
- mainAttrs.putValue(SIGNATURE_VERSION_NAME, SIGNATURE_VERSION_VALUE);
- if (mApkSignedHeaderValue != null) {
- mainAttrs.putValue(SIGNATURE_ANDROID_APK_SIGNED_NAME, mApkSignedHeaderValue);
- } else {
- mainAttrs.remove(SIGNATURE_ANDROID_APK_SIGNED_NAME);
- }
-
- mDirty = true;
- }
-
- /*
- * At this point we have a valid in-memory signature file with a valid header. mDirty
- * states whether this is the same as the file-based signature file.
- *
- * Now, check we have the same files in the zip as in the signature file and that all
- * digests match. While we do this, make sure the manifest is also up-do-date.
- *
- * We ignore all signature-related files that exist in the zip that are signature-related.
- * This are defined in the jar format specification.
- */
- Set<StoredEntry> allEntries =
- mManifestExtension.zFile().entries().stream()
- .filter(se -> !isIgnoredFile(se.getCentralDirectoryHeader().getName()))
- .collect(Collectors.toSet());
-
- Set<String> sigEntriesToRemove = Sets.newHashSet(mSignatureFile.getEntries().keySet());
- Set<String> manEntriesToRemove = Sets.newHashSet(mManifestExtension.allEntries().keySet());
- for (StoredEntry se : allEntries) {
- /*
- * Update the entry's digest, if needed.
- */
- setDigestForEntry(se);
-
- /*
- * This entry exists in the file, so remove it from the list of entries to remove
- * from the manifest and signature file.
- */
- sigEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
- manEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
- }
-
- for (String toRemoveInSignature : sigEntriesToRemove) {
- mSignatureFile.getEntries().remove(toRemoveInSignature);
- mDirty = true;
- }
-
- for (String toRemoveInManifest : manEntriesToRemove) {
- mManifestExtension.removeEntry(toRemoveInManifest);
- }
- }
-
- /**
- * This method will recompute the manifest's digest and will update the signature file if the
- * manifest has changed. It then writes the signature file, if dirty for any reason (including
- * from recomputing the manifest's digest).
- *
- * @throws IOException failed to read / write zip data
- */
- private void updateSignatureIfNeeded() throws IOException {
- byte[] manifestData = mManifestExtension.getManifestBytes();
- byte[] manifestDataDigest = mMessageDigest.digest(manifestData);
-
-
- String manifestDataDigestTxt = Base64.getEncoder().encodeToString(manifestDataDigest);
-
- if (!manifestDataDigestTxt.equals(mSignatureFile.getMainAttributes().getValue(
- mDigestAlgorithm.manifestAttributeName))) {
- mSignatureFile
- .getMainAttributes()
- .putValue(mDigestAlgorithm.manifestAttributeName, manifestDataDigestTxt);
- mDirty = true;
- }
-
- if (!mDirty) {
- return;
- }
-
- ByteArrayOutputStream signatureBytes = new ByteArrayOutputStream();
- mSignatureFile.write(signatureBytes);
-
- mManifestExtension.zFile().add(
- SIGNATURE_FILE,
- new ByteArrayInputStream(signatureBytes.toByteArray()));
-
- String digitalSignatureFile = SIGNATURE_BASE + "." + mPrivateKey.getAlgorithm();
- try {
- mManifestExtension.zFile().add(
- digitalSignatureFile,
- new ByteArrayInputStream(computePkcs7Signature(signatureBytes.toByteArray())));
- } catch (CertificateEncodingException | OperatorCreationException | CMSException e) {
- throw new IOException("Failed to digitally sign signature file.", e);
- }
-
- mDirty = false;
- }
-
- /**
- * A new file has been added.
- *
- * @param entry the entry added
- * @throws IOException failed to add the entry to the signature file (or failed to compute the
- * entry's signature)
- */
- private void added(@NonNull StoredEntry entry) throws IOException {
- setDigestForEntry(entry);
- }
-
- /**
- * Adds / updates the signature for an entry. If this entry has no signature, or its digest
- * doesn't match the one in the signature file (or manifest), it will be updated.
- *
- * @param entry the entry
- * @throws IOException failed to compute the entry's digest
- */
- private void setDigestForEntry(@NonNull StoredEntry entry) throws IOException {
- String entryName = entry.getCentralDirectoryHeader().getName();
- byte[] entryDigestArray = mMessageDigest.digest(entry.read());
- String entryDigest = Base64.getEncoder().encodeToString(entryDigestArray);
-
- Attributes signatureAttributes = mSignatureFile.getEntries().get(entryName);
- if (signatureAttributes == null) {
- signatureAttributes = new Attributes();
- mSignatureFile.getEntries().put(entryName, signatureAttributes);
- mDirty = true;
- }
-
- if (!entryDigest.equals(signatureAttributes.getValue(
- mDigestAlgorithm.entryAttributeName))) {
- signatureAttributes.putValue(mDigestAlgorithm.entryAttributeName, entryDigest);
- mDirty = true;
- }
-
- /*
- * setAttribute will not mark the manifest as changed if the attribute is already there
- * and with the same value.
- */
- mManifestExtension.setAttribute(entryName, mDigestAlgorithm.entryAttributeName,
- entryDigest);
- }
-
- /**
- * File has been removed.
- *
- * @param entry the entry removed
- */
- private void removed(@NonNull StoredEntry entry) {
- mSignatureFile.getEntries().remove(entry.getCentralDirectoryHeader().getName());
- mManifestExtension.removeEntry(entry.getCentralDirectoryHeader().getName());
- mDirty = true;
- }
-
- /**
- * Checks if a file should be ignored when signing.
- *
- * @param name the file name
- * @return should it be ignored
- */
- public static boolean isIgnoredFile(@NonNull String name) {
- String metaInfPfx = ManifestGenerationExtension.META_INF_DIR + "/";
- boolean inMetaInf = name.startsWith(metaInfPfx)
- && !name.substring(metaInfPfx.length()).contains("/");
-
- /*
- * Only files in META-INF can be ignored. Files in sub-directories of META-INF are not
- * ignored.
- */
- if (!inMetaInf) {
- return false;
- }
-
- String nameLc = name.toLowerCase(Locale.US);
-
- /*
- * All files with names that match (case insensitive) the ignored list are ignored.
- */
- if (IGNORED_FILES_LC.contains(nameLc)) {
- return true;
- }
-
- for (String pfx : IGNORED_PREFIXES_LC) {
- if (nameLc.startsWith(pfx)) {
- return true;
- }
- }
-
- for (String sfx : IGNORED_SUFFIXES_LC) {
- if (nameLc.endsWith(sfx)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Computes the digital signature of an array of data.
- *
- * @param data the data
- * @return the digital signature
- * @throws IOException failed to read/write signature data
- * @throws CertificateEncodingException failed to sign the data
- * @throws OperatorCreationException failed to sign the data
- * @throws CMSException failed to sign the data
- */
- private byte[] computePkcs7Signature(@NonNull byte[] data) throws IOException,
- CertificateEncodingException, OperatorCreationException, CMSException {
- CMSProcessableByteArray cmsData = new CMSProcessableByteArray(data);
-
- ArrayList<X509Certificate> certList = new ArrayList<>();
- certList.add(mCertificate);
- JcaCertStore certs = new JcaCertStore(certList);
-
- CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
- String signatureAlgName = mSignatureAlgorithm.signatureAlgorithmName(mDigestAlgorithm);
- ContentSigner shaSigner =
- new JcaContentSignerBuilder(signatureAlgName).build(mPrivateKey);
- gen.addSignerInfoGenerator(
- new JcaSignerInfoGeneratorBuilder(
- new JcaDigestCalculatorProviderBuilder()
- .build())
- .setDirectSignature(true)
- .build(shaSigner, mCertificate));
- gen.addCertificates(certs);
- CMSSignedData sigData = gen.generate(cmsData, false);
-
- ByteArrayOutputStream outputBytes = new ByteArrayOutputStream();
-
- /*
- * DEROutputStream is not closeable! OMG!
- */
- DEROutputStream dos = null;
- try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
- dos = new DEROutputStream(outputBytes);
- dos.writeObject(asn1.readObject());
-
- DEROutputStream toClose = dos;
- dos = null;
- toClose.close();
- } catch (IOException e) {
- if (dos != null) {
- try {
- dos.close();
- } catch (IOException ee) {
- e.addSuppressed(ee);
- }
- }
- }
-
- return outputBytes.toByteArray();
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ApkSignerV2.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ApkSignerV2.java
deleted file mode 100644
index 66b1518..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ApkSignerV2.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.sign.v2;
-
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.ApkZLibPair;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.security.DigestException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.KeyFactory;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.security.interfaces.ECKey;
-import java.security.interfaces.RSAKey;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * APK Signature Scheme v2 signer.
- *
- * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
- * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
- * uncompressed contents of ZIP entries.
- *
- * <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
- */
-public abstract class ApkSignerV2 {
- /*
- * The two main goals of APK Signature Scheme v2 are:
- * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
- * cover every byte of the APK being signed.
- * 2. Enable much faster signature and integrity verification. This is achieved by requiring
- * only a minimal amount of APK parsing before the signature is verified, thus completely
- * bypassing ZIP entry decompression and by making integrity verification parallelizable by
- * employing a hash tree.
- *
- * The generated signature block is wrapped into an APK Signing Block and inserted into the
- * original APK immediately before the start of ZIP Central Directory. This is to ensure that
- * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
- * extensibility. For example, a future signature scheme could insert its signatures there as
- * well. The contract of the APK Signing Block is that all contents outside of the block must be
- * protected by signatures inside the block.
- */
-
- private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
-
- private static final byte[] APK_SIGNING_BLOCK_MAGIC =
- new byte[] {
- 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
- 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
- };
- private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
- private ApkSignerV2() {}
-
- /**
- * Signer configuration.
- */
- public static final class SignerConfig {
- /**
- * Private key.
- */
- @NonNull
- public PrivateKey privateKey;
-
- /**
- * Certificates, with the first certificate containing the public key corresponding to
- * {@link #privateKey}.
- */
- @NonNull
- public List<X509Certificate> certificates;
-
- /**
- * List of signature algorithms with which to sign. At least one algorithm must be
- * provided.
- */
- @NonNull
- public List<SignatureAlgorithm> signatureAlgorithms;
- }
-
- /**
- * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
- * provided key.
- *
- * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
- * AndroidManifest.xml minSdkVersion attribute)
- *
- * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
- * APK Signature Scheme v2
- */
- public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
- @NonNull PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
- String keyAlgorithm = signingKey.getAlgorithm();
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
- // deterministic signatures which make life easier for OTA updates (fewer files
- // changed when deterministic signature schemes are used).
-
- // Pick a digest which is no weaker than the key.
- int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
- if (modulusLengthBits <= 3072) {
- // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
- return ImmutableList.of(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
- } else {
- // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
- // digest being the weak link. SHA-512 is the next strongest supported digest.
- return ImmutableList.of(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
- }
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- // DSA is supported only with SHA-256.
- return ImmutableList.of(SignatureAlgorithm.DSA_WITH_SHA256);
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- // Pick a digest which is no weaker than the key.
- int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
- if (keySizeBits <= 256) {
- // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
- return ImmutableList.of(SignatureAlgorithm.ECDSA_WITH_SHA256);
- } else {
- // Keys longer than 256 bit need to be paired with a stronger digest to avoid the
- // digest being the weak link. SHA-512 is the next strongest supported digest.
- return ImmutableList.of(SignatureAlgorithm.ECDSA_WITH_SHA512);
- }
- } else {
- throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
- }
- }
-
- /**
- * Signs the provided APK using APK Signature Scheme v2 and returns the APK Signing Block
- * containing the signature.
- *
- * <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
- * of META-INF/*.SF files of APK being signed must contain the
- * {@code X-Android-APK-Signed: 2} attribute.
- *
- * @param signerConfigs signer configurations, one for each signer. At least one configuration
- * must be provided.
- *
- * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
- * cannot be used in general
- * @throws SignatureException if an error occurs when computing digests of generating
- * signatures
- */
- @NonNull
- public static byte[] generateApkSigningBlock(
- @NonNull DigestSource beforeCentralDir,
- @NonNull DigestSource centralDir,
- @NonNull DigestSource eocd,
- @NonNull List<SignerConfig> signerConfigs)
- throws InvalidKeyException, SignatureException {
- if (signerConfigs.isEmpty()) {
- throw new IllegalArgumentException(
- "No signer configs provided. At least one is required");
- }
-
- // Figure out which digest(s) to use for APK contents.
- Set<ContentDigestAlgorithm> contentDigestAlgorithms = Sets.newHashSetWithExpectedSize(1);
- for (SignerConfig signerConfig : signerConfigs) {
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
- }
- }
-
- // Compute digests of APK contents.
- Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
- try {
- contentDigests =
- computeContentDigests(
- contentDigestAlgorithms,
- new DigestSource[] {beforeCentralDir, centralDir, eocd});
- } catch (DigestException e) {
- throw new SignatureException("Failed to compute digests of APK", e);
- }
-
- // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
- return generateApkSigningBlock(signerConfigs, contentDigests);
- }
-
- @NonNull
- private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
- @NonNull Set<ContentDigestAlgorithm> digestAlgorithms,
- @NonNull DigestSource[] contents) throws DigestException {
- // For each digest algorithm the result is computed as follows:
- // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
- // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
- // No chunks are produced for empty (zero length) segments.
- // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
- // length in bytes (uint32 little-endian) and the chunk's contents.
- // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
- // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
- // segments in-order.
-
- long chunkCountLong = 0;
- for (DigestSource input : contents) {
- chunkCountLong += getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
- }
- if (chunkCountLong >= Integer.MAX_VALUE) {
- throw new DigestException("Input too long: " + chunkCountLong + " chunks");
- }
- int chunkCount = (int) chunkCountLong;
-
- ContentDigestAlgorithm[] digestAlgorithmsArray =
- digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]);
- MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length];
- byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][];
- int[] digestOutputSizes = new int[digestAlgorithmsArray.length];
- for (int i = 0; i < digestAlgorithmsArray.length; i++) {
- ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
- int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes();
- digestOutputSizes[i] = digestOutputSizeBytes;
- byte[] concatenationOfChunkCountAndChunkDigests =
- new byte[5 + chunkCount * digestOutputSizeBytes];
- concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
- setUnsignedInt32LittleEndian(
- chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
- digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
- String jcaAlgorithmName = digestAlgorithm.getJcaMessageDigestAlgorithmName();
- try {
- mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(jcaAlgorithmName + " MessageDigest not supported", e);
- }
- }
-
- byte[] chunkContentPrefix = new byte[5];
- chunkContentPrefix[0] = (byte) 0xa5;
- int chunkIndex = 0;
- // Optimization opportunity: digests of chunks can be computed in parallel. However,
- // determining the number of computations to be performed in parallel is non-trivial. This
- // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched
- // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU
- // cores, load on the system from other threads of execution and other processes, size of
- // input.
- // For now, we compute these digests sequentially and thus have the luxury of improving
- // performance by writing the digest of each chunk into a pre-allocated buffer at exactly
- // the right position. This avoids unnecessary allocations, copying, and enables the final
- // digest to be more efficient because it's presented with all of its input in one go.
- for (DigestSource input : contents) {
- long inputOffset = 0;
- long inputRemaining = input.size();
- while (inputRemaining > 0) {
- int chunkSize =
- (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
- setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
- for (int i = 0; i < mds.length; i++) {
- mds[i].update(chunkContentPrefix);
- }
- try {
- input.feedDigests(inputOffset, chunkSize, mds);
- } catch (IOException e) {
- throw new DigestException("Failed to digest chunk #" + chunkIndex, e);
- }
- for (int i = 0; i < digestAlgorithmsArray.length; i++) {
- MessageDigest md = mds[i];
- byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
- int expectedDigestSizeBytes = digestOutputSizes[i];
- int actualDigestSizeBytes =
- md.digest(
- concatenationOfChunkCountAndChunkDigests,
- 5 + chunkIndex * expectedDigestSizeBytes,
- expectedDigestSizeBytes);
- if (actualDigestSizeBytes != expectedDigestSizeBytes) {
- throw new RuntimeException(
- "Unexpected output size of " + md.getAlgorithm()
- + " digest: " + actualDigestSizeBytes);
- }
- }
- inputOffset += chunkSize;
- inputRemaining -= chunkSize;
- chunkIndex++;
- }
- }
-
- Map<ContentDigestAlgorithm, byte[]> result =
- Maps.newHashMapWithExpectedSize(digestAlgorithmsArray.length);
- for (int i = 0; i < digestAlgorithmsArray.length; i++) {
- ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
- byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
- MessageDigest md = mds[i];
- byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests);
- result.put(digestAlgorithm, digest);
- }
- return result;
- }
-
- private static final long getChunkCount(long inputSize, int chunkSize) {
- return (inputSize + chunkSize - 1) / chunkSize;
- }
-
- private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
- result[offset] = (byte) (value & 0xff);
- result[offset + 1] = (byte) ((value >> 8) & 0xff);
- result[offset + 2] = (byte) ((value >> 16) & 0xff);
- result[offset + 3] = (byte) ((value >> 24) & 0xff);
- }
-
- private static byte[] generateApkSigningBlock(
- List<SignerConfig> signerConfigs,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws InvalidKeyException, SignatureException {
- byte[] apkSignatureSchemeV2Block =
- generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
- return generateApkSigningBlock(apkSignatureSchemeV2Block);
- }
-
- private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
- // FORMAT:
- // uint64: size (excluding this field)
- // repeated ID-value pairs:
- // uint64: size (excluding this field)
- // uint32: ID
- // (size - 4) bytes: value
- // uint64: size (same as the one above)
- // uint128: magic
-
- int resultSize =
- 8 // size
- + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
- + 8 // size
- + 16 // magic
- ;
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- long blockSizeFieldValue = resultSize - 8;
- result.putLong(blockSizeFieldValue);
-
- long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
- result.putLong(pairSizeFieldValue);
- result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
- result.put(apkSignatureSchemeV2Block);
-
- result.putLong(blockSizeFieldValue);
- result.put(APK_SIGNING_BLOCK_MAGIC);
-
- return result.array();
- }
-
- private static byte[] generateApkSignatureSchemeV2Block(
- List<SignerConfig> signerConfigs,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws InvalidKeyException, SignatureException {
- // FORMAT:
- // * length-prefixed sequence of length-prefixed signer blocks.
-
- List<byte[]> signerBlocks = Lists.newArrayListWithExpectedSize(signerConfigs.size());
- int signerNumber = 0;
- for (SignerConfig signerConfig : signerConfigs) {
- signerNumber++;
- byte[] signerBlock;
- try {
- signerBlock = generateSignerBlock(signerConfig, contentDigests);
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
- } catch (SignatureException e) {
- throw new SignatureException("Signer #" + signerNumber + " failed", e);
- }
- signerBlocks.add(signerBlock);
- }
-
- return encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
- });
- }
-
- private static byte[] generateSignerBlock(
- SignerConfig signerConfig,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws InvalidKeyException, SignatureException {
- if (signerConfig.certificates.isEmpty()) {
- throw new SignatureException("No certificates configured for signer");
- }
- PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
-
- byte[] encodedPublicKey = encodePublicKey(publicKey);
-
- V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
- try {
- signedData.certificates = encodeCertificates(signerConfig.certificates);
- } catch (CertificateEncodingException e) {
- throw new SignatureException("Failed to encode certificates", e);
- }
-
- List<ApkZLibPair<Integer, byte[]>> digests =
- Lists.newArrayListWithExpectedSize(signerConfig.signatureAlgorithms.size());
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- ContentDigestAlgorithm contentDigestAlgorithm =
- signatureAlgorithm.getContentDigestAlgorithm();
- byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
- if (contentDigest == null) {
- throw new RuntimeException(
- contentDigestAlgorithm + " content digest for " + signatureAlgorithm
- + " not computed");
- }
- digests.add(new ApkZLibPair<>(signatureAlgorithm.getId(), contentDigest));
- }
- signedData.digests = digests;
-
- V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
- // FORMAT:
- // * length-prefixed sequence of length-prefixed digests:
- // * uint32: signature algorithm ID
- // * length-prefixed bytes: digest of contents
- // * length-prefixed sequence of certificates:
- // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
- // * length-prefixed sequence of length-prefixed additional attributes:
- // * uint32: ID
- // * (length - 4) bytes: value
- signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
- encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
- // additional attributes
- new byte[0],
- });
- signer.publicKey = encodedPublicKey;
- signer.signatures =
- Lists.newArrayListWithExpectedSize(signerConfig.signatureAlgorithms.size());
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
- signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
- String jcaSignatureAlgorithm = sigAlgAndParams.v1;
- AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.v2;
- byte[] signatureBytes;
- try {
- Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
- signature.initSign(signerConfig.privateKey);
- if (jcaSignatureAlgorithmParams != null) {
- signature.setParameter(jcaSignatureAlgorithmParams);
- }
- signature.update(signer.signedData);
- signatureBytes = signature.sign();
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
- } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
- | SignatureException e) {
- throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
- }
-
- try {
- Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
- signature.initVerify(publicKey);
- if (jcaSignatureAlgorithmParams != null) {
- signature.setParameter(jcaSignatureAlgorithmParams);
- }
- signature.update(signer.signedData);
- if (!signature.verify(signatureBytes)) {
- throw new SignatureException("Signature did not verify");
- }
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
- + " signature using public key from certificate", e);
- } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
- | SignatureException e) {
- throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
- + " signature using public key from certificate", e);
- }
-
- signer.signatures.add(new ApkZLibPair<>(signatureAlgorithm.getId(), signatureBytes));
- }
-
- // FORMAT:
- // * length-prefixed signed data
- // * length-prefixed sequence of length-prefixed signatures:
- // * uint32: signature algorithm ID
- // * length-prefixed bytes: signature of signed data
- // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
- return encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- signer.signedData,
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
- signer.signatures),
- signer.publicKey,
- });
- }
-
- private static final class V2SignatureSchemeBlock {
- private static final class Signer {
- public byte[] signedData;
- public List<ApkZLibPair<Integer, byte[]>> signatures;
- public byte[] publicKey;
- }
-
- private static final class SignedData {
- public List<ApkZLibPair<Integer, byte[]>> digests;
- public List<byte[]> certificates;
- }
- }
-
- private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
- byte[] encodedPublicKey = null;
- if ("X.509".equals(publicKey.getFormat())) {
- encodedPublicKey = publicKey.getEncoded();
- }
- if (encodedPublicKey == null) {
- try {
- encodedPublicKey =
- KeyFactory.getInstance(publicKey.getAlgorithm())
- .getKeySpec(publicKey, X509EncodedKeySpec.class)
- .getEncoded();
- } catch (NoSuchAlgorithmException e) {
- throw new InvalidKeyException(
- "Failed to obtain X.509 encoded form of public key " + publicKey
- + " of class " + publicKey.getClass().getName(),
- e);
- } catch (InvalidKeySpecException e) {
- throw new InvalidKeyException(
- "Failed to obtain X.509 encoded form of public key " + publicKey
- + " of class " + publicKey.getClass().getName(),
- e);
- }
- }
- if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
- throw new InvalidKeyException(
- "Failed to obtain X.509 encoded form of public key " + publicKey
- + " of class " + publicKey.getClass().getName());
- }
- return encodedPublicKey;
- }
-
- private static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
- throws CertificateEncodingException {
- List<byte[]> result = Lists.newArrayListWithExpectedSize(certificates.size());
- for (X509Certificate certificate : certificates) {
- result.add(certificate.getEncoded());
- }
- return result;
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
- return encodeAsSequenceOfLengthPrefixedElements(
- sequence.toArray(new byte[sequence.size()][]));
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
- int payloadSize = 0;
- for (byte[] element : sequence) {
- payloadSize += 4 + element.length;
- }
- ByteBuffer result = ByteBuffer.allocate(payloadSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- for (byte[] element : sequence) {
- result.putInt(element.length);
- result.put(element);
- }
- return result.array();
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
- List<ApkZLibPair<Integer, byte[]>> sequence) {
- int resultSize = 0;
- for (ApkZLibPair<Integer, byte[]> element : sequence) {
- resultSize += 12 + element.v2.length;
- }
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- for (ApkZLibPair<Integer, byte[]> element : sequence) {
- byte[] second = element.v2;
- result.putInt(8 + second.length);
- result.putInt(element.v1);
- result.putInt(second.length);
- result.put(second);
- }
- return result.array();
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ByteArrayDigestSource.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ByteArrayDigestSource.java
deleted file mode 100644
index 8aafa95..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ByteArrayDigestSource.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.sign.v2;
-
-import java.security.MessageDigest;
-
-import com.android.annotations.NonNull;
-import com.google.common.base.Preconditions;
-
-/**
- * {@code byte[]} which is fed into {@link MessageDigest} instances.
- */
-public class ByteArrayDigestSource implements DigestSource {
- private final byte[] mBuf;
-
- /**
- * Constructs a new {@code ByteArrayDigestSource} instance which obtains its data from the
- * provided byte array. Changes to the byte array's contents are reflected visible in this
- * source.
- */
- public ByteArrayDigestSource(@NonNull byte[] buf) {
- mBuf = buf;
- }
-
- @Override
- public long size() {
- return mBuf.length;
- }
-
- @Override
- public void feedDigests(long offset, int size, @NonNull MessageDigest[] digests) {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- Preconditions.checkArgument(offset <= mBuf.length, "offset too large: %s", offset);
- int offsetInBuf = (int) offset;
- Preconditions.checkPositionIndex(offsetInBuf, mBuf.length, "offset out of range");
- int availableSize = mBuf.length - offsetInBuf;
- Preconditions.checkArgument(
- size <= availableSize,
- "offset (%s) + size (%s) > array length (%s)", offset, size, mBuf.length);
-
- for (MessageDigest md : digests) {
- md.update(mBuf, offsetInBuf, size);
- }
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ContentDigestAlgorithm.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ContentDigestAlgorithm.java
deleted file mode 100644
index d17b3b2..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ContentDigestAlgorithm.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.sign.v2;
-
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
-enum ContentDigestAlgorithm {
- /** SHA2-256 over 1 MB chunks. */
- CHUNKED_SHA256("SHA-256", 256 / 8),
-
- /** SHA2-512 over 1 MB chunks. */
- CHUNKED_SHA512("SHA-512", 512 / 8);
-
- private final String mJcaMessageDigestAlgorithmName;
- private final int mChunkDigestOutputSizeBytes;
-
- private ContentDigestAlgorithm(
- String jcaMessageDigestAlgorithmName, int chunkDigestOutputSizeBytes) {
- mJcaMessageDigestAlgorithmName = jcaMessageDigestAlgorithmName;
- mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
- }
-
- /**
- * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
- * chunks by this content digest algorithm.
- */
- String getJcaMessageDigestAlgorithmName() {
- return mJcaMessageDigestAlgorithmName;
- }
-
- /**
- * Returns the size (in bytes) of the digest of a chunk of content.
- */
- int getChunkDigestOutputSizeBytes() {
- return mChunkDigestOutputSizeBytes;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/DigestSource.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/DigestSource.java
deleted file mode 100644
index b2afd2f..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/DigestSource.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.sign.v2;
-
-import java.io.IOException;
-import java.security.MessageDigest;
-
-import com.android.annotations.NonNull;
-
-/**
- * Source of data which is fed into {@link MessageDigest} instances.
- *
- * <p>This abstraction serves two purposes:
- * <ul>
- * <li>Transparent digesting of different types of sources, such as {@code byte[]},
- * {@code ZFile}, {@link java.nio.ByteBuffer} and/or memory-mapped file.</li>
- * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
- * would have worked as the unifying abstraction.</li>
- * </ul>
- */
-public interface DigestSource {
- /**
- * Returns the amount of data (in bytes) contained in this data source.
- */
- long size();
-
- /**
- * Feeds the specified chunk from this data source into each of the provided
- * {@link MessageDigest} instances. Each {@code MessageDigest} instance receives the specified
- * chunk of data in full.
- *
- * @param offset index (in bytes) at which the chunk starts relative to the start of this data
- * source.
- * @param size size (in bytes) of the chunk.
- */
- void feedDigests(long offset, int size, @NonNull MessageDigest[] digests) throws IOException;
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/SignatureAlgorithm.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/SignatureAlgorithm.java
deleted file mode 100644
index bcd6652..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/SignatureAlgorithm.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.sign.v2;
-
-import com.android.builder.internal.utils.ApkZLibPair;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.MGF1ParameterSpec;
-import java.security.spec.PSSParameterSpec;
-
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
-public enum SignatureAlgorithm {
- /**
- * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
- * digested using SHA2-256 in 1 MB chunks.
- */
- RSA_PSS_WITH_SHA256(
- 0x0101,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withRSA/PSS",
- new PSSParameterSpec(
- "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))),
-
- /**
- * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
- * digested using SHA2-512 in 1 MB chunks.
- */
- RSA_PSS_WITH_SHA512(
- 0x0102,
- ContentDigestAlgorithm.CHUNKED_SHA512,
- new ApkZLibPair<>(
- "SHA512withRSA/PSS",
- new PSSParameterSpec(
- "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))),
-
- /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- RSA_PKCS1_V1_5_WITH_SHA256(
- 0x0103,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withRSA", null)),
-
- /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
- RSA_PKCS1_V1_5_WITH_SHA512(
- 0x0104,
- ContentDigestAlgorithm.CHUNKED_SHA512,
- new ApkZLibPair<>("SHA512withRSA", null)),
-
- /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- ECDSA_WITH_SHA256(
- 0x0201,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withECDSA", null)),
-
- /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
- ECDSA_WITH_SHA512(
- 0x0202,
- ContentDigestAlgorithm.CHUNKED_SHA512,
- new ApkZLibPair<>("SHA512withECDSA", null)),
-
- /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- DSA_WITH_SHA256(
- 0x0301,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withDSA", null));
-
- private final int mId;
- private final ContentDigestAlgorithm mContentDigestAlgorithm;
- private final ApkZLibPair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
-
- private SignatureAlgorithm(int id,
- ContentDigestAlgorithm contentDigestAlgorithm,
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams) {
- mId = id;
- mContentDigestAlgorithm = contentDigestAlgorithm;
- mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
- }
-
- /**
- * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
- */
- int getId() {
- return mId;
- }
-
- /**
- * Returns the content digest algorithm associated with this signature algorithm.
- */
- ContentDigestAlgorithm getContentDigestAlgorithm() {
- return mContentDigestAlgorithm;
- }
-
- /**
- * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
- * (or null if not needed) to parameterize the {@code Signature}.
- */
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
- return mJcaSignatureAlgAndParams;
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ZFileDigestSource.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ZFileDigestSource.java
deleted file mode 100644
index a211ba7..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ZFileDigestSource.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.sign.v2;
-
-import java.io.IOException;
-import java.security.MessageDigest;
-
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.google.common.base.Preconditions;
-
-/**
- * Contiguous section of {@link ZFile} which is fed into {@link MessageDigest} instances.
- */
-public class ZFileDigestSource implements DigestSource {
- private final ZFile mFile;
- private final long mOffset;
- private final long mSize;
-
- /**
- * Constructs a new {@code ZFileDigestSource} representing the section of the file starting
- * at the provided {@code offset} and extending for the provided {@code size} number of bytes.
- */
- public ZFileDigestSource(@NonNull ZFile file, long offset, long size) {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- mFile = file;
- mOffset = offset;
- mSize = size;
- }
-
-
- @Override
- public long size() {
- return mSize;
- }
-
- @Override
- public void feedDigests(long offset, int size, @NonNull MessageDigest[] digests)
- throws IOException {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- Preconditions.checkArgument(offset <= mSize, "offset: %s, file size: %s", offset, mSize);
- long chunkStartOffset = mOffset + offset;
- long availableSize = mSize - offset;
- Preconditions.checkArgument(
- size <= availableSize, "offset: %s, size: %s, file size: %s", offset, size, mSize);
-
- byte[] chunk = new byte[size];
- mFile.directFullyRead(chunkStartOffset, chunk);
- for (MessageDigest md : digests) {
- md.update(chunk);
- }
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreator.java b/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreator.java
deleted file mode 100644
index b07e855..0000000
--- a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreator.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.zfile;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.AlignmentRule;
-import com.android.builder.internal.packaging.zip.AlignmentRules;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.packaging.ApkCreator;
-import com.android.builder.packaging.ApkCreatorFactory;
-import com.google.common.base.Preconditions;
-import com.google.common.io.Closer;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-/**
- * {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK.
- */
-class ApkZFileCreator implements ApkCreator {
-
- /**
- * Suffix for native libraries.
- */
- private static final String NATIVE_LIBRARIES_SUFFIX = ".so";
-
- /**
- * Shared libraries are alignment at 4096 boundaries.
- */
- private static final AlignmentRule SO_RULE =
- AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096);
-
- /**
- * The zip file.
- */
- @NonNull
- private final ZFile mZip;
-
- /**
- * Has the zip file been closed?
- */
- private boolean mClosed;
-
- /**
- * Predicate defining which files should not be compressed.
- */
- @NonNull
- private final Predicate<String> mNoCompressPredicate;
-
- /**
- * Creates a new creator.
- *
- * @param creationData the data needed to create the APK
- * @param options zip file options
- * @throws IOException failed to create the zip
- */
- ApkZFileCreator(
- @NonNull ApkCreatorFactory.CreationData creationData,
- @NonNull ZFileOptions options)
- throws IOException {
-
- switch (creationData.getNativeLibrariesPackagingMode()) {
- case COMPRESSED:
- mNoCompressPredicate = creationData.getNoCompressPredicate();
- break;
- case UNCOMPRESSED_AND_ALIGNED:
- mNoCompressPredicate =
- creationData.getNoCompressPredicate().or(
- name -> name.endsWith(NATIVE_LIBRARIES_SUFFIX));
- options.setAlignmentRule(
- AlignmentRules.compose(SO_RULE, options.getAlignmentRule()));
- break;
- default:
- throw new AssertionError();
- }
-
- mZip = ZFiles.apk(
- creationData.getApkPath(),
- options,
- creationData.getPrivateKey(),
- creationData.getCertificate(),
- creationData.isV1SigningEnabled(),
- creationData.isV2SigningEnabled(),
- creationData.getBuiltBy(),
- creationData.getCreatedBy(),
- creationData.getMinSdkVersion());
- mClosed = false;
- }
-
- @Override
- public void writeZip(@NonNull File zip, @Nullable Function<String, String> transform,
- @Nullable Predicate<String> isIgnored) throws IOException {
- Preconditions.checkState(!mClosed, "mClosed == true");
- Preconditions.checkArgument(zip.isFile(), "!zip.isFile()");
-
- Closer closer = Closer.create();
- try {
- ZFile toMerge = closer.register(new ZFile(zip));
-
- Predicate<String> predicate;
- if (isIgnored == null) {
- predicate = s -> false;
- } else {
- predicate = isIgnored;
- }
-
- mZip.mergeFrom(toMerge, predicate);
- } catch (Throwable t) {
- throw closer.rethrow(t);
- } finally {
- closer.close();
- }
- }
-
- @Override
- public void writeFile(@NonNull File inputFile, @NonNull String apkPath) throws IOException {
- Preconditions.checkState(!mClosed, "mClosed == true");
-
- boolean mayCompress = !mNoCompressPredicate.test(apkPath);
-
- Closer closer = Closer.create();
- try {
- FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile));
- mZip.add(apkPath, inputFileStream, mayCompress);
- } catch (IOException e) {
- throw closer.rethrow(e, IOException.class);
- } catch (Throwable t) {
- throw closer.rethrow(t);
- } finally {
- closer.close();
- }
- }
-
- @Override
- public void deleteFile(@NonNull String apkPath) throws IOException {
- Preconditions.checkState(!mClosed, "mClosed == true");
-
- StoredEntry entry = mZip.get(apkPath);
- if (entry != null) {
- entry.delete();
- }
- }
-
- @Override
- public void close() throws IOException {
- if (mClosed) {
- return;
- }
-
- mZip.close();
- mClosed = true;
- }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZFileOptions.java b/src/main/java/com/android/builder/internal/packaging/zip/ZFileOptions.java
deleted file mode 100644
index 2345837..0000000
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZFileOptions.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.builder.internal.packaging.zip;
-
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.compress.DeflateExecutionCompressor;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import java.util.zip.Deflater;
-
-/**
- * Options to create a {@link ZFile}.
- */
-public class ZFileOptions {
-
- /**
- * The byte tracker.
- */
- @NonNull
- private ByteTracker mTracker;
-
- /**
- * The compressor to use.
- */
- @NonNull
- private Compressor mCompressor;
-
- /**
- * Should timestamps be zeroed?
- */
- private boolean mNoTimestamps;
-
- /**
- * The alignment rule to use.
- */
- @NonNull
- private AlignmentRule mAlignmentRule;
-
- /**
- * Should the extra field be used to cover empty space?
- */
- private boolean mCoverEmptySpaceUsingExtraField;
-
- /**
- * Should files be automatically sorted before update?
- */
- private boolean mAutoSortFiles;
-
- /**
- * Should validation of the data descriptors of entries be skipped? See
- * {@link #getSkipDataDescriptorValidation()}
- */
- private boolean mSkipDataDescriptionValidation;
-
- /**
- * Creates a new options object. All options are set to their defaults.
- */
- public ZFileOptions() {
- mTracker = new ByteTracker();
- mCompressor =
- new DeflateExecutionCompressor(
- MoreExecutors.directExecutor(), mTracker, Deflater.DEFAULT_COMPRESSION);
- mAlignmentRule = AlignmentRules.compose();
- }
-
- /**
- * Obtains the ZFile's byte tracker.
- *
- * @return the byte tracker
- */
- @NonNull
- public ByteTracker getTracker() {
- return mTracker;
- }
-
- /**
- * Obtains the compressor to use.
- *
- * @return the compressor
- */
- @NonNull
- public Compressor getCompressor() {
- return mCompressor;
- }
-
- /**
- * Sets the compressor to use.
- *
- * @param compressor the compressor
- */
- public void setCompressor(@NonNull Compressor compressor) {
- mCompressor = compressor;
- }
-
- /**
- * Obtains whether timestamps should be zeroed.
- *
- * @return should timestamps be zeroed?
- */
- public boolean getNoTimestamps() {
- return mNoTimestamps;
- }
-
- /**
- * Sets whether timestamps should be zeroed.
- *
- * @param noTimestamps should timestamps be zeroed?
- */
- public void setNoTimestamps(boolean noTimestamps) {
- mNoTimestamps = noTimestamps;
- }
-
- /**
- * Obtains the alignment rule.
- *
- * @return the alignment rule
- */
- @NonNull
- public AlignmentRule getAlignmentRule() {
- return mAlignmentRule;
- }
-
- /**
- * Sets the alignment rule.
- *
- * @param alignmentRule the alignment rule
- */
- public void setAlignmentRule(@NonNull AlignmentRule alignmentRule) {
- mAlignmentRule = alignmentRule;
- }
-
- /**
- * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for
- * an explanation on using the extra field for covering empty spaces.
- *
- * @return should the extra field be used to cover empty spaces?
- */
- public boolean getCoverEmptySpaceUsingExtraField() {
- return mCoverEmptySpaceUsingExtraField;
- }
-
- /**
- * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an
- * explanation on using the extra field for covering empty spaces.
- *
- * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces?
- */
- public void setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {
- mCoverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField;
- }
-
- /**
- * Obtains whether files should be automatically sorted before updating the zip file. See
- * {@link ZFile} for an explanation on automatic sorting.
- *
- * @return should the file be automatically sorted?
- */
- public boolean getAutoSortFiles() {
- return mAutoSortFiles;
- }
-
- /**
- * Sets whether files should be automatically sorted before updating the zip file. See
- * {@link ZFile} for an explanation on automatic sorting.
- *
- * @param autoSortFiles should the file be automatically sorted?
- */
- public void setAutoSortFiles(boolean autoSortFiles) {
- mAutoSortFiles = autoSortFiles;
- }
-
- /**
- * Should data descriptor validation be skipped? This should generally be
- * set to false. However, some tools (proguard -- http://b.android.com/221057) generate zips
- * with incorrect data descriptors and to open the zips we need to skip the validation of data
- * descriptors.
- *
- * @return should data descriptors be validated?
- */
- public boolean getSkipDataDescriptorValidation() {
- return mSkipDataDescriptionValidation;
- }
-
- /**
- * Sets whether data descriptors validation should be skipped. See
- * {@link #getSkipDataDescriptorValidation()}.
- *
- * @param skip should validation be skipped?
- */
- public void setSkipDataDescriptionValidation(boolean skip) {
- mSkipDataDescriptionValidation = skip;
- }
-}
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/FullApkSignTest.java b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
similarity index 80%
rename from src/test/java/com/android/builder/internal/packaging/sign/FullApkSignTest.java
rename to src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
index e9c2c90..f72f63c 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/FullApkSignTest.java
+++ b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
@@ -14,19 +14,19 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotNull;
-import com.android.builder.internal.packaging.zip.AlignmentRule;
-import com.android.builder.internal.packaging.zip.AlignmentRules;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.internal.packaging.zip.ZFileTestConstants;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
-import com.android.builder.internal.utils.ApkZLibPair;
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.zip.AlignmentRule;
+import com.android.apkzlib.zip.AlignmentRules;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import com.android.apkzlib.zip.ZFileTestConstants;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.security.PrivateKey;
@@ -36,7 +36,7 @@
import org.junit.rules.TemporaryFolder;
/**
- * Tests that verify {@link FullApkSignExtension}.
+ * Tests that verify APK Signature Scheme v2 signing using {@link SigningExtension}.
*/
public class FullApkSignTest {
@@ -63,9 +63,8 @@
* Generate a signed zip.
*/
ZFile zf = new ZFile(out, options);
- FullApkSignExtension signExtension =
- new FullApkSignExtension(zf, 13, signData.v2, signData.v1);
- signExtension.register();
+ new SigningExtension(13, signData.v2, signData.v1, false, true)
+ .register(zf);
String f1Name = "abc";
byte[] f1Data = new byte[] { 1, 1, 1, 1 };
zf.add(f1Name, new ByteArrayInputStream(f1Data));
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/JarSigningTest.java b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
similarity index 88%
rename from src/test/java/com/android/builder/internal/packaging/sign/JarSigningTest.java
rename to src/test/java/com/android/apkzlib/sign/JarSigningTest.java
index 78c633e..35aeeaf 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/JarSigningTest.java
+++ b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
-import com.android.builder.internal.utils.ApkZLibPair;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import java.io.ByteArrayInputStream;
@@ -48,6 +48,7 @@
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZFile zf = new ZFile(zipFile)) {
+ ApkZFileTestUtils.addAndroidManifest(zf);
ManifestGenerationExtension manifestExtension =
new ManifestGenerationExtension("Me", "Me");
manifestExtension.register(zf);
@@ -55,9 +56,7 @@
ApkZLibPair<PrivateKey, X509Certificate> p =
SignatureTestUtils.generateSignaturePre18();
- SignatureExtension signatureExtension =
- new SignatureExtension(manifestExtension, 12, p.v2, p.v1, null);
- signatureExtension.register();
+ new SigningExtension(12, p.v2, p.v1, true, false).register(zf);
}
try (ZFile verifyZFile = new ZFile(zipFile)) {
@@ -78,6 +77,7 @@
ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18();
try (ZFile zf1 = new ZFile(zipFile)) {
+ ApkZFileTestUtils.addAndroidManifest(zf1);
zf1.add("directory/file",
new ByteArrayInputStream("useless text".getBytes(Charsets.US_ASCII)));
}
@@ -85,7 +85,7 @@
try (ZFile zf2 = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
me.register(zf2);
- new SignatureExtension(me, 10, p.v2, p.v1, null).register();
+ new SigningExtension(10, p.v2, p.v1, true, false).register(zf2);
}
try (ZFile zf3 = new ZFile(zipFile)) {
@@ -121,7 +121,7 @@
Attributes signAttrs = signature.getAttributes("directory/file");
assertNotNull(signAttrs);
assertEquals(1, signAttrs.size());
- assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", signAttrs.getValue("SHA1-Digest"));
+ assertEquals("LGSOwy4uGcUWoc+ZhS8ukzmf0fY=", signAttrs.getValue("SHA1-Digest"));
StoredEntry rsaEntry = zf3.get("META-INF/CERT.RSA");
assertNotNull(rsaEntry);
@@ -132,6 +132,7 @@
public void signJarWithPrexistingSimpleTextFilePos18() throws Exception {
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZFile zf1 = new ZFile(zipFile)) {
+ ApkZFileTestUtils.addAndroidManifest(zf1);
zf1.add("directory/file", new ByteArrayInputStream("useless text".getBytes(
Charsets.US_ASCII)));
}
@@ -141,7 +142,7 @@
try (ZFile zf2 = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
me.register(zf2);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf2);
}
try (ZFile zf3 = new ZFile(zipFile)) {
@@ -178,7 +179,7 @@
Attributes signAttrs = signature.getAttributes("directory/file");
assertNotNull(signAttrs);
assertEquals(1, signAttrs.size());
- assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=",
+ assertEquals("dBnaLpqNjmUnLlZF4tNqOcDWL8wy8Tsw1ZYFqTZhjIs=",
signAttrs.getValue("SHA-256-Digest"));
StoredEntry ecdsaEntry = zf3.get("META-INF/CERT.EC");
@@ -190,15 +191,14 @@
public void v2SignAddsApkSigningBlock() throws Exception {
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZFile zf = new ZFile(zipFile)) {
+ ApkZFileTestUtils.addAndroidManifest(zf);
ManifestGenerationExtension manifestExtension =
new ManifestGenerationExtension("Me", "Me");
manifestExtension.register(zf);
ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18();
- FullApkSignExtension signatureExtension =
- new FullApkSignExtension(zf, 12, p.v2, p.v1);
- signatureExtension.register();
+ new SigningExtension(12, p.v2, p.v1, false, true).register(zf);
}
try (ZFile verifyZFile = new ZFile(zipFile)) {
@@ -224,10 +224,11 @@
String createdBy = "Uses Android";
try (ZFile zf1 = new ZFile(zipFile)) {
+ ApkZFileTestUtils.addAndroidManifest(zf1);
zf1.add(file1Name, new ByteArrayInputStream(file1Contents));
ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
me.register(zf1);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf1);
zf1.update();
@@ -237,7 +238,7 @@
try (InputStream manifestIs = manifestEntry.open()) {
Manifest manifest = new Manifest(manifestIs);
- assertEquals(1, manifest.getEntries().size());
+ assertEquals(2, manifest.getEntries().size());
Attributes file1Attrs = manifest.getEntries().get(file1Name);
assertNotNull(file1Attrs);
@@ -261,7 +262,7 @@
try (InputStream manifestIs = manifestEntry.open()) {
Manifest manifest = new Manifest(manifestIs);
- assertEquals(1, manifest.getEntries().size());
+ assertEquals(2, manifest.getEntries().size());
Attributes file1Attrs = manifest.getEntries().get(file1Name);
assertNotNull(file1Attrs);
@@ -277,9 +278,10 @@
file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha);
try (ZFile zf2 = new ZFile(zipFile)) {
+ ApkZFileTestUtils.addAndroidManifest(zf2);
ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
me.register(zf2);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf2);
zf2.add(file1Name, new ByteArrayInputStream(file1Contents));
@@ -291,7 +293,7 @@
try (InputStream manifestIs = manifestEntry.open()) {
Manifest manifest = new Manifest(manifestIs);
- assertEquals(1, manifest.getEntries().size());
+ assertEquals(2, manifest.getEntries().size());
Attributes file1Attrs = manifest.getEntries().get(file1Name);
assertNotNull(file1Attrs);
@@ -301,7 +303,7 @@
}
@Test
- public void openSignedJarDoesNotForcesWriteifSignatureIsNotCorrect() throws Exception {
+ public void openSignedJarDoesNotForcesWriteIfSignatureIsNotCorrect() throws Exception {
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePos18();
@@ -310,9 +312,10 @@
byte[] fileContents = "Very interesting contents".getBytes(Charsets.US_ASCII);
try (ZFile zf = new ZFile(zipFile)) {
+ ApkZFileTestUtils.addAndroidManifest(zf);
ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
me.register(zf);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
zf.add(fileName, new ByteArrayInputStream(fileContents));
}
@@ -327,7 +330,7 @@
try (ZFile zf = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
me.register(zf);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
}
/*
@@ -364,7 +367,7 @@
try (ZFile zf = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
me.register(zf);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
}
/*
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/ManifestGenerationTest.java b/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
similarity index 91%
rename from src/test/java/com/android/builder/internal/packaging/sign/ManifestGenerationTest.java
rename to src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
index 5d50522..f0817d0 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/ManifestGenerationTest.java
+++ b/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,25 +14,25 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
import com.google.common.base.Charsets;
import com.google.common.io.Closer;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
+import java.util.HashSet;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
-import org.mockito.internal.util.collections.Sets;
public class ManifestGenerationTest {
@@ -66,7 +66,7 @@
assertEquals("Manifest-Version: 1.0", lines[0].trim());
- Set<String> linesSet = Sets.newSet();
+ Set<String> linesSet = new HashSet<>();
for (String l : lines) {
linesSet.add(l.trim());
}
@@ -102,7 +102,7 @@
assertEquals("Manifest-Version: 1.0", lines[0].trim());
- Set<String> linesSet = Sets.newSet();
+ Set<String> linesSet = new HashSet<>();
for (String l : lines) {
linesSet.add(l.trim());
}
@@ -140,7 +140,7 @@
assertEquals("Manifest-Version: 1.0", lines[0].trim());
- Set<String> linesSet = Sets.newSet();
+ Set<String> linesSet = new HashSet<>();
for (String l : lines) {
linesSet.add(l.trim());
}
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/SignatureTestUtils.java b/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
similarity index 94%
rename from src/test/java/com/android/builder/internal/packaging/sign/SignatureTestUtils.java
rename to src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
index f827a43..fb1d322 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/SignatureTestUtils.java
+++ b/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.ApkZLibPair;
+import com.android.apkzlib.utils.ApkZLibPair;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -30,6 +29,7 @@
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
+import javax.annotation.Nonnull;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
@@ -54,7 +54,7 @@
* @return the pair with the private key and certificate
* @throws Exception failed to generate the signature data
*/
- @NonNull
+ @Nonnull
public static ApkZLibPair<PrivateKey, X509Certificate> generateSignaturePre18()
throws Exception {
return generateSignature("RSA", "SHA1withRSA");
@@ -66,7 +66,7 @@
* @return the pair with the private key and certificate
* @throws Exception failed to generate the signature data
*/
- @NonNull
+ @Nonnull
public static ApkZLibPair<PrivateKey, X509Certificate> generateSignaturePos18()
throws Exception {
return generateSignature("EC", "SHA256withECDSA");
@@ -80,10 +80,10 @@
* @return the pair with the private key and certificate
* @throws Exception failed to generate the signature data
*/
- @NonNull
+ @Nonnull
public static ApkZLibPair<PrivateKey, X509Certificate> generateSignature(
- @NonNull String sign,
- @NonNull String full)
+ @Nonnull String sign,
+ @Nonnull String full)
throws Exception {
// http://stackoverflow.com/questions/28538785/
// easy-way-to-generate-a-self-signed-certificate-for-java-security-keystore-using
diff --git a/src/test/java/com/android/builder/internal/utils/ApkZFileTestUtils.java b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
similarity index 70%
rename from src/test/java/com/android/builder/internal/utils/ApkZFileTestUtils.java
rename to src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
index 88e3c80..1ef087f 100644
--- a/src/test/java/com/android/builder/internal/utils/ApkZFileTestUtils.java
+++ b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
@@ -14,18 +14,21 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
import static org.junit.Assert.assertTrue;
-import com.android.annotations.NonNull;
+import com.android.apkzlib.zip.ZFile;
+import com.android.testutils.TestResources;
import com.google.common.base.Preconditions;
+import com.google.common.io.ByteSource;
import com.google.common.io.Resources;
+import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
-import java.net.URL;
+import javax.annotation.Nonnull;
/**
* Utility functions for tests.
@@ -41,8 +44,8 @@
* @return the bytes read
* @throws Exception failed to read the file
*/
- @NonNull
- public static byte[] readSegment(@NonNull File file, long start, int length) throws Exception {
+ @Nonnull
+ public static byte[] readSegment(@Nonnull File file, long start, int length) throws Exception {
Preconditions.checkArgument(start >= 0, "start < 0");
Preconditions.checkArgument(length >= 0, "length < 0");
@@ -71,24 +74,22 @@
* @param path the path
* @return the test resource
*/
- @NonNull
- public static File getResource(@NonNull String path) {
- URL url = Resources.getResource(ApkZFileTestUtils.class, path);
- File resource = new File(url.getFile());
+ @Nonnull
+ public static File getResource(@Nonnull String path) {
+ File resource = TestResources.getFile(ApkZFileTestUtils.class, path);
assertTrue(resource.exists());
return resource;
}
/**
- * Sleeps the current thread for enough time to ensure that the local file system had enough
- * time to notice a "tick". This method is usually called in tests when it is necessary to
- * ensure filesystem writes are detected through timestamp modification.
+ * Obtains the test resource with the given path.
*
- * @throws InterruptedException waiting interrupted
- * @throws IOException issues creating a temporary file
+ * @param path the path
+ * @return the test resource
*/
- public static void waitForFileSystemTick() throws InterruptedException, IOException {
- waitForFileSystemTick(getFreshTimestamp());
+ @Nonnull
+ public static ByteSource getResourceBytes(@Nonnull String path) {
+ return Resources.asByteSource(Resources.getResource(ApkZFileTestUtils.class, path));
}
/**
@@ -107,6 +108,22 @@
}
}
+ /*
+ * Adds a basic compiled AndroidManifest to the given ZFile containing minSdkVersion equal 15
+ * and targetSdkVersion equal 25.
+ */
+ public static void addAndroidManifest(ZFile zf) throws IOException {
+ zf.add("AndroidManifest.xml", new ByteArrayInputStream(getAndroidManifest()));
+ }
+
+ /*
+ * Provides a basic compiled AndroidManifest containing minSdkVersion equal 15 and
+ * targetSdkVersion equal 25.
+ */
+ public static byte[] getAndroidManifest() throws IOException {
+ return ApkZFileTestUtils.getResourceBytes("/testData/packaging/AndroidManifest.xml").read();
+ }
+
/**
* Obtains the timestamp of a newly-created file.
*
diff --git a/src/test/java/com/android/builder/internal/utils/CachedFileContentsTest.java b/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/utils/CachedFileContentsTest.java
rename to src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
index 282ca47..f9654d7 100644
--- a/src/test/java/com/android/builder/internal/utils/CachedFileContentsTest.java
+++ b/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
diff --git a/src/test/java/com/android/builder/internal/utils/CachedSupplierTest.java b/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/utils/CachedSupplierTest.java
rename to src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
index e8ce7b3..e687bf3 100644
--- a/src/test/java/com/android/builder/internal/utils/CachedSupplierTest.java
+++ b/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
diff --git a/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java b/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java
new file mode 100644
index 0000000..1731ba9
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zfile;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.apkzlib.zip.CompressionMethod;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.file.Files;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ApkAlignmentTest {
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void soFilesUncompressedAndAligned() throws Exception {
+ File apk = new File(mTemporaryFolder.getRoot(), "a.apk");
+
+ File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so");
+ Files.write(soFile.toPath(), new byte[500]);
+
+ ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+ ApkCreatorFactory.CreationData creationData =
+ new ApkCreatorFactory.CreationData(
+ apk,
+ null,
+ null,
+ false,
+ false,
+ null,
+ null,
+ 20,
+ NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED,
+ path -> false);
+
+ ApkCreator creator = cf.make(creationData);
+
+ creator.writeFile(soFile, "/doesnt_work.so");
+ creator.close();
+
+ try (ZFile zf = new ZFile(apk)) {
+ StoredEntry soEntry = zf.get("/doesnt_work.so");
+ assertNotNull(soEntry);
+ assertEquals(
+ CompressionMethod.STORE,
+ soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ long offset =
+ soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+ assertTrue(offset % 4096 == 0);
+ }
+ }
+
+ @Test
+ public void soFilesMergedFromZipsCanBeUncompressedAndAligned() throws Exception {
+
+ // Create a zip file with a compressed, unaligned so file.
+ File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZFile zf = new ZFile(zipToMerge)) {
+ zf.add("/zero.so", new ByteArrayInputStream(new byte[500]));
+ }
+
+ try (ZFile zf = new ZFile(zipToMerge)) {
+ StoredEntry zeroSo = zf.get("/zero.so");
+ assertNotNull(zeroSo);
+ assertEquals(
+ CompressionMethod.DEFLATE,
+ zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ long offset =
+ zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize();
+ assertFalse(offset % 4096 == 0);
+ }
+
+ // Create an APK and merge the zip file.
+ File apk = new File(mTemporaryFolder.getRoot(), "b.apk");
+ ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+ ApkCreatorFactory.CreationData creationData =
+ new ApkCreatorFactory.CreationData(
+ apk,
+ null,
+ null,
+ false,
+ false,
+ null,
+ null,
+ 20,
+ NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED,
+ path -> false);
+
+ try (ApkCreator creator = cf.make(creationData)) {
+ creator.writeZip(zipToMerge, null, null);
+ }
+
+ // Make sure the file is uncompressed and aligned.
+ try (ZFile zf = new ZFile(apk)) {
+ StoredEntry soEntry = zf.get("/zero.so");
+ assertNotNull(soEntry);
+ assertEquals(
+ CompressionMethod.STORE,
+ soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ long offset =
+ soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+ assertTrue(offset % 4096 == 0);
+
+ byte[] data = soEntry.read();
+ assertEquals(500, data.length);
+ for (int i = 0; i < data.length; i++) {
+ assertEquals(0, data[i]);
+ }
+ }
+ }
+
+ @Test
+ public void soFilesUncompressedAndNotAligned() throws Exception {
+ File apk = new File(mTemporaryFolder.getRoot(), "a.apk");
+
+ File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so");
+ Files.write(soFile.toPath(), new byte[500]);
+
+ ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+ ApkCreatorFactory.CreationData creationData =
+ new ApkCreatorFactory.CreationData(
+ apk,
+ null,
+ null,
+ false,
+ false,
+ null,
+ null,
+ 20,
+ NativeLibrariesPackagingMode.COMPRESSED,
+ path -> false);
+
+ ApkCreator creator = cf.make(creationData);
+
+ creator.writeFile(soFile, "/doesnt_work.so");
+ creator.close();
+
+ try (ZFile zf = new ZFile(apk)) {
+ StoredEntry soEntry = zf.get("/doesnt_work.so");
+ assertNotNull(soEntry);
+ assertEquals(
+ CompressionMethod.DEFLATE,
+ soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ long offset =
+ soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+ assertTrue(offset % 4096 != 0);
+ }
+ }
+
+ @Test
+ public void soFilesMergedFromZipsCanBeUncompressedAndNotAligned() throws Exception {
+
+ // Create a zip file with a compressed, unaligned so file.
+ File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZFile zf = new ZFile(zipToMerge)) {
+ zf.add("/zero.so", new ByteArrayInputStream(new byte[500]));
+ }
+
+ try (ZFile zf = new ZFile(zipToMerge)) {
+ StoredEntry zeroSo = zf.get("/zero.so");
+ assertNotNull(zeroSo);
+ assertEquals(
+ CompressionMethod.DEFLATE,
+ zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ long offset =
+ zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize();
+ assertFalse(offset % 4096 == 0);
+ }
+
+ // Create an APK and merge the zip file.
+ File apk = new File(mTemporaryFolder.getRoot(), "b.apk");
+ ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+ ApkCreatorFactory.CreationData creationData =
+ new ApkCreatorFactory.CreationData(
+ apk,
+ null,
+ null,
+ false,
+ false,
+ null,
+ null,
+ 20,
+ NativeLibrariesPackagingMode.COMPRESSED,
+ path -> false);
+
+ try (ApkCreator creator = cf.make(creationData)) {
+ creator.writeZip(zipToMerge, null, null);
+ }
+
+ // Make sure the file is uncompressed and aligned.
+ try (ZFile zf = new ZFile(apk)) {
+ StoredEntry soEntry = zf.get("/zero.so");
+ assertNotNull(soEntry);
+ assertEquals(
+ CompressionMethod.DEFLATE,
+ soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ long offset =
+ soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+ assertTrue(offset % 4096 != 0);
+
+ byte[] data = soEntry.read();
+ assertEquals(500, data.length);
+ for (int i = 0; i < data.length; i++) {
+ assertEquals(0, data[i]);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/AlignmentTest.java b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
similarity index 87%
rename from src/test/java/com/android/builder/internal/packaging/zip/AlignmentTest.java
rename to src/test/java/com/android/apkzlib/zip/AlignmentTest.java
index 3241bd3..e94a876 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/AlignmentTest.java
+++ b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import static com.android.builder.internal.utils.ApkZFileTestUtils.readSegment;
+import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment;
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
@@ -770,4 +770,87 @@
assertArrayEquals(recognizable2, readSegment(zipFile, 150, recognizable2.length));
assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
}
+
+ @Test
+ public void alignCoveringEmptySpaceWhenExtraFieldIsInvalid() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(AlignmentRules.constant(100));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
+ StoredEntry foo = zf.get("foo");
+ assertNotNull(foo);
+ foo.setLocalExtra(new ExtraField(new byte[] { 0, 0 }));
+ zf.add("bar", new ByteArrayInputStream(new byte[] { 5, 6, 7, 8 }));
+ }
+ }
+
+ @Test
+ public void fourByteAlignment() throws Exception {
+ // When aligning with 4 bytes, there are are only 3 possible cases:
+ // - We're 2 bytes short and so need to add +6 bytes (6 bytes for header + no zeroes)
+ // - We're 3 bytes short and so need to add +7 bytes (6 bytes for header + 1 zero)
+ // - We're 1 byte short and so need to add +9 bytes (6 bytes for header + 3 zeroes)
+
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(AlignmentRules.constant(4));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ // File header starts at 0.
+ // File name starts at 30 (LOCAL_HEADER_SIZE).
+ // If unaligned we would have data starting at 33, but with aligned we have data
+ // starting at 40 (36 isn't enough for the extra data header).
+ String fooName = "foo";
+ byte[] fooData = new byte[] { 1, 2, 3, 4, 5 };
+ zf.add(fooName, new ByteArrayInputStream(fooData), false);
+ zf.update();
+ StoredEntry foo = zf.get(fooName);
+ long fooOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + fooName.length() + 7;
+ assertEquals(fooOffset, foo.getLocalHeaderSize());
+
+ // Bar header starts at 45 (foo data starts at 40 and is 5 bytes long).
+ // Bar header ends at 75.
+ // If unaligned we would have data starting at 78, but with aligned we have data
+ // starting at 84 (80 isn't enough for the extra header).
+ String barName = "bar";
+ byte[] barData = new byte[] { 6 };
+ zf.add(barName, new ByteArrayInputStream(barData), false);
+ zf.update();
+
+ StoredEntry bar = zf.get(barName);
+ long barStart = bar.getCentralDirectoryHeader().getOffset();
+ assertEquals(fooOffset + fooData.length, barStart);
+
+ long barStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + barName.length() + 6;
+ assertEquals(barStartOffset, bar.getLocalHeaderSize());
+
+ // Xpto header starts at 85 (bar data starts at 84 and is 1 byte long).
+ // Xpto header ends at 115.
+ // If unaligned we would have data starting at 119, but with aligned we have data
+ // starting at 128 (120 & 124 are not enough for the extra header).
+ String xptoName = "xpto";
+ byte[] xptoData = new byte[] { 7, 8, 9, 10 };
+ zf.add(xptoName, new ByteArrayInputStream(xptoData), false);
+ zf.update();
+
+ StoredEntry xpto = zf.get(xptoName);
+ long xptoStart = xpto.getCentralDirectoryHeader().getOffset();
+ assertEquals(barStart + barStartOffset + barData.length, xptoStart);
+
+ long xptoStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + xptoName.length() + 9;
+ assertEquals(xptoStartOffset, xpto.getLocalHeaderSize());
+
+ // Dummy header starts at 133 (xpto data starts at 128 and is 6 bytes long).
+ String dummyName = "dummy";
+ byte[] dummyData = new byte[] { 11 };
+ zf.add(dummyName, new ByteArrayInputStream(dummyData), false);
+ zf.update();
+
+ StoredEntry dummy = zf.get(dummyName);
+ long dummyStart = dummy.getCentralDirectoryHeader().getOffset();
+ assertEquals(xptoStart + xptoStartOffset + xptoData.length, dummyStart);
+ }
+ }
}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/EncodeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
similarity index 82%
rename from src/test/java/com/android/builder/internal/packaging/zip/EncodeUtilsTest.java
rename to src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
index 91e9676..8648aa0 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/EncodeUtilsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -56,4 +56,17 @@
(byte) 0xd0, (byte) 0xb0 }, encoded);
assertEquals(kazakhCapital, EncodeUtils.decode(encoded, flags));
}
+
+ @Test
+ public void asciiDecodeAsUtf8() {
+ byte[] greatWallChinese =
+ new byte[] {
+ (byte) 0xe9, (byte) 0x95, (byte) 0xB7, (byte) 0xe5, (byte) 0x9F, (byte) 0x8E
+ };
+
+ GPFlags flags = GPFlags.make(false);
+
+ String text = EncodeUtils.decode(greatWallChinese, flags);
+ assertEquals("\u9577\u57ce", text);
+ }
}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ExtraFieldTest.java b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
similarity index 93%
rename from src/test/java/com/android/builder/internal/packaging/zip/ExtraFieldTest.java
rename to src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
index d4275d0..2371849 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ExtraFieldTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
@@ -332,4 +333,29 @@
assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 2, 4, 2, 4 }, sData);
}
}
+
+ @Test
+ public void parseInvalidExtraFieldWithInvalidHeader() throws Exception {
+ byte[] raw = new byte[1];
+ ExtraField ef = new ExtraField(raw);
+ try {
+ ef.getSegments();
+ fail();
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void parseInvalidExtraFieldWithInsufficientData() throws Exception {
+ // Remember: 0x05, 0x00 = 5 in little endian!
+ byte[] raw = new byte[] { /* Header */ 0x01, 0x02, /* Size */ 0x05, 0x00, /* Data */ 0x01 };
+ ExtraField ef = new ExtraField(raw);
+ try {
+ ef.getSegments();
+ fail();
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/FileUseMapTest.java b/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/packaging/zip/FileUseMapTest.java
rename to src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
index a53e9cb..0ee0129 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/FileUseMapTest.java
+++ b/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/OldApkReadTest.java b/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
similarity index 90%
rename from src/test/java/com/android/builder/internal/packaging/zip/OldApkReadTest.java
rename to src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
index 3121c44..61a08d7 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/OldApkReadTest.java
+++ b/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
import java.io.File;
import org.junit.Test;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ReadWithDifferentCompressionLevelsTest.java b/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
similarity index 92%
rename from src/test/java/com/android/builder/internal/packaging/zip/ReadWithDifferentCompressionLevelsTest.java
rename to src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
index 154dfcd..4301710 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ReadWithDifferentCompressionLevelsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
import java.io.File;
import org.junit.Test;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileNotificationTest.java b/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
similarity index 96%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileNotificationTest.java
rename to src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
index b7c490f..84be460 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileNotificationTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
@@ -14,28 +14,26 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.utils.ApkZLibPair;
-import com.android.builder.internal.utils.IOExceptionRunnable;
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.utils.IOExceptionRunnable;
import com.google.common.collect.Lists;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.mockito.Mockito;
-
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.List;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
public class ZFileNotificationTest {
private static class KeepListener extends ZFileExtension {
@@ -77,7 +75,7 @@
@Nullable
@Override
- public IOExceptionRunnable added(@NonNull StoredEntry entry,
+ public IOExceptionRunnable added(@Nonnull StoredEntry entry,
@Nullable StoredEntry replaced) {
added.add(new ApkZLibPair<>(entry, replaced));
return returnRunnable;
@@ -85,7 +83,7 @@
@Nullable
@Override
- public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
+ public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
removed.add(entry);
return returnRunnable;
}
diff --git a/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java b/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java
new file mode 100644
index 0000000..a030a83
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.Nonnull;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ZFileReadOnlyTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void cannotCreateRoFileOnNonExistingFile() throws Exception {
+ try {
+ new ZFile(new File(temporaryFolder.getRoot(), "foo.zip"), new ZFileOptions(), true);
+ fail();
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ @Nonnull
+ private File makeTestZip() throws IOException {
+ File zip = new File(temporaryFolder.getRoot(), "foo.zip");
+ try (ZFile zf = new ZFile(zip)) {
+ zf.add("bar", new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4, 5 }));
+ }
+
+ return zip;
+ }
+
+ @Test
+ public void cannotUpdateInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.update();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotAddFilesInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.add("bar2", new ByteArrayInputStream(new byte[] { 6, 7, }));
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotAddRecursivelyInRoMode() throws Exception {
+ File folder = temporaryFolder.newFolder();
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.addAllRecursively(folder);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotReplaceFilesInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.add("bar", new ByteArrayInputStream(new byte[] { 6, 7 }));
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotDeleteFilesInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ StoredEntry bar = zf.get("bar");
+ assertNotNull(bar);
+ try {
+ bar.delete();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotMergeInRoMode() throws Exception {
+ try (ZFile toMerge = new ZFile(new File(temporaryFolder.getRoot(), "a.zip"))) {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.mergeFrom(toMerge, s -> false);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+ }
+
+ @Test
+ public void cannotTouchInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.touch();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotRealignInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.realign();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotAddExtensionInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.addZFileExtension(new ZFileExtension() {});
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotDirectWriteInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.directWrite(0, new byte[1]);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotSetEocdCommentInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.setEocdComment(new byte[2]);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotSetCentralDirectoryOffsetInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.setExtraDirectoryOffset(4);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void cannotSortZipContentsInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ try {
+ zf.sortZipContents();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expeted.
+ }
+ }
+ }
+
+ @Test
+ public void canOpenAndReadFilesInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ StoredEntry bar = zf.get("bar");
+ assertNotNull(bar);
+ assertArrayEquals(new byte[] { 0, 1, 2, 3, 4, 5 }, bar.read());
+ }
+ }
+
+ @Test
+ public void canGetDirectoryAndEocdBytesInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ zf.getCentralDirectoryBytes();
+ zf.getEocdBytes();
+ zf.getEocdComment();
+ }
+ }
+
+ @Test
+ public void canDirectReadInRoMode() throws Exception {
+ try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+ zf.directRead(0, new byte[2]);
+ }
+ }
+}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileSortTest.java b/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileSortTest.java
rename to src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
index 41e5301..869f73a 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileSortTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import com.android.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.File;
+import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTest.java b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
similarity index 75%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileTest.java
rename to src/test/java/com/android/apkzlib/zip/ZFileTest.java
index 0b54b44..b7f2979 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
@@ -14,23 +14,23 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
-import static com.android.builder.internal.utils.ApkZFileTestUtils.readSegment;
+import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.compress.DeflateExecutionCompressor;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-import com.android.builder.internal.packaging.zip.utils.RandomAccessFileUtils;
+import com.android.apkzlib.zip.compress.DeflateExecutionCompressor;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.RandomAccessFileUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
@@ -38,11 +38,6 @@
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.common.io.Files;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -51,6 +46,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
+import java.util.Locale;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -59,6 +55,10 @@
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
+import javax.annotation.Nonnull;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
public class ZFileTest {
@Rule
@@ -148,6 +148,21 @@
try (ZFile zf = new ZFile(testZip)) {
assertEquals(1, zf.entries().size());
+ assertTrue(zf.getCentralDirectoryOffset() > 0);
+ assertTrue(zf.getEocdOffset() > 0);
+ }
+ }
+
+ @Test
+ public void readOnlyV2SignedApkSupport() throws Exception {
+ File testZip = ZipTestUtils.cloneRsrc("v2-signed.apk", mTemporaryFolder);
+
+ assertTrue(testZip.setWritable(false));
+
+ try (ZFile zf = new ZFile(testZip)) {
+ assertEquals(416, zf.entries().size());
+ assertTrue(zf.getCentralDirectoryOffset() > 0);
+ assertTrue(zf.getEocdOffset() > 0);
}
}
@@ -938,6 +953,31 @@
filetMignonKorean + " " + isGoodJapanese,
entry.getCentralDirectoryHeader().getName());
assertArrayEquals(
+ "Stuff about food is good.\n".getBytes(Charsets.US_ASCII), entry.read());
+ }
+ }
+
+ @Test
+ public void utf8NamesSupportedOnReadingWithoutUtf8Flag() throws Exception {
+ File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder);
+
+ // Reset bytes 7 and 122 that have the flag in the local header and central directory.
+ byte[] data = Files.toByteArray(zip);
+ data[7] = 0;
+ data[122] = 0;
+ Files.write(data, zip);
+
+ try (ZFile f = new ZFile(zip)) {
+ assertEquals(1, f.entries().size());
+
+ StoredEntry entry = f.entries().iterator().next();
+ String filetMignonKorean = "\uc548\uc2eC \uc694\ub9ac";
+ String isGoodJapanese = "\u3068\u3066\u3082\u826f\u3044";
+
+ assertEquals(
+ filetMignonKorean + " " + isGoodJapanese,
+ entry.getCentralDirectoryHeader().getName());
+ assertArrayEquals(
"Stuff about food is good.\n".getBytes(Charsets.US_ASCII),
entry.read());
}
@@ -1027,9 +1067,9 @@
boolean[] done = new boolean[1];
options.setCompressor(new DeflateExecutionCompressor(executor, options.getTracker(),
Deflater.BEST_COMPRESSION) {
- @NonNull
+ @Nonnull
@Override
- protected CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+ protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
throws Exception {
Thread.sleep(500);
CompressionResult cr = super.immediateCompress(source);
@@ -1371,6 +1411,7 @@
/*
* Open the zip file and compute where the local header CRC32 is.
*/
+ long crcOffset;
try (ZFile zf = new ZFile(zipFile)) {
StoredEntry se = zf.get("foo");
assertNotNull(se);
@@ -1378,41 +1419,403 @@
/*
* Twelve bytes from the CD offset, we have the start of the CRC32 of the zip entry.
- * Corrupt it.
*/
- byte[] crc = new byte[4];
- zf.directFullyRead(cdOffset - 12, crc);
- crc[0]++;
- zf.directWrite(cdOffset - 12, crc);
+ crcOffset = cdOffset - 12;
}
/*
- * Now open the zip file and it should fail.
+ * Corrupt the CRC32.
*/
- try {
- new ZFile(zipFile);
- fail();
- } catch (IOException e) {
- /*
- * We should be complaining about the CRC32 somewhere...
- */
- boolean foundCrc32Complain = false;
-
- assertTrue(
- Throwables.getCausalChain(e).stream()
- .map(Throwable::getMessage)
- .anyMatch(s -> s.contains("CRC32")));
+ byte[] crc = readSegment(zipFile, crcOffset, 4);
+ crc[0]++;
+ try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) {
+ raf.seek(crcOffset);
+ raf.write(crc);
}
/*
- * But opening with data validation skip should work.
+ * Now open the zip file and it should write a message in the log.
*/
ZFileOptions options = new ZFileOptions();
- options.setSkipDataDescriptionValidation(true);
+ options.setVerifyLogFactory(VerifyLogs::unlimited);
try (ZFile zf = new ZFile(zipFile, options)) {
+ VerifyLog vl = zf.getVerifyLog();
+ assertTrue(vl.getLogs().isEmpty());
+ StoredEntry fooEntry = zf.get("foo");
+ vl = fooEntry.getVerifyLog();
+ assertEquals(1, vl.getLogs().size());
+ assertTrue(vl.getLogs().get(0).contains("CRC32"));
+ }
+ }
+
+ @Test
+ public void detectIncorrectVersionToExtractInCentralDirectory() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ /*
+ * Create a valid zip file.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.add("foo", new ByteArrayInputStream(new byte[0]));
+ }
+
+ /*
+ * Change the "version to extract" in the central directory to 0x7777.
+ */
+ int versionToExtractOffset =
+ ZFileTestConstants.LOCAL_HEADER_SIZE
+ + 3
+ + CentralDirectory.F_VERSION_EXTRACT.offset();
+ byte[] allZipBytes = Files.toByteArray(zipFile);
+ allZipBytes[versionToExtractOffset] = 0x77;
+ allZipBytes[versionToExtractOffset + 1] = 0x77;
+ Files.write(allZipBytes, zipFile);
+
+ /*
+ * Opening the file and it should write a message in the log. The entry has the right
+ * version to extract (20), but it issues a warning because it is not equal to the one
+ * in the central directory.
+ */
+ ZFileOptions options = new ZFileOptions();
+ options.setVerifyLogFactory(VerifyLogs::unlimited);
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ VerifyLog vl = zf.getVerifyLog();
+ assertEquals(1, vl.getLogs().size());
+ assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
+ assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
+ StoredEntry fooEntry = zf.get("foo");
+ vl = fooEntry.getVerifyLog();
+ assertEquals(1, vl.getLogs().size());
+ assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
+ assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
+ }
+ }
+
+ @Test
+ public void detectIncorrectVersionToExtractInLocalHeader() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ /*
+ * Create a valid zip file.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.add("foo", new ByteArrayInputStream(new byte[0]));
+ }
+
+ /*
+ * Change the "version to extract" in the local header to 0x7777.
+ */
+ int versionToExtractOffset = StoredEntry.F_VERSION_EXTRACT.offset();
+ byte[] allZipBytes = Files.toByteArray(zipFile);
+ allZipBytes[versionToExtractOffset] = 0x77;
+ allZipBytes[versionToExtractOffset + 1] = 0x77;
+ Files.write(allZipBytes, zipFile);
+
+ /*
+ * Opening the file should log an error message.
+ */
+ ZFileOptions options = new ZFileOptions();
+ options.setVerifyLogFactory(VerifyLogs::unlimited);
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ VerifyLog vl = zf.getVerifyLog();
+ assertTrue(vl.getLogs().isEmpty());
+ StoredEntry fooEntry = zf.get("foo");
+ vl = fooEntry.getVerifyLog();
+ assertEquals(1, vl.getLogs().size());
+ assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
+ assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
+ }
+ }
+
+ @Test
+ public void sortZipContentsWithDeferredCrc32() throws Exception {
+ /*
+ * Create a zip file with deferred CRC32 and files in non-alphabetical order.
+ * ZipOutputStream always creates deferred CRC32 entries.
+ */
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ zos.putNextEntry(new ZipEntry("b"));
+ zos.write(new byte[1000]);
+ zos.putNextEntry(new ZipEntry("a"));
+ zos.write(new byte[1000]);
+ }
+
+ /*
+ * Now open the zip using a ZFile and sort the contents and check that the deferred CRC32
+ * bits were reset.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry a = zf.get("a");
+ assertNotNull(a);
+ assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType());
+ StoredEntry b = zf.get("b");
+ assertNotNull(b);
+ assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType());
+ assertTrue(
+ a.getCentralDirectoryHeader().getOffset()
+ > b.getCentralDirectoryHeader().getOffset());
+
+ zf.sortZipContents();
+ zf.update();
+
+ a = zf.get("a");
+ assertNotNull(a);
+ assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType());
+ b = zf.get("b");
+ assertNotNull(b);
+ assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType());
+
+ assertTrue(
+ a.getCentralDirectoryHeader().getOffset()
+ < b.getCentralDirectoryHeader().getOffset());
+ }
+
+ /*
+ * Open the file again and check there are no warnings.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ VerifyLog vl = zf.getVerifyLog();
+ assertEquals(0, vl.getLogs().size());
+
+ StoredEntry a = zf.get("a");
+ assertNotNull(a);
+ vl = a.getVerifyLog();
+ assertEquals(0, vl.getLogs().size());
+
+ StoredEntry b = zf.get("b");
+ assertNotNull(b);
+ vl = b.getVerifyLog();
+ assertEquals(0, vl.getLogs().size());
+ }
+ }
+
+ @Test
+ public void alignZipContentsWithDeferredCrc32() throws Exception {
+ /*
+ * Create an unaligned zip file with deferred CRC32 and files in non-alphabetical order.
+ * We need an uncompressed file to make realigning have any effect.
+ */
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ zos.putNextEntry(new ZipEntry("x"));
+ zos.write(new byte[1000]);
+ zos.putNextEntry(new ZipEntry("y"));
+ zos.write(new byte[1000]);
+ ZipEntry zEntry = new ZipEntry("z");
+ zEntry.setSize(1000);
+ zEntry.setMethod(ZipEntry.STORED);
+ zEntry.setCrc(Hashing.crc32().hashBytes(new byte[1000]).asInt());
+ zos.putNextEntry(zEntry);
+ zos.write(new byte[1000]);
+ }
+
+ /*
+ * Now open the zip using a ZFile and realign the contents and check that the deferred CRC32
+ * bits were reset.
+ */
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constant(2000));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ StoredEntry x = zf.get("x");
+ assertNotNull(x);
+ assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType());
+ StoredEntry y = zf.get("y");
+ assertNotNull(y);
+ assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType());
+ StoredEntry z = zf.get("z");
+ assertNotNull(z);
+ assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType());
+
+ zf.realign();
+ zf.update();
+
+ x = zf.get("x");
+ assertNotNull(x);
+ assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType());
+ y = zf.get("y");
+ assertNotNull(y);
+ assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType());
+ z = zf.get("z");
+ assertNotNull(z);
+ assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType());
+ }
+ }
+
+ @Test
+ public void openingZFileDoesNotRemoveDataDescriptors() throws Exception {
+ /*
+ * Create a zip file with deferred CRC32.
+ */
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ zos.putNextEntry(new ZipEntry("a"));
+ zos.write(new byte[1000]);
+ }
+
+ /*
+ * Open using ZFile and check that the deferred CRC32 is there.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry se = zf.get("a");
+ assertNotNull(se);
+ assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType());
+ }
+
+ /*
+ * Open using ZFile (again) and check that the deferred CRC32 is there.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry se = zf.get("a");
+ assertNotNull(se);
+ assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType());
+ }
+ }
+
+ @Test
+ public void zipCommentsAreSaved() throws Exception {
+ File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFileWithComments))) {
+ zos.setComment("foo");
+ }
+
+ /*
+ * Open the zip file and check the comment is there.
+ */
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ byte[] comment = zf.getEocdComment();
+ assertArrayEquals(new byte[] { 'f', 'o', 'o' }, comment);
+
/*
- * Nothing to do.
+ * Modify the comment and write the file.
*/
+ zf.setEocdComment(new byte[] { 'b', 'a', 'r', 'r' });
+ }
+
+ /*
+ * Open the file and see that the comment is there (both with java and zfile).
+ */
+ try (ZipFile zf2 = new ZipFile(zipFileWithComments)) {
+ assertEquals("barr", zf2.getComment());
+ }
+
+ try (ZFile zf3 = new ZFile(zipFileWithComments)) {
+ assertArrayEquals(new byte[] { 'b', 'a', 'r', 'r' }, zf3.getEocdComment());
+ }
+ }
+
+ @Test
+ public void eocdCommentsWithMoreThan64kNotAllowed() throws Exception {
+ File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ try {
+ zf.setEocdComment(new byte[65536]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ zf.setEocdComment(new byte[65535]);
+ }
+ }
+
+ @Test
+ public void eocdCommentsWithTheEocdMarkerAreAllowed() throws Exception {
+ File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+ byte[] data = new byte[100];
+ data[50] = 0x50; // Signature
+ data[51] = 0x4b;
+ data[52] = 0x05;
+ data[53] = 0x06;
+ data[54] = 0x00; // Number of disk
+ data[55] = 0x00;
+ data[56] = 0x00; // Disk CD start
+ data[57] = 0x00;
+ data[54] = 0x01; // Total records 1
+ data[55] = 0x00;
+ data[56] = 0x02; // Total records 2, must be = to total records 1
+ data[57] = 0x00;
+
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ zf.setEocdComment(data);
+ }
+
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ assertArrayEquals(data, zf.getEocdComment());
+ }
+ }
+
+ @Test
+ public void eocdCommentsWithTheEocdMarkerThatAreInvalidAreNotAllowed() throws Exception {
+ File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+ byte[] data = new byte[100];
+ data[50] = 0x50;
+ data[51] = 0x4b;
+ data[52] = 0x05;
+ data[53] = 0x06;
+ data[67] = 0x00;
+
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ try {
+ zf.setEocdComment(data);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+ }
+
+ @Test
+ public void zipCommentsArePreservedWithFileChanges() throws Exception {
+ File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+ byte[] comment = new byte[] { 1, 3, 4 };
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ zf.add("foo", new ByteArrayInputStream(new byte[50]));
+ zf.setEocdComment(comment);
+ }
+
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ assertArrayEquals(comment, zf.getEocdComment());
+ zf.add("bar", new ByteArrayInputStream(new byte[100]));
+ }
+
+ try (ZFile zf = new ZFile(zipFileWithComments)) {
+ assertArrayEquals(comment, zf.getEocdComment());
+ }
+ }
+
+ @Test
+ public void overlappingZipEntries() throws Exception {
+ File myZip = ZipTestUtils.cloneRsrc("overlapping.zip", mTemporaryFolder);
+ try (ZFile zf = new ZFile(myZip)) {
+ fail();
+ } catch (IOException e) {
+ assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/bbb"));
+ assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd"));
+ assertFalse(Throwables.getStackTraceAsString(e).contains("Central Directory"));
+ }
+ }
+
+ @Test
+ public void overlappingZipEntryWithCentralDirectory() throws Exception {
+ File myZip = ZipTestUtils.cloneRsrc("overlapping2.zip", mTemporaryFolder);
+ try (ZFile zf = new ZFile(myZip)) {
+ fail();
+ } catch (IOException e) {
+ assertFalse(Throwables.getStackTraceAsString(e).contains("overlapping/bbb"));
+ assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd"));
+ assertTrue(Throwables.getStackTraceAsString(e).contains("Central Directory"));
+ }
+ }
+
+ @Test
+ public void readFileWithOffsetBeyondFileEnd() throws Exception {
+ File myZip = ZipTestUtils.cloneRsrc("entry-outside-file.zip", mTemporaryFolder);
+ try (ZFile zf = new ZFile(myZip)) {
+ fail();
+ } catch (IOException e) {
+ assertTrue(Throwables.getStackTraceAsString(e).contains("entry-outside-file/foo"));
+ assertTrue(Throwables.getStackTraceAsString(e).contains("EOF"));
}
}
}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTestConstants.java b/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
similarity index 95%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileTestConstants.java
rename to src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
index 40189cb..fbf5739 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTestConstants.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
/**
* Constants used in tests.
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZipMergeTest.java b/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
similarity index 92%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZipMergeTest.java
rename to src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
index fb29395..0090bc7 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZipMergeTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -23,22 +23,19 @@
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import com.android.builder.internal.utils.CachedFileContents;
+import com.android.apkzlib.utils.CachedFileContents;
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
-import com.google.common.io.Files;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
public class ZipMergeTest {
@Rule
@@ -96,7 +93,7 @@
public void mergeZipWithDeferredCrc() throws Exception {
File foo = mTemporaryFolder.newFile("foo");
- byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
+ byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html");
try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
fooOut.putNextEntry(new ZipEntry("w"));
@@ -127,8 +124,8 @@
public void mergeZipKeepsDeflatedAndStored() throws Exception {
File foo = mTemporaryFolder.newFile("foo");
- byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
- byte[] lBytes = Files.toByteArray(ZipTestUtils.rsrcFile("images/lena.png"));
+ byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html");
+ byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png");
try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
fooOut.putNextEntry(new ZipEntry("w"));
@@ -175,8 +172,8 @@
public void mergeZipWithSorting() throws Exception {
File foo = mTemporaryFolder.newFile("foo");
- byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
- byte[] lBytes = Files.toByteArray(ZipTestUtils.rsrcFile("images/lena.png"));
+ byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html");
+ byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png");
try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
fooOut.putNextEntry(new ZipEntry("w"));
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZipTestUtils.java b/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
similarity index 71%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZipTestUtils.java
rename to src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
index 7d2e6df..aaff18b 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZipTestUtils.java
+++ b/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
@@ -14,19 +14,16 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
import com.google.common.io.Files;
-
-import org.junit.rules.TemporaryFolder;
-
import java.io.File;
import java.io.IOException;
+import javax.annotation.Nonnull;
+import org.junit.rules.TemporaryFolder;
/**
* Utility method for zip tests.
@@ -34,19 +31,15 @@
class ZipTestUtils {
/**
- * Obtains the file with a resource with the given name. This is a file that lays in
- * the packaging subdirectory of test resources.
+ * Obtains the data of a resource with the given name.
*
* @param rsrcName the resource name inside packaging resource folder
- * @return the resource file, guaranteed to exist
+ * @return the resource data
+ * @throws IOException I/O failed
*/
- @NonNull
- static File rsrcFile(@NonNull String rsrcName) {
- File packagingRoot = ApkZFileTestUtils.getResource("/testData/packaging");
- String rsrcPath = packagingRoot.getAbsolutePath() + "/" + rsrcName;
- File rsrcFile = new File(rsrcPath);
- assertTrue(rsrcFile.isFile());
- return rsrcFile;
+ @Nonnull
+ static byte[] rsrcBytes(@Nonnull String rsrcName) throws IOException {
+ return ApkZFileTestUtils.getResourceBytes("/testData/packaging/" + rsrcName).read();
}
/**
@@ -59,7 +52,7 @@
* @return the file that was created with the resource
* @throws IOException failed to clone the resource
*/
- static File cloneRsrc(@NonNull String rsrcName, @NonNull TemporaryFolder folder)
+ static File cloneRsrc(@Nonnull String rsrcName, @Nonnull TemporaryFolder folder)
throws IOException {
String cloneName;
if (rsrcName.contains("/")) {
@@ -83,12 +76,15 @@
* @return the file that was created with the resource
* @throws IOException failed to clone the resource
*/
- static File cloneRsrc(@NonNull String rsrcName, @NonNull TemporaryFolder folder,
- @NonNull String cloneName) throws IOException {
+ static File cloneRsrc(
+ @Nonnull String rsrcName,
+ @Nonnull TemporaryFolder folder,
+ @Nonnull String cloneName)
+ throws IOException {
File result = new File(folder.getRoot(), cloneName);
assertFalse(result.exists());
- Files.copy(rsrcFile(rsrcName), result);
+ Files.write(rsrcBytes(rsrcName), result);
return result;
}
}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZipToolsTest.java b/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
similarity index 84%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZipToolsTest.java
rename to src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
index c0bd788..e45f117 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZipToolsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -22,31 +22,28 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
-
-import org.junit.Assume;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
import java.io.ByteArrayInputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class ZipToolsTest {
@@ -70,7 +67,7 @@
public String mName;
@Rule
- @NonNull
+ @Nonnull
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
@Parameterized.Parameters(name = "{4} {index}")
@@ -103,11 +100,11 @@
private File cloneZipFile() throws Exception {
File zfile = mTemporaryFolder.newFile("file.zip");
- Files.copy(ZipTestUtils.rsrcFile(mZipFile), zfile);
+ Files.write(ZipTestUtils.rsrcBytes(mZipFile), zfile);
return zfile;
}
- private static void assertFileInZip(@NonNull ZFile zfile, @NonNull String name) throws Exception {
+ private static void assertFileInZip(@Nonnull ZFile zfile, @Nonnull String name) throws Exception {
StoredEntry root = zfile.get(name);
assertNotNull(root);
@@ -115,7 +112,7 @@
byte[] inZipData = ByteStreams.toByteArray(is);
is.close();
- byte[] inFileData = Files.toByteArray(ZipTestUtils.rsrcFile(name));
+ byte[] inFileData = ZipTestUtils.rsrcBytes(name);
assertArrayEquals(inFileData, inZipData);
}
@@ -156,14 +153,18 @@
File zfile = new File (mTemporaryFolder.getRoot(), "zfile.zip");
try (ZFile zf = new ZFile(zfile, options)) {
- zf.add("root", new FileInputStream(ZipTestUtils.rsrcFile("root")));
+ zf.add("root", new ByteArrayInputStream(ZipTestUtils.rsrcBytes("root")));
zf.add("images/", new ByteArrayInputStream(new byte[0]));
- zf.add("images/lena.png", new FileInputStream(ZipTestUtils.rsrcFile("images/lena.png")));
+ zf.add(
+ "images/lena.png",
+ new ByteArrayInputStream(ZipTestUtils.rsrcBytes("images/lena.png")));
zf.add("text-files/", new ByteArrayInputStream(new byte[0]));
- zf.add("text-files/rfc2460.txt", new FileInputStream(
- ZipTestUtils.rsrcFile("text-files/rfc2460.txt")));
- zf.add("text-files/wikipedia.html",
- new FileInputStream(ZipTestUtils.rsrcFile("text-files/wikipedia.html")));
+ zf.add(
+ "text-files/rfc2460.txt",
+ new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/rfc2460.txt")));
+ zf.add(
+ "text-files/wikipedia.html",
+ new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/wikipedia.html")));
}
List<String> command = Lists.newArrayList(mUnzipCommand);
@@ -200,13 +201,13 @@
assertSize(new String[] { "images/", "images" }, 0, sizes);
assertSize(new String[] { "text-files/", "text-files"}, 0, sizes);
- assertSize(new String[] { "root" }, ZipTestUtils.rsrcFile("root").length(), sizes);
+ assertSize(new String[] { "root" }, ZipTestUtils.rsrcBytes("root").length, sizes);
assertSize(new String[] { "images/lena.png", "images\\lena.png" },
- ZipTestUtils.rsrcFile("images/lena.png").length(), sizes);
+ ZipTestUtils.rsrcBytes("images/lena.png").length, sizes);
assertSize(new String[] { "text-files/rfc2460.txt", "text-files\\rfc2460.txt" },
- ZipTestUtils.rsrcFile("text-files/rfc2460.txt").length(), sizes);
+ ZipTestUtils.rsrcBytes("text-files/rfc2460.txt").length, sizes);
assertSize(new String[] { "text-files/wikipedia.html", "text-files\\wikipedia.html" },
- ZipTestUtils.rsrcFile("text-files/wikipedia.html").length(), sizes);
+ ZipTestUtils.rsrcBytes("text-files/wikipedia.html").length, sizes);
}
private static void assertSize(String[] names, long size, Map<String, Integer> sizes) {
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/compress/MultiCompressorTest.java b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
similarity index 81%
rename from src/test/java/com/android/builder/internal/packaging/zip/compress/MultiCompressorTest.java
rename to src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
index 2fc7bf7..4f2eaf0 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/compress/MultiCompressorTest.java
+++ b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
@@ -14,19 +14,18 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import com.android.builder.internal.packaging.zip.CentralDirectoryHeaderCompressInfo;
-import com.android.builder.internal.packaging.zip.CompressionMethod;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
-import com.google.common.io.Files;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.android.apkzlib.zip.CentralDirectoryHeaderCompressInfo;
+import com.android.apkzlib.zip.CompressionMethod;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
import com.google.common.util.concurrent.MoreExecutors;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -40,11 +39,9 @@
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
private static byte[] getCompressibleData() throws Exception {
- File textFiles = ApkZFileTestUtils.getResource("/testData/packaging/text-files");
- assertTrue(textFiles.isDirectory());
- File wikipediaFile = new File(textFiles, "wikipedia.html");
- assertTrue(wikipediaFile.isFile());
- return Files.asByteSource(wikipediaFile).read();
+ return ApkZFileTestUtils
+ .getResourceBytes("/testData/packaging/text-files/wikipedia.html")
+ .read();
}
private static byte[] compress(byte[] data, int level) throws Exception {
@@ -109,8 +106,9 @@
File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip");
ZFileOptions resultOptions = new ZFileOptions();
- resultOptions.setCompressor(new BestAndDefaultDeflateExecutorCompressor(
- MoreExecutors.sameThreadExecutor(), resultOptions.getTracker(), ratio + 0.001));
+ resultOptions.setCompressor(
+ new BestAndDefaultDeflateExecutorCompressor(
+ MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio + 0.001));
try (
ZFile defaultZFile = new ZFile(defaultFile);
@@ -138,8 +136,9 @@
File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip");
ZFileOptions resultOptions = new ZFileOptions();
- resultOptions.setCompressor(new BestAndDefaultDeflateExecutorCompressor(
- MoreExecutors.sameThreadExecutor(), resultOptions.getTracker(), ratio - 0.001));
+ resultOptions.setCompressor(
+ new BestAndDefaultDeflateExecutorCompressor(
+ MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio - 0.001));
try (
ZFile defaultZFile = new ZFile(defaultFile);
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtilsTest.java
rename to src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
index 855f39f..3264290 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtilsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertArrayEquals;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
similarity index 96%
rename from src/test/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtilsTest.java
rename to src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
index 4c098c8..012d587 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtilsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
import static org.junit.Assert.assertEquals;
diff --git a/src/test/resources/testData/packaging/AndroidManifest.xml b/src/test/resources/testData/packaging/AndroidManifest.xml
new file mode 100644
index 0000000..060ec31
--- /dev/null
+++ b/src/test/resources/testData/packaging/AndroidManifest.xml
Binary files differ
diff --git a/src/test/resources/testData/packaging/entry-outside-file.zip b/src/test/resources/testData/packaging/entry-outside-file.zip
new file mode 100644
index 0000000..ffd6be9
--- /dev/null
+++ b/src/test/resources/testData/packaging/entry-outside-file.zip
Binary files differ
diff --git a/src/test/resources/testData/packaging/overlapping.zip b/src/test/resources/testData/packaging/overlapping.zip
new file mode 100644
index 0000000..7f6144c
--- /dev/null
+++ b/src/test/resources/testData/packaging/overlapping.zip
Binary files differ
diff --git a/src/test/resources/testData/packaging/overlapping2.zip b/src/test/resources/testData/packaging/overlapping2.zip
new file mode 100644
index 0000000..eecefa9
--- /dev/null
+++ b/src/test/resources/testData/packaging/overlapping2.zip
Binary files differ
diff --git a/src/test/resources/testData/packaging/text-files/.gitattributes b/src/test/resources/testData/packaging/text-files/.gitattributes
new file mode 100644
index 0000000..fa1385d
--- /dev/null
+++ b/src/test/resources/testData/packaging/text-files/.gitattributes
@@ -0,0 +1 @@
+* -text
diff --git a/src/test/resources/testData/packaging/v2-signed.apk b/src/test/resources/testData/packaging/v2-signed.apk
new file mode 100644
index 0000000..7f48475
--- /dev/null
+++ b/src/test/resources/testData/packaging/v2-signed.apk
Binary files differ