aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java')
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java412
1 files changed, 412 insertions, 0 deletions
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java
new file mode 100644
index 0000000000..c1293d6f92
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java
@@ -0,0 +1,412 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.google.devtools.build.singlejar;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.CRC32;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+/**
+ * A helper class to validate zip files and provide reasonable diagnostics (better than what zip
+ * does). We might want to make this into a fully-fledged binary some day.
+ */
+final class ZipTester {
+
+ // The following constants are ZIP-specific.
+ private static final int LOCAL_FILE_HEADER_MARKER = 0x04034b50;
+ private static final int DATA_DESCRIPTOR_MARKER = 0x08074b50;
+ private static final int CENTRAL_DIRECTORY_MARKER = 0x02014b50;
+ private static final int END_OF_CENTRAL_DIRECTORY_MARKER = 0x06054b50;
+
+ private static final int FILE_HEADER_BUFFER_SIZE = 26; // without marker
+ private static final int DATA_DESCRIPTOR_BUFFER_SIZE = 12; // without marker
+
+ private static final int DIRECTORY_ENTRY_BUFFER_SIZE = 42; // without marker
+ private static final int END_OF_CENTRAL_DIRECTORY_BUFFER_SIZE = 18; // without marker
+
+ // Set if the size, compressed size and CRC are set to zero, and present in
+ // the data descriptor after the data.
+ private static final int SIZE_MASKED_FLAG = 1 << 3;
+
+ private static final int STORED_METHOD = 0;
+ private static final int DEFLATE_METHOD = 8;
+
+ private static final int VERSION_STORED = 10; // Version 1.0
+ private static final int VERSION_DEFLATE = 20; // Version 2.0
+
+ private static class Entry {
+ private final long pos;
+ private final String name;
+ private final int flags;
+ private final int method;
+ private final int dosTime;
+ Entry(long pos, String name, int flags, int method, int dosTime) {
+ this.pos = pos;
+ this.name = name;
+ this.flags = flags;
+ this.method = method;
+ this.dosTime = dosTime;
+ }
+ }
+
+ private final InputStream in;
+ private final byte[] buffer = new byte[1024];
+ private int bufferLength;
+ private int bufferOffset;
+ private long pos;
+
+ private List<Entry> entries = new ArrayList<Entry>();
+
+ public ZipTester(InputStream in) {
+ this.in = in;
+ }
+
+ public ZipTester(byte[] data) {
+ this(new ByteArrayInputStream(data));
+ }
+
+ private void warn(String msg) {
+ System.err.println("WARNING: " + msg);
+ }
+
+ private void readMoreData(String action) throws IOException {
+ if ((bufferLength > 0) && (bufferOffset > 0)) {
+ System.arraycopy(buffer, bufferOffset, buffer, 0, bufferLength);
+ }
+ if (bufferLength >= buffer.length) {
+ // The buffer size is specifically chosen to avoid this situation.
+ throw new AssertionError("Internal error: buffer overrun.");
+ }
+ bufferOffset = 0;
+ int bytesRead = in.read(buffer, bufferLength, buffer.length - bufferLength);
+ if (bytesRead <= 0) {
+ throw new IOException("Unexpected end of file, while " + action);
+ }
+ bufferLength += bytesRead;
+ }
+
+ private int readByte(String action) throws IOException {
+ if (bufferLength == 0) {
+ readMoreData(action);
+ }
+ byte result = buffer[bufferOffset];
+ bufferOffset++; bufferLength--;
+ pos++;
+ return result & 0xff;
+ }
+
+ private long getUnsignedInt(String action) throws IOException {
+ int a = readByte(action);
+ int b = readByte(action);
+ int c = readByte(action);
+ int d = readByte(action);
+ return ((d << 24) | (c << 16) | (b << 8) | a) & 0xffffffffL;
+ }
+
+ private void readFully(byte[] buffer, String action) throws IOException {
+ for (int i = 0; i < buffer.length; i++) {
+ buffer[i] = (byte) readByte(action);
+ }
+ }
+
+ private void skip(long length, String action) throws IOException {
+ for (long i = 0; i < length; i++) {
+ readByte(action);
+ }
+ }
+
+ private int getUnsignedShort(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ return (b << 8) | a;
+ }
+
+ private long getUnsignedInt(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ int c = source[offset + 2] & 0xff;
+ int d = source[offset + 3] & 0xff;
+ return ((d << 24) | (c << 16) | (b << 8) | a) & 0xffffffffL;
+ }
+
+ private class DeflateInputStream extends InputStream {
+
+ private final byte[] singleByteBuffer = new byte[1];
+ private int consumedBytes;
+ private final Inflater inflater = new Inflater(true);
+ private long totalBytesRead;
+
+ private int inflateData(byte[] dest, int off, int len)
+ throws IOException {
+ consumedBytes = 0;
+ int bytesProduced = 0;
+ int bytesConsumed = 0;
+ while ((bytesProduced == 0) && !inflater.finished()) {
+ inflater.setInput(buffer, bufferOffset + bytesConsumed, bufferLength - bytesConsumed);
+ int remainingBefore = inflater.getRemaining();
+ try {
+ bytesProduced = inflater.inflate(dest, off, len);
+ } catch (DataFormatException e) {
+ throw new IOException("Invalid deflate stream in ZIP file.", e);
+ }
+ bytesConsumed += remainingBefore - inflater.getRemaining();
+ consumedBytes = bytesConsumed;
+ if (bytesProduced == 0) {
+ if (inflater.needsDictionary()) {
+ // The DEFLATE algorithm as used in the ZIP file format does not
+ // require an additional dictionary.
+ throw new AssertionError("Inflater unexpectedly requires a dictionary.");
+ } else if (inflater.needsInput()) {
+ readMoreData("need more data for deflate");
+ } else if (inflater.finished()) {
+ return 0;
+ } else {
+ // According to the Inflater specification, this cannot happen.
+ throw new AssertionError("Inflater unexpectedly produced no output.");
+ }
+ }
+ }
+ return bytesProduced;
+ }
+
+ @Override
+ public int read(byte b[], int off, int len) throws IOException {
+ if (inflater.finished()) {
+ return -1;
+ }
+ int length = inflateData(b, off, len);
+ totalBytesRead += consumedBytes;
+ bufferLength -= consumedBytes;
+ bufferOffset += consumedBytes;
+ pos += consumedBytes;
+ return length == 0 ? -1 : length;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int bytesRead = read(singleByteBuffer, 0, 1);
+ return (bytesRead == -1) ? -1 : (singleByteBuffer[0] & 0xff);
+ }
+ }
+
+ private void readEntry() throws IOException {
+ long entrypos = pos - 4;
+ String entryDesc = "file entry at " + Long.toHexString(entrypos);
+ byte[] entryBuffer = new byte[FILE_HEADER_BUFFER_SIZE];
+ readFully(entryBuffer, "reading file header");
+ int versionToExtract = getUnsignedShort(entryBuffer, 0);
+ int flags = getUnsignedShort(entryBuffer, 2);
+ int method = getUnsignedShort(entryBuffer, 4);
+ int dosTime = (int) getUnsignedInt(entryBuffer, 6);
+ int crc32 = (int) getUnsignedInt(entryBuffer, 10);
+ long compressedSize = getUnsignedInt(entryBuffer, 14);
+ long uncompressedSize = getUnsignedInt(entryBuffer, 18);
+ int filenameLength = getUnsignedShort(entryBuffer, 22);
+ int extraLength = getUnsignedShort(entryBuffer, 24);
+
+ byte[] filename = new byte[filenameLength];
+ readFully(filename, "reading file name");
+ skip(extraLength, "skipping extra data");
+
+ String name = new String(filename, "UTF-8");
+ for (int i = 0; i < filename.length; i++) {
+ if ((filename[i] < ' ') || (filename[i] > 127)) {
+ warn(entryDesc + ": file name has unexpected non-ascii characters");
+ }
+ }
+ entryDesc = "file entry '" + name + "' at " + Long.toHexString(entrypos);
+
+ if ((method != STORED_METHOD) && (method != DEFLATE_METHOD)) {
+ throw new IOException(entryDesc + ": unknown method " + method);
+ }
+ if ((flags != 0) && (flags != SIZE_MASKED_FLAG)) {
+ throw new IOException(entryDesc + ": unknown flags " + flags);
+ }
+ if ((method == STORED_METHOD) && (versionToExtract != VERSION_STORED)) {
+ warn(entryDesc + ": unexpected version to extract for stored entry " + versionToExtract);
+ }
+ if ((method == DEFLATE_METHOD) && (versionToExtract != VERSION_DEFLATE)) {
+// warn(entryDesc + ": unexpected version to extract for deflated entry " + versionToExtract);
+ }
+
+ if (method == STORED_METHOD) {
+ if (compressedSize != uncompressedSize) {
+ throw new IOException(entryDesc + ": stored entries should have identical compressed and "
+ + "uncompressed sizes");
+ }
+ skip(compressedSize, entryDesc + "skipping data");
+ } else {
+ // No OS resources are actually allocated.
+ @SuppressWarnings("resource") DeflateInputStream deflater = new DeflateInputStream();
+ long generatedBytes = 0;
+ byte[] deflated = new byte[1024];
+ int readBytes;
+ CRC32 crc = new CRC32();
+ while ((readBytes = deflater.read(deflated)) > 0) {
+ crc.update(deflated, 0, readBytes);
+ generatedBytes += readBytes;
+ }
+ int actualCrc32 = (int) crc.getValue();
+ long consumedBytes = deflater.totalBytesRead;
+ if (flags == SIZE_MASKED_FLAG) {
+ long id = getUnsignedInt("reading footer marker");
+ if (id != DATA_DESCRIPTOR_MARKER) {
+ throw new IOException(entryDesc + ": expected footer at " + Long.toHexString(pos - 4)
+ + ", but found " + Long.toHexString(id));
+ }
+ byte[] footer = new byte[DATA_DESCRIPTOR_BUFFER_SIZE];
+ readFully(footer, "reading footer");
+ crc32 = (int) getUnsignedInt(footer, 0);
+ compressedSize = getUnsignedInt(footer, 4);
+ uncompressedSize = getUnsignedInt(footer, 8);
+ }
+
+ if (consumedBytes != compressedSize) {
+ throw new IOException(entryDesc + ": amount of compressed data does not match value "
+ + "specified in the zip (specified: " + compressedSize + ", actual: " + consumedBytes
+ + ")");
+ }
+ if (generatedBytes != uncompressedSize) {
+ throw new IOException(entryDesc + ": amount of uncompressed data does not match value "
+ + "specified in the zip (specified: " + uncompressedSize + ", actual: "
+ + generatedBytes + ")");
+ }
+ if (crc32 != actualCrc32) {
+ throw new IOException(entryDesc + ": specified crc checksum does not match actual check "
+ + "sum");
+ }
+ }
+ entries.add(new Entry(entrypos, name, flags, method, dosTime));
+ }
+
+ @SuppressWarnings("unused") // A couple of unused local variables.
+ private void validateCentralDirectoryEntry(Entry entry) throws IOException {
+ long entrypos = pos - 4;
+ String entryDesc = "file directory entry '" + entry.name + "' at " + Long.toHexString(entrypos);
+
+ byte[] entryBuffer = new byte[DIRECTORY_ENTRY_BUFFER_SIZE];
+ readFully(entryBuffer, "reading central directory entry");
+ int versionMadeBy = getUnsignedShort(entryBuffer, 0);
+ int versionToExtract = getUnsignedShort(entryBuffer, 2);
+ int flags = getUnsignedShort(entryBuffer, 4);
+ int method = getUnsignedShort(entryBuffer, 6);
+ int dosTime = (int) getUnsignedInt(entryBuffer, 8);
+ int crc32 = (int) getUnsignedInt(entryBuffer, 12);
+ long compressedSize = getUnsignedInt(entryBuffer, 16);
+ long uncompressedSize = getUnsignedInt(entryBuffer, 20);
+ int filenameLength = getUnsignedShort(entryBuffer, 24);
+ int extraLength = getUnsignedShort(entryBuffer, 26);
+ int commentLength = getUnsignedShort(entryBuffer, 28);
+ int diskNumberStart = getUnsignedShort(entryBuffer, 30);
+ int internalAttributes = getUnsignedShort(entryBuffer, 32);
+ int externalAttributes = (int) getUnsignedInt(entryBuffer, 34);
+ long offset = getUnsignedInt(entryBuffer, 38);
+
+ byte[] filename = new byte[filenameLength];
+ readFully(filename, "reading file name");
+ skip(extraLength, "skipping extra data");
+ String name = new String(filename, "UTF-8");
+
+ if (!name.equals(entry.name)) {
+ throw new IOException(entryDesc + ": file name in central directory does not match original "
+ + "name");
+ }
+ if (offset != entry.pos) {
+ throw new IOException(entryDesc);
+ }
+ if (flags != entry.flags) {
+ throw new IOException(entryDesc);
+ }
+ if (method != entry.method) {
+ throw new IOException(entryDesc);
+ }
+ if (dosTime != entry.dosTime) {
+ throw new IOException(entryDesc);
+ }
+ }
+
+ private void validateCentralDirectory() throws IOException {
+ boolean first = true;
+ for (Entry entry : entries) {
+ if (first) {
+ first = false;
+ } else {
+ long id = getUnsignedInt("reading marker");
+ if (id != CENTRAL_DIRECTORY_MARKER) {
+ throw new IOException();
+ }
+ }
+ validateCentralDirectoryEntry(entry);
+ }
+ }
+
+ @SuppressWarnings("unused") // A couple of unused local variables.
+ private void validateEndOfCentralDirectory() throws IOException {
+ long id = getUnsignedInt("expecting end of central directory");
+ byte[] entryBuffer = new byte[END_OF_CENTRAL_DIRECTORY_BUFFER_SIZE];
+ readFully(entryBuffer, "reading end of central directory");
+ int diskNumber = getUnsignedShort(entryBuffer, 0);
+ int startDiskNumber = getUnsignedShort(entryBuffer, 2);
+ int numEntries = getUnsignedShort(entryBuffer, 4);
+ int numTotalEntries = getUnsignedShort(entryBuffer, 6);
+ long centralDirectorySize = getUnsignedInt(entryBuffer, 8);
+ long centralDirectoryOffset = getUnsignedInt(entryBuffer, 12);
+ int commentLength = getUnsignedShort(entryBuffer, 16);
+ if (diskNumber != 0) {
+ throw new IOException(String.format("diskNumber=%d", diskNumber));
+ }
+ if (startDiskNumber != 0) {
+ throw new IOException(String.format("startDiskNumber=%d", diskNumber));
+ }
+ if (numEntries != numTotalEntries) {
+ throw new IOException(String.format("numEntries=%d numTotalEntries=%d",
+ numEntries, numTotalEntries));
+ }
+ if (numEntries != (entries.size() % 0x10000)) {
+ throw new IOException("bad number of entries in central directory footer");
+ }
+ if (numTotalEntries != (entries.size() % 0x10000)) {
+ throw new IOException("bad number of entries in central directory footer");
+ }
+ if (commentLength != 0) {
+ throw new IOException("Zip file comment is unexpected");
+ }
+ if (id != END_OF_CENTRAL_DIRECTORY_MARKER) {
+ throw new IOException("Expected end of central directory marker");
+ }
+ }
+
+ public void validate() throws IOException {
+ while (true) {
+ long id = getUnsignedInt("reading marker");
+ if (id == LOCAL_FILE_HEADER_MARKER) {
+ readEntry();
+ } else if (id == CENTRAL_DIRECTORY_MARKER) {
+ validateCentralDirectory();
+ validateEndOfCentralDirectory();
+ return;
+ } else {
+ throw new IOException("unexpected result for marker: "
+ + Long.toHexString(id) + " at position " + Long.toHexString(pos - 4));
+ }
+ }
+ }
+}