ASN.1 BER data value reader am: 9d5a125db8 am: 2072659ac3 am: 52806410b9
am: 169cfa1d6a
Change-Id: I20fe1947f97c0ef16805da7a9c470d67e4f6cb2f
diff --git a/BUILD b/BUILD
index a836f2f..10d3c0e 100644
--- a/BUILD
+++ b/BUILD
@@ -24,9 +24,10 @@
)
java_test(
- name = "ApkUtilsTest",
- srcs = [
- "src/test/java/com/android/apksig/apk/ApkUtilsTest.java",
- ],
+ name = "all",
+ srcs = glob([
+ "src/test/java/com/android/apksig/**/*.java",
+ ]),
+ test_class = "com.android.apksig.AllTests",
deps = [":apksig"],
)
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java
new file mode 100644
index 0000000..f5604ff
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java
@@ -0,0 +1,115 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
+ */
+public class BerDataValue {
+ private final ByteBuffer mEncoded;
+ private final ByteBuffer mEncodedContents;
+ private final int mTagClass;
+ private final boolean mConstructed;
+ private final int mTagNumber;
+
+ BerDataValue(
+ ByteBuffer encoded,
+ ByteBuffer encodedContents,
+ int tagClass,
+ boolean constructed,
+ int tagNumber) {
+ mEncoded = encoded;
+ mEncodedContents = encodedContents;
+ mTagClass = tagClass;
+ mConstructed = constructed;
+ mTagNumber = tagNumber;
+ }
+
+ /**
+ * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
+ * constants.
+ */
+ public int getTagClass() {
+ return mTagClass;
+ }
+
+ /**
+ * Returns {@code true} if the content octets of this data value are the complete BER encoding
+ * of one or more data values, {@code false} if the content octets of this data value directly
+ * represent the value.
+ */
+ public boolean isConstructed() {
+ return mConstructed;
+ }
+
+ /**
+ * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
+ * constants.
+ */
+ public int getTagNumber() {
+ return mTagNumber;
+ }
+
+ /**
+ * Returns the encoded form of this data value.
+ */
+ public ByteBuffer getEncoded() {
+ return mEncoded.slice();
+ }
+
+ /**
+ * Returns the encoded contents of this data value.
+ */
+ public ByteBuffer getEncodedContents() {
+ return mEncodedContents.slice();
+ }
+
+ /**
+ * Returns a new reader of the contents of this data value.
+ */
+ public BerDataValueReader contentsReader() {
+ return new ByteBufferBerDataValueReader(getEncodedContents());
+ }
+
+ /**
+ * Returns a new reader which returns just this data value. This may be useful for re-reading
+ * this value in different contexts.
+ */
+ public BerDataValueReader dataValueReader() {
+ return new ParsedValueReader(this);
+ }
+
+ private static final class ParsedValueReader implements BerDataValueReader {
+ private final BerDataValue mValue;
+ private boolean mValueOutput;
+
+ public ParsedValueReader(BerDataValue value) {
+ mValue = value;
+ }
+
+ @Override
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ if (mValueOutput) {
+ return null;
+ }
+ mValueOutput = true;
+ return mValue;
+ }
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java
new file mode 100644
index 0000000..11ef6c3
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+/**
+ * Indicates that an ASN.1 data value being read could not be decoded using
+ * Basic Encoding Rules (BER).
+ */
+public class BerDataValueFormatException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public BerDataValueFormatException(String message) {
+ super(message);
+ }
+
+ public BerDataValueFormatException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java
new file mode 100644
index 0000000..eb2f383
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java
@@ -0,0 +1,34 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+/**
+ * Reader of ASN.1 Basic Encoding Rules (BER) data values.
+ *
+ * <p>BER data value reader returns data values, one by one, from a source. The interpretation of
+ * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
+ * the elements of a SEQUENCE value) is left to clients of the reader.
+ */
+public interface BerDataValueReader {
+
+ /**
+ * Returns the next data value or {@code null} if end of input has been reached.
+ *
+ * @throws BerDataValueFormatExcepton if the value being read is malformed.
+ */
+ BerDataValue readDataValue() throws BerDataValueFormatException;
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
new file mode 100644
index 0000000..f893a65
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
@@ -0,0 +1,124 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
+ */
+public abstract class BerEncoding {
+ private BerEncoding() {}
+
+ /**
+ * Constructed vs primitive flag in the first identifier byte.
+ */
+ public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
+
+ /**
+ * Tag class: UNIVERSAL
+ */
+ public static final int TAG_CLASS_UNIVERSAL = 0;
+
+ /**
+ * Tag class: APPLICATION
+ */
+ public static final int TAG_CLASS_APPLICATION = 1;
+
+ /**
+ * Tag class: CONTEXT SPECIFIC
+ */
+ public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
+
+ /**
+ * Tag class: PRIVATE
+ */
+ public static final int TAG_CLASS_PRIVATE = 3;
+
+ /**
+ * Tag number: INTEGER
+ */
+ public static final int TAG_NUMBER_INTEGER = 0x2;
+
+ /**
+ * Tag number: OCTET STRING
+ */
+ public static final int TAG_NUMBER_OCTET_STRING = 0x4;
+
+ /**
+ * Tag number: NULL
+ */
+ public static final int TAG_NUMBER_NULL = 0x05;
+
+ /**
+ * Tag number: OBJECT IDENTIFIER
+ */
+ public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
+
+ /**
+ * Tag number: SEQUENCE
+ */
+ public static final int TAG_NUMBER_SEQUENCE = 0x10;
+
+ /**
+ * Tag number: SET
+ */
+ public static final int TAG_NUMBER_SET = 0x11;
+
+ public static String tagNumberToString(int tagNumber) {
+ switch (tagNumber) {
+ case TAG_NUMBER_INTEGER:
+ return "INTEGER";
+ case TAG_NUMBER_OCTET_STRING:
+ return "OCTET STRING";
+ case TAG_NUMBER_NULL:
+ return "NULL";
+ case TAG_NUMBER_OBJECT_IDENTIFIER:
+ return "OBJECT IDENTIFIER";
+ case TAG_NUMBER_SEQUENCE:
+ return "SEQUENCE";
+ case TAG_NUMBER_SET:
+ return "SET";
+ default:
+ return "0x" + Integer.toHexString(tagNumber);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the provided first identifier byte indicates that the data value uses
+ * constructed encoding for its contents, or {@code false} if the data value uses primitive
+ * encoding for its contents.
+ */
+ public static boolean isConstructed(byte firstIdentifierByte) {
+ return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0;
+ }
+
+ /**
+ * Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
+ * constants.
+ */
+ public static int getTagClass(byte firstIdentifierByte) {
+ return (firstIdentifierByte & 0xff) >> 6;
+ }
+
+ /**
+ * Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
+ * constants.
+ */
+ public static int getTagNumber(byte firstIdentifierByte) {
+ return firstIdentifierByte & 0x1f;
+ }
+}
+
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java b/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java
new file mode 100644
index 0000000..adf2a25
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java
@@ -0,0 +1,182 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class ByteBufferBerDataValueReader implements BerDataValueReader {
+ private final ByteBuffer mBuf;
+
+ public ByteBufferBerDataValueReader(ByteBuffer buf) {
+ if (buf == null) {
+ throw new NullPointerException("buf == null");
+ }
+ mBuf = buf;
+ }
+
+ @Override
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ int startPosition = mBuf.position();
+ if (!mBuf.hasRemaining()) {
+ return null;
+ }
+ byte firstIdentifierByte = mBuf.get();
+ int tagNumber = readTagNumber(firstIdentifierByte);
+
+ if (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Missing length");
+ }
+ int firstLengthByte = mBuf.get() & 0xff;
+ int contentsLength;
+ int contentsOffsetInTag;
+ if ((firstLengthByte & 0x80) == 0) {
+ // short form length
+ contentsLength = readShortFormLength(firstLengthByte);
+ contentsOffsetInTag = mBuf.position() - startPosition;
+ skipDefiniteLengthContents(contentsLength);
+ } else if (firstLengthByte != 0x80) {
+ // long form length
+ contentsLength = readLongFormLength(firstLengthByte);
+ contentsOffsetInTag = mBuf.position() - startPosition;
+ skipDefiniteLengthContents(contentsLength);
+ } else {
+ // indefinite length -- value ends with 0x00 0x00
+ contentsOffsetInTag = mBuf.position() - startPosition;
+ contentsLength = skipIndefiniteLengthContents();
+ }
+
+ // Create the encoded data value ByteBuffer
+ int endPosition = mBuf.position();
+ mBuf.position(startPosition);
+ int bufOriginalLimit = mBuf.limit();
+ mBuf.limit(endPosition);
+ ByteBuffer encoded = mBuf.slice();
+ mBuf.position(mBuf.limit());
+ mBuf.limit(bufOriginalLimit);
+
+ // Create the encoded contents ByteBuffer
+ encoded.position(contentsOffsetInTag);
+ encoded.limit(contentsOffsetInTag + contentsLength);
+ ByteBuffer encodedContents = encoded.slice();
+ encoded.clear();
+
+ return new BerDataValue(
+ encoded,
+ encodedContents,
+ BerEncoding.getTagClass(firstIdentifierByte),
+ BerEncoding.isConstructed(firstIdentifierByte),
+ tagNumber);
+ }
+
+ private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
+ int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
+ if (tagNumber == 0x1f) {
+ // high-tag-number form, where the tag number follows this byte in base-128
+ // big-endian form, where each byte has the highest bit set, except for the last
+ // byte
+ return readHighTagNumber();
+ } else {
+ // low-tag-number form
+ return tagNumber;
+ }
+ }
+
+ private int readHighTagNumber() throws BerDataValueFormatException {
+ // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+ // byte
+ int b;
+ int result = 0;
+ do {
+ if (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Truncated tag number");
+ }
+ b = mBuf.get();
+ if (result > Integer.MAX_VALUE >>> 7) {
+ throw new BerDataValueFormatException("Tag number too large");
+ }
+ result <<= 7;
+ result |= b & 0x7f;
+ } while ((b & 0x80) != 0);
+ return result;
+ }
+
+ private int readShortFormLength(int firstLengthByte) throws BerDataValueFormatException {
+ return firstLengthByte & 0x7f;
+ }
+
+ private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException {
+ // The low 7 bits of the first byte represent the number of bytes (following the first
+ // byte) in which the length is in big-endian base-256 form
+ int byteCount = firstLengthByte & 0x7f;
+ if (byteCount > 4) {
+ throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+ }
+ int result = 0;
+ for (int i = 0; i < byteCount; i++) {
+ if (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Truncated length");
+ }
+ int b = mBuf.get();
+ if (result > Integer.MAX_VALUE >>> 8) {
+ throw new BerDataValueFormatException("Length too large");
+ }
+ result <<= 8;
+ result |= b & 0xff;
+ }
+ return result;
+ }
+
+ private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
+ if (mBuf.remaining() < contentsLength) {
+ throw new BerDataValueFormatException(
+ "Truncated contents. Need: " + contentsLength + " bytes, available: "
+ + mBuf.remaining());
+ }
+ mBuf.position(mBuf.position() + contentsLength);
+ }
+
+ private int skipIndefiniteLengthContents() throws BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00
+ boolean prevZeroByte = false;
+ int bytesRead = 0;
+ while (true) {
+ if (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+
+ }
+ int b = mBuf.get();
+ bytesRead++;
+ if (bytesRead < 0) {
+ throw new BerDataValueFormatException("Indefinite-length contents too long");
+ }
+ if (b == 0) {
+ if (prevZeroByte) {
+ // End of contents reached -- we've read the value and its terminator 0x00 0x00
+ return bytesRead - 2;
+ }
+ prevZeroByte = true;
+ } else {
+ prevZeroByte = false;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java b/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java
new file mode 100644
index 0000000..9dfec15
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java
@@ -0,0 +1,271 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class InputStreamBerDataValueReader implements BerDataValueReader {
+ private final InputStream mIn;
+
+ public InputStreamBerDataValueReader(InputStream in) {
+ if (in == null) {
+ throw new NullPointerException("in == null");
+ }
+ mIn = in;
+ }
+
+ @SuppressWarnings("resource")
+ @Override
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ RecordingInputStream in = new RecordingInputStream(mIn);
+
+ try {
+ int firstIdentifierByte = in.read();
+ if (firstIdentifierByte == -1) {
+ // End of input
+ return null;
+ }
+ int tagNumber = readTagNumber(in, firstIdentifierByte);
+
+ int firstLengthByte = in.read();
+ if (firstLengthByte == -1) {
+ throw new BerDataValueFormatException("Missing length");
+ }
+
+ int contentsLength;
+ int contentsOffsetInDataValue;
+ if ((firstLengthByte & 0x80) == 0) {
+ // short form length
+ contentsLength = readShortFormLength(firstLengthByte);
+ contentsOffsetInDataValue = in.getReadByteCount();
+ skipDefiniteLengthContents(in, contentsLength);
+ } else if ((firstLengthByte & 0xff) != 0x80) {
+ // long form length
+ contentsLength = readLongFormLength(in, firstLengthByte);
+ contentsOffsetInDataValue = in.getReadByteCount();
+ skipDefiniteLengthContents(in, contentsLength);
+ } else {
+ // indefinite length
+ contentsOffsetInDataValue = in.getReadByteCount();
+ contentsLength = skipIndefiniteLengthContents(in);
+ }
+
+ byte[] encoded = in.getReadBytes();
+ ByteBuffer encodedContents =
+ ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
+ return new BerDataValue(
+ ByteBuffer.wrap(encoded),
+ encodedContents,
+ BerEncoding.getTagClass((byte) firstIdentifierByte),
+ BerEncoding.isConstructed((byte) firstIdentifierByte),
+ tagNumber);
+ } catch (IOException e) {
+ throw new BerDataValueFormatException("Failed to read data value", e);
+ }
+ }
+
+ private static int readTagNumber(InputStream in, int firstIdentifierByte)
+ throws IOException, BerDataValueFormatException {
+ int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
+ if (tagNumber == 0x1f) {
+ // high-tag-number form
+ return readHighTagNumber(in);
+ } else {
+ // low-tag-number form
+ return tagNumber;
+ }
+ }
+
+ private static int readHighTagNumber(InputStream in)
+ throws IOException, BerDataValueFormatException {
+ // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+ // byte where the highest bit is not set
+ int b;
+ int result = 0;
+ do {
+ b = in.read();
+ if (b == -1) {
+ throw new BerDataValueFormatException("Truncated tag number");
+ }
+ if (result > Integer.MAX_VALUE >>> 7) {
+ throw new BerDataValueFormatException("Tag number too large");
+ }
+ result <<= 7;
+ result |= b & 0x7f;
+ } while ((b & 0x80) != 0);
+ return result;
+ }
+
+ private static int readShortFormLength(int firstLengthByte) throws BerDataValueFormatException {
+ return firstLengthByte & 0x7f;
+ }
+
+ private static int readLongFormLength(InputStream in, int firstLengthByte)
+ throws IOException, BerDataValueFormatException {
+ // The low 7 bits of the first byte represent the number of bytes (following the first
+ // byte) in which the length is in big-endian base-256 form
+ int byteCount = firstLengthByte & 0x7f;
+ if (byteCount > 4) {
+ throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+ }
+ int result = 0;
+ for (int i = 0; i < byteCount; i++) {
+ int b = in.read();
+ if (b == -1) {
+ throw new BerDataValueFormatException("Truncated length");
+ }
+ if (result > Integer.MAX_VALUE >>> 8) {
+ throw new BerDataValueFormatException("Length too large");
+ }
+ result <<= 8;
+ result |= b & 0xff;
+ }
+ return result;
+ }
+
+ private static void skipDefiniteLengthContents(InputStream in, int len)
+ throws IOException, BerDataValueFormatException {
+ long bytesRead = 0;
+ while (len > 0) {
+ int skipped = (int) in.skip(len);
+ if (skipped <= 0) {
+ throw new BerDataValueFormatException(
+ "Truncated definite-length contents: " + bytesRead + " bytes read"
+ + ", " + len + " missing");
+ }
+ len -= skipped;
+ bytesRead += skipped;
+ }
+ }
+
+ private static int skipIndefiniteLengthContents(InputStream in)
+ throws IOException, BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00
+ boolean prevZeroByte = false;
+ int bytesRead = 0;
+ while (true) {
+ int b = in.read();
+ if (b == -1) {
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+ }
+ bytesRead++;
+ if (bytesRead < 0) {
+ throw new BerDataValueFormatException("Indefinite-length contents too long");
+ }
+ if (b == 0) {
+ if (prevZeroByte) {
+ // End of contents reached -- we've read the value and its terminator 0x00 0x00
+ return bytesRead - 2;
+ }
+ prevZeroByte = true;
+ continue;
+ } else {
+ prevZeroByte = false;
+ }
+ }
+ }
+
+ private static class RecordingInputStream extends InputStream {
+ private final InputStream mIn;
+ private final ByteArrayOutputStream mBuf;
+
+ private RecordingInputStream(InputStream in) {
+ mIn = in;
+ mBuf = new ByteArrayOutputStream();
+ }
+
+ public byte[] getReadBytes() {
+ return mBuf.toByteArray();
+ }
+
+ public int getReadByteCount() {
+ return mBuf.size();
+ }
+
+ @Override
+ public int read() throws IOException {
+ int b = mIn.read();
+ if (b != -1) {
+ mBuf.write(b);
+ }
+ return b;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int len = mIn.read(b);
+ if (len > 0) {
+ mBuf.write(b, 0, len);
+ }
+ return len;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ len = mIn.read(b, off, len);
+ if (len > 0) {
+ mBuf.write(b, off, len);
+ }
+ return len;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (n <= 0) {
+ return mIn.skip(n);
+ }
+
+ byte[] buf = new byte[4096];
+ int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
+ if (len > 0) {
+ mBuf.write(buf, 0, len);
+ }
+ return (len < 0) ? 0 : len;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return super.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {}
+
+ @Override
+ public synchronized void reset() throws IOException {
+ throw new IOException("mark/reset not supported");
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+ }
+}
diff --git a/src/test/java/com/android/apksig/AllTests.java b/src/test/java/com/android/apksig/AllTests.java
new file mode 100644
index 0000000..a9f3f5e
--- /dev/null
+++ b/src/test/java/com/android/apksig/AllTests.java
@@ -0,0 +1,27 @@
+/*
+ * 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.apksig;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ com.android.apksig.apk.AllTests.class,
+ com.android.apksig.internal.AllTests.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/apk/AllTests.java b/src/test/java/com/android/apksig/apk/AllTests.java
new file mode 100644
index 0000000..64895ab
--- /dev/null
+++ b/src/test/java/com/android/apksig/apk/AllTests.java
@@ -0,0 +1,26 @@
+/*
+ * 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.apksig.apk;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ ApkUtilsTest.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/AllTests.java b/src/test/java/com/android/apksig/internal/AllTests.java
new file mode 100644
index 0000000..d83df58
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/AllTests.java
@@ -0,0 +1,26 @@
+/*
+ * 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.apksig.internal;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ com.android.apksig.internal.asn1.AllTests.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/asn1/AllTests.java b/src/test/java/com/android/apksig/internal/asn1/AllTests.java
new file mode 100644
index 0000000..9f7265a
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/AllTests.java
@@ -0,0 +1,26 @@
+/*
+ * 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.apksig.internal.asn1;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ com.android.apksig.internal.asn1.ber.AllTests.class
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/AllTests.java b/src/test/java/com/android/apksig/internal/asn1/ber/AllTests.java
new file mode 100644
index 0000000..6916164
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/AllTests.java
@@ -0,0 +1,28 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ BerDataValueTest.class,
+ ByteBufferBerDataValueReaderTest.class,
+ InputStreamBerDataValueReaderTest.class,
+})
+public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueReaderTestBase.java b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueReaderTestBase.java
new file mode 100644
index 0000000..c74a74f
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueReaderTestBase.java
@@ -0,0 +1,256 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import static com.android.apksig.internal.test.MoreAsserts.assertByteBufferEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.android.apksig.internal.test.HexEncoding;
+
+/**
+ * Base class for unit tests of ASN.1 BER (see {@code X.690}) data value reader implementations.
+ *
+ * <p>Subclasses need to provide only an implementation of {@link #createReader(byte[])} and
+ * subclass-specific tests.
+ */
+public abstract class BerDataValueReaderTestBase {
+
+ /**
+ * Returns a new reader initialized with the provided input.
+ */
+ protected abstract BerDataValueReader createReader(byte[] input);
+
+ @Test
+ public void testEmptyInput() throws Exception {
+ assertNull(readDataValue(""));
+ }
+
+ @Test
+ public void testEndOfInput() throws Exception {
+ BerDataValueReader reader = createReader("3000"); // SEQUENCE with empty contents
+ assertNotNull(reader.readDataValue());
+ // End of input has been reached
+ assertNull(reader.readDataValue());
+ // Null should also be returned on consecutive invocations
+ assertNull(reader.readDataValue());
+ }
+
+ @Test
+ public void testSingleByteTagId() throws Exception {
+ BerDataValue dataValue = readDataValue("1000");
+ assertEquals(BerEncoding.TAG_CLASS_UNIVERSAL, dataValue.getTagClass());
+ assertFalse(dataValue.isConstructed());
+ assertEquals(0x10, dataValue.getTagNumber());
+
+ dataValue = readDataValue("3900");
+ assertEquals(BerEncoding.TAG_CLASS_UNIVERSAL, dataValue.getTagClass());
+ assertTrue(dataValue.isConstructed());
+ assertEquals(0x19, dataValue.getTagNumber());
+
+ dataValue = readDataValue("6700");
+ assertEquals(BerEncoding.TAG_CLASS_APPLICATION, dataValue.getTagClass());
+ assertTrue(dataValue.isConstructed());
+ assertEquals(7, dataValue.getTagNumber());
+
+ dataValue = readDataValue("8600");
+ assertEquals(BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC, dataValue.getTagClass());
+ assertFalse(dataValue.isConstructed());
+ assertEquals(6, dataValue.getTagNumber());
+
+ dataValue = readDataValue("fe00");
+ assertEquals(BerEncoding.TAG_CLASS_PRIVATE, dataValue.getTagClass());
+ assertTrue(dataValue.isConstructed());
+ assertEquals(0x1e, dataValue.getTagNumber());
+ }
+
+ @Test
+ public void testHighTagNumber() throws Exception {
+ assertEquals(7, readDataValue("3f0700").getTagNumber());
+ assertEquals(7, readDataValue("3f800700").getTagNumber());
+ assertEquals(7, readDataValue("3f80800700").getTagNumber());
+ assertEquals(7, readDataValue("3f8080800700").getTagNumber());
+ assertEquals(7, readDataValue("3f808080808080808080808080808080800700").getTagNumber());
+ assertEquals(375, readDataValue("3f827700").getTagNumber());
+ assertEquals(268435455, readDataValue("3fffffff7f00").getTagNumber());
+ assertEquals(Integer.MAX_VALUE, readDataValue("3f87ffffff7f00").getTagNumber());
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testHighTagNumberTooLarge() throws Exception {
+ readDataValue("3f888080800000"); // Integer.MAX_VALUE + 1
+ }
+
+ // @Test(expected = BerDataValueFormatException.class)
+ public void testTruncatedHighTagNumberLastOctetMissing() throws Exception {
+ readDataValue("9f80"); // terminating octet must not have the highest bit set
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testTruncatedBeforeFirstLengthOctet() throws Exception {
+ readDataValue("30");
+ }
+
+ @Test
+ public void testShortFormLength() throws Exception {
+ assertByteBufferEquals(new byte[0], readDataValue("3000").getEncodedContents());
+ assertByteBufferEquals(
+ HexEncoding.decode("010203"), readDataValue("3003010203").getEncodedContents());
+ }
+
+ @Test
+ public void testLongFormLength() throws Exception {
+ assertByteBufferEquals(new byte[0], readDataValue("308100").getEncodedContents());
+ assertByteBufferEquals(
+ HexEncoding.decode("010203"), readDataValue("30820003010203").getEncodedContents());
+ assertEquals(
+ 255,
+ readDataValue(concat(HexEncoding.decode("3081ff"), new byte[255]))
+ .getEncodedContents().remaining());
+ assertEquals(
+ 0x110,
+ readDataValue(concat(HexEncoding.decode("30820110"), new byte[0x110]))
+ .getEncodedContents().remaining());
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testTruncatedLongFormLengthBeforeFirstLengthByte() throws Exception {
+ readDataValue("3081");
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testTruncatedLongFormLengthLastLengthByteMissing() throws Exception {
+ readDataValue("308200");
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testLongFormLengthTooLarge() throws Exception {
+ readDataValue("3084ffffffff");
+ }
+
+ @Test
+ public void testIndefiniteFormLength() throws Exception {
+ assertByteBufferEquals(new byte[0], readDataValue("30800000").getEncodedContents());
+ assertByteBufferEquals(
+ HexEncoding.decode("010203"), readDataValue("30800102030000").getEncodedContents());
+ assertByteBufferEquals(
+ HexEncoding.decode(
+ "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"),
+ readDataValue(
+ "3080"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "000102030405060708090a0b0c0d0e0f"
+ + "0000"
+ ).getEncodedContents());
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testDefiniteLengthContentsTruncatedBeforeFirstContentOctet() throws Exception {
+ readDataValue("3001");
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testIndefiniteLengthContentsTruncatedBeforeFirstContentOctet() throws Exception {
+ readDataValue("3080");
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testTruncatedDefiniteLengthContents() throws Exception {
+ readDataValue("30030102");
+ }
+
+ @Test(expected = BerDataValueFormatException.class)
+ public void testTruncatedIndefiniteLengthContents() throws Exception {
+ readDataValue("308001020300");
+ }
+
+ @Test
+ public void testEmptyDefiniteLengthContents() throws Exception {
+ assertByteBufferEquals(new byte[0], readDataValue("3000").getEncodedContents());
+ }
+
+ @Test
+ public void testEmptyIndefiniteLengthContents() throws Exception {
+ assertByteBufferEquals(new byte[0], readDataValue("30800000").getEncodedContents());
+ }
+
+ @Test
+ public void testReadAdvancesPosition() throws Exception {
+ BerDataValueReader reader = createReader("37018f050001020304");
+ assertByteBufferEquals(HexEncoding.decode("37018f"), reader.readDataValue().getEncoded());
+ assertByteBufferEquals(HexEncoding.decode("0500"), reader.readDataValue().getEncoded());
+ assertByteBufferEquals(HexEncoding.decode("01020304"), reader.readDataValue().getEncoded());
+ assertNull(reader.readDataValue());
+ }
+
+ private BerDataValueReader createReader(String hexEncodedInput) {
+ return createReader(HexEncoding.decode(hexEncodedInput));
+ }
+
+ private BerDataValue readDataValue(byte[] input)
+ throws BerDataValueFormatException {
+ return createReader(input).readDataValue();
+ }
+
+ private BerDataValue readDataValue(String hexEncodedInput)
+ throws BerDataValueFormatException {
+ return createReader(hexEncodedInput).readDataValue();
+ }
+
+ private static byte[] concat(byte[] arr1, byte[] arr2) {
+ byte[] result = new byte[arr1.length + arr2.length];
+ System.arraycopy(arr1, 0, result, 0, arr1.length);
+ System.arraycopy(arr2, 0, result, arr1.length, arr2.length);
+ return result;
+ }
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueTest.java b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueTest.java
new file mode 100644
index 0000000..9f40e1e
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/BerDataValueTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import static com.android.apksig.internal.test.MoreAsserts.assertByteBufferEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.ByteBuffer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.android.apksig.internal.test.HexEncoding;
+
+@RunWith(JUnit4.class)
+public class BerDataValueTest {
+ private static final BerDataValue TEST_VALUE1 =
+ new BerDataValue(
+ ByteBuffer.wrap(HexEncoding.decode("aa")),
+ ByteBuffer.wrap(HexEncoding.decode("bb")),
+ BerEncoding.TAG_CLASS_UNIVERSAL,
+ true,
+ BerEncoding.TAG_NUMBER_SEQUENCE);
+
+ private static final BerDataValue TEST_VALUE2 =
+ new BerDataValue(
+ ByteBuffer.wrap(HexEncoding.decode("cc")),
+ ByteBuffer.wrap(HexEncoding.decode("dd")),
+ BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC,
+ false,
+ BerEncoding.TAG_NUMBER_OCTET_STRING);
+
+ @Test
+ public void testGetTagClass() {
+ assertEquals(BerEncoding.TAG_CLASS_UNIVERSAL, TEST_VALUE1.getTagClass());
+ assertEquals(BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC, TEST_VALUE2.getTagClass());
+ }
+
+ @Test
+ public void testIsConstructed() {
+ assertTrue(TEST_VALUE1.isConstructed());
+ assertFalse(TEST_VALUE2.isConstructed());
+ }
+
+ @Test
+ public void testGetTagNumber() {
+ assertEquals(BerEncoding.TAG_NUMBER_SEQUENCE, TEST_VALUE1.getTagNumber());
+ assertEquals(BerEncoding.TAG_NUMBER_OCTET_STRING, TEST_VALUE2.getTagNumber());
+ }
+
+ @Test
+ public void testGetEncoded() {
+ assertByteBufferEquals(HexEncoding.decode("aa"), TEST_VALUE1.getEncoded());
+ assertByteBufferEquals(HexEncoding.decode("cc"), TEST_VALUE2.getEncoded());
+ }
+
+ @Test
+ public void testGetEncodedReturnsSlice() {
+ // Assert that changing the position of returned ByteBuffer does not affect ByteBuffers
+ // returned in the future
+ ByteBuffer encoded = TEST_VALUE1.getEncoded();
+ assertByteBufferEquals(HexEncoding.decode("aa"), encoded);
+ encoded.position(encoded.limit());
+ assertByteBufferEquals(HexEncoding.decode("aa"), TEST_VALUE1.getEncoded());
+ }
+
+ @Test
+ public void testGetEncodedContents() {
+ assertByteBufferEquals(HexEncoding.decode("bb"), TEST_VALUE1.getEncodedContents());
+ assertByteBufferEquals(HexEncoding.decode("dd"), TEST_VALUE2.getEncodedContents());
+ }
+
+ @Test
+ public void testGetEncodedContentsReturnsSlice() {
+ // Assert that changing the position of returned ByteBuffer does not affect ByteBuffers
+ // returned in the future
+ ByteBuffer encoded = TEST_VALUE1.getEncodedContents();
+ assertByteBufferEquals(HexEncoding.decode("bb"), encoded);
+ encoded.position(encoded.limit());
+ assertByteBufferEquals(HexEncoding.decode("bb"), TEST_VALUE1.getEncodedContents());
+ }
+
+ @Test
+ public void testDataValueReader() throws BerDataValueFormatException {
+ BerDataValueReader reader = TEST_VALUE1.dataValueReader();
+ assertSame(TEST_VALUE1, reader.readDataValue());
+ assertNull(reader.readDataValue());
+ assertNull(reader.readDataValue());
+ }
+
+ @Test
+ public void testContentsReader() throws BerDataValueFormatException {
+ BerDataValue dataValue =
+ new BerDataValue(
+ ByteBuffer.allocate(0),
+ ByteBuffer.wrap(HexEncoding.decode("300203040500")),
+ BerEncoding.TAG_CLASS_UNIVERSAL,
+ true,
+ BerEncoding.TAG_NUMBER_SEQUENCE);
+ BerDataValueReader reader = dataValue.contentsReader();
+ assertEquals(ByteBufferBerDataValueReader.class, reader.getClass());
+ assertByteBufferEquals(HexEncoding.decode("30020304"), reader.readDataValue().getEncoded());
+ assertByteBufferEquals(HexEncoding.decode("0500"), reader.readDataValue().getEncoded());
+ assertNull(reader.readDataValue());
+ }
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReaderTest.java b/src/test/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReaderTest.java
new file mode 100644
index 0000000..8875e5c
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReaderTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import java.nio.ByteBuffer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ByteBufferBerDataValueReaderTest extends BerDataValueReaderTestBase {
+
+ @Override
+ protected ByteBufferBerDataValueReader createReader(byte[] input) {
+ return new ByteBufferBerDataValueReader(ByteBuffer.wrap(input));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testConstructWithNullByteBuffer() throws Exception {
+ new ByteBufferBerDataValueReader(null);
+ }
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReaderTest.java b/src/test/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReaderTest.java
new file mode 100644
index 0000000..4c4086b
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReaderTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.apksig.internal.asn1.ber;
+
+import java.io.ByteArrayInputStream;
+
+import org.junit.Test;
+
+public class InputStreamBerDataValueReaderTest extends BerDataValueReaderTestBase {
+
+ @Override
+ protected InputStreamBerDataValueReader createReader(byte[] input) {
+ return new InputStreamBerDataValueReader(new ByteArrayInputStream(input));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testConstructWithNullByteBuffer() throws Exception {
+ new InputStreamBerDataValueReader(null);
+ }
+}
diff --git a/src/test/java/com/android/apksig/internal/test/HexEncoding.java b/src/test/java/com/android/apksig/internal/test/HexEncoding.java
new file mode 100644
index 0000000..cfd2590
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/test/HexEncoding.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2012 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.apksig.internal.test;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Hexadecimal encoding where each byte is represented by two hexadecimal digits.
+ *
+ * @hide
+ */
+public class HexEncoding {
+
+ /** Hidden constructor to prevent instantiation. */
+ private HexEncoding() {}
+
+ private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+
+ /**
+ * Encodes the provided data as a hexadecimal string.
+ */
+ public static String encode(byte[] data) {
+ return encode(data, 0, data.length);
+ }
+
+ /**
+ * Encodes the provided data as a hexadecimal string.
+ */
+ public static String encode(byte[] data, int offset, int len) {
+ StringBuilder result = new StringBuilder(len * 2);
+ for (int i = 0; i < len; i++) {
+ byte b = data[offset + i];
+ result.append(HEX_DIGITS[(b >>> 4) & 0x0f]);
+ result.append(HEX_DIGITS[b & 0x0f]);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Encodes the provided data as a hexadecimal string.
+ */
+ public static String encode(ByteBuffer buf) {
+ return encode(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
+ }
+
+ /**
+ * Decodes the provided hexadecimal string into an array of bytes.
+ */
+ public static byte[] decode(String encoded) {
+ // IMPLEMENTATION NOTE: Special care is taken to permit odd number of hexadecimal digits.
+ int resultLengthBytes = (encoded.length() + 1) / 2;
+ byte[] result = new byte[resultLengthBytes];
+ int resultOffset = 0;
+ int encodedCharOffset = 0;
+ if ((encoded.length() % 2) != 0) {
+ // Odd number of digits -- the first digit is the lower 4 bits of the first result byte.
+ result[resultOffset++] =
+ (byte) getHexadecimalDigitValue(encoded.charAt(encodedCharOffset));
+ encodedCharOffset++;
+ }
+ for (int len = encoded.length(); encodedCharOffset < len; encodedCharOffset += 2) {
+ result[resultOffset++] = (byte)
+ ((getHexadecimalDigitValue(encoded.charAt(encodedCharOffset)) << 4)
+ | getHexadecimalDigitValue(encoded.charAt(encodedCharOffset + 1)));
+ }
+ return result;
+ }
+
+ private static int getHexadecimalDigitValue(char c) {
+ if ((c >= 'a') && (c <= 'f')) {
+ return (c - 'a') + 0x0a;
+ } else if ((c >= 'A') && (c <= 'F')) {
+ return (c - 'A') + 0x0a;
+ } else if ((c >= '0') && (c <= '9')) {
+ return c - '0';
+ } else {
+ throw new IllegalArgumentException(
+ "Invalid hexadecimal digit at position : '"
+ + c + "' (0x" + Integer.toHexString(c) + ")");
+ }
+ }
+}
diff --git a/src/test/java/com/android/apksig/internal/test/MoreAsserts.java b/src/test/java/com/android/apksig/internal/test/MoreAsserts.java
new file mode 100644
index 0000000..409e95e
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/test/MoreAsserts.java
@@ -0,0 +1,51 @@
+/*
+ * 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.apksig.internal.test;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.nio.ByteBuffer;
+
+public abstract class MoreAsserts {
+ private MoreAsserts() {}
+
+ /**
+ * Asserts that the contents of the provided {@code ByteBuffer} are as expected. This method
+ * does not change the position or the limit of the provided buffer.
+ */
+ public static void assertByteBufferEquals(byte[] expected, ByteBuffer actual) {
+ assertByteBufferEquals(null, expected, actual);
+ }
+
+ /**
+ * Asserts that the contents of the provided {@code ByteBuffer} are as expected. This method
+ * does not change the position or the limit of the provided buffer.
+ */
+ public static void assertByteBufferEquals(String message, byte[] expected, ByteBuffer actual) {
+ byte[] actualArr;
+ if ((actual.hasArray())
+ && (actual.arrayOffset() == 0) && (actual.array().length == actual.remaining())) {
+ actualArr = actual.array();
+ } else {
+ actualArr = new byte[actual.remaining()];
+ int actualOriginalPos = actual.position();
+ actual.get(actualArr);
+ actual.position(actualOriginalPos);
+ }
+ assertArrayEquals(message, expected, actualArr);
+ }
+}