Merge "Use Builder pattern for ApkVerifier parameters."
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
index d509a48..f12b47f 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
@@ -23,9 +23,13 @@
 import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
 import com.android.apksigner.core.internal.util.AndroidSdkVersion;
 import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.util.DataSources;
 import com.android.apksigner.core.zip.ZipFormatException;
 
+import java.io.Closeable;
+import java.io.File;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
@@ -42,6 +46,8 @@
  *
  * <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
  * the verifier to be used for checking whether an APK's signatures will verify on Android.
+ *
+ * <p>Use {@link Builder} to obtain instances of this verifier.
  */
 public class ApkVerifier {
 
@@ -49,6 +55,57 @@
     private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
             Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2");
 
+    private final File mApkFile;
+    private final DataSource mApkDataSource;
+
+    private final int mMinSdkVersion;
+    private final int mMaxSdkVersion;
+
+    private ApkVerifier(
+            File apkFile,
+            DataSource apkDataSource,
+            int minSdkVersion,
+            int maxSdkVersion) {
+        mApkFile = apkFile;
+        mApkDataSource = apkDataSource;
+        mMinSdkVersion = minSdkVersion;
+        mMaxSdkVersion = maxSdkVersion;
+    }
+
+    /**
+     * Verifies the APK's signatures and returns the result of verification. The APK can be
+     * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+     * The verification result also includes errors, warnings, and information about signers.
+     *
+     * @throws IOException if an I/O error is encountered while reading the APK
+     * @throws ZipFormatException if the APK is malformed at ZIP format level
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *         required cryptographic algorithm implementation is missing
+     * @throws IllegalStateException if this verifier's configuration is missing required
+     *         information.
+     */
+    public Result verify() throws IOException, ZipFormatException, NoSuchAlgorithmException,
+            IllegalStateException {
+        Closeable in = null;
+        try {
+            DataSource apk;
+            if (mApkDataSource != null) {
+                apk = mApkDataSource;
+            } else if (mApkFile != null) {
+                RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+                in = f;
+                apk = DataSources.asDataSource(f, 0, f.length());
+            } else {
+                throw new IllegalStateException("APK not provided");
+            }
+            return verify(apk, mMinSdkVersion, mMaxSdkVersion);
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+    }
+
     /**
      * Verifies the APK's signatures and returns the result of verification. The APK can be
      * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
@@ -65,7 +122,7 @@
      * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
      *         required cryptographic algorithm implementation is missing
      */
-    public Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion)
+    private static Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion)
             throws IOException, ZipFormatException, NoSuchAlgorithmException {
         if (minSdkVersion < 0) {
             throw new IllegalArgumentException(
@@ -1050,17 +1107,16 @@
      */
     private static class ByteArray {
         private final byte[] mArray;
+        private final int mHashCode;
 
         private ByteArray(byte[] arr) {
             mArray = arr;
+            mHashCode = Arrays.hashCode(mArray);
         }
 
         @Override
         public int hashCode() {
-            final int prime = 31;
-            int result = 1;
-            result = prime * result + Arrays.hashCode(mArray);
-            return result;
+            return mHashCode;
         }
 
         @Override
@@ -1075,10 +1131,103 @@
                 return false;
             }
             ByteArray other = (ByteArray) obj;
+            if (hashCode() != other.hashCode()) {
+                return false;
+            }
             if (!Arrays.equals(mArray, other.mArray)) {
                 return false;
             }
             return true;
         }
     }
+
+    /**
+     * Builder of {@link ApkVerifier} instances.
+     *
+     * <p>Although not required, it is best to provide the SDK version (API Level) of the oldest
+     * Android platform on which the APK is supposed to be installed -- see
+     * {@link #setMinCheckedPlatformVersion(int)}. Without this information, APKs which use security
+     * features not supported on ancient Android platforms (e.g., SHA-256 digests or ECDSA
+     * signatures) will not verify.
+     */
+    public static class Builder {
+        private final File mApkFile;
+        private final DataSource mApkDataSource;
+
+        private int mMinSdkVersion = 1;
+        private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+        /**
+         * Constructs a new {@code Builder} for verifying the provided APK file.
+         */
+        public Builder(File apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkFile = apk;
+            mApkDataSource = null;
+        }
+
+        /**
+         * Constructs a new {@code Builder} for verifying the provided APK.
+         */
+        public Builder(DataSource apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkDataSource = apk;
+            mApkFile = null;
+        }
+
+        /**
+         * Sets the oldest Android platform version for which the APK is verified. APK verification
+         * will confirm that the APK is expected to install successfully on all known Android
+         * platforms starting from the platform version with the provided API Level.
+         *
+         * <p>By default, the APK is checked for all platform versions. Thus, APKs which use
+         * security features not supported on ancient Android platforms (e.g., SHA-256 digests or
+         * ECDSA signatures) will not verify by default.
+         *
+         * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+         *
+         * @see #setCheckedPlatformVersions(int, int)
+         */
+        public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+            mMinSdkVersion = minSdkVersion;
+            mMaxSdkVersion = Integer.MAX_VALUE;
+            return this;
+        }
+
+        /**
+         * Sets the range of Android platform versions for which the APK is verified. APK
+         * verification will confirm that the APK is expected to install successfully on Android
+         * platforms whose API Levels fall into this inclusive range.
+         *
+         * <p>By default, the APK is checked for all platform versions. Thus, APKs which use
+         * security features not supported on ancient Android platforms (e.g., SHA-256 digests or
+         * ECDSA signatures) will not verify by default.
+         *
+         * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+         * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+         *
+         * @see #setMinCheckedPlatformVersion(int)
+         */
+        public Builder setCheckedPlatformVersions(int minSdkVersion, int maxSdkVersion) {
+            mMinSdkVersion = minSdkVersion;
+            mMaxSdkVersion = maxSdkVersion;
+            return this;
+        }
+
+        /**
+         * Returns an {@link ApkVerifier} initialized according to the configuration of this
+         * builder.
+         */
+        public ApkVerifier build() {
+            return new ApkVerifier(
+                    mApkFile,
+                    mApkDataSource,
+                    mMinSdkVersion,
+                    mMaxSdkVersion);
+        }
+    }
 }
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java
new file mode 100644
index 0000000..208033d
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java
@@ -0,0 +1,165 @@
+/*
+ * 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.apksigner.core.internal.util;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+
+/**
+ * {@link DataSource} backed by a {@link RandomAccessFile}.
+ */
+public class RandomAccessFileDataSource implements DataSource {
+
+    private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+    private final RandomAccessFile mFile;
+    private final long mOffset;
+    private final long mSize;
+
+    /**
+     * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
+     * specified the whole file. Changes to the contents of the file, including the size of the
+     * file, will be visible in this data source.
+     */
+    public RandomAccessFileDataSource(RandomAccessFile file) {
+        mFile = file;
+        mOffset = 0;
+        mSize = -1;
+    }
+
+    /**
+     * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
+     * specified region of the provided file. Changes to the contents of the file will be visible in
+     * this data source.
+     */
+    public RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) {
+        if (offset < 0) {
+            throw new IllegalArgumentException("offset: " + size);
+        }
+        if (size < 0) {
+            throw new IllegalArgumentException("size: " + size);
+        }
+        mFile = file;
+        mOffset = offset;
+        mSize = size;
+    }
+
+    @Override
+    public long size() {
+        if (mSize == -1) {
+            try {
+                return mFile.length();
+            } catch (IOException e) {
+                return 0;
+            }
+        } else {
+            return mSize;
+        }
+    }
+
+    @Override
+    public RandomAccessFileDataSource slice(long offset, long size) {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if ((offset == 0) && (size == sourceSize)) {
+            return this;
+        }
+
+        return new RandomAccessFileDataSource(mFile, mOffset + offset, size);
+    }
+
+    @Override
+    public void feed(long offset, long size, DataSink sink) throws IOException {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if (size == 0) {
+            return;
+        }
+
+        long chunkOffsetInFile = mOffset + 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);
+            synchronized (mFile) {
+                mFile.seek(chunkOffsetInFile);
+                mFile.readFully(buf, 0, chunkSize);
+            }
+            sink.consume(buf, 0, chunkSize);
+            chunkOffsetInFile += chunkSize;
+            remaining -= chunkSize;
+        }
+    }
+
+    @Override
+    public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if (size == 0) {
+            return;
+        }
+
+        long offsetInFile = mOffset + offset;
+        int remaining = size;
+        FileChannel fileChannel = mFile.getChannel();
+        while (remaining > 0) {
+            int chunkSize;
+            synchronized (mFile) {
+                fileChannel.position(offsetInFile);
+                chunkSize = fileChannel.read(dest);
+            }
+            offsetInFile += chunkSize;
+            remaining -= chunkSize;
+        }
+    }
+
+    @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) {
+        if (offset < 0) {
+            throw new IllegalArgumentException("offset: " + offset);
+        }
+        if (size < 0) {
+            throw new IllegalArgumentException("size: " + size);
+        }
+        if (offset > sourceSize) {
+            throw new IllegalArgumentException(
+                    "offset (" + offset + ") > source size (" + sourceSize + ")");
+        }
+        long endOffset = offset + size;
+        if (endOffset < offset) {
+            throw new IllegalArgumentException(
+                    "offset (" + offset + ") + size (" + size + ") overflow");
+        }
+        if (endOffset > sourceSize) {
+            throw new IllegalArgumentException(
+                    "offset (" + offset + ") + size (" + size
+                            + ") > source size (" + sourceSize  +")");
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
index 6ce0ac8..1cbb0af 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
@@ -1,7 +1,9 @@
 package com.android.apksigner.core.util;
 
 import com.android.apksigner.core.internal.util.ByteBufferDataSource;
+import com.android.apksigner.core.internal.util.RandomAccessFileDataSource;
 
+import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 
 /**
@@ -21,4 +23,26 @@
         }
         return new ByteBufferDataSource(buffer);
     }
+
+    /**
+     * Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the
+     * file, including changes to size of file, will be visible in the data source.
+     */
+    public static DataSource asDataSource(RandomAccessFile file) {
+        if (file == null) {
+            throw new NullPointerException();
+        }
+        return new RandomAccessFileDataSource(file);
+    }
+
+    /**
+     * Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}.
+     * Changes to the file will be visible in the data source.
+     */
+    public static DataSource asDataSource(RandomAccessFile file, long offset, long size) {
+        if (file == null) {
+            throw new NullPointerException();
+        }
+        return new RandomAccessFileDataSource(file, offset, size);
+    }
 }