aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/java_tools
diff options
context:
space:
mode:
authorGravatar Googler <noreply@google.com>2015-05-08 14:26:14 +0000
committerGravatar Han-Wen Nienhuys <hanwen@google.com>2015-05-08 17:01:14 +0000
commit5821646f64394747e8fa68733f362147931e9037 (patch)
treebae5b0661ebf809af7008d21cb193295746cc870 /src/java_tools
parent65e22fd188c458e952dc95694f49a5bedb32d7f1 (diff)
Rewrite of ZipCombiner to improve performance and maintainability
Poorly performing features of the API have been deprecated in favor of better alternatives: - use addZip(File) over addZip(InputStream) or addZip(String, InputStream) - use addFile(ZipFileEntry) over addFile(String, Date, InputStream, DirectoryEntryInfo) New zip package for high performance ZIP file manipulation. Can directly work with compressed ZIP entry data and has support for Zip64 (forces Zip32 by default). -- MOS_MIGRATED_REVID=93128639
Diffstat (limited to 'src/java_tools')
-rw-r--r--src/java_tools/singlejar/BUILD29
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ExtraData.java41
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JarUtils.java2
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JavaIoFileSystem.java5
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SimpleFileSystem.java6
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java22
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ZipCombiner.java1758
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingInputStream.java85
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingOutputStream.java54
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraData.java103
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraDataList.java161
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileData.java288
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileEntry.java440
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipReader.java510
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java728
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipWriter.java229
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java14
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java32
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java588
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataListTest.java117
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataTest.java77
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileDataTest.java121
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileEntryTest.java212
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipReaderTest.java544
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipTests.java27
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipUtilTest.java180
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipWriterTest.java493
27 files changed, 5147 insertions, 1719 deletions
diff --git a/src/java_tools/singlejar/BUILD b/src/java_tools/singlejar/BUILD
index bc33f28df9..6e1a388ff2 100644
--- a/src/java_tools/singlejar/BUILD
+++ b/src/java_tools/singlejar/BUILD
@@ -2,10 +2,10 @@ package(default_visibility = ["//src:__subpackages__"])
java_library(
name = "libSingleJar",
- srcs = glob(["java/**/*.java"]),
+ srcs = glob(["java/**/singlejar/**/*.java"]),
deps = [
+ ":zip",
"//src/main/java:shell",
- "//third_party:guava",
"//third_party:jsr305",
],
)
@@ -18,10 +18,11 @@ java_binary(
java_test(
name = "tests",
- srcs = glob(["javatests/**/*.java"]),
+ srcs = glob(["javatests/**/singlejar/**/*.java"]),
args = ["com.google.devtools.build.singlejar.SingleJarTests"],
deps = [
":libSingleJar",
+ ":zip",
"//src/main/java:shell",
"//src/test/java:testutil",
"//third_party:guava",
@@ -30,3 +31,25 @@ java_test(
"//third_party:truth",
],
)
+
+java_library(
+ name = "zip",
+ srcs = glob(["java/**/zip/**/*.java"]),
+ deps = [
+ "//third_party:jsr305",
+ ],
+)
+
+java_test(
+ name = "zipTests",
+ srcs = glob(["javatests/**/zip/**/*.java"]),
+ args = ["com.google.devtools.build.zip.ZipTests"],
+ deps = [
+ ":zip",
+ "//src/test/java:testutil",
+ "//third_party:guava",
+ "//third_party:guava-testlib",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ExtraData.java b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ExtraData.java
deleted file mode 100644
index 2e8cb75e02..0000000000
--- a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ExtraData.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2014 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;
-
-/**
- * A holder class for extra data in a ZIP entry.
- *
- * <p>Note: This class performs no defensive copying of the byte array, so the
- * byte array passed into this class or returned from this class may not be
- * modified.
- */
-final class ExtraData {
-
- private final short id;
- private final byte[] data;
-
- public ExtraData(short id, byte[] data) {
- this.id = id;
- this.data = data;
- }
-
- public short getId() {
- return id;
- }
-
- public byte[] getData() {
- return data;
- }
-}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JarUtils.java b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JarUtils.java
index a9c8ee3f65..a21f58307d 100644
--- a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JarUtils.java
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JarUtils.java
@@ -14,6 +14,8 @@
package com.google.devtools.build.singlejar;
+import com.google.devtools.build.zip.ExtraData;
+
import java.io.IOException;
import java.util.Date;
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JavaIoFileSystem.java b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JavaIoFileSystem.java
index 0da6e33040..f66f23dba2 100644
--- a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JavaIoFileSystem.java
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/JavaIoFileSystem.java
@@ -37,6 +37,11 @@ public final class JavaIoFileSystem implements SimpleFileSystem {
}
@Override
+ public File getFile(String filename) throws IOException {
+ return new File(filename);
+ }
+
+ @Override
public boolean delete(String filename) {
return new File(filename).delete();
}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SimpleFileSystem.java b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SimpleFileSystem.java
index 844f12b714..890ab093e7 100644
--- a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SimpleFileSystem.java
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SimpleFileSystem.java
@@ -16,6 +16,7 @@ package com.google.devtools.build.singlejar;
import com.google.devtools.build.singlejar.OptionFileExpander.OptionFileProvider;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -36,6 +37,11 @@ public interface SimpleFileSystem extends OptionFileProvider {
OutputStream getOutputStream(String filename) throws IOException;
/**
+ * Returns the File object for this filename.
+ */
+ File getFile(String filename) throws IOException;
+
+ /**
* Delete the file with the given name and return whether deleting it was
* successfull.
*/
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java
index 4551fd1813..8fd677e1b7 100644
--- a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/SingleJar.java
@@ -19,6 +19,7 @@ import com.google.devtools.build.singlejar.ZipCombiner.OutputMode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -80,10 +81,10 @@ public class SingleJar {
protected boolean includeBuildData = true;
/** List of build information properties files */
- protected List<String> buildInformationFiles = new ArrayList<String>();
+ protected List<String> buildInformationFiles = new ArrayList<>();
/** Extraneous build informations (key=value) */
- protected List<String> buildInformations = new ArrayList<String>();
+ protected List<String> buildInformations = new ArrayList<>();
/** The (optional) native executable that will be prepended to this JAR. */
private String launcherBin = null;
@@ -223,21 +224,8 @@ public class SingleJar {
// Copy the jars into the jar file.
for (String inputJar : inputJars) {
- InputStream in = fileSystem.getInputStream(inputJar);
- try {
- combiner.addZip(inputJar, in);
- InputStream inToClose = in;
- in = null;
- inToClose.close();
- } finally {
- if (in != null) {
- try {
- in.close();
- } catch (IOException e) {
- // Preserve original exception.
- }
- }
- }
+ File jar = fileSystem.getFile(inputJar);
+ combiner.addZip(jar);
}
// Close the output file. If something goes wrong here, delete the file.
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ZipCombiner.java b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ZipCombiner.java
index d38c6d4493..64e9695e07 100644
--- a/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ZipCombiner.java
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/singlejar/ZipCombiner.java
@@ -14,44 +14,46 @@
package com.google.devtools.build.singlejar;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.devtools.build.singlejar.ZipEntryFilter.CustomMergeStrategy;
import com.google.devtools.build.singlejar.ZipEntryFilter.StrategyCallback;
+import com.google.devtools.build.zip.ExtraData;
+import com.google.devtools.build.zip.ExtraDataList;
+import com.google.devtools.build.zip.ZipFileEntry;
+import com.google.devtools.build.zip.ZipFileEntry.Compression;
+import com.google.devtools.build.zip.ZipReader;
+import com.google.devtools.build.zip.ZipUtil;
+import com.google.devtools.build.zip.ZipWriter;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
-import java.io.FilterOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.util.Date;
-import java.util.GregorianCalendar;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.CRC32;
-import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
+import java.util.zip.DeflaterInputStream;
import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
import javax.annotation.Nullable;
-import javax.annotation.concurrent.NotThreadSafe;
/**
* An object that combines multiple ZIP files into a single file. It only
* supports a subset of the ZIP format, specifically:
* <ul>
* <li>It only supports STORE and DEFLATE storage methods.</li>
- * <li>There may be no data before the first file or between files.</li>
- * <li>It ignores any data after the last file.</li>
+ * <li>It only supports 32-bit ZIP files.</li>
* </ul>
*
* <p>These restrictions are also present in the JDK implementations
@@ -68,63 +70,8 @@ import javax.annotation.concurrent.NotThreadSafe;
* <p>Also see:
* <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP format</a>
*/
-@NotThreadSafe
-public final class ZipCombiner implements AutoCloseable {
-
- /**
- * A Date set to the 1/1/1980, 00:00:00, the minimum value that can be stored
- * in a ZIP file.
- */
- public static final Date DOS_EPOCH = new GregorianCalendar(1980, 0, 1, 0, 0, 0).getTime();
-
- private static final int DEFAULT_CENTRAL_DIRECTORY_BLOCK_SIZE = 1048576; // 1 MB for each block
-
- // 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 = 30;
-
- private static final int VERSION_TO_EXTRACT_OFFSET = 4;
- private static final int GENERAL_PURPOSE_FLAGS_OFFSET = 6;
- private static final int COMPRESSION_METHOD_OFFSET = 8;
- private static final int MTIME_OFFSET = 10;
- private static final int MDATE_OFFSET = 12;
- private static final int CRC32_OFFSET = 14;
- private static final int COMPRESSED_SIZE_OFFSET = 18;
- private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
- private static final int FILENAME_LENGTH_OFFSET = 26;
- private static final int EXTRA_LENGTH_OFFSET = 28;
-
- private static final int DIRECTORY_ENTRY_BUFFER_SIZE = 46;
-
- // 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 final long MAXIMUM_DATA_SIZE = 0xffffffffL;
-
- // This class relies on the buffer to have sufficient space for a complete
- // file name. 2^16 is the maximum number of bytes in a file name.
- private static final int BUFFER_SIZE = 65536;
-
- /** An empty entry used to skip files that have already been copied (or skipped). */
- private static final FileEntry COPIED_FILE_ENTRY = new FileEntry(null, null, 0);
-
- /** An empty entry used to mark files that have already been renamed. */
- private static final FileEntry RENAMED_FILE_ENTRY = new FileEntry(null, null, 0);
-
- /** A zero length array of ExtraData. */
- public static final ExtraData[] NO_EXTRA_ENTRIES = new ExtraData[0];
-
+public class ZipCombiner implements AutoCloseable {
+ public static final Date DOS_EPOCH = new Date(ZipUtil.DOS_EPOCH);
/**
* Whether to compress or decompress entries.
*/
@@ -136,47 +83,41 @@ public final class ZipCombiner implements AutoCloseable {
DONT_CARE,
/**
- * Output all entries using DEFLATE method, except directory entries. It is
- * always more efficient to store directory entries uncompressed.
+ * Output all entries using DEFLATE method, except directory entries. It is always more
+ * efficient to store directory entries uncompressed.
*/
FORCE_DEFLATE,
/**
* Output all entries using STORED method.
*/
- FORCE_STORED;
+ FORCE_STORED,
}
- // A two-element enum for copyOrSkip type methods.
- private static enum SkipMode {
+ /**
+ * The type of action to take for a ZIP file entry.
+ */
+ private enum ActionType {
+
+ /**
+ * Skip the entry.
+ */
+ SKIP,
/**
- * Copy the read data to the output stream.
+ * Copy the entry.
*/
COPY,
/**
- * Do not write anything to the output stream.
+ * Rename the entry.
*/
- SKIP;
- }
+ RENAME,
- /**
- * Stores internal information about merges or skips.
- */
- private static final class FileEntry {
-
- /** If null, the file should be skipped. Otherwise, it should be merged. */
- private final CustomMergeStrategy mergeStrategy;
- private final ByteArrayOutputStream outputBuffer;
- private final int dosTime;
-
- private FileEntry(CustomMergeStrategy mergeStrategy, ByteArrayOutputStream outputBuffer,
- int dosTime) {
- this.mergeStrategy = mergeStrategy;
- this.outputBuffer = outputBuffer;
- this.dosTime = dosTime;
- }
+ /**
+ * Merge the entry.
+ */
+ MERGE;
}
/**
@@ -188,6 +129,7 @@ public final class ZipCombiner implements AutoCloseable {
* whose meaning depends on the value of {@code madeByVersion}, but is usually a reasonable
* default.
*/
+ @Deprecated
public static final DirectoryEntryInfo DEFAULT_DIRECTORY_ENTRY_INFO =
new DirectoryEntryInfo((short) -1, 0);
@@ -196,6 +138,7 @@ public final class ZipCombiner implements AutoCloseable {
* This does not contain all the information stored in the central directory record, only the
* information that can be customized and is not automatically calculated or detected.
*/
+ @Deprecated
public static final class DirectoryEntryInfo {
private final short madeByVersion;
private final int externalFileAttribute;
@@ -225,1394 +168,545 @@ public final class ZipCombiner implements AutoCloseable {
}
/**
- * The central directory, which is grown as required; instead of using a single large buffer, we
- * store a sequence of smaller buffers. With a single large buffer, whenever we grow the buffer by
- * 2x, we end up requiring 3x the memory temporarily, which can lead to OOM problems even if there
- * would still be enough memory.
- *
- * <p>The invariants for the fields are as follows:
+ * Encapsulates the action to take for a ZIP file entry along with optional details specific to
+ * the action type. The minimum requirements per type are:
* <ul>
- * <li>All blocks must have the same size.
- * <li>The list of blocks must contain all blocks, including the current block (even if empty).
- * <li>The current block offset must apply to the last block in the list, which is
- * simultaneously the current block.
- * <li>The current block may only be {@code null} if the list is empty.
+ * <li>SKIP: none.</li>
+ * <li>COPY: none.</li>
+ * <li>RENAME: newName.</li>
+ * <li>MERGE: strategy, mergeBuffer.</li>
* </ul>
+ *
+ * <p>An action can be easily changed from one type to another by using
+ * {@link EntryAction#EntryAction(ActionType, EntryAction)}.
*/
- private static final class CentralDirectory {
- private final int blockSize; // We allow this to be overridden for testing.
- private List<byte[]> blockList = new ArrayList<>();
- private byte[] currentBlock;
- private int currentBlockOffset = 0;
- private int size = 0;
-
- CentralDirectory(int centralDirectoryBlockSize) {
- this.blockSize = centralDirectoryBlockSize;
- }
+ private static final class EntryAction {
+ private final ActionType type;
+ @Nullable private final Date date;
+ @Nullable private final String newName;
+ @Nullable private final CustomMergeStrategy strategy;
+ @Nullable private final ByteArrayOutputStream mergeBuffer;
/**
- * Appends the given data to the central directory and returns the start
- * offset within the central directory to allow back-patching.
+ * Create an action of the specified type with no extra details.
*/
- int writeToCentralDirectory(byte[] b, int off, int len) {
- checkArgument(len >= 0);
- int offsetStarted = size;
- while (len > 0) {
- if (currentBlock == null
- || currentBlockOffset >= currentBlock.length) {
- currentBlock = new byte[blockSize];
- currentBlockOffset = 0;
- blockList.add(currentBlock);
- }
- int maxCopy = Math.min(blockSize - currentBlockOffset, len);
- System.arraycopy(b, off, currentBlock, currentBlockOffset, maxCopy);
- off += maxCopy;
- len -= maxCopy;
- size += maxCopy;
- currentBlockOffset += maxCopy;
- }
- return offsetStarted;
- }
-
- /** Calls through to {@link #writeToCentralDirectory(byte[], int, int)}. */
- int writeToCentralDirectory(byte[] b) {
- return writeToCentralDirectory(b, 0, b.length);
+ public EntryAction(ActionType type) {
+ this(type, null, null, null, null);
}
/**
- * Writes an unsigned int in little-endian byte order to the central directory at the
- * given offset. Does not perform range checking.
+ * Create a duplicate action with a different {@link ActionType}.
*/
- void setUnsignedInt(int offset, int value) {
- blockList.get(cdIndex(offset + 0))[cdOffset(offset + 0)] = (byte) (value & 0xff);
- blockList.get(cdIndex(offset + 1))[cdOffset(offset + 1)] = (byte) ((value >> 8) & 0xff);
- blockList.get(cdIndex(offset + 2))[cdOffset(offset + 2)] = (byte) ((value >> 16) & 0xff);
- blockList.get(cdIndex(offset + 3))[cdOffset(offset + 3)] = (byte) ((value >> 24) & 0xff);
- }
-
- private int cdIndex(int offset) {
- return offset / blockSize;
- }
-
- private int cdOffset(int offset) {
- return offset % blockSize;
+ public EntryAction(ActionType type, EntryAction action) {
+ this(type, action.getDate(), action.getNewName(), action.getStrategy(),
+ action.getMergeBuffer());
}
/**
- * Writes the central directory to the given output stream and returns the size, i.e., the
- * number of bytes written.
+ * Create an action of the specified type and details.
+ *
+ * @param type the type of action
+ * @param date the custom date to set on the entry
+ * @param newName the custom name to create the entry as
+ * @param strategy the {@link CustomMergeStrategy} to use for merging this entry
+ * @param mergeBuffer the output stream to use for merge results
*/
- int writeTo(OutputStream out) throws IOException {
- for (int i = 0; i < blockList.size() - 1; i++) {
- out.write(blockList.get(i));
- }
- if (currentBlock != null) {
- out.write(currentBlock, 0, currentBlockOffset);
- }
- return size;
+ public EntryAction(ActionType type, Date date, String newName, CustomMergeStrategy strategy,
+ ByteArrayOutputStream mergeBuffer) {
+ checkArgument(type != ActionType.RENAME || newName != null,
+ "NewName must not be null if the ActionType is RENAME.");
+ checkArgument(type != ActionType.MERGE || strategy != null,
+ "Strategy must not be null if the ActionType is MERGE.");
+ checkArgument(type != ActionType.MERGE || mergeBuffer != null,
+ "MergeBuffer must not be null if the ActionType is MERGE.");
+ this.type = type;
+ this.date = date;
+ this.newName = newName;
+ this.strategy = strategy;
+ this.mergeBuffer = mergeBuffer;
}
- }
-
- /**
- * An output stream that counts how many bytes were written.
- */
- private static final class ByteCountingOutputStream extends FilterOutputStream {
- private long bytesWritten = 0L;
- ByteCountingOutputStream(OutputStream out) {
- super(out);
+ /** Returns the type. */
+ public ActionType getType() {
+ return type;
}
- @Override
- public void write(byte[] b, int off, int len) throws IOException {
- out.write(b, off, len);
- bytesWritten += len;
+ /** Returns the date. */
+ public Date getDate() {
+ return date;
}
- @Override
- public void write(int b) throws IOException {
- out.write(b);
- bytesWritten++;
+ /** Returns the new name. */
+ public String getNewName() {
+ return newName;
}
- }
-
- private final OutputMode mode;
- private final ZipEntryFilter entryFilter;
-
- private final ByteCountingOutputStream out;
-
- // An input buffer to allow reading blocks of data. Keeping it here avoids
- // another copy operation that would be required by the BufferedInputStream.
- // The valid data is between bufferOffset and bufferOffset+bufferLength (exclusive).
- private final byte[] buffer = new byte[BUFFER_SIZE];
- private int bufferOffset = 0;
- private int bufferLength = 0;
-
- private String currentInputFile;
-
- // An intermediate buffer for the file header data. Keeping it here avoids
- // creating a new buffer for every entry.
- private final byte[] headerBuffer = new byte[FILE_HEADER_BUFFER_SIZE];
-
- // An intermediate buffer for a central directory entry. Keeping it here
- // avoids creating a new buffer for every entry.
- private final byte[] directoryEntryBuffer = new byte[DIRECTORY_ENTRY_BUFFER_SIZE];
-
- // The Inflater is a class member to avoid creating a new instance for every
- // entry in the ZIP file.
- private final Inflater inflater = new Inflater(true);
-
- // The contents of this buffer are never read. The Inflater is only used to
- // determine the length of the compressed data, and the buffer is a throw-
- // away buffer for the decompressed data.
- private final byte[] inflaterBuffer = new byte[BUFFER_SIZE];
-
- private final Map<String, FileEntry> fileNames = new HashMap<>();
-
- private final CentralDirectory centralDirectory;
- private int fileCount = 0;
-
- private boolean finished = false;
-
- // Package private for testing.
- ZipCombiner(OutputMode mode, ZipEntryFilter entryFilter, OutputStream out,
- int centralDirectoryBlockSize) {
- this.mode = mode;
- this.entryFilter = entryFilter;
- this.out = new ByteCountingOutputStream(new BufferedOutputStream(out));
- this.centralDirectory = new CentralDirectory(centralDirectoryBlockSize);
- }
-
- /**
- * Creates a new instance with the given parameters. The {@code entryFilter}
- * is called for every entry in the ZIP files and the combined ZIP file is
- * written to {@code out}. The output mode determines whether entries must be
- * written in compressed or decompressed form. Note that the result is
- * invalid if an exception is thrown from any of the methods in this class,
- * and before a call to {@link #close} or {@link #finish}.
- */
- public ZipCombiner(OutputMode mode, ZipEntryFilter entryFilter, OutputStream out) {
- this(mode, entryFilter, out, DEFAULT_CENTRAL_DIRECTORY_BLOCK_SIZE);
- }
-
- /**
- * Creates a new instance with the given parameters and the DONT_CARE mode.
- */
- public ZipCombiner(ZipEntryFilter entryFilter, OutputStream out) {
- this(OutputMode.DONT_CARE, entryFilter, out);
- }
-
- /**
- * Creates a new instance with the {@link CopyEntryFilter} as the filter and
- * the given mode and output stream.
- */
- public ZipCombiner(OutputMode mode, OutputStream out) {
- this(mode, new CopyEntryFilter(), out);
- }
-
- /**
- * Creates a new instance with the {@link CopyEntryFilter} as the filter, the
- * DONT_CARE mode and the given output stream.
- */
- public ZipCombiner(OutputStream out) {
- this(OutputMode.DONT_CARE, new CopyEntryFilter(), out);
- }
-
- /**
- * Returns whether the output zip already contains a file or directory with
- * the given name.
- */
- public boolean containsFile(String filename) {
- return fileNames.containsKey(filename);
- }
-
- /**
- * Makes a write call to the output stream, and updates the current offset.
- */
- private void write(byte[] b, int off, int len) throws IOException {
- out.write(b, off, len);
- }
-
- /** Calls through to {@link #write(byte[], int, int)}. */
- private void write(byte[] b) throws IOException {
- write(b, 0, b.length);
- }
- /**
- * Reads at least one more byte into the internal buffer. This method must
- * only be called when more data is necessary to correctly decode the ZIP
- * format.
- *
- * <p>This method automatically compacts the existing data in the buffer by
- * moving it to the beginning of the buffer.
- *
- * @throws EOFException if no more data is available from the input stream
- * @throws IOException if the underlying stream throws one
- */
- private void readMoreData(InputStream in) throws IOException {
- if ((bufferLength > 0) && (bufferOffset > 0)) {
- System.arraycopy(buffer, bufferOffset, buffer, 0, bufferLength);
+ /** Returns the strategy. */
+ public CustomMergeStrategy getStrategy() {
+ return strategy;
}
- 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 EOFException();
- }
- bufferLength += bytesRead;
- }
- /**
- * Reads data until the buffer is filled with at least {@code length} bytes.
- *
- * @throws IllegalArgumentException if not 0 <= length <= buffer.length
- * @throws IOException if the underlying input stream throws one or the end
- * of the input stream is reached before the required
- * number of bytes is read
- */
- private void readFully(InputStream in, int length) throws IOException {
- checkArgument(length >= 0, "length too small: %s", length);
- checkArgument(length <= buffer.length, "length too large: %s", length);
- while (bufferLength < length) {
- readMoreData(in);
+ /** Returns the mergeBuffer. */
+ public ByteArrayOutputStream getMergeBuffer() {
+ return mergeBuffer;
}
}
- /**
- * Reads an unsigned short in little-endian byte order from the buffer at the
- * given offset. Does not perform range checking.
- */
- private int getUnsignedShort(byte[] source, int offset) {
- int a = source[offset + 0] & 0xff;
- int b = source[offset + 1] & 0xff;
- return (b << 8) | a;
- }
-
- /**
- * Reads an unsigned int in little-endian byte order from the buffer at the
- * given offset. Does not perform range checking.
- */
- 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;
- }
-
- /**
- * Writes an unsigned short in little-endian byte order to the buffer at the
- * given offset. Does not perform range checking.
- */
- private void setUnsignedShort(byte[] target, int offset, short value) {
- target[offset + 0] = (byte) (value & 0xff);
- target[offset + 1] = (byte) ((value >> 8) & 0xff);
- }
-
- /**
- * Writes an unsigned int in little-endian byte order to the buffer at the
- * given offset. Does not perform range checking.
- */
- private void setUnsignedInt(byte[] target, int offset, int value) {
- target[offset + 0] = (byte) (value & 0xff);
- target[offset + 1] = (byte) ((value >> 8) & 0xff);
- target[offset + 2] = (byte) ((value >> 16) & 0xff);
- target[offset + 3] = (byte) ((value >> 24) & 0xff);
- }
+ private final class FilterCallback implements StrategyCallback {
+ private String filename;
+ private final AtomicBoolean called = new AtomicBoolean();
- /**
- * Copies or skips {@code length} amount of bytes from the input stream to the
- * output stream. If the internal buffer is not empty, those bytes are copied
- * first. When the method returns, there may be more bytes remaining in the
- * buffer.
- *
- * @throws IOException if the underlying stream throws one
- */
- private void copyOrSkipData(InputStream in, long length, SkipMode skip) throws IOException {
- checkArgument(length >= 0);
- while (length > 0) {
- if (bufferLength == 0) {
- readMoreData(in);
- }
- int bytesToWrite = (length < bufferLength) ? (int) length : bufferLength;
- if (skip == SkipMode.COPY) {
- write(buffer, bufferOffset, bytesToWrite);
- }
- bufferOffset += bytesToWrite;
- bufferLength -= bytesToWrite;
- length -= bytesToWrite;
+ public void resetForFile(String filename) {
+ this.filename = filename;
+ this.called.set(false);
}
- }
- /**
- * Copies or skips {@code length} amount of bytes from the input stream to the
- * output stream. If the internal buffer is not empty, those bytes are copied
- * first. When the method returns, there may be more bytes remaining in the
- * buffer. In addition to writing to the output stream, it also writes to the
- * central directory.
- *
- * @throws IOException if the underlying stream throws one
- */
- private void forkOrSkipData(InputStream in, long length, SkipMode skip) throws IOException {
- checkArgument(length >= 0);
- while (length > 0) {
- if (bufferLength == 0) {
- readMoreData(in);
- }
- int bytesToWrite = (length < bufferLength) ? (int) length : bufferLength;
- if (skip == SkipMode.COPY) {
- write(buffer, bufferOffset, bytesToWrite);
- centralDirectory.writeToCentralDirectory(buffer, bufferOffset, bytesToWrite);
- }
- bufferOffset += bytesToWrite;
- bufferLength -= bytesToWrite;
- length -= bytesToWrite;
+ @Override public void skip() throws IOException {
+ checkCall();
+ actions.put(filename, new EntryAction(ActionType.SKIP));
}
- }
-
- /**
- * A mutable integer reference value to allow returning two values from a
- * method.
- */
- private static class MutableInt {
- private int value;
-
- MutableInt(int initialValue) {
- this.value = initialValue;
+ @Override public void copy(Date date) throws IOException {
+ checkCall();
+ actions.put(filename, new EntryAction(ActionType.COPY, date, null, null, null));
}
- public void setValue(int value) {
- this.value = value;
+ @Override public void rename(String newName, Date date) throws IOException {
+ checkCall();
+ actions.put(filename, new EntryAction(ActionType.RENAME, date, newName, null, null));
}
- public int getValue() {
- return value;
+ @Override public void customMerge(Date date, CustomMergeStrategy strategy) throws IOException {
+ checkCall();
+ actions.put(filename, new EntryAction(ActionType.MERGE, date, null, strategy,
+ new ByteArrayOutputStream()));
}
- }
- /**
- * Uses the inflater to decompress some data into the given buffer. This
- * method performs no error checking on the input parameters and also does
- * not update the buffer parameters of the input buffer (such as bufferOffset
- * and bufferLength). It's only here to avoid code duplication.
- *
- * <p>The Inflater may not be in the finished state when this method is
- * called.
- *
- * <p>This method returns 0 if it read data and reached the end of the
- * DEFLATE stream without producing output. In that case, {@link
- * Inflater#finished} is guaranteed to return true.
- *
- * @throws IOException if the underlying stream throws an IOException or if
- * illegal data is encountered
- */
- private int inflateData(InputStream in, byte[] dest, int off, int len, MutableInt consumed)
- throws IOException {
- // Defend against Inflater.finished() returning true.
- consumed.setValue(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();
- consumed.setValue(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(in);
- } else if (inflater.finished()) {
- return 0;
- } else {
- // According to the Inflater specification, this cannot happen.
- throw new AssertionError("Inflater unexpectedly produced no output.");
- }
- }
+ private void checkCall() {
+ checkState(called.compareAndSet(false, true), "The callback was already called once.");
}
- return bytesProduced;
}
- /**
- * Copies or skips data from the input stream to the output stream. To
- * determine the length of the data, the data is decompressed with the
- * DEFLATE algorithm, which stores the length implicitly as part of the
- * compressed data, using a combination of end markers and length indicators.
- *
- * @see <a href="http://www.ietf.org/rfc/rfc1951.txt">RFC 1951</a>
- *
- * @throws IOException if the underlying stream throws an IOException
- */
- private long copyOrSkipDeflateData(InputStream in, SkipMode skip) throws IOException {
- long bytesCopied = 0;
- inflater.reset();
- MutableInt consumedBytes = new MutableInt(0);
- while (!inflater.finished()) {
- // Neither the uncompressed data nor the length of it is used. The
- // decompression is only required to determine the correct length of the
- // compressed data to copy.
- inflateData(in, inflaterBuffer, 0, inflaterBuffer.length, consumedBytes);
- int bytesRead = consumedBytes.getValue();
- if (skip == SkipMode.COPY) {
- write(buffer, bufferOffset, bytesRead);
- }
- bufferOffset += bytesRead;
- bufferLength -= bytesRead;
- bytesCopied += bytesRead;
- }
- return bytesCopied;
+ /** Returns a {@link Deflater} for performing ZIP compression. */
+ private static Deflater getDeflater() {
+ return new Deflater(Deflater.DEFAULT_COMPRESSION, true);
}
- /**
- * Returns a 32-bit integer containing a ZIP-compatible encoding of the given
- * date. Only dates between 1980 and 2107 (inclusive) are supported.
- *
- * <p>The upper 16 bits contain the year, month, and day. The lower 16 bits
- * contain the hour, minute, and second. The resolution of the second field
- * is only 4 bits, which means that the only even second values can be
- * stored - this method rounds down to the nearest even value.
- *
- * @throws IllegalArgumentException if the given date is outside the
- * supported range
- */
- // Only visible for testing.
- static int dateToDosTime(Date date) {
- Calendar calendar = new GregorianCalendar();
- calendar.setTime(date);
- int year = calendar.get(Calendar.YEAR);
- if (year < 1980) {
- throw new IllegalArgumentException("date must be in or after 1980");
- }
- // The ZIP format only provides 7 bits for the year.
- if (year > 2107) {
- throw new IllegalArgumentException("date must before 2107");
- }
- int month = calendar.get(Calendar.MONTH) + 1; // Months from Calendar are zero-based.
- int day = calendar.get(Calendar.DAY_OF_MONTH);
- int hour = calendar.get(Calendar.HOUR_OF_DAY);
- int minute = calendar.get(Calendar.MINUTE);
- int second = calendar.get(Calendar.SECOND);
- return ((year - 1980) << 25) | (month << 21) | (day << 16)
- | (hour << 11) | (minute << 5) | (second >> 1);
+ /** Returns a {@link Inflater} for performing ZIP decompression. */
+ private static Inflater getInflater() {
+ return new Inflater(true);
}
- /**
- * Fills the directory entry, using the information from the header buffer,
- * and writes it to the central directory. It returns the offset into the
- * central directory that can be used for patching the entry. Requires that
- * the entire entry header is present in {@link #headerBuffer}. It also uses
- * the {@link ByteCountingOutputStream#bytesWritten}, so it must be called
- * just before the header is written to the output stream.
- *
- * @throws IOException if the current offset is too large for the ZIP format
- */
- private int fillDirectoryEntryBuffer(
- DirectoryEntryInfo directoryEntryInfo) throws IOException {
- // central file header signature
- setUnsignedInt(directoryEntryBuffer, 0, CENTRAL_DIRECTORY_MARKER);
- short version = (short) getUnsignedShort(headerBuffer, VERSION_TO_EXTRACT_OFFSET);
- short curMadeMyVersion = (directoryEntryInfo.madeByVersion == -1)
- ? version : directoryEntryInfo.madeByVersion;
- setUnsignedShort(directoryEntryBuffer, 4, curMadeMyVersion); // version made by
- // version needed to extract
- setUnsignedShort(directoryEntryBuffer, 6, version);
- // general purpose bit flag
- setUnsignedShort(directoryEntryBuffer, 8,
- (short) getUnsignedShort(headerBuffer, GENERAL_PURPOSE_FLAGS_OFFSET));
- // compression method
- setUnsignedShort(directoryEntryBuffer, 10,
- (short) getUnsignedShort(headerBuffer, COMPRESSION_METHOD_OFFSET));
- // last mod file time, last mod file date
- setUnsignedShort(directoryEntryBuffer, 12,
- (short) getUnsignedShort(headerBuffer, MTIME_OFFSET));
- setUnsignedShort(directoryEntryBuffer, 14,
- (short) getUnsignedShort(headerBuffer, MDATE_OFFSET));
- // crc-32
- setUnsignedInt(directoryEntryBuffer, 16, (int) getUnsignedInt(headerBuffer, CRC32_OFFSET));
- // compressed size
- setUnsignedInt(directoryEntryBuffer, 20,
- (int) getUnsignedInt(headerBuffer, COMPRESSED_SIZE_OFFSET));
- // uncompressed size
- setUnsignedInt(directoryEntryBuffer, 24,
- (int) getUnsignedInt(headerBuffer, UNCOMPRESSED_SIZE_OFFSET));
- // file name length
- setUnsignedShort(directoryEntryBuffer, 28,
- (short) getUnsignedShort(headerBuffer, FILENAME_LENGTH_OFFSET));
- // extra field length
- setUnsignedShort(directoryEntryBuffer, 30,
- (short) getUnsignedShort(headerBuffer, EXTRA_LENGTH_OFFSET));
- setUnsignedShort(directoryEntryBuffer, 32, (short) 0); // file comment length
- setUnsignedShort(directoryEntryBuffer, 34, (short) 0); // disk number start
- setUnsignedShort(directoryEntryBuffer, 36, (short) 0); // internal file attributes
- setUnsignedInt(directoryEntryBuffer, 38, directoryEntryInfo.externalFileAttribute);
- if (out.bytesWritten >= MAXIMUM_DATA_SIZE) {
- throw new IOException("Unable to handle files bigger than 2^32 bytes.");
+ /** Copies all data from the input stream to the output stream. */
+ private static long copyStream(InputStream from, OutputStream to) throws IOException {
+ byte[] buf = new byte[0x1000];
+ long total = 0;
+ int r;
+ while ((r = from.read(buf)) != -1) {
+ to.write(buf, 0, r);
+ total += r;
}
- // relative offset of local header
- setUnsignedInt(directoryEntryBuffer, 42, (int) out.bytesWritten);
- fileCount++;
- return centralDirectory.writeToCentralDirectory(directoryEntryBuffer);
+ return total;
}
- /**
- * Fix the directory entry with the correct crc32, compressed size, and
- * uncompressed size.
- */
- private void fixDirectoryEntry(int offset, long crc32, long compressedSize,
- long uncompressedSize) {
- // The constants from the top don't apply here, because this is the central directory entry.
- centralDirectory.setUnsignedInt(offset + 16, (int) crc32); // crc-32
- centralDirectory.setUnsignedInt(offset + 20, (int) compressedSize); // compressed size
- centralDirectory.setUnsignedInt(offset + 24, (int) uncompressedSize); // uncompressed size
- }
+ private final OutputMode mode;
+ private final ZipEntryFilter entryFilter;
+ private final FilterCallback callback;
+ private final ZipWriter out;
- /**
- * (Un)Compresses and copies the current ZIP file entry. Requires that the
- * entire entry header is present in {@link #headerBuffer}. It currently
- * drops the extra data in the process.
- *
- * @throws IOException if the underlying stream throws an IOException
- */
- private void modifyAndCopyEntry(String filename, InputStream in, int dosTime)
- throws IOException {
- final int method = getUnsignedShort(headerBuffer, COMPRESSION_METHOD_OFFSET);
- final int flags = getUnsignedShort(headerBuffer, GENERAL_PURPOSE_FLAGS_OFFSET);
- final int fileNameLength = getUnsignedShort(headerBuffer, FILENAME_LENGTH_OFFSET);
- final int extraFieldLength = getUnsignedShort(headerBuffer, EXTRA_LENGTH_OFFSET);
- // TODO(bazel-team): Read and copy the extra data if present.
-
- forkOrSkipData(in, fileNameLength, SkipMode.SKIP);
- forkOrSkipData(in, extraFieldLength, SkipMode.SKIP);
- if (method == STORED_METHOD) {
- long compressedSize = getUnsignedInt(headerBuffer, COMPRESSED_SIZE_OFFSET);
- copyStreamToEntry(filename, new FixedLengthInputStream(in, compressedSize), dosTime,
- NO_EXTRA_ENTRIES, true, DEFAULT_DIRECTORY_ENTRY_INFO);
- } else if (method == DEFLATE_METHOD) {
- inflater.reset();
- copyStreamToEntry(filename, new DeflateInputStream(in), dosTime, NO_EXTRA_ENTRIES, false,
- DEFAULT_DIRECTORY_ENTRY_INFO);
- if ((flags & SIZE_MASKED_FLAG) != 0) {
- copyOrSkipData(in, 16, SkipMode.SKIP);
- }
- } else {
- throw new AssertionError("This should have been checked in validateHeader().");
- }
- }
+ private final Map<String, ZipFileEntry> entries;
+ private final Map<String, EntryAction> actions;
/**
- * Copies or skips the current ZIP file entry. Requires that the entire entry
- * header is present in {@link #headerBuffer}. It uses the current mode to
- * decide whether to compress or decompress the entry.
+ * Creates a {@link ZipCombiner} for combining ZIP files using the specified {@link OutputMode},
+ * {@link ZipEntryFilter}, and destination {@link OutputStream}.
*
- * @throws IOException if the underlying stream throws an IOException
+ * @param mode the compression preference for the output ZIP file
+ * @param entryFilter the filter to use when adding ZIP files to the combined output
+ * @param out the {@link OutputStream} for writing the combined ZIP file
*/
- private void copyOrSkipEntry(String filename, InputStream in, SkipMode skip, Date date,
- DirectoryEntryInfo directoryEntryInfo) throws IOException {
- copyOrSkipEntry(filename, in, skip, date, directoryEntryInfo, false);
+ public ZipCombiner(OutputMode mode, ZipEntryFilter entryFilter, OutputStream out) {
+ this.mode = mode;
+ this.entryFilter = entryFilter;
+ this.callback = new FilterCallback();
+ this.out = new ZipWriter(new BufferedOutputStream(out), UTF_8);
+ this.entries = new HashMap<>();
+ this.actions = new HashMap<>();
}
/**
- * Renames and otherwise copies the current ZIP file entry. Requires that the entire
- * entry header is present in {@link #headerBuffer}. It uses the current mode to
- * decide whether to compress or decompress the entry.
+ * Creates a {@link ZipCombiner} for combining ZIP files using the specified
+ * {@link ZipEntryFilter}, and destination {@link OutputStream}. Uses the DONT_CARE
+ * {@link OutputMode}.
*
- * @throws IOException if the underlying stream throws an IOException
+ * @param entryFilter the filter to use when adding ZIP files to the combined output
+ * @param out the {@link OutputStream} for writing the combined ZIP file
*/
- private void renameEntry(String filename, InputStream in, Date date,
- DirectoryEntryInfo directoryEntryInfo) throws IOException {
- copyOrSkipEntry(filename, in, SkipMode.COPY, date, directoryEntryInfo, true);
+ public ZipCombiner(ZipEntryFilter entryFilter, OutputStream out) {
+ this(OutputMode.DONT_CARE, entryFilter, out);
}
/**
- * Copies or skips the current ZIP file entry. Requires that the entire entry
- * header is present in {@link #headerBuffer}. It uses the current mode to
- * decide whether to compress or decompress the entry.
+ * Creates a {@link ZipCombiner} for combining ZIP files using the specified {@link OutputMode},
+ * and destination {@link OutputStream}. Uses a {@link CopyEntryFilter} as the
+ * {@link ZipEntryFilter}.
*
- * @throws IOException if the underlying stream throws an IOException
+ * @param mode the compression preference for the output ZIP file
+ * @param out the {@link OutputStream} for writing the combined ZIP file
*/
- private void copyOrSkipEntry(String filename, InputStream in, SkipMode skip, Date date,
- DirectoryEntryInfo directoryEntryInfo, boolean rename) throws IOException {
- final int method = getUnsignedShort(headerBuffer, COMPRESSION_METHOD_OFFSET);
-
- // We can cast here, because the result is only treated as a bitmask.
- int dosTime = date == null ? (int) getUnsignedInt(headerBuffer, MTIME_OFFSET)
- : dateToDosTime(date);
- if (skip == SkipMode.COPY) {
- if ((mode == OutputMode.FORCE_DEFLATE) && (method == STORED_METHOD)
- && !filename.endsWith("/")) {
- modifyAndCopyEntry(filename, in, dosTime);
- return;
- } else if ((mode == OutputMode.FORCE_STORED) && (method == DEFLATE_METHOD)) {
- modifyAndCopyEntry(filename, in, dosTime);
- return;
- }
- }
-
- int directoryOffset = copyOrSkipEntryHeader(filename, in, date, directoryEntryInfo,
- skip, rename);
-
- copyOrSkipEntryData(filename, in, skip, directoryOffset);
+ public ZipCombiner(OutputMode mode, OutputStream out) {
+ this(mode, new CopyEntryFilter(), out);
}
/**
- * Copies or skips the header of an entry, including filename and extra data.
- * Requires that the entire entry header is present in {@link #headerBuffer}.
+ * Creates a {@link ZipCombiner} for combining ZIP files using the specified destination
+ * {@link OutputStream}. Uses the DONT_CARE {@link OutputMode} and a {@link CopyEntryFilter} as
+ * the {@link ZipEntryFilter}.
*
- * @returns the enrty offset in the central directory
- * @throws IOException if the underlying stream throws an IOException
+ * @param out the {@link OutputStream} for writing the combined ZIP file
*/
- private int copyOrSkipEntryHeader(String filename, InputStream in, Date date,
- DirectoryEntryInfo directoryEntryInfo, SkipMode skip, boolean rename)
- throws IOException {
- final int fileNameLength = getUnsignedShort(headerBuffer, FILENAME_LENGTH_OFFSET);
- final int extraFieldLength = getUnsignedShort(headerBuffer, EXTRA_LENGTH_OFFSET);
-
- byte[] fileNameAsBytes = null;
- if (rename) {
- // If the entry is renamed, we patch the filename length in the buffer
- // before it's copied, and before writing to the central directory.
- fileNameAsBytes = filename.getBytes(UTF_8);
- checkArgument(fileNameAsBytes.length <= 65535,
- "File name too long: %s bytes (max. 65535)", fileNameAsBytes.length);
- setUnsignedShort(headerBuffer, FILENAME_LENGTH_OFFSET, (short) fileNameAsBytes.length);
- }
-
- int directoryOffset = 0;
- if (skip == SkipMode.COPY) {
- if (date != null) {
- int dosTime = dateToDosTime(date);
- setUnsignedShort(headerBuffer, MTIME_OFFSET, (short) dosTime); // lower 16 bits
- setUnsignedShort(headerBuffer, MDATE_OFFSET, (short) (dosTime >> 16)); // upper 16 bits
- }
- // Call this before writing the data out, so that we get the correct offset.
- directoryOffset = fillDirectoryEntryBuffer(directoryEntryInfo);
- write(headerBuffer, 0, FILE_HEADER_BUFFER_SIZE);
- }
- if (!rename) {
- forkOrSkipData(in, fileNameLength, skip);
- } else {
- forkOrSkipData(in, fileNameLength, SkipMode.SKIP);
- write(fileNameAsBytes);
- centralDirectory.writeToCentralDirectory(fileNameAsBytes);
- }
- forkOrSkipData(in, extraFieldLength, skip);
- return directoryOffset;
+ public ZipCombiner(OutputStream out) {
+ this(OutputMode.DONT_CARE, new CopyEntryFilter(), out);
}
/**
- * Copy or skip the data of an entry. Requires that the
- * entire entry header is present in {@link #headerBuffer}.
+ * Write all contents from the {@link InputStream} as a prefix file for the combined ZIP file.
*
- * @throws IOException if the underlying stream throws an IOException
+ * @param in the {@link InputStream} containing the prefix file data
+ * @throws IOException if there is an error writing the prefix file
*/
- private void copyOrSkipEntryData(String filename, InputStream in, SkipMode skip,
- int directoryOffset) throws IOException {
- final int flags = getUnsignedShort(headerBuffer, GENERAL_PURPOSE_FLAGS_OFFSET);
- final int method = getUnsignedShort(headerBuffer, COMPRESSION_METHOD_OFFSET);
- if ((flags & SIZE_MASKED_FLAG) != 0) {
- // The compressed data size is unknown.
- if (method != DEFLATE_METHOD) {
- throw new AssertionError("This should have been checked in validateHeader().");
- }
- copyOrSkipDeflateData(in, skip);
- // The flags indicate that a data descriptor must follow the data.
- readFully(in, 16);
- if (getUnsignedInt(buffer, bufferOffset) != DATA_DESCRIPTOR_MARKER) {
- throw new IOException("Missing data descriptor for " + filename + " in " + currentInputFile
- + ".");
- }
- long crc32 = getUnsignedInt(buffer, bufferOffset + 4);
- long compressedSize = getUnsignedInt(buffer, bufferOffset + 8);
- long uncompressedSize = getUnsignedInt(buffer, bufferOffset + 12);
- if (skip == SkipMode.COPY) {
- fixDirectoryEntry(directoryOffset, crc32, compressedSize, uncompressedSize);
- }
- copyOrSkipData(in, 16, skip);
- } else {
- // The size value is present in the header, so just copy that amount.
- long compressedSize = getUnsignedInt(headerBuffer, COMPRESSED_SIZE_OFFSET);
- copyOrSkipData(in, compressedSize, skip);
- }
+ public void prependExecutable(InputStream in) throws IOException {
+ out.startPrefixFile();
+ copyStream(in, out);
+ out.endPrefixFile();
}
/**
- * An input stream that reads a fixed number of bytes from the given input
- * stream before it returns end-of-input. It uses the local buffer, so it
- * can't be static.
+ * Adds a directory entry to the combined ZIP file using the specified filename and date.
+ *
+ * @param filename the name of the directory to create
+ * @param date the modified time to assign to the directory
+ * @throws IOException if there is an error writing the directory entry
*/
- private class FixedLengthInputStream extends InputStream {
-
- private final InputStream in;
- private long remainingBytes;
- private final byte[] singleByteBuffer = new byte[1];
-
- FixedLengthInputStream(InputStream in, long remainingBytes) {
- this.in = in;
- this.remainingBytes = remainingBytes;
- }
-
- @Override
- public int read() throws IOException {
- int bytesRead = read(singleByteBuffer, 0, 1);
- return (bytesRead == -1) ? -1 : singleByteBuffer[0];
- }
-
- @Override
- public int read(byte b[], int off, int len) throws IOException {
- checkArgument(len >= 0);
- checkArgument(off >= 0);
- checkArgument(off + len <= b.length);
- if (remainingBytes == 0) {
- return -1;
- }
- if (bufferLength == 0) {
- readMoreData(in);
- }
- int bytesToCopy = len;
- if (remainingBytes < bytesToCopy) {
- bytesToCopy = (int) remainingBytes;
- }
- if (bufferLength < bytesToCopy) {
- bytesToCopy = bufferLength;
- }
- System.arraycopy(buffer, bufferOffset, b, off, bytesToCopy);
- bufferOffset += bytesToCopy;
- bufferLength -= bytesToCopy;
- remainingBytes -= bytesToCopy;
- return bytesToCopy;
- }
+ public void addDirectory(String filename, Date date) throws IOException {
+ addDirectory(filename, date, new ExtraData[0]);
}
/**
- * An input stream that reads from a given input stream, decoding that data
- * according to the DEFLATE algorithm. The DEFLATE data stream implicitly
- * contains its own end-of-input marker. It uses the local buffer, so it
- * can't be static.
+ * Adds a directory entry to the combined ZIP file using the specified filename, date, and extra
+ * data.
+ *
+ * @param filename the name of the directory to create
+ * @param date the modified time to assign to the directory
+ * @param extra the extra field data to add to the directory entry
+ * @throws IOException if there is an error writing the directory entry
*/
- private class DeflateInputStream extends InputStream {
-
- private final InputStream in;
- private final byte[] singleByteBuffer = new byte[1];
- private final MutableInt consumedBytes = new MutableInt(0);
+ public void addDirectory(String filename, Date date, ExtraData[] extra) throws IOException {
+ checkArgument(filename.endsWith("/"), "Directory names must end with a /");
+ checkState(!entries.containsKey(filename),
+ "Zip already contains a directory named %s", filename);
- DeflateInputStream(InputStream in) {
- this.in = in;
- }
-
- @Override
- public int read() throws IOException {
- int bytesRead = read(singleByteBuffer, 0, 1);
- // Do an unsigned cast on the byte from the buffer if it exists.
- return (bytesRead == -1) ? -1 : (singleByteBuffer[0] & 0xff);
- }
-
- @Override
- public int read(byte b[], int off, int len) throws IOException {
- if (inflater.finished()) {
- return -1;
- }
- int length = inflateData(in, b, off, len, consumedBytes);
- int bytesRead = consumedBytes.getValue();
- bufferOffset += bytesRead;
- bufferLength -= bytesRead;
- return length == 0 ? -1 : length;
- }
+ ZipFileEntry entry = new ZipFileEntry(filename);
+ entry.setMethod(Compression.STORED);
+ entry.setCrc(0);
+ entry.setSize(0);
+ entry.setCompressedSize(0);
+ entry.setTime(date != null ? date.getTime() : new Date().getTime());
+ entry.setExtra(new ExtraDataList(extra));
+ out.putNextEntry(entry);
+ out.closeEntry();
+ entries.put(filename, entry);
}
/**
- * Handles a custom merge operation with the given strategy. This method
- * creates an appropriate input stream and hands it to the strategy for
- * processing. Requires that the entire entry header is present in {@link
- * #headerBuffer}.
+ * Adds a file with the specified name to the combined ZIP file.
*
- * @throws IOException if one of the underlying stream throws an IOException,
- * if the ZIP entry data is inconsistent, or if the
- * implementation cannot handle the compression method
- * given in the ZIP entry
+ * @param filename the name of the file to create
+ * @param in the {@link InputStream} containing the file data
+ * @throws IOException if there is an error writing the file entry
+ * @throws IllegalArgumentException if the combined ZIP file already contains a file of the same
+ * name.
*/
- private void handleCustomMerge(final InputStream in, CustomMergeStrategy mergeStrategy,
- ByteArrayOutputStream outputBuffer) throws IOException {
- final int flags = getUnsignedShort(headerBuffer, GENERAL_PURPOSE_FLAGS_OFFSET);
- final int method = getUnsignedShort(headerBuffer, COMPRESSION_METHOD_OFFSET);
- final long compressedSize = getUnsignedInt(headerBuffer, COMPRESSED_SIZE_OFFSET);
-
- final int fileNameLength = getUnsignedShort(headerBuffer, FILENAME_LENGTH_OFFSET);
- final int extraFieldLength = getUnsignedShort(headerBuffer, EXTRA_LENGTH_OFFSET);
-
- copyOrSkipData(in, fileNameLength, SkipMode.SKIP);
- copyOrSkipData(in, extraFieldLength, SkipMode.SKIP);
- if (method == STORED_METHOD) {
- mergeStrategy.merge(new FixedLengthInputStream(in, compressedSize), outputBuffer);
- } else if (method == DEFLATE_METHOD) {
- inflater.reset();
- // TODO(bazel-team): Defend against the mergeStrategy not reading the complete input.
- mergeStrategy.merge(new DeflateInputStream(in), outputBuffer);
- if ((flags & SIZE_MASKED_FLAG) != 0) {
- copyOrSkipData(in, 16, SkipMode.SKIP);
- }
- } else {
- throw new AssertionError("This should have been checked in validateHeader().");
- }
+ public void addFile(String filename, InputStream in) throws IOException {
+ addFile(filename, null, in);
}
/**
- * Implementation of the strategy callback.
+ * Adds a file with the specified name and date to the combined ZIP file.
+ *
+ * @param filename the name of the file to create
+ * @param date the modified time to assign to the file
+ * @param in the {@link InputStream} containing the file data
+ * @throws IOException if there is an error writing the file entry
+ * @throws IllegalArgumentException if the combined ZIP file already contains a file of the same
+ * name.
*/
- private class TheStrategyCallback implements StrategyCallback {
-
- private String filename;
- private final InputStream in;
-
- // Use an atomic boolean to make sure that only a single call goes
- // through, even if there are multiple concurrent calls. Paranoid
- // defensive programming.
- private final AtomicBoolean callDone = new AtomicBoolean();
-
- TheStrategyCallback(String filename, InputStream in) {
- this.filename = filename;
- this.in = in;
- }
-
- // Verify that this is the first call and throw an exception if not.
- private void checkCall() {
- checkState(callDone.compareAndSet(false, true), "The callback was already called once.");
- }
-
- @Override
- public void copy(Date date) throws IOException {
- checkCall();
- if (!containsFile(filename)) {
- fileNames.put(filename, COPIED_FILE_ENTRY);
- copyOrSkipEntry(filename, in, SkipMode.COPY, date, DEFAULT_DIRECTORY_ENTRY_INFO);
- } else { // can't copy, name already used for renamed entry
- copyOrSkipEntry(filename, in, SkipMode.SKIP, null, DEFAULT_DIRECTORY_ENTRY_INFO);
- }
- }
-
- @Override
- public void rename(String newName, Date date) throws IOException {
- checkCall();
- if (!containsFile(newName)) {
- fileNames.put(newName, RENAMED_FILE_ENTRY);
- renameEntry(newName, in, date, DEFAULT_DIRECTORY_ENTRY_INFO);
- } else {
- copyOrSkipEntry(filename, in, SkipMode.SKIP, null, DEFAULT_DIRECTORY_ENTRY_INFO);
- }
- filename = newName;
- }
-
- @Override
- public void skip() throws IOException {
- checkCall();
- if (!containsFile(filename)) {// don't overwrite possible RENAMED_FILE_ENTRY value
- fileNames.put(filename, COPIED_FILE_ENTRY);
- }
- copyOrSkipEntry(filename, in, SkipMode.SKIP, null, DEFAULT_DIRECTORY_ENTRY_INFO);
- }
-
- @Override
- public void customMerge(Date date, CustomMergeStrategy strategy) throws IOException {
- checkCall();
- ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
- fileNames.put(filename, new FileEntry(strategy, outputBuffer, dateToDosTime(date)));
- handleCustomMerge(in, strategy, outputBuffer);
- }
+ public void addFile(String filename, Date date, InputStream in) throws IOException {
+ ZipFileEntry entry = new ZipFileEntry(filename);
+ entry.setTime(date != null ? date.getTime() : new Date().getTime());
+ addFile(entry, in);
}
/**
- * Validates that the current entry obeys all the restrictions of this implementation.
+ * Adds a file with attributes specified by the {@link ZipFileEntry} to the combined ZIP file.
*
- * @throws IOException if the current entry doesn't obey the restrictions
+ * @param entry the {@link ZipFileEntry} containing the entry meta-data
+ * @param in the {@link InputStream} containing the file data
+ * @throws IOException if there is an error writing the file entry
+ * @throws IllegalArgumentException if the combined ZIP file already contains a file of the same
+ * name.
*/
- private void validateHeader() throws IOException {
- // We only handle DEFLATE and STORED, like java.util.zip.
- final int method = getUnsignedShort(headerBuffer, COMPRESSION_METHOD_OFFSET);
- if ((method != DEFLATE_METHOD) && (method != STORED_METHOD)) {
- throw new IOException("Unable to handle compression methods other than DEFLATE!");
- }
+ public void addFile(ZipFileEntry entry, InputStream in) throws IOException {
+ checkNotNull(entry, "Zip entry must not be null.");
+ checkNotNull(in, "Input stream must not be null.");
+ checkArgument(!entries.containsKey(entry.getName()), "Zip already contains a file named '%s'.",
+ entry.getName());
- // If the method is STORED, then the size must be available in the header.
- final int flags = getUnsignedShort(headerBuffer, GENERAL_PURPOSE_FLAGS_OFFSET);
- if ((method == STORED_METHOD) && ((flags & SIZE_MASKED_FLAG) != 0)) {
- throw new IOException("If the method is STORED, then the size must be available in the"
- + " header!");
- }
-
- // If the method is STORED, the compressed and uncompressed sizes must be equal.
- final long compressedSize = getUnsignedInt(headerBuffer, COMPRESSED_SIZE_OFFSET);
- final long uncompressedSize = getUnsignedInt(headerBuffer, UNCOMPRESSED_SIZE_OFFSET);
- if ((method == STORED_METHOD) && (compressedSize != uncompressedSize)) {
- throw new IOException("Compressed and uncompressed sizes for STORED entry differ!");
- }
+ ByteArrayOutputStream uncompressed = new ByteArrayOutputStream();
+ copyStream(in, uncompressed);
- // The compressed or uncompressed size being set to 0xffffffff is a strong indicator that the
- // ZIP file is in ZIP64 mode, which supports files larger than 2^32.
- // TODO(bazel-team): Support the ZIP64 extension.
- if ((compressedSize == MAXIMUM_DATA_SIZE) || (uncompressedSize == MAXIMUM_DATA_SIZE)) {
- throw new IOException("Unable to handle ZIP64 compressed files.");
- }
+ writeEntryFromBuffer(new ZipFileEntry(entry), uncompressed.toByteArray());
}
/**
- * Reads a file entry from the input stream, calls the entryFilter to
- * determine what to do with the entry, and performs the requested operation.
- * Returns true if the input stream contained another entry.
+ * Adds a new entry into the output, by reading the input stream until it returns end of stream.
+ * This method does not call {@link ZipEntryFilter#accept}.
*
- * @throws IOException if one of the underlying stream throws an IOException,
- * if the ZIP contains unsupported, inconsistent or
- * incomplete data or if the filter throws an IOException
+ * @throws IOException if one of the underlying streams throws an IOException
+ * or if the input stream returns more data than
+ * supported by the ZIP format
+ * @throws IllegalStateException if an entry with the given name already
+ * exists
+ * @throws IllegalArgumentException if the given file name is longer than
+ * supported by the ZIP format
*/
- private boolean handleNextEntry(final InputStream in) throws IOException {
- // Just try to read the complete header and fail if it didn't work.
- try {
- readFully(in, FILE_HEADER_BUFFER_SIZE);
- } catch (EOFException e) {
- return false;
- }
-
- System.arraycopy(buffer, bufferOffset, headerBuffer, 0, FILE_HEADER_BUFFER_SIZE);
- bufferOffset += FILE_HEADER_BUFFER_SIZE;
- bufferLength -= FILE_HEADER_BUFFER_SIZE;
- if (getUnsignedInt(headerBuffer, 0) != LOCAL_FILE_HEADER_MARKER) {
- return false;
- }
- validateHeader();
-
- final int fileNameLength = getUnsignedShort(headerBuffer, FILENAME_LENGTH_OFFSET);
- readFully(in, fileNameLength);
- // TODO(bazel-team): If I read the spec correctly, this should be UTF-8 rather than ISO-8859-1.
- final String filename = new String(buffer, bufferOffset, fileNameLength, ISO_8859_1);
-
- FileEntry handler = fileNames.get(filename);
- // The handler is null if this is the first time we see an entry with this filename,
- // or if all previous entries with this name were renamed by the filter (and we can
- // pretend we didn't encounter the name yet).
- // If the handler is RENAMED_FILE_ENTRY, a previous entry was renamed as filename,
- // in which case the filter should now be invoked for this name for the first time,
- // giving the filter a chance to choose an unique name.
- if (handler == null || handler == RENAMED_FILE_ENTRY) {
- TheStrategyCallback callback = new TheStrategyCallback(filename, in);
- entryFilter.accept(filename, callback);
- if (fileNames.get(callback.filename) == null && fileNames.get(filename) == null) {
- throw new IllegalStateException();
- }
- } else if (handler.mergeStrategy == null) {
- copyOrSkipEntry(filename, in, SkipMode.SKIP, null, DEFAULT_DIRECTORY_ENTRY_INFO);
- } else {
- handleCustomMerge(in, handler.mergeStrategy, handler.outputBuffer);
- }
- return true;
+ @Deprecated
+ public void addFile(String filename, Date date, InputStream in,
+ DirectoryEntryInfo directoryEntryInfo) throws IOException {
+ ZipFileEntry entry = new ZipFileEntry(filename);
+ entry.setTime(date != null ? date.getTime() : new Date().getTime());
+ entry.setVersion(directoryEntryInfo.madeByVersion);
+ entry.setExternalAttributes(directoryEntryInfo.externalFileAttribute);
+ addFile(entry, in);
}
/**
- * Clears the internal buffer.
+ * Adds the contents of a ZIP file to the combined ZIP file using the specified
+ * {@link ZipEntryFilter} to determine the appropriate action for each file.
+ *
+ * @param in the InputStream of the ZIP file to add to the combined ZIP file
+ * @throws IOException if there is an error reading the ZIP file or writing entries to the
+ * combined ZIP file
*/
- private void clearBuffer() {
- bufferOffset = 0;
- bufferLength = 0;
+ @Deprecated
+ public void addZip(InputStream in) throws IOException {
+ addZip(null, in);
}
/**
- * Copies another ZIP file into the output. If multiple entries with the same
- * name are present, the first such entry is copied, but the others are
- * ignored. This is also true for multiple invocations of this method. The
- * {@code inputName} parameter is used to provide better error messages in the
- * case of a failure to decode the ZIP file.
+ * Adds the contents of a ZIP file to the combined ZIP file using the specified
+ * {@link ZipEntryFilter} to determine the appropriate action for each file.
*
- * @throws IOException if one of the underlying stream throws an IOException,
- * if the ZIP contains unsupported, inconsistent or
- * incomplete data or if the filter throws an IOException
+ * @param inputName the name of the ZIP file to add for providing better error messages
+ * @param in the InputStream of the ZIP file to add to the combined ZIP file
+ * @throws IOException if there is an error reading the ZIP file or writing entries to the
+ * combined ZIP file
*/
+ @Deprecated
public void addZip(String inputName, InputStream in) throws IOException {
- if (finished) {
- throw new IllegalStateException();
- }
- if (in == null) {
- throw new NullPointerException();
- }
- clearBuffer();
- currentInputFile = inputName;
- while (handleNextEntry(in)) {/*handleNextEntry has side-effect.*/}
- }
-
- public void addZip(InputStream in) throws IOException {
- addZip(null, in);
- }
-
- private void copyStreamToEntry(String filename, InputStream in, int dosTime,
- ExtraData[] extraDataEntries, boolean compress, DirectoryEntryInfo directoryEntryInfo)
- throws IOException {
- fileNames.put(filename, COPIED_FILE_ENTRY);
-
- byte[] fileNameAsBytes = filename.getBytes(UTF_8);
- checkArgument(fileNameAsBytes.length <= 65535,
- "File name too long: %s bytes (max. 65535)", fileNameAsBytes.length);
-
- // Note: This method can be called with an input stream that uses the buffer field of this
- // class. We use a local buffer here to avoid conflicts.
- byte[] localBuffer = new byte[4096];
-
- byte[] uncompressedData = null;
- if (!compress) {
- ByteArrayOutputStream temp = new ByteArrayOutputStream();
- int bytesRead;
- while ((bytesRead = in.read(localBuffer)) != -1) {
- temp.write(localBuffer, 0, bytesRead);
- }
- uncompressedData = temp.toByteArray();
- }
- byte[] extraData = null;
- if (extraDataEntries.length != 0) {
- int totalLength = 0;
- for (ExtraData extra : extraDataEntries) {
- int length = extra.getData().length;
- if (totalLength > 0xffff - 4 - length) {
- throw new IOException("Total length of extra data too big.");
+ File file = Files.createTempFile(inputName, null).toFile();
+ Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ addZip(file);
+ file.deleteOnExit();
+ }
+
+ /**
+ * Adds the contents of a ZIP file to the combined ZIP file using the specified
+ * {@link ZipEntryFilter} to determine the appropriate action for each file.
+ *
+ * @param zipFile the ZIP file to add to the combined ZIP file
+ * @throws IOException if there is an error reading the ZIP file or writing entries to the
+ * combined ZIP file
+ */
+ public void addZip(File zipFile) throws IOException {
+ try (ZipReader zip = new ZipReader(zipFile)) {
+ for (ZipFileEntry entry : zip.entries()) {
+ String filename = entry.getName();
+ EntryAction action = getAction(filename);
+ switch (action.getType()) {
+ case SKIP:
+ break;
+ case COPY:
+ case RENAME:
+ writeEntry(zip, entry, action);
+ break;
+ case MERGE:
+ entries.put(filename, null);
+ InputStream in = zip.getRawInputStream(entry);
+ if (entry.getMethod() == Compression.DEFLATED) {
+ in = new InflaterInputStream(in, getInflater());
+ }
+ action.getStrategy().merge(in, action.getMergeBuffer());
+ break;
}
- totalLength += length + 4;
- }
- extraData = new byte[totalLength];
- int position = 0;
- for (ExtraData extra : extraDataEntries) {
- byte[] data = extra.getData();
- setUnsignedShort(extraData, position + 0, extra.getId());
- setUnsignedShort(extraData, position + 2, (short) data.length);
- System.arraycopy(data, 0, extraData, position + 4, data.length);
- position += data.length + 4;
}
}
+ }
- // write header
- Arrays.fill(headerBuffer, (byte) 0);
- setUnsignedInt(headerBuffer, 0, LOCAL_FILE_HEADER_MARKER); // file header signature
- if (compress) {
- setUnsignedShort(headerBuffer, 4, (short) VERSION_DEFLATE); // version to extract
- setUnsignedShort(headerBuffer, 6, (short) SIZE_MASKED_FLAG); // general purpose bit flag
- setUnsignedShort(headerBuffer, 8, (short) DEFLATE_METHOD); // compression method
- } else {
- setUnsignedShort(headerBuffer, 4, (short) VERSION_STORED); // version to extract
- setUnsignedShort(headerBuffer, 6, (short) 0); // general purpose bit flag
- setUnsignedShort(headerBuffer, 8, (short) STORED_METHOD); // compression method
- }
- setUnsignedShort(headerBuffer, 10, (short) dosTime); // mtime
- setUnsignedShort(headerBuffer, 12, (short) (dosTime >> 16)); // mdate
- if (uncompressedData != null) {
- CRC32 crc = new CRC32();
- crc.update(uncompressedData);
- setUnsignedInt(headerBuffer, 14, (int) crc.getValue()); // crc32
- setUnsignedInt(headerBuffer, 18, uncompressedData.length); // compressed size
- setUnsignedInt(headerBuffer, 22, uncompressedData.length); // uncompressed size
- } else {
- setUnsignedInt(headerBuffer, 14, 0); // crc32
- setUnsignedInt(headerBuffer, 18, 0); // compressed size
- setUnsignedInt(headerBuffer, 22, 0); // uncompressed size
- }
- setUnsignedShort(headerBuffer, 26, (short) fileNameAsBytes.length); // file name length
- if (extraData != null) {
- setUnsignedShort(headerBuffer, 28, (short) extraData.length); // extra field length
- } else {
- setUnsignedShort(headerBuffer, 28, (short) 0); // extra field length
+ /** Returns the action to take for a file of the given filename. */
+ private EntryAction getAction(String filename) throws IOException {
+ // If this filename has not been encountered before (no entry for filename) or this filename
+ // has been renamed (RENAME entry for filename), the desired action should be recomputed.
+ if (!actions.containsKey(filename) || actions.get(filename).getType() == ActionType.RENAME) {
+ callback.resetForFile(filename);
+ entryFilter.accept(filename, callback);
}
+ checkState(actions.containsKey(filename),
+ "Action for file '%s' should have been set by ZipEntryFilter.", filename);
- // This call works for both compressed or uncompressed entries.
- int directoryOffset = fillDirectoryEntryBuffer(directoryEntryInfo);
- write(headerBuffer);
- write(fileNameAsBytes);
- centralDirectory.writeToCentralDirectory(fileNameAsBytes);
- if (extraData != null) {
- write(extraData);
- centralDirectory.writeToCentralDirectory(extraData);
+ EntryAction action = actions.get(filename);
+ // Only copy if this is the first instance of filename.
+ if (action.getType() == ActionType.COPY && entries.containsKey(filename)) {
+ action = new EntryAction(ActionType.SKIP, action);
+ actions.put(filename, action);
}
-
- // write data
- if (uncompressedData != null) {
- write(uncompressedData);
- } else {
- try (DeflaterOutputStream deflaterStream = new DeflaterOutputStream()) {
- int bytesRead;
- while ((bytesRead = in.read(localBuffer)) != -1) {
- deflaterStream.write(localBuffer, 0, bytesRead);
- }
- deflaterStream.finish();
-
- // write data descriptor
- Arrays.fill(headerBuffer, (byte) 0);
- setUnsignedInt(headerBuffer, 0, DATA_DESCRIPTOR_MARKER);
- setUnsignedInt(headerBuffer, 4, deflaterStream.getCRC()); // crc32
- setUnsignedInt(headerBuffer, 8, deflaterStream.getCompressedSize()); // compressed size
- setUnsignedInt(headerBuffer, 12, deflaterStream.getUncompressedSize()); // uncompressed size
- write(headerBuffer, 0, 16);
- fixDirectoryEntry(directoryOffset, deflaterStream.getCRC(),
- deflaterStream.getCompressedSize(), deflaterStream.getUncompressedSize());
+ // Only rename if there is not already an entry with filename or filename's action is SKIP.
+ if (action.getType() == ActionType.RENAME) {
+ if (actions.containsKey(action.getNewName())
+ && actions.get(action.getNewName()).getType() == ActionType.SKIP) {
+ action = new EntryAction(ActionType.SKIP, action);
+ }
+ if (entries.containsKey(action.getNewName())) {
+ action = new EntryAction(ActionType.SKIP, action);
}
}
+ return action;
}
- /**
- * Adds a new entry into the output, by reading the input stream until it
- * returns end of stream. Equivalent to
- * {@link #addFile(String, Date, InputStream, DirectoryEntryInfo)}, but uses
- * {@link #DEFAULT_DIRECTORY_ENTRY_INFO} for the file's directory entry.
- */
- public void addFile(String filename, Date date, InputStream in) throws IOException {
- addFile(filename, date, in, DEFAULT_DIRECTORY_ENTRY_INFO);
- }
+ /** Writes an entry with the given name, date and external file attributes from the buffer. */
+ private void writeEntryFromBuffer(ZipFileEntry entry, byte[] uncompressed) throws IOException {
+ CRC32 crc = new CRC32();
+ crc.update(uncompressed);
- /**
- * Adds a new entry into the output, by reading the input stream until it
- * returns end of stream. This method does not call {@link
- * ZipEntryFilter#accept}.
- *
- * @throws IOException if one of the underlying streams throws an IOException
- * or if the input stream returns more data than
- * supported by the ZIP format
- * @throws IllegalStateException if an entry with the given name already
- * exists
- * @throws IllegalArgumentException if the given file name is longer than
- * supported by the ZIP format
- */
- public void addFile(String filename, Date date, InputStream in,
- DirectoryEntryInfo directoryEntryInfo) throws IOException {
- checkNotFinished();
- if (in == null) {
- throw new NullPointerException();
- }
- if (filename == null) {
- throw new NullPointerException();
+ entry.setCrc(crc.getValue());
+ entry.setSize(uncompressed.length);
+ if (mode == OutputMode.FORCE_STORED) {
+ entry.setMethod(Compression.STORED);
+ entry.setCompressedSize(uncompressed.length);
+ writeEntry(entry, new ByteArrayInputStream(uncompressed));
+ } else {
+ ByteArrayOutputStream compressed = new ByteArrayOutputStream();
+ copyStream(new DeflaterInputStream(new ByteArrayInputStream(uncompressed), getDeflater()),
+ compressed);
+ entry.setMethod(Compression.DEFLATED);
+ entry.setCompressedSize(compressed.size());
+ writeEntry(entry, new ByteArrayInputStream(compressed.toByteArray()));
}
- checkState(!fileNames.containsKey(filename),
- "jar already contains a file named %s", filename);
- int dosTime = dateToDosTime(date != null ? date : new Date());
- copyStreamToEntry(filename, in, dosTime, NO_EXTRA_ENTRIES,
- mode != OutputMode.FORCE_STORED, // Always compress if we're allowed to.
- directoryEntryInfo);
}
/**
- * Adds a new directory entry into the output. This method does not call
- * {@link ZipEntryFilter#accept}. Uses {@link #DEFAULT_DIRECTORY_ENTRY_INFO} for the added
- * directory entry.
- *
- * @throws IOException if one of the underlying streams throws an IOException
- * @throws IllegalStateException if an entry with the given name already
- * exists
- * @throws IllegalArgumentException if the given file name is longer than
- * supported by the ZIP format
+ * Writes an entry from the specified source {@link ZipReader} and {@link ZipFileEntry} using the
+ * specified {@link EntryAction}.
+ *
+ * <p>Writes the output entry from the input entry performing inflation or deflation as needed
+ * and applies any values from the {@link EntryAction} as needed.
*/
- public void addDirectory(String filename, Date date, ExtraData[] extraDataEntries)
+ private void writeEntry(ZipReader zip, ZipFileEntry entry, EntryAction action)
throws IOException {
- checkNotFinished();
- checkArgument(filename.endsWith("/")); // Can also throw NPE.
- checkState(!fileNames.containsKey(filename),
- "jar already contains a directory named %s", filename);
- int dosTime = dateToDosTime(date != null ? date : new Date());
- copyStreamToEntry(filename, new ByteArrayInputStream(new byte[0]), dosTime, extraDataEntries,
- false, // Never compress directory entries.
- DEFAULT_DIRECTORY_ENTRY_INFO);
+ checkArgument(action.getType() != ActionType.SKIP,
+ "Cannot write a zip entry whose action is of type SKIP.");
+
+ ZipFileEntry outEntry = new ZipFileEntry(entry);
+ if (action.getType() == ActionType.RENAME) {
+ checkNotNull(action.getNewName(),
+ "ZipEntryFilter actions of type RENAME must not have a null filename.");
+ outEntry.setName(action.getNewName());
+ }
+
+ if (action.getDate() != null) {
+ outEntry.setTime(action.getDate().getTime());
+ }
+
+ InputStream data;
+ if (mode == OutputMode.FORCE_DEFLATE && entry.getMethod() != Compression.DEFLATED) {
+ // The output mode is deflate, but the entry compression is not. Create a deflater stream
+ // from the raw file data and deflate to a temporary byte array to determine the deflated
+ // size. Then use this byte array as the input stream for writing the entry.
+ ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+ copyStream(new DeflaterInputStream(zip.getRawInputStream(entry), getDeflater()), tmp);
+ data = new ByteArrayInputStream(tmp.toByteArray());
+ outEntry.setMethod(Compression.DEFLATED);
+ outEntry.setCompressedSize(tmp.size());
+ } else if (mode == OutputMode.FORCE_STORED && entry.getMethod() != Compression.STORED) {
+ // The output mode is stored, but the entry compression is not; create an inflater stream
+ // from the raw file data.
+ data = new InflaterInputStream(zip.getRawInputStream(entry), getInflater());
+ outEntry.setMethod(Compression.STORED);
+ outEntry.setCompressedSize(entry.getSize());
+ } else {
+ // Entry compression agrees with output mode; use the raw file data as is.
+ data = zip.getRawInputStream(entry);
+ }
+ writeEntry(outEntry, data);
}
/**
- * Adds a new directory entry into the output. This method does not call
- * {@link ZipEntryFilter#accept}.
- *
- * @throws IOException if one of the underlying streams throws an IOException
- * @throws IllegalStateException if an entry with the given name already
- * exists
- * @throws IllegalArgumentException if the given file name is longer than
- * supported by the ZIP format
+ * Writes the specified {@link ZipFileEntry} using the data from the given {@link InputStream}.
*/
- public void addDirectory(String filename, Date date)
- throws IOException {
- addDirectory(filename, date, NO_EXTRA_ENTRIES);
+ private void writeEntry(ZipFileEntry entry, InputStream data) throws IOException {
+ out.putNextEntry(entry);
+ copyStream(data, out);
+ out.closeEntry();
+ entries.put(entry.getName(), entry);
}
/**
- * A deflater output stream that also counts uncompressed and compressed
- * numbers of bytes and computes the CRC so that the data descriptor marker
- * is written correctly.
+ * Returns true if the combined ZIP file already contains a file of the specified file name.
*
- * <p>Not static, so it can access the write() methods.
+ * @param filename the filename of the file whose presence in the combined ZIP file is to be
+ * tested
+ * @return true if the combined ZIP file contains the specified file
*/
- private class DeflaterOutputStream extends OutputStream {
-
- private final Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
- private final CRC32 crc = new CRC32();
- private final byte[] outputBuffer = new byte[4096];
- private long uncompressedBytes = 0;
- private long compressedBytes = 0;
-
- @Override
- public void write(int b) throws IOException {
- byte[] buf = new byte[] { (byte) (b & 0xff) };
- write(buf, 0, buf.length);
- }
-
- @Override
- public void write(byte b[], int off, int len) throws IOException {
- checkNotFinished();
- uncompressedBytes += len;
- crc.update(b, off, len);
- deflater.setInput(b, off, len);
- while (!deflater.needsInput()) {
- deflate();
- }
- }
-
- @Override
- public void close() throws IOException {
- super.close();
- deflater.end();
- }
-
- /**
- * Writes out the remaining buffered data without closing the output
- * stream.
- */
- public void finish() throws IOException {
- checkNotFinished();
- deflater.finish();
- while (!deflater.finished()) {
- deflate();
- }
- if ((compressedBytes >= MAXIMUM_DATA_SIZE) || (uncompressedBytes >= MAXIMUM_DATA_SIZE)) {
- throw new IOException("Too much data for ZIP entry.");
- }
- }
-
- private void deflate() throws IOException {
- int length = deflater.deflate(outputBuffer);
- ZipCombiner.this.write(outputBuffer, 0, length);
- compressedBytes += length;
- }
-
- public int getCRC() {
- return (int) crc.getValue();
- }
-
- public int getCompressedSize() {
- return (int) compressedBytes;
- }
-
- public int getUncompressedSize() {
- return (int) uncompressedBytes;
- }
-
- private void checkNotFinished() {
- if (deflater.finished()) {
- throw new IllegalStateException();
- }
- }
+ public boolean containsFile(String filename) {
+ // TODO(apell): may be slightly different behavior because v1 returns true on skipped names.
+ return entries.containsKey(filename);
}
/**
- * Writes any remaining output data to the output stream and also creates the
- * merged entries by calling the {@link CustomMergeStrategy} implementations
- * given back from the ZIP entry filter.
+ * Writes any remaining output data to the output stream and also creates the merged entries by
+ * calling the {@link CustomMergeStrategy} implementations given back from the
+ * {@link ZipEntryFilter}.
*
- * @throws IOException if the output stream or the filter throws an
- * IOException
+ * @throws IOException if the output stream or the filter throws an IOException
* @throws IllegalStateException if this method was already called earlier
*/
public void finish() throws IOException {
- checkNotFinished();
- finished = true;
- for (Map.Entry<String, FileEntry> entry : fileNames.entrySet()) {
+ for (Entry<String, EntryAction> entry : actions.entrySet()) {
String filename = entry.getKey();
- CustomMergeStrategy mergeStrategy = entry.getValue().mergeStrategy;
- ByteArrayOutputStream outputBuffer = entry.getValue().outputBuffer;
- int dosTime = entry.getValue().dosTime;
- if (mergeStrategy == null) {
- // Do nothing.
- } else {
- mergeStrategy.finish(outputBuffer);
- copyStreamToEntry(filename, new ByteArrayInputStream(outputBuffer.toByteArray()), dosTime,
- NO_EXTRA_ENTRIES, true, DEFAULT_DIRECTORY_ENTRY_INFO);
- }
- }
-
- // Write central directory.
- if (out.bytesWritten >= MAXIMUM_DATA_SIZE) {
- throw new IOException("Unable to handle files bigger than 2^32 bytes.");
- }
- int startOfCentralDirectory = (int) out.bytesWritten;
- int centralDirectorySize = centralDirectory.writeTo(out);
-
- // end of central directory signature
- setUnsignedInt(directoryEntryBuffer, 0, END_OF_CENTRAL_DIRECTORY_MARKER);
- // number of this disk
- setUnsignedShort(directoryEntryBuffer, 4, (short) 0);
- // number of the disk with the start of the central directory
- setUnsignedShort(directoryEntryBuffer, 6, (short) 0);
- // total number of entries in the central directory on this disk
- setUnsignedShort(directoryEntryBuffer, 8, (short) fileCount);
- // total number of entries in the central directory
- setUnsignedShort(directoryEntryBuffer, 10, (short) fileCount);
- // size of the central directory
- setUnsignedInt(directoryEntryBuffer, 12, centralDirectorySize);
- // offset of start of central directory with respect to the starting disk number
- setUnsignedInt(directoryEntryBuffer, 16, startOfCentralDirectory);
- // .ZIP file comment length
- setUnsignedShort(directoryEntryBuffer, 20, (short) 0);
- write(directoryEntryBuffer, 0, 22);
-
- out.flush();
- }
+ EntryAction action = entry.getValue();
+ if (action.getType() == ActionType.MERGE) {
+ ByteArrayOutputStream uncompressed = action.getMergeBuffer();
+ action.getStrategy().finish(uncompressed);
- private void checkNotFinished() {
- if (finished) {
- throw new IllegalStateException();
+ ZipFileEntry e = new ZipFileEntry(filename);
+ e.setTime(action.getDate() != null ? action.getDate().getTime() : new Date().getTime());
+ writeEntryFromBuffer(e, uncompressed.toByteArray());
+ }
}
+ out.finish();
}
/**
* Writes any remaining output data to the output stream and closes it.
*
- * @throws IOException if the output stream or the filter throws an
- * IOException
+ * @throws IOException if the output stream or the filter throws an IOException
*/
- @Override
- public void close() throws IOException {
- if (!finished) {
- finish();
- }
+ @Override public void close() throws IOException {
+ finish();
out.close();
}
- /**
- * Turns this JAR file into an executable JAR by prepending an executable.
- * JAR files are placed at the end of a file, and executables are placed
- * at the beginning, so a file can be both, if desired.
- *
- * @param launcherIn The InputStream, from which the launcher is read.
- * @throws NullPointerException if launcherIn is null
- * @throws IOException if reading from launcherIn or writing to the output
- * stream throws an IOException.
- */
- public void prependExecutable(InputStream launcherIn) throws IOException {
- if (launcherIn == null) {
- throw new NullPointerException("No launcher specified");
- }
- byte[] buf = new byte[BUFFER_SIZE];
- int bytesRead;
- while ((bytesRead = launcherIn.read(buf)) > 0) {
- out.write(buf, 0, bytesRead);
- }
- }
-
- /**
- * Ensures the truth of an expression involving one or more parameters to the calling method.
- */
+ /** Ensures the truth of an expression involving one or more parameters to the calling method. */
private static void checkArgument(boolean expression,
@Nullable String errorMessageTemplate,
@Nullable Object... errorMessageArgs) {
@@ -1621,18 +715,18 @@ public final class ZipCombiner implements AutoCloseable {
}
}
- /**
- * Ensures the truth of an expression involving one or more parameters to the calling method.
- */
- private static void checkArgument(boolean expression) {
- if (!expression) {
- throw new IllegalArgumentException();
+ /** Ensures that an object reference passed as a parameter to the calling method is not null. */
+ public static <T> T checkNotNull(T reference,
+ @Nullable String errorMessageTemplate,
+ @Nullable Object... errorMessageArgs) {
+ if (reference == null) {
+ // If either of these parameters is null, the right thing happens anyway
+ throw new NullPointerException(String.format(errorMessageTemplate, errorMessageArgs));
}
+ return reference;
}
- /**
- * Ensures the truth of an expression involving state.
- */
+ /** Ensures the truth of an expression involving state. */
private static void checkState(boolean expression,
@Nullable String errorMessageTemplate,
@Nullable Object... errorMessageArgs) {
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingInputStream.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingInputStream.java
new file mode 100644
index 0000000000..bac26e2d95
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingInputStream.java
@@ -0,0 +1,85 @@
+// 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.zip;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An {@link InputStream} that counts the number of bytes read.
+ */
+public final class CountingInputStream extends FilterInputStream {
+
+ private static <T> T checkNotNull(T reference) {
+ if (reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ private long count;
+ private long mark = -1;
+
+ /**
+ * Wraps another input stream, counting the number of bytes read.
+ *
+ * @param in the input stream to be wrapped
+ */
+ public CountingInputStream(InputStream in) {
+ super(checkNotNull(in));
+ }
+
+ /** Returns the number of bytes read. */
+ public long getCount() {
+ return count;
+ }
+
+ @Override public int read() throws IOException {
+ int result = in.read();
+ count += result == -1 ? 0 : 1;
+ return result;
+ }
+
+ @Override public int read(byte[] b, int off, int len) throws IOException {
+ int result = in.read(b, off, len);
+ count += result == -1 ? 0 : result;
+ return result;
+ }
+
+ @Override public long skip(long n) throws IOException {
+ long result = in.skip(n);
+ count += result;
+ return result;
+ }
+
+ @Override public synchronized void mark(int readlimit) {
+ in.mark(readlimit);
+ mark = count;
+ // it's okay to mark even if mark isn't supported, as reset won't work
+ }
+
+ @Override public synchronized void reset() throws IOException {
+ if (!in.markSupported()) {
+ throw new IOException("Mark not supported");
+ }
+ if (mark == -1) {
+ throw new IOException("Mark not set");
+ }
+
+ in.reset();
+ count = mark;
+ }
+} \ No newline at end of file
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingOutputStream.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingOutputStream.java
new file mode 100644
index 0000000000..a08159ff14
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/CountingOutputStream.java
@@ -0,0 +1,54 @@
+// 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.zip;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** An OutputStream that counts the number of bytes written. */
+final class CountingOutputStream extends FilterOutputStream {
+
+ private long count;
+
+ /**
+ * Wraps another output stream, counting the number of bytes written.
+ *
+ * @param out the output stream to be wrapped
+ */
+ public CountingOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ /** Returns the number of bytes written. */
+ public long getCount() {
+ return count;
+ }
+
+ @Override public void write(int b) throws IOException {
+ out.write(b);
+ count++;
+ }
+
+ @Override public void write(byte[] b) throws IOException {
+ out.write(b);
+ count += b.length;
+ }
+
+ @Override public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ count += len;
+ }
+} \ No newline at end of file
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraData.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraData.java
new file mode 100644
index 0000000000..197f0f4246
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraData.java
@@ -0,0 +1,103 @@
+// Copyright 2014 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.zip;
+
+import java.util.Arrays;
+
+/**
+ * A holder class for extra data in a ZIP entry.
+ */
+public final class ExtraData {
+ static final int ID_OFFSET = 0;
+ static final int LENGTH_OFFSET = 2;
+ static final int FIXED_DATA_SIZE = 4;
+
+ private final int index;
+ private final byte[] buffer;
+
+ /**
+ * Creates a new {@link ExtraData} record with the specified id and data.
+ *
+ * @param id the ID tag for this extra data record
+ * @param data the data payload for this extra data record
+ */
+ public ExtraData(short id, byte[] data) {
+ if (data.length > 0xffff) {
+ throw new IllegalArgumentException(String.format("Data is too long. Is %d; max %d",
+ data.length, 0xffff));
+ }
+ index = 0;
+ buffer = new byte[FIXED_DATA_SIZE + data.length];
+ ZipUtil.shortToLittleEndian(buffer, ID_OFFSET, id);
+ ZipUtil.shortToLittleEndian(buffer, LENGTH_OFFSET, (short) data.length);
+ System.arraycopy(data, 0, buffer, FIXED_DATA_SIZE, data.length);
+ }
+
+ /**
+ * Creates a new {@link ExtraData} record using the buffer as the backing data store.
+ *
+ * <p><em>NOTE:</em> does not perform any defensive copying. Any modification to the buffer will
+ * alter the extra data record and can make it invalid.
+ *
+ * @param buffer the array containing the extra data record
+ * @param index the index where the extra data record is located
+ * @throws IllegalArgumentException if buffer does not contain a well formed extra data record
+ * at index
+ */
+ ExtraData(byte[] buffer, int index) {
+ if (index >= buffer.length) {
+ throw new IllegalArgumentException("index past end of buffer");
+ }
+ if (buffer.length - index < FIXED_DATA_SIZE) {
+ throw new IllegalArgumentException("incomplete extra data entry in buffer");
+ }
+ int length = ZipUtil.getUnsignedShort(buffer, index + LENGTH_OFFSET);
+ if (buffer.length - index - FIXED_DATA_SIZE < length) {
+ throw new IllegalArgumentException("incomplete extra data entry in buffer");
+ }
+ this.buffer = buffer;
+ this.index = index;
+ }
+
+ /** Returns the Id of the extra data record. */
+ public short getId() {
+ return ZipUtil.get16(buffer, index + ID_OFFSET);
+ }
+
+ /** Returns the total length of the extra data record in bytes. */
+ public int getLength() {
+ return getDataLength() + FIXED_DATA_SIZE;
+ }
+
+ /** Returns the length of the data payload of the extra data record in bytes. */
+ public int getDataLength() {
+ return ZipUtil.getUnsignedShort(buffer, index + LENGTH_OFFSET);
+ }
+
+ /** Returns a byte array copy of the data payload. */
+ public byte[] getData() {
+ return Arrays.copyOfRange(buffer, index + FIXED_DATA_SIZE, index + getLength());
+ }
+
+ /** Returns a byte array copy of the entire record. */
+ public byte[] getBytes() {
+ return Arrays.copyOfRange(buffer, index, index + getLength());
+ }
+
+ /** Returns the byte at index from the entire record. */
+ byte getByte(int index) {
+ return buffer[this.index + index];
+ }
+}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraDataList.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraDataList.java
new file mode 100644
index 0000000000..b3cd252e2e
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ExtraDataList.java
@@ -0,0 +1,161 @@
+// 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.zip;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+/**
+ * A list of {@link ExtraData} records to be associated with a {@link ZipFileEntry}. Supports
+ * creating the list directly from a byte array and modifying the list without reallocating the
+ * underlying buffer.
+ */
+public class ExtraDataList {
+ private final LinkedHashMap<Short, ExtraData> entries;
+
+ /**
+ * Create a new empty extra data list.
+ *
+ * <p><em>NOTE:</em> entries in a list created this way will be backed by their own storage.
+ */
+ public ExtraDataList() {
+ entries = new LinkedHashMap<>();
+ }
+
+ /**
+ * Creates an extra data list from the given extra data records.
+ *
+ * <p><em>NOTE:</em> entries in a list created this way will be backed by their own storage.
+ *
+ * @param extra the extra data records
+ */
+ public ExtraDataList(ExtraData... extra) {
+ this();
+ for (ExtraData e : extra) {
+ add(e);
+ }
+ }
+
+ /**
+ * Creates an extra data list from the entries contained in the given array.
+ *
+ * <p><em>NOTE:</em> entries in a list created this way will be backed by the buffer. No defensive
+ * copying is performed.
+ *
+ * @param buffer the array containing sequential extra data entries
+ */
+ public ExtraDataList(byte[] buffer) {
+ if (buffer.length > 0xffff) {
+ throw new IllegalArgumentException("invalid extra field length");
+ }
+ entries = new LinkedHashMap<>();
+ int index = 0;
+ while (index < buffer.length) {
+ ExtraData extra = new ExtraData(buffer, index);
+ entries.put(extra.getId(), extra);
+ index += extra.getLength();
+ }
+ }
+
+ /**
+ * Returns the extra data record with the specified id, or null if it does not exist.
+ */
+ public ExtraData get(short id) {
+ return entries.get(id);
+ }
+
+ /**
+ * Removes and returns the extra data record with the specified id if it exists.
+ *
+ * <p><em>NOTE:</em> does not modify the underlying storage, only marks the record as removed.
+ */
+ public ExtraData remove(short id) {
+ return entries.remove(id);
+ }
+
+ /**
+ * Returns if the list contains an extra data record with the specified id.
+ */
+ public boolean contains(short id) {
+ return entries.containsKey(id);
+ }
+
+ /**
+ * Adds a new entry to the end of the list.
+ *
+ * @throws IllegalArgumentException if adding the entry will make the list too long for the ZIP
+ * format
+ */
+ public void add(ExtraData entry) {
+ if (getLength() + entry.getLength() > 0xffff) {
+ throw new IllegalArgumentException("adding entry will make the extra field be too long");
+ }
+ entries.put(entry.getId(), entry);
+ }
+
+ /**
+ * Returns the overall length of the list in bytes.
+ */
+ public int getLength() {
+ int length = 0;
+ for (ExtraData e : entries.values()) {
+ length += e.getLength();
+ }
+ return length;
+ }
+
+ /**
+ * Creates and returns a byte array of the extra data list.
+ */
+ public byte[] getBytes() {
+ byte[] extra = new byte[getLength()];
+ try {
+ getByteStream().read(extra);
+ } catch (IOException impossible) {
+ throw new AssertionError(impossible);
+ }
+ return extra;
+ }
+
+ /**
+ * Returns an input stream for reading the extra data list entries.
+ */
+ public InputStream getByteStream() {
+ return new InputStream() {
+ private final Iterator<ExtraData> itr = entries.values().iterator();
+ private ExtraData entry;
+ private int index;
+
+ @Override
+ public int read() {
+ if (entry == null) {
+ if (itr.hasNext()) {
+ entry = itr.next();
+ index = 0;
+ } else {
+ return -1;
+ }
+ }
+ byte val = entry.getByte(index++);
+ if (index >= entry.getLength()) {
+ entry = null;
+ }
+ return val & 0xff;
+ }
+ };
+ }
+}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileData.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileData.java
new file mode 100644
index 0000000000..aaea5b90ad
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileData.java
@@ -0,0 +1,288 @@
+// 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.zip;
+
+import com.google.devtools.build.zip.ZipFileEntry.Feature;
+
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.zip.ZipException;
+
+import javax.annotation.Nullable;
+
+/**
+ * A representation of a ZIP file. Contains the file comment, encoding, and entries. Also contains
+ * internal information about the structure and location of ZIP file parts.
+ */
+class ZipFileData {
+ private final Charset charset;
+ private String comment;
+
+ private long centralDirectorySize;
+ private long centralDirectoryOffset;
+ private long expectedEntries;
+ private long numEntries;
+ private final Map<String, ZipFileEntry> entries;
+
+ private boolean maybeZip64;
+ private boolean isZip64;
+ private long zip64EndOfCentralDirectoryOffset;
+
+ /**
+ * Creates a new ZIP file with the specified charset encoding.
+ */
+ public ZipFileData(Charset charset) {
+ if (charset == null) {
+ throw new NullPointerException();
+ }
+ this.charset = charset;
+ comment = "";
+ entries = new LinkedHashMap<>();
+ }
+
+ /**
+ * Returns the encoding of the file.
+ */
+ public Charset getCharset() {
+ return charset;
+ }
+
+ /**
+ * Returns the file comment.
+ */
+ public String getComment() {
+ return comment;
+ }
+
+ /**
+ * Sets the file comment from the raw byte data in the file. Converts the bytes to a string using
+ * the file's charset encoding.
+ *
+ * @throws ZipException if the comment is longer than allowed by the ZIP format
+ */
+ public void setComment(byte[] comment) throws ZipException {
+ if (comment == null) {
+ throw new NullPointerException();
+ }
+ if (comment.length > 0xffff) {
+ throw new ZipException(String.format("File comment too long. Is %d; max %d.",
+ comment.length, 0xffff));
+ }
+ this.comment = fromBytes(comment);
+ }
+
+ /**
+ * Sets the file comment.
+ *
+ * @throws ZipException if the comment will be longer than allowed by the ZIP format when encoded
+ * using the file's charset encoding
+ */
+ public void setComment(String comment) throws ZipException {
+ setComment(getBytes(comment));
+ }
+
+ /**
+ * Returns the size of the central directory in bytes.
+ */
+ public long getCentralDirectorySize() {
+ return centralDirectorySize;
+ }
+
+ /**
+ * Sets the size of the central directory in bytes. If the size is larger than 0xffffffff, the
+ * file is set to Zip64 mode.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
+ * section 4.4.23
+ */
+ public void setCentralDirectorySize(long centralDirectorySize) {
+ this.centralDirectorySize = centralDirectorySize;
+ if (centralDirectorySize > 0xffffffffL) {
+ setZip64(true);
+ }
+ }
+
+ /**
+ * Returns the file offset of the start of the central directory.
+ */
+ public long getCentralDirectoryOffset() {
+ return centralDirectoryOffset;
+ }
+
+ /**
+ * Sets the file offset of the start of the central directory. If the offset is larger than
+ * 0xffffffff, the file is set to Zip64 mode.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
+ * section 4.4.24
+ */
+ public void setCentralDirectoryOffset(long offset) {
+ this.centralDirectoryOffset = offset;
+ if (centralDirectoryOffset > 0xffffffffL) {
+ setZip64(true);
+ }
+ }
+
+ /**
+ * Returns the number of entries expected to be in the ZIP file. This value is determined from the
+ * end of central directory record.
+ */
+ public long getExpectedEntries() {
+ return expectedEntries;
+ }
+
+ /**
+ * Sets the number of entries expected to be in the ZIP file. This value should be set by reading
+ * the end of central directory record.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
+ * section 4.4.22
+ */
+ public void setExpectedEntries(long count) {
+ this.expectedEntries = count;
+ if (expectedEntries > 0xffff) {
+ setZip64(true);
+ }
+ }
+
+ /**
+ * Returns the number of entries actually in the ZIP file. This value is derived from the number
+ * of times {@link #addEntry(ZipFileEntry)} was called.
+ *
+ * <p><em>NOTE:</em> This value should be used rather than getting the size from the
+ * {@link Collection} returned from {@link #getEntries()}, because the value may be too large to
+ * be properly represented by an int.
+ */
+ public long getNumEntries() {
+ return numEntries;
+ }
+
+ /**
+ * Sets the number of entries actually in the ZIP file. If the value is larger than 0xffff, the
+ * file is set to Zip64 mode.
+ */
+ private void setNumEntries(long numEntries) {
+ this.numEntries = numEntries;
+ if (numEntries > 0xffff) {
+ setZip64(true);
+ }
+ }
+
+ /**
+ * Returns a collection of all entries in the ZIP file.
+ */
+ public Collection<ZipFileEntry> getEntries() {
+ return entries.values();
+ }
+
+ /**
+ * Returns the entry with the given name, or null if it does not exist.
+ */
+ public ZipFileEntry getEntry(@Nullable String name) {
+ return entries.get(name);
+ }
+
+ /**
+ * Adds an entry to the ZIP file. If this causes the actual number of entries to exceed
+ * 0xffffffff, or if the file requires Zip64 features, the file is set to Zip64 mode.
+ */
+ public void addEntry(ZipFileEntry entry) {
+ entries.put(entry.getName(), entry);
+ setNumEntries(numEntries + 1);
+ if (entry.getFeatureSet().contains(Feature.ZIP64_SIZE)
+ || entry.getFeatureSet().contains(Feature.ZIP64_CSIZE)
+ || entry.getFeatureSet().contains(Feature.ZIP64_OFFSET)) {
+ setZip64(true);
+ }
+ }
+
+ /**
+ * Returns if the file may be in Zip64 mode. This is true if any of the values in the end of
+ * central directory record are -1.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
+ * section 4.4.19 - 4.4.24
+ */
+ public boolean isMaybeZip64() {
+ return maybeZip64;
+ }
+
+ /**
+ * Set if the file may be in Zip64 mode. This is true if any of the values in the end of
+ * central directory record are -1.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
+ * section 4.4.19 - 4.4.24
+ */
+ public void setMaybeZip64(boolean maybeZip64) {
+ this.maybeZip64 = maybeZip64;
+ }
+
+ /**
+ * Returns if the file is in Zip64 mode. This is true if any of a number of fields exceed the
+ * maximum value.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a> for
+ * details
+ */
+ public boolean isZip64() {
+ return isZip64;
+ }
+
+ /**
+ * Set if the file is in Zip64 mode. This is true if any of a number of fields exceed the maximum
+ * value.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a> for
+ * details
+ */
+ public void setZip64(boolean isZip64) {
+ this.isZip64 = isZip64;
+ setMaybeZip64(true);
+ }
+
+ /**
+ * Returns the file offset of the Zip64 end of central directory record. The record is only
+ * present if {@link #isZip64()} returns true.
+ */
+ public long getZip64EndOfCentralDirectoryOffset() {
+ return zip64EndOfCentralDirectoryOffset;
+ }
+
+ /**
+ * Sets the file offset of the Zip64 end of central directory record and sets the file to Zip64
+ * mode.
+ */
+ public void setZip64EndOfCentralDirectoryOffset(long offset) {
+ this.zip64EndOfCentralDirectoryOffset = offset;
+ setZip64(true);
+ }
+
+ /**
+ * Returns the byte representation of the specified string using the file's charset encoding.
+ */
+ public byte[] getBytes(String string) {
+ return string.getBytes(charset);
+ }
+
+ /**
+ * Returns the string represented by the specified byte array using the file's charset encoding.
+ */
+ public String fromBytes(byte[] bytes) {
+ return new String(bytes, charset);
+ }
+}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileEntry.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileEntry.java
new file mode 100644
index 0000000000..e8687f1c74
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipFileEntry.java
@@ -0,0 +1,440 @@
+// 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.zip;
+
+import java.util.EnumSet;
+
+import javax.annotation.Nullable;
+
+/**
+ * A full representation of a ZIP file entry.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a> for
+ * a description of the entry fields. (Section 4.3.7 and 4.4)
+ */
+public final class ZipFileEntry {
+
+ /** Compression method for ZIP entries. */
+ public enum Compression {
+ STORED((short) 0, Feature.STORED),
+ DEFLATED((short) 8, Feature.DEFLATED);
+
+ public static Compression fromValue(int value) {
+ for (Compression c : Compression.values()) {
+ if (c.getValue() == value) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ private short value;
+ private Feature feature;
+
+ private Compression(short value, Feature feature) {
+ this.value = value;
+ this.feature = feature;
+ }
+
+ public short getValue() {
+ return value;
+ }
+
+ public short getMinVersion() {
+ return feature.getMinVersion();
+ }
+
+ Feature getFeature() {
+ return feature;
+ }
+ }
+
+ /** General purpose bit flag for ZIP entries. */
+ public enum Flag {
+ DATA_DESCRIPTOR(3);
+
+ private int bit;
+
+ private Flag(int bit) {
+ this.bit = bit;
+ }
+
+ public int getBit() {
+ return bit;
+ }
+ }
+
+ /** Zip file features that entries may use. */
+ enum Feature {
+ DEFAULT((short) 0x0a),
+ STORED((short) 0x0a),
+ DEFLATED((short) 0x14),
+ ZIP64_SIZE((short) 0x2d),
+ ZIP64_CSIZE((short) 0x2d),
+ ZIP64_OFFSET((short) 0x2d);
+
+ private short minVersion;
+
+ private Feature(short minVersion) {
+ this.minVersion = minVersion;
+ }
+
+ public short getMinVersion() {
+ return minVersion;
+ }
+
+ static short getMinRequiredVersion(EnumSet<Feature> featureSet) {
+ short minVersion = Feature.DEFAULT.getMinVersion();
+ for (Feature feature : featureSet) {
+ minVersion = (short) Math.max(minVersion, feature.getMinVersion());
+ }
+ return minVersion;
+ }
+ }
+
+ private String name;
+ private long time = -1;
+ private long crc = -1;
+ private long size = -1;
+ private long csize = -1;
+ private Compression method;
+ private short version = -1;
+ private short versionNeeded = -1;
+ private short flags;
+ private short internalAttributes;
+ private int externalAttributes;
+ private long localHeaderOffset = -1;
+ private ExtraDataList extra;
+ @Nullable private String comment;
+
+ private EnumSet<Feature> featureSet;
+
+ /**
+ * Creates a new ZIP entry with the specified name.
+ *
+ * @throws NullPointerException if the entry name is null
+ */
+ public ZipFileEntry(String name) {
+ this.featureSet = EnumSet.of(Feature.DEFAULT);
+ setName(name);
+ setMethod(Compression.STORED);
+ setExtra(new ExtraDataList());
+ }
+
+ /**
+ * Creates a new ZIP entry with fields taken from the specified ZIP entry.
+ */
+ public ZipFileEntry(ZipFileEntry e) {
+ this.name = e.getName();
+ this.time = e.getTime();
+ this.crc = e.getCrc();
+ this.size = e.getSize();
+ this.csize = e.getCompressedSize();
+ this.method = e.getMethod();
+ this.version = e.getVersion();
+ this.versionNeeded = e.getVersionNeeded();
+ this.flags = e.getFlags();
+ this.internalAttributes = e.getInternalAttributes();
+ this.externalAttributes = e.getExternalAttributes();
+ this.localHeaderOffset = e.getLocalHeaderOffset();
+ this.extra = e.getExtra();
+ this.comment = e.getComment();
+ this.featureSet = EnumSet.copyOf(e.getFeatureSet());
+ }
+
+ /**
+ * Sets the name of the entry.
+ */
+ public void setName(String name) {
+ if (name == null) {
+ throw new NullPointerException();
+ }
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of the entry.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the modification time of the entry.
+ *
+ * @param time the entry modification time in number of milliseconds since the epoch
+ */
+ public void setTime(long time) {
+ this.time = time;
+ }
+
+ /**
+ * Returns the modification time of the entry, or -1 if not specified.
+ */
+ public long getTime() {
+ return time;
+ }
+
+ /**
+ * Sets the CRC-32 checksum of the uncompressed entry data.
+ *
+ * @throws IllegalArgumentException if the specified CRC-32 value is less than 0 or greater than
+ * 0xFFFFFFFF
+ */
+ public void setCrc(long crc) {
+ if (crc < 0 || crc > 0xffffffffL) {
+ throw new IllegalArgumentException("invalid entry crc-32");
+ }
+ this.crc = crc;
+ }
+
+ /**
+ * Returns the CRC-32 checksum of the uncompressed entry data, or -1 if not known.
+ */
+ public long getCrc() {
+ return crc;
+ }
+
+ /**
+ * Sets the uncompressed size of the entry data in bytes.
+ *
+ * @throws IllegalArgumentException if the specified size is less than 0
+ */
+ public void setSize(long size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("invalid entry size");
+ }
+ if (size > 0xffffffffL) {
+ featureSet.add(Feature.ZIP64_SIZE);
+ } else {
+ featureSet.remove(Feature.ZIP64_SIZE);
+ }
+ this.size = size;
+ }
+
+ /**
+ * Returns the uncompressed size of the entry data, or -1 if not known.
+ */
+ public long getSize() {
+ return size;
+ }
+
+ /**
+ * Sets the size of the compressed entry data in bytes.
+ *
+ * @throws IllegalArgumentException if the specified size is less than 0
+ */
+ public void setCompressedSize(long csize) {
+ if (csize < 0) {
+ throw new IllegalArgumentException("invalid entry size");
+ }
+ if (csize > 0xffffffffL) {
+ featureSet.add(Feature.ZIP64_CSIZE);
+ } else {
+ featureSet.remove(Feature.ZIP64_CSIZE);
+ }
+ this.csize = csize;
+ }
+
+ /**
+ * Returns the size of the compressed entry data, or -1 if not known. In the case of a stored
+ * entry, the compressed size will be the same as the uncompressed size of the entry.
+ */
+ public long getCompressedSize() {
+ return csize;
+ }
+
+ /**
+ * Sets the compression method for the entry.
+ */
+ public void setMethod(Compression method) {
+ if (method == null) {
+ throw new NullPointerException();
+ }
+ if (this.method != null) {
+ featureSet.remove(this.method.getFeature());
+ }
+ this.method = method;
+ featureSet.add(this.method.getFeature());
+ }
+
+ /**
+ * Returns the compression method of the entry.
+ */
+ public Compression getMethod() {
+ return method;
+ }
+
+ /**
+ * Sets the made by version for the entry.
+ */
+ public void setVersion(short version) {
+ this.version = version;
+ }
+
+ /**
+ * Returns the made by version of the entry, accounting for assigned version and feature set.
+ */
+ public short getVersion() {
+ return (short) Math.max(version, Feature.getMinRequiredVersion(featureSet));
+ }
+
+ /**
+ * Sets the version needed to extract the entry.
+ */
+ public void setVersionNeeded(short versionNeeded) {
+ this.versionNeeded = versionNeeded;
+ }
+
+ /**
+ * Returns the version needed to extract the entry, accounting for assigned version and feature
+ * set.
+ */
+ public short getVersionNeeded() {
+ return (short) Math.max(versionNeeded, Feature.getMinRequiredVersion(featureSet));
+ }
+
+ /**
+ * Sets the general purpose bit flags for the entry.
+ */
+ public void setFlags(short flags) {
+ this.flags = flags;
+ }
+
+ /**
+ * Sets or clears the specified bit of the general purpose bit flags.
+ *
+ * @param flag the flag to set or clear
+ * @param set whether the flag is to be set or cleared
+ */
+ public void setFlag(Flag flag, boolean set) {
+ short mask = 0x0000;
+ mask |= 1 << flag.getBit();
+ if (set) {
+ flags |= mask;
+ } else {
+ flags &= ~mask;
+ }
+ }
+
+ /**
+ * Returns the general purpose bit flags of the entry.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
+ * section 4.4.4.
+ */
+ public short getFlags() {
+ return flags;
+ }
+
+ /**
+ * Sets the internal file attributes of the entry.
+ */
+ public void setInternalAttributes(short internalAttributes) {
+ this.internalAttributes = internalAttributes;
+ }
+
+ /**
+ * Returns the internal file attributes of the entry.
+ */
+ public short getInternalAttributes() {
+ return internalAttributes;
+ }
+
+ /**
+ * Sets the external file attributes of the entry.
+ */
+ public void setExternalAttributes(int externalAttributes) {
+ this.externalAttributes = externalAttributes;
+ }
+
+ /**
+ * Returns the external file attributes of the entry.
+ */
+ public int getExternalAttributes() {
+ return externalAttributes;
+ }
+
+ /**
+ * Sets the file offset, in bytes, of the location of the local file header for the entry.
+ *
+ * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
+ * section 4.4.16
+ *
+ * @throws IllegalArgumentException if the specified local header offset is less than 0
+ */
+ void setLocalHeaderOffset(long localHeaderOffset) {
+ if (localHeaderOffset < 0) {
+ throw new IllegalArgumentException("invalid local header offset");
+ }
+ if (localHeaderOffset > 0xffffffffL) {
+ featureSet.add(Feature.ZIP64_OFFSET);
+ } else {
+ featureSet.remove(Feature.ZIP64_OFFSET);
+ }
+ this.localHeaderOffset = localHeaderOffset;
+ }
+
+ /**
+ * Returns the file offset of the local header of the entry.
+ */
+ public long getLocalHeaderOffset() {
+ return localHeaderOffset;
+ }
+
+ /**
+ * Sets the optional extra field data for the entry.
+ *
+ * @throws IllegalArgumentException if the length of the specified extra field data is greater
+ * than 0xFFFF bytes
+ */
+ public void setExtra(ExtraDataList extra) {
+ if (extra == null) {
+ throw new NullPointerException();
+ }
+ if (extra.getLength() > 0xffff) {
+ throw new IllegalArgumentException("invalid extra field length");
+ }
+ this.extra = extra;
+ }
+
+ /**
+ * Returns the extra field data for the entry.
+ */
+ public ExtraDataList getExtra() {
+ return extra;
+ }
+
+ /**
+ * Sets the optional comment string for the entry.
+ */
+ public void setComment(@Nullable String comment) {
+ this.comment = comment;
+ }
+
+ /**
+ * Returns the comment string for the entry, or null if none.
+ */
+ public String getComment() {
+ return comment;
+ }
+
+ /**
+ * Returns the feature set that this entry uses.
+ */
+ EnumSet<Feature> getFeatureSet() {
+ return featureSet;
+ }
+}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipReader.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipReader.java
new file mode 100644
index 0000000000..b4ccd5ceb2
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipReader.java
@@ -0,0 +1,510 @@
+// 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.zip;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.devtools.build.zip.ZipFileEntry.Compression;
+import com.google.devtools.build.zip.ZipUtil.CentralDirectoryFileHeader;
+import com.google.devtools.build.zip.ZipUtil.EndOfCentralDirectoryRecord;
+import com.google.devtools.build.zip.ZipUtil.LocalFileHeader;
+import com.google.devtools.build.zip.ZipUtil.Zip64EndOfCentralDirectory;
+import com.google.devtools.build.zip.ZipUtil.Zip64EndOfCentralDirectoryLocator;
+
+import java.io.BufferedInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.Channels;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+/**
+ * A ZIP file reader.
+ *
+ * <p>This class provides entry data in the form of {@link ZipFileEntry}, which provides more detail
+ * about the entry than the JDK equivalent {@link ZipEntry}. In addition to providing
+ * {@link InputStream}s for entries, similar to JDK {@link ZipFile#getInputStream(ZipEntry)}, it
+ * also provides access to the raw byte entry data via {@link #getRawInputStream(ZipFileEntry)}.
+ *
+ * <p>Using the raw access capabilities allows for more efficient ZIP file processing, such as
+ * merging, by not requiring each entry's data to be decompressed when read.
+ *
+ * <p><em>NOTE:</em> The entries are read from the central directory. If the entry is not listed
+ * there, it will not be returned from {@link #entries()} or {@link #getEntry(String)}.
+ */
+public class ZipReader implements Closeable, AutoCloseable {
+
+ /** An input stream for reading the file data of a ZIP file entry. */
+ private class ZipEntryInputStream extends InputStream {
+ private InputStream stream;
+ private long rem;
+
+ /**
+ * Opens an input stream for reading at the beginning of the ZIP file entry's content.
+ *
+ * @param zipEntry the ZIP file entry to open the input stream for
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ private ZipEntryInputStream(ZipFileEntry zipEntry) throws IOException {
+ stream = new BufferedInputStream(Channels.newInputStream(
+ in.getChannel().position(zipEntry.getLocalHeaderOffset())));
+
+ byte[] fileHeader = new byte[LocalFileHeader.FIXED_DATA_SIZE];
+ stream.read(fileHeader);
+
+ if (!ZipUtil.arrayStartsWith(fileHeader,
+ ZipUtil.intToLittleEndian(LocalFileHeader.SIGNATURE))) {
+ throw new ZipException(String.format("The file '%s' is not a correctly formatted zip file: "
+ + "Expected a File Header at file offset %d, but was not present.",
+ file.getName(), zipEntry.getLocalHeaderOffset()));
+ }
+
+ int nameLength = ZipUtil.getUnsignedShort(fileHeader,
+ LocalFileHeader.FILENAME_LENGTH_OFFSET);
+ int extraFieldLength = ZipUtil.getUnsignedShort(fileHeader,
+ LocalFileHeader.EXTRA_FIELD_LENGTH_OFFSET);
+ stream.skip(nameLength + extraFieldLength);
+ rem = zipEntry.getSize();
+ if (zipEntry.getMethod() == Compression.DEFLATED) {
+ stream = new InflaterInputStream(stream, new Inflater(true));
+ }
+ }
+
+ @Override public int available() throws IOException {
+ return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem;
+ }
+
+ @Override public void close() throws IOException {
+ }
+
+ @Override public void mark(int readlimit) {
+ }
+
+ @Override public boolean markSupported() {
+ return false;
+ }
+
+ @Override public int read() throws IOException {
+ byte[] b = new byte[1];
+ if (read(b, 0, 1) == 1) {
+ return b[0] & 0xff;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override public int read(byte[] b, int off, int len) throws IOException {
+ if (rem == 0) {
+ return -1;
+ }
+ if (len > rem) {
+ len = available();
+ }
+ len = stream.read(b, off, len);
+ rem -= len;
+ return len;
+ }
+
+ @Override public long skip(long n) throws IOException {
+ if (n > rem) {
+ n = rem;
+ }
+ n = stream.skip(n);
+ rem -= n;
+ return n;
+ }
+
+ @Override public void reset() throws IOException {
+ throw new IOException("Reset is not supported on this type of stream.");
+ }
+ }
+
+ /** An input stream for reading the raw file data of a ZIP file entry. */
+ private class RawZipEntryInputStream extends InputStream {
+ private InputStream stream;
+ private long rem;
+
+ /**
+ * Opens an input stream for reading at the beginning of the ZIP file entry's content.
+ *
+ * @param zipEntry the ZIP file entry to open the input stream for
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ private RawZipEntryInputStream(ZipFileEntry zipEntry) throws IOException {
+ stream = new BufferedInputStream(Channels.newInputStream(
+ in.getChannel().position(zipEntry.getLocalHeaderOffset())));
+
+ byte[] fileHeader = new byte[LocalFileHeader.FIXED_DATA_SIZE];
+ stream.read(fileHeader);
+
+ if (!ZipUtil.arrayStartsWith(fileHeader,
+ ZipUtil.intToLittleEndian(LocalFileHeader.SIGNATURE))) {
+ throw new ZipException(String.format("The file '%s' is not a correctly formatted zip file: "
+ + "Expected a File Header at file offset %d, but was not present.",
+ file.getName(), zipEntry.getLocalHeaderOffset()));
+ }
+
+ int nameLength = ZipUtil.getUnsignedShort(fileHeader,
+ LocalFileHeader.FILENAME_LENGTH_OFFSET);
+ int extraFieldLength = ZipUtil.getUnsignedShort(fileHeader,
+ LocalFileHeader.EXTRA_FIELD_LENGTH_OFFSET);
+ stream.skip(nameLength + extraFieldLength);
+ rem = zipEntry.getCompressedSize();
+ }
+
+ @Override public int available() throws IOException {
+ return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem;
+ }
+
+ @Override public void close() throws IOException {
+ }
+
+ @Override public void mark(int readlimit) {
+ }
+
+ @Override public boolean markSupported() {
+ return false;
+ }
+
+ @Override public int read() throws IOException {
+ byte[] b = new byte[1];
+ if (read(b, 0, 1) == 1) {
+ return b[0] & 0xff;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override public int read(byte[] b, int off, int len) throws IOException {
+ if (rem == 0) {
+ return -1;
+ }
+ if (len > rem) {
+ len = available();
+ }
+ len = stream.read(b, off, len);
+ rem -= len;
+ return len;
+ }
+
+ @Override public long skip(long n) throws IOException {
+ if (n > rem) {
+ n = rem;
+ }
+ n = stream.skip(n);
+ rem -= n;
+ return n;
+ }
+
+ @Override public void reset() throws IOException {
+ throw new IOException("Reset is not supported on this type of stream.");
+ }
+ }
+
+ private final File file;
+ private final RandomAccessFile in;
+ private final ZipFileData zipData;
+
+ /**
+ * Opens a zip file for raw acceess.
+ *
+ * <p>The UTF-8 charset is used to decode the entry names and comments.
+ *
+ * @param file the zip file
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ public ZipReader(File file) throws IOException {
+ this(file, UTF_8);
+ }
+
+ /**
+ * Opens a zip file for raw acceess.
+ *
+ * @param file the zip file
+ * @param charset the charset to use to decode the entry names and comments
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ public ZipReader(File file, Charset charset) throws IOException {
+ this(file, charset, false);
+ }
+
+ /**
+ * Opens a zip file for raw acceess.
+ *
+ * @param file the zip file
+ * @param charset the charset to use to decode the entry names and comments
+ * @param strictEntries force parsing to use the number of entries recorded in the end of
+ * central directory as the correct value, not as an estimate
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ public ZipReader(File file, Charset charset, boolean strictEntries) throws IOException {
+ if (file == null || charset == null) {
+ throw new NullPointerException();
+ }
+ this.file = file;
+ this.in = new RandomAccessFile(file, "r");
+ this.zipData = new ZipFileData(charset);
+ readCentralDirectory(strictEntries);
+ }
+
+ /**
+ * Returns the ZIP file comment.
+ */
+ public String getComment() {
+ return zipData.getComment();
+ }
+
+ /**
+ * Returns a collection of the ZIP file entries.
+ */
+ public Collection<ZipFileEntry> entries() {
+ return zipData.getEntries();
+ }
+
+ /**
+ * Returns the ZIP file entry for the specified name, or null if not found.
+ */
+ public ZipFileEntry getEntry(String name) {
+ return zipData.getEntry(name);
+ }
+
+ /**
+ * Returns the number of entries in the ZIP file.
+ */
+ public long size() {
+ return zipData.getNumEntries();
+ }
+
+ /**
+ * Returns an input stream for reading the contents of the specified ZIP file entry.
+ *
+ * <p>Closing this ZIP file will, in turn, close all input streams that have been returned by
+ * invocations of this method.
+ *
+ * @param entry the ZIP file entry
+ * @return the input stream for reading the contents of the specified zip file entry
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ public InputStream getInputStream(ZipFileEntry entry) throws IOException {
+ if (!zipData.getEntry(entry.getName()).equals(entry)) {
+ throw new ZipException(String.format(
+ "Zip file '%s' does not contain the requested entry '%s'.", file.getName(),
+ entry.getName()));
+ }
+ return new ZipEntryInputStream(entry);
+ }
+
+ /**
+ * Returns an input stream for reading the raw contents of the specified ZIP file entry.
+ *
+ * <p><em>NOTE:</em> No inflating will take place; The data read from the input stream will be
+ * the exact byte content of the ZIP file entry on disk.
+ *
+ * <p>Closing this ZIP file will, in turn, close all input streams that have been returned by
+ * invocations of this method.
+ *
+ * @param entry the ZIP file entry
+ * @return the input stream for reading the contents of the specified zip file entry
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ public InputStream getRawInputStream(ZipFileEntry entry) throws IOException {
+ if (!zipData.getEntry(entry.getName()).equals(entry)) {
+ throw new ZipException(String.format(
+ "Zip file '%s' does not contain the requested entry '%s'.", file.getName(),
+ entry.getName()));
+ }
+ return new RawZipEntryInputStream(entry);
+ }
+
+ /**
+ * Closes the ZIP file.
+ *
+ * <p>Closing this ZIP file will close all of the input streams previously returned by invocations
+ * of the {@link #getRawInputStream(ZipFileEntry)} method.
+ */
+ @Override public void close() throws IOException {
+ in.close();
+ }
+
+ /**
+ * Finds, reads and parses ZIP file entries from the central directory.
+ *
+ * @param strictEntries force parsing to use the number of entries recorded in the end of
+ * central directory as the correct value, not as an estimate
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ private void readCentralDirectory(boolean strictEntries) throws IOException {
+ long eocdLocation = findEndOfCentralDirectoryRecord();
+ InputStream stream = new BufferedInputStream(Channels.newInputStream(
+ in.getChannel().position(eocdLocation)));
+ EndOfCentralDirectoryRecord.read(stream, zipData);
+
+ if (zipData.isMaybeZip64()) {
+ try {
+ stream = new BufferedInputStream(Channels.newInputStream(in.getChannel()
+ .position(eocdLocation - Zip64EndOfCentralDirectoryLocator.FIXED_DATA_SIZE)));
+ Zip64EndOfCentralDirectoryLocator.read(stream, zipData);
+
+ stream = new BufferedInputStream(Channels.newInputStream(in.getChannel()
+ .position(zipData.getZip64EndOfCentralDirectoryOffset())));
+ Zip64EndOfCentralDirectory.read(stream, zipData);
+ } catch (ZipException e) {
+ // expected if not in Zip64 format
+ }
+ }
+
+ if (zipData.isZip64() || strictEntries) {
+ // If in Zip64 format or using strict entry numbers, use the parsed information as is to read
+ // the central directory file headers.
+ readCentralDirectoryFileHeaders(zipData.getExpectedEntries(),
+ zipData.getCentralDirectoryOffset());
+ } else {
+ // If not in Zip64 format, compute central directory offset by end of central directory record
+ // offset and central directory size to allow reading large non-compliant Zip32 directories.
+ long centralDirectoryOffset = eocdLocation - zipData.getCentralDirectorySize();
+ // If the lower 4 bytes match, the above calculation is correct; otherwise fallback to
+ // reported offset.
+ if ((int) centralDirectoryOffset == (int) zipData.getCentralDirectoryOffset()) {
+ readCentralDirectoryFileHeaders(centralDirectoryOffset);
+ } else {
+ readCentralDirectoryFileHeaders(zipData.getExpectedEntries(),
+ zipData.getCentralDirectoryOffset());
+ }
+ }
+ }
+
+ /**
+ * Looks for the target sub array in the buffer scanning backwards starting at offset. Returns the
+ * index where the target is found or -1 if not found.
+ *
+ * @param target the sub array to find
+ * @param buffer the array to scan
+ * @param offset the index of where to begin scanning
+ * @return the index of target within buffer or -1 if not found
+ */
+ private int scanBackwards(byte[] target, byte[] buffer, int offset) {
+ int start = Math.min(offset, buffer.length - target.length);
+ for (int i = start; i >= 0; i--) {
+ for (int j = 0; j < target.length; j++) {
+ if (buffer[i + j] != target[j]) {
+ break;
+ } else if (j == target.length - 1) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Finds the file offset of the end of central directory record.
+ *
+ * @return the file offset of the end of central directory record
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ private long findEndOfCentralDirectoryRecord() throws IOException {
+ byte[] signature = ZipUtil.intToLittleEndian(EndOfCentralDirectoryRecord.SIGNATURE);
+ byte[] buffer = new byte[(int) Math.min(64, in.length())];
+ int readLength = buffer.length;
+ if (readLength < EndOfCentralDirectoryRecord.FIXED_DATA_SIZE) {
+ throw new ZipException(String.format("Zip file '%s' is malformed. It does not contain an end"
+ + " of central directory record.", file.getName()));
+ }
+
+ long offset = in.length() - buffer.length;
+ while (offset >= 0) {
+ in.seek(offset);
+ in.readFully(buffer, 0, readLength);
+ int signatureLocation = scanBackwards(signature, buffer, buffer.length);
+ while (signatureLocation != -1) {
+ long eocdSize = in.length() - offset - signatureLocation;
+ if (eocdSize >= EndOfCentralDirectoryRecord.FIXED_DATA_SIZE) {
+ int commentLength = ZipUtil.getUnsignedShort(buffer, signatureLocation
+ + EndOfCentralDirectoryRecord.COMMENT_LENGTH_OFFSET);
+ long readCommentLength = eocdSize - EndOfCentralDirectoryRecord.FIXED_DATA_SIZE;
+ if (commentLength == readCommentLength) {
+ return offset + signatureLocation;
+ }
+ }
+ signatureLocation = scanBackwards(signature, buffer, signatureLocation - 1);
+ }
+ readLength = buffer.length - 3;
+ buffer[buffer.length - 3] = buffer[0];
+ buffer[buffer.length - 2] = buffer[1];
+ buffer[buffer.length - 1] = buffer[2];
+ offset -= readLength;
+ }
+ throw new ZipException(String.format("Zip file '%s' is malformed. It does not contain an end"
+ + " of central directory record.", file.getName()));
+ }
+
+ /**
+ * Reads and parses ZIP file entries from the central directory.
+ *
+ * @param count the number of entries in the central directory
+ * @param fileOffset the file offset of the start of the central directory
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ private void readCentralDirectoryFileHeaders(long count, long fileOffset) throws IOException {
+ InputStream centralDirectory = new BufferedInputStream(
+ Channels.newInputStream(in.getChannel().position(fileOffset)));
+ for (long i = 0; i < count; i++) {
+ ZipFileEntry entry = CentralDirectoryFileHeader.read(centralDirectory, zipData.getCharset());
+ zipData.addEntry(entry);
+ }
+ }
+
+ /**
+ * Reads and parses ZIP file entries from the central directory.
+ *
+ * @param fileOffset the file offset of the start of the central directory
+ * @throws ZipException if a ZIP format error has occurred
+ * @throws IOException if an I/O error has occurred
+ */
+ private void readCentralDirectoryFileHeaders(long fileOffset) throws IOException {
+ CountingInputStream centralDirectory = new CountingInputStream(new BufferedInputStream(
+ Channels.newInputStream(in.getChannel().position(fileOffset))));
+ while (centralDirectory.getCount() < zipData.getCentralDirectorySize()) {
+ ZipFileEntry entry = CentralDirectoryFileHeader.read(centralDirectory, zipData.getCharset());
+ zipData.addEntry(entry);
+ }
+ }
+}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java
new file mode 100644
index 0000000000..2ba4caf4f2
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java
@@ -0,0 +1,728 @@
+// 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.zip;
+
+import com.google.devtools.build.zip.ZipFileEntry.Compression;
+import com.google.devtools.build.zip.ZipFileEntry.Feature;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.GregorianCalendar;
+import java.util.zip.ZipException;
+
+/** A utility class for reading and writing {@link ZipFileEntry}s from byte arrays. */
+public class ZipUtil {
+
+ /**
+ * Midnight Jan 1st 1980. Uses the current time zone as the DOS format does not support time zones
+ * and will always assume the current zone.
+ */
+ public static final long DOS_EPOCH =
+ new GregorianCalendar(1980, Calendar.JANUARY, 1, 0, 0, 0).getTimeInMillis();
+
+ /** 23:59:59 Dec 31st 2107. The maximum date representable in DOS format. */
+ public static final long MAX_DOS_DATE =
+ new GregorianCalendar(2107, Calendar.DECEMBER, 31, 23, 59, 59).getTimeInMillis();
+
+ /** Converts a integral value to the corresponding little endian array. */
+ private static byte[] integerToLittleEndian(byte[] buf, int offset, long value, int numBytes) {
+ for (int i = 0; i < numBytes; i++) {
+ buf[i + offset] = (byte) ((value & (0xffL << (i * 8))) >> (i * 8));
+ }
+ return buf;
+ }
+
+ /** Converts a short to the corresponding 2-byte little endian array. */
+ static byte[] shortToLittleEndian(short value) {
+ return integerToLittleEndian(new byte[2], 0, value, 2);
+ }
+
+ /** Writes a short to the buffer as a 2-byte little endian array starting at offset. */
+ static byte[] shortToLittleEndian(byte[] buf, int offset, short value) {
+ return integerToLittleEndian(buf, offset, value, 2);
+ }
+
+ /** Converts an int to the corresponding 4-byte little endian array. */
+ static byte[] intToLittleEndian(int value) {
+ return integerToLittleEndian(new byte[4], 0, value, 4);
+ }
+
+ /** Writes an int to the buffer as a 4-byte little endian array starting at offset. */
+ static byte[] intToLittleEndian(byte[] buf, int offset, int value) {
+ return integerToLittleEndian(buf, offset, value, 4);
+ }
+
+ /** Converts a long to the corresponding 8-byte little endian array. */
+ static byte[] longToLittleEndian(long value) {
+ return integerToLittleEndian(new byte[8], 0, value, 8);
+ }
+
+ /** Writes a long to the buffer as a 8-byte little endian array starting at offset. */
+ static byte[] longToLittleEndian(byte[] buf, int offset, long value) {
+ return integerToLittleEndian(buf, offset, value, 8);
+ }
+
+ /** Reads 16 bits in little-endian byte order from the buffer at the given offset. */
+ static short get16(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ return (short) ((b << 8) | a);
+ }
+
+ /** Reads 32 bits in little-endian byte order from the buffer at the given offset. */
+ static int get32(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;
+ }
+
+ /** Reads 64 bits in little-endian byte order from the buffer at the given offset. */
+ static long get64(byte[] source, int offset) {
+ long a = source[offset + 0] & 0xffL;
+ long b = source[offset + 1] & 0xffL;
+ long c = source[offset + 2] & 0xffL;
+ long d = source[offset + 3] & 0xffL;
+ long e = source[offset + 4] & 0xffL;
+ long f = source[offset + 5] & 0xffL;
+ long g = source[offset + 6] & 0xffL;
+ long h = source[offset + 7] & 0xffL;
+ return (h << 56) | (g << 48) | (f << 40) | (e << 32) | (d << 24) | (c << 16) | (b << 8) | a;
+ }
+
+ /**
+ * Reads an unsigned short in little-endian byte order from the buffer at the given offset.
+ * Casts to an int to allow proper numerical comparison.
+ */
+ static int getUnsignedShort(byte[] source, int offset) {
+ return get16(source, offset) & 0xffff;
+ }
+
+ /**
+ * Reads an unsigned int in little-endian byte order from the buffer at the given offset.
+ * Casts to a long to allow proper numerical comparison.
+ */
+ static long getUnsignedInt(byte[] source, int offset) {
+ return get32(source, offset) & 0xffffffffL;
+ }
+
+ /**
+ * Reads an unsigned long in little-endian byte order from the buffer at the given offset.
+ * Performs bounds checking to see if the unsigned long will be properly represented in Java's
+ * signed value.
+ */
+ static long getUnsignedLong(byte[] source, int offset) throws ZipException {
+ long result = get64(source, offset);
+ if (result < 0) {
+ throw new ZipException("The requested unsigned long value is too large for Java's signed"
+ + "values. This Zip file is unsupported");
+ }
+ return result;
+ }
+
+ /** Checks if the timestamp is representable as a valid DOS timestamp. */
+ private static boolean isValidInDos(long timestamp) {
+ Calendar time = Calendar.getInstance();
+ time.setTimeInMillis(timestamp);
+ Calendar minTime = Calendar.getInstance();
+ minTime.setTimeInMillis(DOS_EPOCH);
+ Calendar maxTime = Calendar.getInstance();
+ maxTime.setTimeInMillis(MAX_DOS_DATE);
+ return (!time.before(minTime) && !time.after(maxTime));
+ }
+
+ /** Converts a unix timestamp into a 32-bit DOS timestamp. */
+ static int unixToDosTime(long timestamp) {
+ Calendar time = Calendar.getInstance();
+ time.setTimeInMillis(timestamp);
+
+ if (!isValidInDos(timestamp)) {
+ DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ throw new IllegalArgumentException(String.format("%s is not representable in the DOS time"
+ + " format. It must be in the range %s to %s", df.format(time.getTime()),
+ df.format(new Date(DOS_EPOCH)), df.format(new Date(MAX_DOS_DATE))));
+ }
+
+ int dos = time.get(Calendar.SECOND) >> 1;
+ dos |= time.get(Calendar.MINUTE) << 5;
+ dos |= time.get(Calendar.HOUR_OF_DAY) << 11;
+ dos |= time.get(Calendar.DAY_OF_MONTH) << 16;
+ dos |= (time.get(Calendar.MONTH) + 1) << 21;
+ dos |= (time.get(Calendar.YEAR) - 1980) << 25;
+ return dos;
+ }
+
+ /** Converts a 32-bit DOS timestamp into a unix timestamp. */
+ static long dosToUnixTime(int timestamp) {
+ Calendar time = Calendar.getInstance();
+ time.clear();
+ time.set(Calendar.SECOND, (timestamp << 1) & 0x3e);
+ time.set(Calendar.MINUTE, (timestamp >> 5) & 0x3f);
+ time.set(Calendar.HOUR_OF_DAY, (timestamp >> 11) & 0x1f);
+ time.set(Calendar.DAY_OF_MONTH, (timestamp >> 16) & 0x1f);
+ time.set(Calendar.MONTH, ((timestamp >> 21) & 0x0f) - 1);
+ time.set(Calendar.YEAR, ((timestamp >> 25) & 0x7f) + 1980);
+ return time.getTimeInMillis();
+ }
+
+ /** Checks if array starts with target. */
+ static boolean arrayStartsWith(byte[] array, byte[] target) {
+ if (array == null) {
+ return false;
+ }
+ if (target == null) {
+ return true;
+ }
+ if (target.length > array.length) {
+ return false;
+ }
+ for (int i = 0; i < target.length; i++) {
+ if (array[i] != target[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static class LocalFileHeader {
+ static final int SIGNATURE = 0x04034b50;
+ static final int FIXED_DATA_SIZE = 30;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int VERSION_OFFSET = 4;
+ static final int FLAGS_OFFSET = 6;
+ static final int METHOD_OFFSET = 8;
+ static final int MOD_TIME_OFFSET = 10;
+ static final int CRC_OFFSET = 14;
+ static final int COMPRESSED_SIZE_OFFSET = 18;
+ static final int UNCOMPRESSED_SIZE_OFFSET = 22;
+ static final int FILENAME_LENGTH_OFFSET = 26;
+ static final int EXTRA_FIELD_LENGTH_OFFSET = 28;
+ static final int VARIABLE_DATA_OFFSET = 30;
+
+ /**
+ * Generates the raw byte data of the local file header for the {@link ZipFileEntry}. Uses the
+ * specified {@link ZipFileData} to encode the file name and comment.
+ * @throws IOException
+ */
+ static byte[] create(ZipFileEntry entry, ZipFileData file, boolean allowZip64)
+ throws IOException {
+ byte[] name = entry.getName().getBytes(file.getCharset());
+ ExtraDataList extra = entry.getExtra();
+
+ EnumSet<Feature> features = entry.getFeatureSet();
+ int size = (int) entry.getSize();
+ int csize = (int) entry.getCompressedSize();
+
+ if (features.contains(Feature.ZIP64_SIZE) || features.contains(Feature.ZIP64_CSIZE)) {
+ if (!allowZip64) {
+ throw new ZipException(String.format("Writing an entry of size %d(%d) without Zip64"
+ + " extensions is not supported.", entry.getSize(), entry.getCompressedSize()));
+ }
+ extra.remove((short) 0x0001);
+ int extraSize = 0;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ size = -1;
+ extraSize += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ csize = -1;
+ extraSize += 8;
+ }
+ byte[] zip64Extra = new byte[ExtraData.FIXED_DATA_SIZE + extraSize];
+ shortToLittleEndian(zip64Extra, ExtraData.ID_OFFSET, (short) 0x0001);
+ shortToLittleEndian(zip64Extra, ExtraData.LENGTH_OFFSET, (short) extraSize);
+ int offset = ExtraData.FIXED_DATA_SIZE;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getSize());
+ offset += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getCompressedSize());
+ offset += 8;
+ }
+ extra.add(new ExtraData(zip64Extra, 0));
+ } else {
+ extra.remove((short) 0x0001);
+ }
+
+ byte[] buf = new byte[FIXED_DATA_SIZE + name.length + extra.getLength()];
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ shortToLittleEndian(buf, VERSION_OFFSET, entry.getVersionNeeded());
+ shortToLittleEndian(buf, FLAGS_OFFSET, entry.getFlags());
+ shortToLittleEndian(buf, METHOD_OFFSET, entry.getMethod().getValue());
+ intToLittleEndian(buf, MOD_TIME_OFFSET, unixToDosTime(entry.getTime()));
+ intToLittleEndian(buf, CRC_OFFSET, (int) (entry.getCrc() & 0xffffffff));
+ intToLittleEndian(buf, COMPRESSED_SIZE_OFFSET, csize);
+ intToLittleEndian(buf, UNCOMPRESSED_SIZE_OFFSET, size);
+ shortToLittleEndian(buf, FILENAME_LENGTH_OFFSET, (short) name.length);
+ shortToLittleEndian(buf, EXTRA_FIELD_LENGTH_OFFSET, (short) extra.getLength());
+ System.arraycopy(name, 0, buf, FIXED_DATA_SIZE, name.length);
+ extra.getByteStream().read(buf, FIXED_DATA_SIZE + name.length, extra.getLength());
+
+ return buf;
+ }
+ }
+
+ static class CentralDirectoryFileHeader {
+ static final int SIGNATURE = 0x02014b50;
+ static final int FIXED_DATA_SIZE = 46;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int VERSION_OFFSET = 4;
+ static final int VERSION_NEEDED_OFFSET = 6;
+ static final int FLAGS_OFFSET = 8;
+ static final int METHOD_OFFSET = 10;
+ static final int MOD_TIME_OFFSET = 12;
+ static final int CRC_OFFSET = 16;
+ static final int COMPRESSED_SIZE_OFFSET = 20;
+ static final int UNCOMPRESSED_SIZE_OFFSET = 24;
+ static final int FILENAME_LENGTH_OFFSET = 28;
+ static final int EXTRA_FIELD_LENGTH_OFFSET = 30;
+ static final int COMMENT_LENGTH_OFFSET = 32;
+ static final int DISK_START_OFFSET = 34;
+ static final int INTERNAL_ATTRIBUTES_OFFSET = 36;
+ static final int EXTERNAL_ATTRIBUTES_OFFSET = 38;
+ static final int LOCAL_HEADER_OFFSET_OFFSET = 42;
+
+ /**
+ * Reads a {@link ZipFileEntry} from the input stream, using the specified {@link Charset} to
+ * decode the filename and comment.
+ */
+ static ZipFileEntry read(InputStream in, Charset charset)
+ throws IOException {
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed Central Directory File Header; does not start with %08x", SIGNATURE));
+ }
+
+ byte[] name = new byte[getUnsignedShort(fixedSizeData, FILENAME_LENGTH_OFFSET)];
+ byte[] extraField = new byte[getUnsignedShort(fixedSizeData, EXTRA_FIELD_LENGTH_OFFSET)];
+ byte[] comment = new byte[getUnsignedShort(fixedSizeData, COMMENT_LENGTH_OFFSET)];
+
+ if (name.length > 0 && in.read(name) != name.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+ if (extraField.length > 0 && in.read(extraField) != extraField.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+ if (comment.length > 0 && in.read(comment) != comment.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+
+ ExtraDataList extra = new ExtraDataList(extraField);
+
+ long csize = getUnsignedInt(fixedSizeData, COMPRESSED_SIZE_OFFSET);
+ long size = getUnsignedInt(fixedSizeData, UNCOMPRESSED_SIZE_OFFSET);
+ long offset = getUnsignedInt(fixedSizeData, LOCAL_HEADER_OFFSET_OFFSET);
+ if (csize == 0xffffffffL || size == 0xffffffffL || offset == 0xffffffffL) {
+ ExtraData zip64Extra = extra.get((short) 0x0001);
+ if (zip64Extra != null) {
+ int index = 0;
+ if (size == 0xffffffffL) {
+ size = ZipUtil.getUnsignedLong(zip64Extra.getData(), index);
+ index += 8;
+ }
+ if (csize == 0xffffffffL) {
+ csize = ZipUtil.getUnsignedLong(zip64Extra.getData(), index);
+ index += 8;
+ }
+ if (offset == 0xffffffffL) {
+ offset = ZipUtil.getUnsignedLong(zip64Extra.getData(), index);
+ index += 8;
+ }
+ }
+ }
+
+ ZipFileEntry entry = new ZipFileEntry(new String(name, charset));
+ entry.setVersion(get16(fixedSizeData, VERSION_OFFSET));
+ entry.setVersionNeeded(get16(fixedSizeData, VERSION_NEEDED_OFFSET));
+ entry.setFlags(get16(fixedSizeData, FLAGS_OFFSET));
+ entry.setMethod(Compression.fromValue(get16(fixedSizeData, METHOD_OFFSET)));
+ long time = dosToUnixTime(get32(fixedSizeData, MOD_TIME_OFFSET));
+ entry.setTime(isValidInDos(time) ? time : DOS_EPOCH);
+ entry.setCrc(getUnsignedInt(fixedSizeData, CRC_OFFSET));
+ entry.setCompressedSize(csize);
+ entry.setSize(size);
+ entry.setInternalAttributes(get16(fixedSizeData, INTERNAL_ATTRIBUTES_OFFSET));
+ entry.setExternalAttributes(get32(fixedSizeData, EXTERNAL_ATTRIBUTES_OFFSET));
+ entry.setLocalHeaderOffset(offset);
+ entry.setExtra(extra);
+ entry.setComment(new String(comment, charset));
+
+ return entry;
+ }
+
+ /**
+ * Generates the raw byte data of the central directory file header for the ZipEntry. Uses the
+ * specified {@link ZipFileData} to encode the file name and comment.
+ * @throws ZipException
+ */
+ static byte[] create(ZipFileEntry entry, ZipFileData file, boolean allowZip64)
+ throws ZipException {
+ if (allowZip64) {
+ addZip64Extra(entry);
+ } else {
+ entry.getExtra().remove((short) 0x0001);
+ }
+ byte[] name = file.getBytes(entry.getName());
+ byte[] extra = entry.getExtra().getBytes();
+ byte[] comment = entry.getComment() != null
+ ? file.getBytes(entry.getComment()) : new byte[]{};
+
+ byte[] buf = new byte[FIXED_DATA_SIZE + name.length + extra.length + comment.length];
+
+ fillFixedSizeData(buf, entry, name.length, extra.length, comment.length, allowZip64);
+ System.arraycopy(name, 0, buf, FIXED_DATA_SIZE, name.length);
+ System.arraycopy(extra, 0, buf, FIXED_DATA_SIZE + name.length, extra.length);
+ System.arraycopy(comment, 0, buf, FIXED_DATA_SIZE + name.length + extra.length,
+ comment.length);
+
+ return buf;
+ }
+
+ /**
+ * Writes the central directory file header for the ZipEntry to an output stream. Uses the
+ * specified {@link ZipFileData} to encode the file name and comment.
+ */
+ static int write(ZipFileEntry entry, ZipFileData file, boolean allowZip64, byte[] buf,
+ OutputStream stream) throws IOException {
+ if (buf == null || buf.length < FIXED_DATA_SIZE) {
+ buf = new byte[FIXED_DATA_SIZE];
+ }
+
+ if (allowZip64) {
+ addZip64Extra(entry);
+ } else {
+ entry.getExtra().remove((short) 0x0001);
+ }
+ byte[] name = entry.getName().getBytes(file.getCharset());
+ byte[] extra = entry.getExtra().getBytes();
+ byte[] comment = entry.getComment() != null
+ ? entry.getComment().getBytes(file.getCharset()) : new byte[]{};
+
+ fillFixedSizeData(buf, entry, name.length, extra.length, comment.length, allowZip64);
+ stream.write(buf, 0, FIXED_DATA_SIZE);
+ stream.write(name);
+ stream.write(extra);
+ stream.write(comment);
+
+ return FIXED_DATA_SIZE + name.length + extra.length + comment.length;
+ }
+
+ /**
+ * Write the fixed size data portion for the specified ZIP entry to the buffer.
+ * @throws ZipException
+ */
+ private static void fillFixedSizeData(byte[] buf, ZipFileEntry entry, int nameLength,
+ int extraLength, int commentLength, boolean allowZip64) throws ZipException {
+ if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_CSIZE)) {
+ throw new ZipException(String.format("Writing an entry with compressed size %d without"
+ + " Zip64 extensions is not supported.", entry.getCompressedSize()));
+ }
+ if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_SIZE)) {
+ throw new ZipException(String.format("Writing an entry of size %d without"
+ + " Zip64 extensions is not supported.", entry.getSize()));
+ }
+ if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_OFFSET)) {
+ throw new ZipException(String.format("Writing an entry with local header offset %d without"
+ + " Zip64 extensions is not supported.", entry.getLocalHeaderOffset()));
+ }
+ int csize = (int) (entry.getFeatureSet().contains(Feature.ZIP64_CSIZE)
+ ? -1 : entry.getCompressedSize());
+ int size = (int) (entry.getFeatureSet().contains(Feature.ZIP64_SIZE)
+ ? -1 : entry.getSize());
+ int offset = (int) (entry.getFeatureSet().contains(Feature.ZIP64_OFFSET)
+ ? -1 : entry.getLocalHeaderOffset());
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ shortToLittleEndian(buf, VERSION_OFFSET, entry.getVersion());
+ shortToLittleEndian(buf, VERSION_NEEDED_OFFSET, entry.getVersionNeeded());
+ shortToLittleEndian(buf, FLAGS_OFFSET, entry.getFlags());
+ shortToLittleEndian(buf, METHOD_OFFSET, entry.getMethod().getValue());
+ intToLittleEndian(buf, MOD_TIME_OFFSET, unixToDosTime(entry.getTime()));
+ intToLittleEndian(buf, CRC_OFFSET, (int) (entry.getCrc() & 0xffffffff));
+ intToLittleEndian(buf, COMPRESSED_SIZE_OFFSET, csize);
+ intToLittleEndian(buf, UNCOMPRESSED_SIZE_OFFSET, size);
+ shortToLittleEndian(buf, FILENAME_LENGTH_OFFSET, (short) (nameLength & 0xffff));
+ shortToLittleEndian(buf, EXTRA_FIELD_LENGTH_OFFSET, (short) (extraLength & 0xffff));
+ shortToLittleEndian(buf, COMMENT_LENGTH_OFFSET, (short) (commentLength & 0xffff));
+ shortToLittleEndian(buf, DISK_START_OFFSET, (short) 0);
+ shortToLittleEndian(buf, INTERNAL_ATTRIBUTES_OFFSET, entry.getInternalAttributes());
+ intToLittleEndian(buf, EXTERNAL_ATTRIBUTES_OFFSET, entry.getExternalAttributes());
+ intToLittleEndian(buf, LOCAL_HEADER_OFFSET_OFFSET, offset);
+ }
+
+ /**
+ * Update the extra data fields to contain a Zip64 extended information field if required
+ */
+ private static void addZip64Extra(ZipFileEntry entry) {
+ EnumSet<Feature> features = entry.getFeatureSet();
+ ExtraDataList extra = entry.getExtra();
+ int extraSize = 0;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ extraSize += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ extraSize += 8;
+ }
+ if (features.contains(Feature.ZIP64_OFFSET)) {
+ extraSize += 8;
+ }
+ if (extraSize > 0) {
+ extra.remove((short) 0x0001);
+ byte[] zip64Extra = new byte[ExtraData.FIXED_DATA_SIZE + extraSize];
+ shortToLittleEndian(zip64Extra, ExtraData.ID_OFFSET, (short) 0x0001);
+ shortToLittleEndian(zip64Extra, ExtraData.LENGTH_OFFSET, (short) extraSize);
+ int offset = ExtraData.FIXED_DATA_SIZE;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getSize());
+ offset += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getCompressedSize());
+ offset += 8;
+ }
+ if (features.contains(Feature.ZIP64_OFFSET)) {
+ longToLittleEndian(zip64Extra, offset, entry.getLocalHeaderOffset());
+ }
+ extra.add(new ExtraData(zip64Extra, 0));
+ }
+ }
+ }
+
+ static class Zip64EndOfCentralDirectory {
+ static final int SIGNATURE = 0x06064b50;
+ static final int FIXED_DATA_SIZE = 56;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int SIZE_OFFSET = 4;
+ static final int VERSION_OFFSET = 12;
+ static final int VERSION_NEEDED_OFFSET = 14;
+ static final int DISK_NUMBER_OFFSET = 16;
+ static final int CD_DISK_OFFSET = 20;
+ static final int DISK_ENTRIES_OFFSET = 24;
+ static final int TOTAL_ENTRIES_OFFSET = 32;
+ static final int CD_SIZE_OFFSET = 40;
+ static final int CD_OFFSET_OFFSET = 48;
+
+ /**
+ * Read the Zip64 end of central directory record from the input stream and parse additional
+ * {@link ZipFileData} from it.
+ */
+ static ZipFileData read(InputStream in, ZipFileData file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading Zip64 End of Central Directory Record.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed Zip64 End of Central Directory; does not start with %08x", SIGNATURE));
+ }
+ file.setZip64(true);
+ file.setCentralDirectoryOffset(getUnsignedLong(fixedSizeData, CD_OFFSET_OFFSET));
+ file.setExpectedEntries(getUnsignedLong(fixedSizeData, TOTAL_ENTRIES_OFFSET));
+ return file;
+ }
+
+ /**
+ * Generates the raw byte data of the Zip64 end of central directory record for the file.
+ */
+ static byte[] create(ZipFileData file) {
+ byte[] buf = new byte[FIXED_DATA_SIZE];
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ longToLittleEndian(buf, SIZE_OFFSET, FIXED_DATA_SIZE - 12);
+ shortToLittleEndian(buf, VERSION_OFFSET, (short) 0x2d);
+ shortToLittleEndian(buf, VERSION_NEEDED_OFFSET, (short) 0x2d);
+ intToLittleEndian(buf, DISK_NUMBER_OFFSET, 0);
+ intToLittleEndian(buf, CD_DISK_OFFSET, 0);
+ longToLittleEndian(buf, DISK_ENTRIES_OFFSET, file.getNumEntries());
+ longToLittleEndian(buf, TOTAL_ENTRIES_OFFSET, file.getNumEntries());
+ longToLittleEndian(buf, CD_SIZE_OFFSET, file.getCentralDirectorySize());
+ longToLittleEndian(buf, CD_OFFSET_OFFSET, file.getCentralDirectoryOffset());
+ return buf;
+ }
+ }
+
+ static class Zip64EndOfCentralDirectoryLocator {
+ static final int SIGNATURE = 0x07064b50;
+ static final int FIXED_DATA_SIZE = 20;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int ZIP64_EOCD_DISK_OFFSET = 4;
+ static final int ZIP64_EOCD_OFFSET_OFFSET = 8;
+ static final int DISK_NUMBER_OFFSET = 16;
+
+ /**
+ * Read the Zip64 end of central directory locator from the input stream and parse additional
+ * {@link ZipFileData} from it.
+ */
+ static ZipFileData read(InputStream in, ZipFileData file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading Zip64 End of Central Directory Locator.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed Zip64 Central Directory Locator; does not start with %08x", SIGNATURE));
+ }
+ file.setZip64(true);
+ file.setZip64EndOfCentralDirectoryOffset(
+ getUnsignedLong(fixedSizeData, ZIP64_EOCD_OFFSET_OFFSET));
+ return file;
+ }
+
+ /**
+ * Generates the raw byte data of the Zip64 end of central directory locator for the file.
+ */
+ static byte[] create(ZipFileData file) {
+ byte[] buf = new byte[FIXED_DATA_SIZE];
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ intToLittleEndian(buf, ZIP64_EOCD_DISK_OFFSET, 0);
+ longToLittleEndian(buf, ZIP64_EOCD_OFFSET_OFFSET, file.getZip64EndOfCentralDirectoryOffset());
+ intToLittleEndian(buf, DISK_NUMBER_OFFSET, 1);
+ return buf;
+ }
+ }
+
+ static class EndOfCentralDirectoryRecord {
+ static final int SIGNATURE = 0x06054b50;
+ static final int FIXED_DATA_SIZE = 22;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int DISK_NUMBER_OFFSET = 4;
+ static final int CD_DISK_OFFSET = 6;
+ static final int DISK_ENTRIES_OFFSET = 8;
+ static final int TOTAL_ENTRIES_OFFSET = 10;
+ static final int CD_SIZE_OFFSET = 12;
+ static final int CD_OFFSET_OFFSET = 16;
+ static final int COMMENT_LENGTH_OFFSET = 20;
+
+ /**
+ * Read the end of central directory record from the input stream and parse {@link ZipFileData}
+ * from it.
+ */
+ static ZipFileData read(InputStream in, ZipFileData file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading End of Central Directory Record.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed End of Central Directory Record; does not start with %08x", SIGNATURE));
+ }
+
+ byte[] comment = new byte[getUnsignedShort(fixedSizeData, COMMENT_LENGTH_OFFSET)];
+ if (comment.length > 0 && in.read(comment) != comment.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading End of Central Directory Record.");
+ }
+ short diskNumber = get16(fixedSizeData, DISK_NUMBER_OFFSET);
+ short centralDirectoryDisk = get16(fixedSizeData, CD_DISK_OFFSET);
+ short entriesOnDisk = get16(fixedSizeData, DISK_ENTRIES_OFFSET);
+ short totalEntries = get16(fixedSizeData, TOTAL_ENTRIES_OFFSET);
+ int centralDirectorySize = get32(fixedSizeData, CD_SIZE_OFFSET);
+ int centralDirectoryOffset = get32(fixedSizeData, CD_OFFSET_OFFSET);
+ if (diskNumber == -1 || centralDirectoryDisk == -1 || entriesOnDisk == -1
+ || totalEntries == -1 || centralDirectorySize == -1 || centralDirectoryOffset == -1) {
+ file.setMaybeZip64(true);
+ }
+ file.setComment(comment);
+ file.setCentralDirectorySize(getUnsignedInt(fixedSizeData, CD_SIZE_OFFSET));
+ file.setCentralDirectoryOffset(getUnsignedInt(fixedSizeData, CD_OFFSET_OFFSET));
+ file.setExpectedEntries(getUnsignedShort(fixedSizeData, TOTAL_ENTRIES_OFFSET));
+ return file;
+ }
+
+ /**
+ * Generates the raw byte data of the end of central directory record for the specified
+ * {@link ZipFileData}.
+ * @throws ZipException if the file comment is too long
+ */
+ static byte[] create(ZipFileData file, boolean allowZip64) throws ZipException {
+ byte[] comment = file.getBytes(file.getComment());
+
+ byte[] buf = new byte[FIXED_DATA_SIZE + comment.length];
+
+ // Allow writing of Zip file without Zip64 extensions for large archives as a special case
+ // since many reading implementations can handle this.
+ short numEntries = (short) (file.getNumEntries() > 0xffff && allowZip64
+ ? -1 : file.getNumEntries());
+ int cdSize = (int) (file.getCentralDirectorySize() > 0xffffffffL && allowZip64
+ ? -1 : file.getCentralDirectorySize());
+ int cdOffset = (int) (file.getCentralDirectoryOffset() > 0xffffffffL && allowZip64
+ ? -1 : file.getCentralDirectoryOffset());
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ shortToLittleEndian(buf, DISK_NUMBER_OFFSET, (short) 0);
+ shortToLittleEndian(buf, CD_DISK_OFFSET, (short) 0);
+ shortToLittleEndian(buf, DISK_ENTRIES_OFFSET, numEntries);
+ shortToLittleEndian(buf, TOTAL_ENTRIES_OFFSET, numEntries);
+ intToLittleEndian(buf, CD_SIZE_OFFSET, cdSize);
+ intToLittleEndian(buf, CD_OFFSET_OFFSET, cdOffset);
+ shortToLittleEndian(buf, COMMENT_LENGTH_OFFSET, (short) comment.length);
+ System.arraycopy(comment, 0, buf, FIXED_DATA_SIZE, comment.length);
+
+ return buf;
+ }
+ }
+
+ static class CentralDirectory {
+ /**
+ * Writes the central directory to an output stream for the specified {@link ZipFileData}.
+ */
+ static void write(ZipFileData file, boolean allowZip64, OutputStream stream)
+ throws IOException {
+ long directorySize = 0;
+ byte[] buf = new byte[CentralDirectoryFileHeader.FIXED_DATA_SIZE];
+ for (ZipFileEntry entry : file.getEntries()) {
+ directorySize += CentralDirectoryFileHeader.write(entry, file, allowZip64, buf, stream);
+ }
+ file.setCentralDirectorySize(directorySize);
+ if (file.isZip64() && allowZip64) {
+ file.setZip64EndOfCentralDirectoryOffset(file.getCentralDirectoryOffset()
+ + file.getCentralDirectorySize());
+ stream.write(Zip64EndOfCentralDirectory.create(file));
+ stream.write(Zip64EndOfCentralDirectoryLocator.create(file));
+ }
+ stream.write(EndOfCentralDirectoryRecord.create(file, allowZip64));
+ }
+ }
+}
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipWriter.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipWriter.java
new file mode 100644
index 0000000000..f2d0cdee55
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipWriter.java
@@ -0,0 +1,229 @@
+// 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.zip;
+
+import com.google.devtools.build.zip.ZipFileEntry.Flag;
+import com.google.devtools.build.zip.ZipUtil.CentralDirectory;
+import com.google.devtools.build.zip.ZipUtil.LocalFileHeader;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.zip.ZipException;
+
+/**
+ * This class implements an output stream filter for writing files in the ZIP file format. It does
+ * not perform its own compression and so allows writing of already compressed file data.
+ */
+public class ZipWriter extends OutputStream {
+ private final CountingOutputStream stream;
+ private final ZipFileData zipData;
+ private final boolean allowZip64;
+ private boolean writingPrefix;
+ private ZipFileEntry entry;
+ private long bytesWritten;
+ private boolean finished;
+
+ /**
+ * Creates a new raw ZIP output stream.
+ *
+ * @param out the actual output stream
+ * @param charset the {@link Charset} to be used to encode the entry names and comments
+ */
+ public ZipWriter(OutputStream out, Charset charset) {
+ this(out, charset, false);
+ }
+
+ /**
+ * Creates a new raw ZIP output stream.
+ *
+ * @param out the actual output stream
+ * @param charset the {@link Charset} to be used to encode the entry names and comments
+ * @param allowZip64 whether the output Zip should be allowed to use Zip64 extensions
+ */
+ public ZipWriter(OutputStream out, Charset charset, boolean allowZip64) {
+ this.stream = new CountingOutputStream(out);
+ this.zipData = new ZipFileData(charset);
+ this.allowZip64 = allowZip64;
+ this.finished = false;
+ }
+
+ /**
+ * Sets the ZIP file comment.
+ *
+ * @param comment the ZIP file comment
+ * @throws ZipException if the comment is longer than 0xffff bytes
+ */
+ public void setComment(String comment) throws ZipException {
+ zipData.setComment(comment);
+ }
+
+ /**
+ * Configures the stream to write prefix file data.
+ *
+ * @throws ZipException if other contents have already been written to the output stream
+ */
+ public void startPrefixFile() throws ZipException {
+ checkNotFinished();
+ if (!zipData.getEntries().isEmpty() || entry != null) {
+ throw new ZipException("Cannot add a prefix file after the zip contents have been started.");
+ }
+ writingPrefix = true;
+ }
+
+ /** Closes the prefix file and positions the output stream to write ZIP entries. */
+ public void endPrefixFile() {
+ checkNotFinished();
+ writingPrefix = false;
+ }
+
+ /**
+ * Begins writing a new ZIP file entry and positions the stream to the start of the entry data.
+ * Closes the current entry if still active.
+ *
+ * <p><em>NOTE:</em> No defensive copying is performed on e. The local header offset and flags
+ * will be modified.
+ *
+ * @param e the ZIP entry to be written
+ * @throws IOException if an I/O error occurred
+ */
+ public void putNextEntry(ZipFileEntry e) throws IOException {
+ checkNotFinished();
+ writingPrefix = false;
+ if (entry != null) {
+ finishEntry();
+ }
+ startEntry(e);
+ }
+
+ /**
+ * Closes the current ZIP entry and positions the stream for writing the next entry.
+ *
+ * @throws ZipException if a ZIP format exception occurred
+ * @throws IOException if an I/O error occurred
+ */
+ public void closeEntry() throws IOException {
+ checkNotFinished();
+ if (entry != null) {
+ finishEntry();
+ }
+ }
+
+ @Override public void write(int b) throws IOException {
+ byte[] buf = new byte[1];
+ buf[0] = (byte) (b & 0xff);
+ write(buf);
+ }
+
+ @Override public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override public synchronized void write(byte[] b, int off, int len) throws IOException {
+ checkNotFinished();
+ if (entry == null && !writingPrefix) {
+ throw new ZipException("Cannot write zip contents without first setting a ZipEntry or"
+ + " starting a prefix file.");
+ }
+ stream.write(b, off, len);
+ bytesWritten += len;
+ }
+
+ /**
+ * Finishes writing the contents of the ZIP output stream without closing the underlying stream.
+ * Use this method when applying multiple filters in succession to the same output stream.
+ *
+ * @throws ZipException if a ZIP file error has occurred
+ * @throws IOException if an I/O exception has occurred
+ */
+ public void finish() throws IOException {
+ checkNotFinished();
+ if (entry != null) {
+ finishEntry();
+ }
+ writeCentralDirectory();
+ finished = true;
+ }
+
+ @Override public void close() throws IOException {
+ if (!finished) {
+ finish();
+ }
+ stream.close();
+ }
+
+ /**
+ * Writes the local file header for the ZIP entry and positions the stream to the start of the
+ * entry data.
+ *
+ * @param e the ZIP entry for which to write the local file header
+ * @throws ZipException if a ZIP file error has occurred
+ * @throws IOException if an I/O exception has occurred
+ */
+ private void startEntry(ZipFileEntry e) throws IOException {
+ if (e.getTime() == -1) {
+ throw new IllegalArgumentException("Zip entry last modified time must be set");
+ }
+ if (e.getCrc() == -1) {
+ throw new IllegalArgumentException("Zip entry CRC-32 must be set");
+ }
+ if (e.getSize() == -1) {
+ throw new IllegalArgumentException("Zip entry uncompressed size must be set");
+ }
+ if (e.getCompressedSize() == -1) {
+ throw new IllegalArgumentException("Zip entry compressed size must be set");
+ }
+ bytesWritten = 0;
+ entry = e;
+ entry.setFlag(Flag.DATA_DESCRIPTOR, false);
+ entry.setLocalHeaderOffset(stream.getCount());
+ stream.write(LocalFileHeader.create(entry, zipData, allowZip64));
+ }
+
+ /**
+ * Closes the current ZIP entry and positions the stream for writing the next entry. Checks that
+ * the amount of data written matches the compressed size indicated by the ZipEntry.
+ *
+ * @throws ZipException if a ZIP file error has occurred
+ * @throws IOException if an I/O exception has occurred
+ */
+ private void finishEntry() throws IOException {
+ if (entry.getCompressedSize() != bytesWritten) {
+ throw new ZipException(String.format("Number of bytes written for the entry %s (%d) does not"
+ + " match the reported compressed size (%d).", entry.getName(), bytesWritten,
+ entry.getCompressedSize()));
+ }
+ zipData.addEntry(entry);
+ entry = null;
+ }
+
+ /**
+ * Writes the ZIP file's central directory.
+ *
+ * @throws ZipException if a ZIP file error has occurred
+ * @throws IOException if an I/O exception has occurred
+ */
+ private void writeCentralDirectory() throws IOException {
+ zipData.setCentralDirectoryOffset(stream.getCount());
+ CentralDirectory.write(zipData, allowZip64, stream);
+ }
+
+ /** Checks that the ZIP file has not been finished yet. */
+ private void checkNotFinished() {
+ if (finished) {
+ throw new IllegalStateException();
+ }
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java
index 8fec585fe0..d6f801f23e 100644
--- a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java
@@ -21,10 +21,13 @@ import static org.junit.Assert.assertNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
@@ -74,6 +77,17 @@ public final class MockSimpleFileSystem implements SimpleFileSystem {
}
@Override
+ public File getFile(String filename) throws IOException {
+ byte[] data = files.get(filename);
+ if (data == null) {
+ throw new FileNotFoundException();
+ }
+ File file = File.createTempFile(filename, null);
+ Files.copy(new ByteArrayInputStream(data), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return file;
+ }
+
+ @Override
public boolean delete(String filename) {
assertEquals(outputFileName, filename);
assertNotNull(out);
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java
index 0c67b61e18..34c4cc0bf5 100644
--- a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java
@@ -14,8 +14,9 @@
package com.google.devtools.build.singlejar;
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.common.base.Joiner;
@@ -78,7 +79,7 @@ public class SingleJarTest {
private final List<String> manifestLines;
public ManifestValidator(List<String> manifestLines) {
- this.manifestLines = new ArrayList<String>(manifestLines);
+ this.manifestLines = new ArrayList<>(manifestLines);
Collections.sort(this.manifestLines);
}
@@ -146,7 +147,7 @@ public class SingleJarTest {
private void assertStripFirstLine(String expected, String testCase) {
byte[] result = SingleJar.stripFirstLine(testCase.getBytes(StandardCharsets.UTF_8));
- assertEquals(expected, new String(result));
+ assertEquals(expected, new String(result, UTF_8));
}
@Test
@@ -428,7 +429,7 @@ public class SingleJarTest {
MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
SingleJar singleJar = new SingleJar(mockFs);
- List<String> args = new ArrayList<String>();
+ List<String> args = new ArrayList<>();
args.add("--output");
args.add("output.jar");
args.addAll(infoPropertyArguments(buildInfo));
@@ -591,8 +592,8 @@ public class SingleJarTest {
singleJar.run(ImmutableList.of("--output", "output.jar", "--exclude_build_data",
"--resources", "a/b/c", "a/b/c"));
fail();
- } catch (IllegalStateException e) {
- assertTrue(e.getMessage().contains("already contains a file named a/b/c"));
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains("already contains a file named 'a/b/c'.");
}
}
@@ -616,19 +617,20 @@ public class SingleJarTest {
public void testCanAddPreamble() throws IOException {
MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
String preamble = "WeThePeople";
- mockFs.addFile(preamble, preamble.getBytes());
+ mockFs.addFile(preamble, preamble.getBytes(UTF_8));
SingleJar singleJar = new SingleJar(mockFs);
singleJar.run(ImmutableList.of("--output", "output.jar",
"--java_launcher", preamble,
"--main_class", "SomeClass"));
- FakeZipFile expectedResult = new FakeZipFile()
- .addPreamble(preamble.getBytes())
- .addEntry("META-INF/", EXTRA_FOR_META_INF)
- .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
- "Manifest-Version: 1.0",
- "Created-By: blaze-singlejar",
- "Main-Class: SomeClass"))
- .addEntry("build-data.properties", redactedBuildData("output.jar", "SomeClass"));
+ FakeZipFile expectedResult =
+ new FakeZipFile()
+ .addPreamble(preamble.getBytes(UTF_8))
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(
+ JarFile.MANIFEST_NAME,
+ new ManifestValidator("Manifest-Version: 1.0", "Created-By: blaze-singlejar",
+ "Main-Class: SomeClass"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar", "SomeClass"));
expectedResult.assertSame(mockFs.toByteArray());
}
}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java
index e5345cb1f8..099454f458 100644
--- a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java
@@ -28,29 +28,40 @@ import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.devtools.build.singlejar.ZipCombiner.OutputMode;
import com.google.devtools.build.singlejar.ZipEntryFilter.CustomMergeStrategy;
+import com.google.devtools.build.zip.ExtraData;
+import com.google.devtools.build.zip.ZipFileEntry;
+import com.google.devtools.build.zip.ZipReader;
+import com.google.devtools.build.zip.ZipUtil;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Calendar;
+import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@@ -59,41 +70,66 @@ import java.util.zip.ZipOutputStream;
*/
@RunWith(JUnit4.class)
public class ZipCombinerTest {
+ @Rule public TemporaryFolder tmp = new TemporaryFolder();
+ @Rule public ExpectedException thrown = ExpectedException.none();
- private static final Date DOS_EPOCH = ZipCombiner.DOS_EPOCH;
-
- private InputStream sampleZip() {
+ private InputStream sampleZipStream() {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!");
return factory.toInputStream();
}
- private InputStream sampleZip2() {
+ private File sampleZip() throws IOException {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!");
+ return writeInputStreamToFile(factory.toInputStream());
+ }
+
+ private File sampleZip2() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello2.txt", "Hello World 2!");
- return factory.toInputStream();
+ return writeInputStreamToFile(factory.toInputStream());
}
- private InputStream sampleZipWithTwoEntries() {
+ private File sampleZipWithTwoEntries() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!");
factory.addFile("hello2.txt", "Hello World 2!");
- return factory.toInputStream();
+ return writeInputStreamToFile(factory.toInputStream());
}
- private InputStream sampleZipWithOneUncompressedEntry() {
+ private InputStream sampleZipWithOneUncompressedEntryStream() {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!", false);
return factory.toInputStream();
}
- private InputStream sampleZipWithTwoUncompressedEntries() {
+ private File sampleZipWithOneUncompressedEntry() throws IOException {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!", false);
+ return writeInputStreamToFile(factory.toInputStream());
+ }
+
+ private InputStream sampleZipWithTwoUncompressedEntriesStream() {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!", false);
factory.addFile("hello2.txt", "Hello World 2!", false);
return factory.toInputStream();
}
+ private File sampleZipWithTwoUncompressedEntries() throws IOException {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!", false);
+ factory.addFile("hello2.txt", "Hello World 2!", false);
+ return writeInputStreamToFile(factory.toInputStream());
+ }
+
+ private File writeInputStreamToFile(InputStream in) throws IOException {
+ File out = tmp.newFile();
+ Files.copy(in, out.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return out;
+ }
+
private void assertEntry(ZipInputStream zipInput, String filename, long time, byte[] content)
throws IOException {
ZipEntry zipEntry = zipInput.getNextEntry();
@@ -111,7 +147,7 @@ public class ZipCombinerTest {
private void assertEntry(ZipInputStream zipInput, String filename, byte[] content)
throws IOException {
- assertEntry(zipInput, filename, ZipCombiner.DOS_EPOCH.getTime(), content);
+ assertEntry(zipInput, filename, ZipUtil.DOS_EPOCH, content);
}
private void assertEntry(ZipInputStream zipInput, String filename, String content)
@@ -125,111 +161,81 @@ public class ZipCombinerTest {
}
@Test
- public void testDateToDosTime() {
- assertEquals(0x210000, ZipCombiner.dateToDosTime(ZipCombiner.DOS_EPOCH));
- Calendar calendar = new GregorianCalendar();
- for (int i = 1980; i <= 2107; i++) {
- calendar.set(i, 0, 1, 0, 0, 0);
- int result = ZipCombiner.dateToDosTime(calendar.getTime());
- assertEquals(i - 1980, result >>> 25);
- assertEquals(1, (result >> 21) & 0xf);
- assertEquals(1, (result >> 16) & 0x1f);
- assertEquals(0, result & 0xffff);
- }
- }
-
- @Test
- public void testDateToDosTimeFailsForBadValues() {
- try {
- Calendar calendar = new GregorianCalendar();
- calendar.set(1979, 0, 1, 0, 0, 0);
- ZipCombiner.dateToDosTime(calendar.getTime());
- fail();
- } catch (IllegalArgumentException e) {
- /* Expected exception. */
- }
- try {
- Calendar calendar = new GregorianCalendar();
- calendar.set(2108, 0, 1, 0, 0, 0);
- ZipCombiner.dateToDosTime(calendar.getTime());
- fail();
- } catch (IllegalArgumentException e) {
- /* Expected exception. */
+ public void testInputStreamZip() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addZip(sampleZipStream());
}
+ FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", true);
+ expectedResult.assertSame(out.toByteArray());
}
@Test
public void testCompressedDontCare() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(sampleZip());
- singleJar.close();
- FakeZipFile expectedResult = new FakeZipFile()
- .addEntry("hello.txt", "Hello World!", true);
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addZip(sampleZip());
+ }
+ FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", true);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testCompressedForceDeflate() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_DEFLATE, out);
- singleJar.addZip(sampleZip());
- singleJar.close();
- FakeZipFile expectedResult = new FakeZipFile()
- .addEntry("hello.txt", "Hello World!", true);
+ try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_DEFLATE, out)) {
+ zipCombiner.addZip(sampleZip());
+ }
+ FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", true);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testCompressedForceStored() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_STORED, out);
- singleJar.addZip(sampleZip());
- singleJar.close();
- FakeZipFile expectedResult = new FakeZipFile()
- .addEntry("hello.txt", "Hello World!", false);
+ try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_STORED, out)) {
+ zipCombiner.addZip(sampleZip());
+ }
+ FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", false);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testUncompressedDontCare() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(sampleZipWithOneUncompressedEntry());
- singleJar.close();
- FakeZipFile expectedResult = new FakeZipFile()
- .addEntry("hello.txt", "Hello World!", false);
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addZip(sampleZipWithOneUncompressedEntry());
+ }
+ FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", false);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testUncompressedForceDeflate() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_DEFLATE, out);
- singleJar.addZip(sampleZipWithOneUncompressedEntry());
- singleJar.close();
- FakeZipFile expectedResult = new FakeZipFile()
- .addEntry("hello.txt", "Hello World!", true);
+ try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_DEFLATE, out)) {
+ zipCombiner.addZip(sampleZipWithOneUncompressedEntry());
+ }
+ FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", true);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testUncompressedForceStored() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_STORED, out);
- singleJar.addZip(sampleZipWithOneUncompressedEntry());
- singleJar.close();
- FakeZipFile expectedResult = new FakeZipFile()
- .addEntry("hello.txt", "Hello World!", false);
+ try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_STORED, out)) {
+ zipCombiner.addZip(sampleZipWithOneUncompressedEntry());
+ }
+ FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", false);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testCopyTwoEntries() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -239,9 +245,9 @@ public class ZipCombinerTest {
@Test
public void testCopyTwoUncompressedEntries() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -251,10 +257,10 @@ public class ZipCombinerTest {
@Test
public void testCombine() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(sampleZip());
- singleJar.addZip(sampleZip2());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addZip(sampleZip());
+ zipCombiner.addZip(sampleZip2());
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -264,16 +270,16 @@ public class ZipCombinerTest {
@Test
public void testDuplicateEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(sampleZip());
- singleJar.addZip(sampleZip());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addZip(sampleZip());
+ zipCombiner.addZip(sampleZip());
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertNull(zipInput.getNextEntry());
}
- // Returns an input stream that can only read one byte at a time.
+ //Returns an input stream that can only read one byte at a time.
private InputStream slowRead(final InputStream in) {
return new InputStream() {
@Override
@@ -301,10 +307,10 @@ public class ZipCombinerTest {
@Test
public void testDuplicateUncompressedEntryWithSlowRead() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(slowRead(sampleZipWithOneUncompressedEntry()));
- singleJar.addZip(slowRead(sampleZipWithOneUncompressedEntry()));
- singleJar.close();
+ ZipCombiner zipCombiner = new ZipCombiner(out);
+ zipCombiner.addZip(slowRead(sampleZipWithOneUncompressedEntryStream()));
+ zipCombiner.addZip(slowRead(sampleZipWithOneUncompressedEntryStream()));
+ zipCombiner.close();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertNull(zipInput.getNextEntry());
@@ -313,10 +319,10 @@ public class ZipCombinerTest {
@Test
public void testDuplicateEntryWithSlowRead() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(slowRead(sampleZip()));
- singleJar.addZip(slowRead(sampleZip()));
- singleJar.close();
+ ZipCombiner zipCombiner = new ZipCombiner(out);
+ zipCombiner.addZip(slowRead(sampleZipStream()));
+ zipCombiner.addZip(slowRead(sampleZipStream()));
+ zipCombiner.close();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertNull(zipInput.getNextEntry());
@@ -325,9 +331,11 @@ public class ZipCombinerTest {
@Test
public void testBadZipFileNoEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("It does not contain an end of central directory record.");
+ zipCombiner.addZip(writeInputStreamToFile(new ByteArrayInputStream(new byte[] {1, 2, 3, 4})));
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertNull(zipInput.getNextEntry());
}
@@ -339,9 +347,9 @@ public class ZipCombinerTest {
@Test
public void testAddFile() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addFile("hello.txt", DOS_EPOCH, asStream("Hello World!"));
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addFile("hello.txt", ZipCombiner.DOS_EPOCH, asStream("Hello World!"));
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertNull(zipInput.getNextEntry());
@@ -350,10 +358,10 @@ public class ZipCombinerTest {
@Test
public void testAddFileAndDuplicateZipEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addFile("hello.txt", DOS_EPOCH, asStream("Hello World!"));
- singleJar.addZip(sampleZip());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ zipCombiner.addFile("hello.txt", ZipCombiner.DOS_EPOCH, asStream("Hello World!"));
+ zipCombiner.addZip(sampleZip());
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertNull(zipInput.getNextEntry());
@@ -381,7 +389,7 @@ public class ZipCombinerTest {
*/
class MockZipEntryFilter implements ZipEntryFilter {
- private Date date = DOS_EPOCH;
+ private Date date = ZipCombiner.DOS_EPOCH;
private final List<String> calls = new ArrayList<>();
// File name to merge strategy map.
private final Map<String, CustomMergeStrategy> behavior =
@@ -420,9 +428,9 @@ public class ZipCombinerTest {
public void testCopyCallsFilter() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZip());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZip());
+ }
assertEquals(Arrays.asList("hello.txt"), mockFilter.calls);
}
@@ -430,10 +438,10 @@ public class ZipCombinerTest {
public void testDuplicateEntryCallsFilterOnce() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZip());
- singleJar.addZip(sampleZip());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZip());
+ zipCombiner.addZip(sampleZip());
+ }
assertEquals(Arrays.asList("hello.txt"), mockFilter.calls);
}
@@ -442,10 +450,10 @@ public class ZipCombinerTest {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZip());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZip());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls);
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -459,10 +467,10 @@ public class ZipCombinerTest {
mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
mockFilter.behavior.put("hello2.txt", SKIP_PLACEHOLDER);
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ }
assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls);
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!\nHello World!");
@@ -474,10 +482,10 @@ public class ZipCombinerTest {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(slowRead(sampleZipWithOneUncompressedEntry()));
- singleJar.addZip(slowRead(sampleZipWithTwoUncompressedEntries()));
- singleJar.close();
+ ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out);
+ zipCombiner.addZip(slowRead(sampleZipWithOneUncompressedEntryStream()));
+ zipCombiner.addZip(slowRead(sampleZipWithTwoUncompressedEntriesStream()));
+ zipCombiner.close();
assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls);
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -490,10 +498,10 @@ public class ZipCombinerTest {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZip());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZip());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls);
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -507,20 +515,20 @@ public class ZipCombinerTest {
mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
mockFilter.behavior.put("hello2.txt", SKIP_PLACEHOLDER);
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ }
assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls);
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!Hello World!");
assertNull(zipInput.getNextEntry());
}
- private InputStream specialZipWithMinusOne() {
+ private File specialZipWithMinusOne() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", new byte[] {-1});
- return factory.toInputStream();
+ return writeInputStreamToFile(factory.toInputStream());
}
@Test
@@ -528,9 +536,9 @@ public class ZipCombinerTest {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(specialZipWithMinusOne());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(specialZipWithMinusOne());
+ }
assertEquals(Arrays.asList("hello.txt"), mockFilter.calls);
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", new byte[] { -1 });
@@ -548,9 +556,9 @@ public class ZipCombinerTest {
}
};
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZip());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZip());
+ }
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", date, "Hello World!");
assertNull(zipInput.getNextEntry());
@@ -562,13 +570,13 @@ public class ZipCombinerTest {
mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
mockFilter.date = new GregorianCalendar(2009, 8, 2, 0, 0, 0).getTime();
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZip());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZip());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls);
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
- assertEntry(zipInput, "hello2.txt", DOS_EPOCH, "Hello World 2!");
+ assertEntry(zipInput, "hello2.txt", ZipCombiner.DOS_EPOCH, "Hello World 2!");
assertEntry(zipInput, "hello.txt", mockFilter.date, "Hello World!\nHello World!");
assertNull(zipInput.getNextEntry());
}
@@ -584,8 +592,8 @@ public class ZipCombinerTest {
}
};
ByteArrayOutputStream out = new ByteArrayOutputStream();
- try (ZipCombiner singleJar = new ZipCombiner(badFilter, out)) {
- singleJar.addZip(sampleZip());
+ try (ZipCombiner zipCombiner = new ZipCombiner(badFilter, out)) {
+ zipCombiner.addZip(sampleZip());
fail();
} catch (IllegalStateException e) {
// Expected exception.
@@ -601,8 +609,8 @@ public class ZipCombinerTest {
}
};
ByteArrayOutputStream out = new ByteArrayOutputStream();
- try (ZipCombiner singleJar = new ZipCombiner(badFilter, out)) {
- singleJar.addZip(sampleZip());
+ try (ZipCombiner zipCombiner = new ZipCombiner(badFilter, out)) {
+ zipCombiner.addZip(sampleZip());
fail();
} catch (IllegalStateException e) {
// Expected exception.
@@ -620,12 +628,13 @@ public class ZipCombinerTest {
mockFilter.renameMap.put("hello.txt", "hello.txt"); // identity rename, not copy
mockFilter.renameMap.put("hello2.txt", "hello2.txt"); // identity rename, not copy
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
- assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt",
- "hello.txt", "hello2.txt").inOrder();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
+ assertThat(mockFilter.calls)
+ .containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
+ .inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -642,12 +651,13 @@ public class ZipCombinerTest {
mockFilter.renameMap.putAll("hello.txt", Arrays.asList("hello1.txt", "hello2.txt"));
mockFilter.renameMap.putAll("hello2.txt", Arrays.asList("world1.txt", "world2.txt"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
- assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt",
- "hello.txt", "hello2.txt").inOrder();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
+ assertThat(mockFilter.calls)
+ .containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
+ .inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "world1.txt", "Hello World 2!");
@@ -668,13 +678,15 @@ public class ZipCombinerTest {
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
mockFilter.renameMap.put("hello2.txt", "hello2.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
- assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt",
- "hello.txt", "hello2.txt", "hello.txt", "hello2.txt").inOrder();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
+ assertThat(mockFilter.calls)
+ .containsExactly(
+ "hello.txt", "hello2.txt", "hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
+ .inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -693,13 +705,14 @@ public class ZipCombinerTest {
mockFilter.renameMap.putAll("hello.txt",
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
- assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt",
- "hello.txt", "hello.txt").inOrder();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
+ assertThat(mockFilter.calls)
+ .containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello.txt")
+ .inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -718,13 +731,14 @@ public class ZipCombinerTest {
mockFilter.renameMap.putAll("hello.txt",
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.addZip(sampleZipWithTwoEntries());
- singleJar.close();
- assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt",
- "hello.txt", "hello.txt").inOrder();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ zipCombiner.addZip(sampleZipWithTwoEntries());
+ }
+ assertThat(mockFilter.calls)
+ .containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello.txt")
+ .inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello3.txt", "Hello World!");
@@ -743,13 +757,15 @@ public class ZipCombinerTest {
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
mockFilter.renameMap.put("hello2.txt", "hello2.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.addZip(sampleZipWithTwoUncompressedEntries());
- singleJar.close();
- assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt",
- "hello.txt", "hello2.txt", "hello.txt", "hello2.txt").inOrder();
+ try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
+ }
+ assertThat(mockFilter.calls)
+ .containsExactly(
+ "hello.txt", "hello2.txt", "hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
+ .inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
@@ -762,66 +778,6 @@ public class ZipCombinerTest {
// the data descriptor marker. It's unfortunately a bit tricky to create such
// a ZIP.
private static final int LOCAL_FILE_HEADER_MARKER = 0x04034b50;
- private static final int DATA_DESCRIPTOR_MARKER = 0x08074b50;
- private static final byte[] DATA_DESCRIPTOR_MARKER_AS_BYTES = new byte[] {
- 0x50, 0x4b, 0x07, 0x08
- };
-
- // Create a ZIP with an data descriptor marker in the DEFLATE content of a
- // file. To do that, we build the ZIP byte by byte.
- private InputStream zipWithUnexpectedDataDescriptorMarker() {
- ByteBuffer out = ByteBuffer.wrap(new byte[200]).order(ByteOrder.LITTLE_ENDIAN);
- out.clear();
- // file header
- out.putInt(LOCAL_FILE_HEADER_MARKER); // file header signature
- out.putShort((short) 6); // version to extract
- out.putShort((short) 8); // general purpose bit flag
- out.putShort((short) ZipOutputStream.DEFLATED); // compression method
- out.putShort((short) 0); // mtime (00:00:00)
- out.putShort((short) 0x21); // mdate (1.1.1980)
- out.putInt(0); // crc32
- out.putInt(0); // compressed size
- out.putInt(0); // uncompressed size
- out.putShort((short) 1); // file name length
- out.putShort((short) 0); // extra field length
- out.put((byte) 'a'); // file name
-
- // file contents
- out.put((byte) 0x01); // deflated content block is last block and uncompressed
- out.putShort((short) 4); // uncompressed block length
- out.putShort((short) ~4); // negated uncompressed block length
- out.putInt(DATA_DESCRIPTOR_MARKER); // 4 bytes uncompressed data
-
- // data descriptor
- out.putInt(DATA_DESCRIPTOR_MARKER); // data descriptor with marker
- out.putInt((int) ZipFactory.calculateCrc32(DATA_DESCRIPTOR_MARKER_AS_BYTES));
- out.putInt(9);
- out.putInt(4);
- // We omit the central directory here. It's currently not used by
- // ZipCombiner or by java.util.zip.ZipInputStream, so that shouldn't be a
- // problem.
- return new ByteArrayInputStream(out.array());
- }
-
- // Check that the created ZIP is correct.
- @Test
- public void testZipWithUnexpectedDataDescriptorMarkerIsCorrect() throws IOException {
- ZipInputStream zipInput = new ZipInputStream(zipWithUnexpectedDataDescriptorMarker());
- assertEntry(zipInput, "a", DATA_DESCRIPTOR_MARKER_AS_BYTES);
- assertNull(zipInput.getNextEntry());
- }
-
- // Check that ZipCombiner handles the ZIP correctly.
- @Test
- public void testZipWithUnexpectedDataDescriptorMarker() throws IOException {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addZip(zipWithUnexpectedDataDescriptorMarker());
- singleJar.close();
- ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
- assertEntry(zipInput, "a", DATA_DESCRIPTOR_MARKER_AS_BYTES);
- assertNull(zipInput.getNextEntry());
- }
// Create a ZIP with a partial entry.
private InputStream zipWithPartialEntry() {
@@ -851,63 +807,75 @@ public class ZipCombinerTest {
@Test
public void testBadZipFilePartialEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- try (ZipCombiner singleJar = new ZipCombiner(out)) {
- singleJar.addZip(zipWithPartialEntry());
- fail();
- } catch (EOFException e) {
- // Expected exception.
+ try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("It does not contain an end of central directory record.");
+ zipCombiner.addZip(writeInputStreamToFile(zipWithPartialEntry()));
}
}
@Test
- public void testSimpleJarAgainstJavaUtil() throws IOException {
+ public void testZipCombinerAgainstJavaUtil() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
- JarOutputStream jarOut = new JarOutputStream(out);
- ZipEntry entry;
- entry = new ZipEntry("META-INF/");
- entry.setTime(DOS_EPOCH.getTime());
- entry.setMethod(JarOutputStream.STORED);
- entry.setSize(0);
- entry.setCompressedSize(0);
- entry.setCrc(0);
- jarOut.putNextEntry(entry);
- entry = new ZipEntry("META-INF/MANIFEST.MF");
- entry.setTime(DOS_EPOCH.getTime());
- entry.setMethod(JarOutputStream.DEFLATED);
- jarOut.putNextEntry(entry);
- jarOut.write(new byte[] { 1, 2, 3, 4 });
- jarOut.close();
- byte[] javaFile = out.toByteArray();
+ try (JarOutputStream jarOut = new JarOutputStream(out)) {
+ ZipEntry entry;
+ entry = new ZipEntry("META-INF/");
+ entry.setTime(ZipCombiner.DOS_EPOCH.getTime());
+ entry.setMethod(JarOutputStream.STORED);
+ entry.setSize(0);
+ entry.setCompressedSize(0);
+ entry.setCrc(0);
+ jarOut.putNextEntry(entry);
+ entry = new ZipEntry("META-INF/MANIFEST.MF");
+ entry.setTime(ZipCombiner.DOS_EPOCH.getTime());
+ entry.setMethod(JarOutputStream.DEFLATED);
+ jarOut.putNextEntry(entry);
+ jarOut.write(new byte[] {1, 2, 3, 4});
+ }
+ File javaFile = writeInputStreamToFile(new ByteArrayInputStream(out.toByteArray()));
out.reset();
- ZipCombiner singleJar = new ZipCombiner(out);
- singleJar.addDirectory("META-INF/", DOS_EPOCH,
- new ExtraData[] { new ExtraData((short) 0xCAFE, new byte[0]) });
- singleJar.addFile("META-INF/MANIFEST.MF", DOS_EPOCH,
- new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
- singleJar.close();
- byte[] singlejarFile = out.toByteArray();
-
- new ZipTester(singlejarFile).validate();
- assertZipFilesEquivalent(singlejarFile, javaFile);
- }
-
- void assertZipFilesEquivalent(byte[] x, byte[] y) {
- assertEquals(x.length, y.length);
-
- for (int i = 0; i < x.length; i++) {
- if (x[i] != y[i]) {
- // Allow general purpose bit 11 (UTF-8 encoding) used in jdk7 to differ
- assertEquals("at position " + i, 0x08, x[i] ^ y[i]);
- // Check that x[i] is the second byte of a general purpose bit flag.
- // Phil Katz, you will never be forgotten.
- assertTrue(
- // Local header
- x[i-7] == 'P' && x[i-6] == 'K' && x[i-5] == 3 && x[i-4] == 4 ||
- // Central directory header
- x[i-9] == 'P' && x[i-8] == 'K' && x[i-7] == 1 && x[i-6] == 2);
- }
+ try (ZipCombiner zipcombiner = new ZipCombiner(out)) {
+ zipcombiner.addDirectory("META-INF/", ZipCombiner.DOS_EPOCH,
+ new ExtraData[] {new ExtraData((short) 0xCAFE, new byte[0])});
+ zipcombiner.addFile("META-INF/MANIFEST.MF", ZipCombiner.DOS_EPOCH,
+ new ByteArrayInputStream(new byte[] {1, 2, 3, 4}));
}
+ File zipCombinerFile = writeInputStreamToFile(new ByteArrayInputStream(out.toByteArray()));
+ byte[] zipCombinerRaw = out.toByteArray();
+
+ new ZipTester(zipCombinerRaw).validate();
+ assertZipFilesEquivalent(new ZipReader(zipCombinerFile), new ZipReader(javaFile));
+ }
+
+ void assertZipFilesEquivalent(ZipReader x, ZipReader y) {
+ Collection<ZipFileEntry> xEntries = x.entries();
+ Collection<ZipFileEntry> yEntries = y.entries();
+ assertThat(xEntries).hasSize(yEntries.size());
+ Iterator<ZipFileEntry> xIter = xEntries.iterator();
+ Iterator<ZipFileEntry> yIter = yEntries.iterator();
+ for (int i = 0; i < xEntries.size(); i++) {
+ assertZipEntryEquivalent(xIter.next(), yIter.next());
+ }
+ }
+
+ void assertZipEntryEquivalent(ZipFileEntry x, ZipFileEntry y) {
+ assertThat(x.getComment()).isEqualTo(y.getComment());
+ assertThat(x.getCompressedSize()).isEqualTo(y.getCompressedSize());
+ assertThat(x.getCrc()).isEqualTo(y.getCrc());
+ assertThat(x.getExternalAttributes()).isEqualTo(y.getExternalAttributes());
+ assertThat(x.getExtra().getBytes()).isEqualTo(y.getExtra().getBytes());
+ assertThat(x.getInternalAttributes()).isEqualTo(y.getInternalAttributes());
+ assertThat(x.getMethod()).isEqualTo(y.getMethod());
+ assertThat(x.getName()).isEqualTo(y.getName());
+ assertThat(x.getSize()).isEqualTo(y.getSize());
+ assertThat(x.getTime()).isEqualTo(y.getTime());
+ assertThat(x.getVersion()).isEqualTo(y.getVersion());
+ assertThat(x.getVersionNeeded()).isEqualTo(y.getVersionNeeded());
+ // Allow general purpose bit 3 (data descriptor) used in jdk7 to differ.
+ // Allow general purpose bit 11 (UTF-8 encoding) used in jdk7 to differ.
+ assertThat(x.getFlags() | (1 << 3) | (1 << 11))
+ .isEqualTo(y.getFlags() | (1 << 3) | (1 << 11));
}
/**
@@ -917,20 +885,18 @@ public class ZipCombinerTest {
@Test
public void testLotsOfFiles() throws IOException {
int fileCount = 100;
- for (int blockSize : new int[] { 1, 2, 3, 4, 10, 1000 }) {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- ZipCombiner zipCombiner = new ZipCombiner(
- OutputMode.DONT_CARE, new CopyEntryFilter(), out, blockSize);
- for (int i = 0; i < fileCount; i++) {
- zipCombiner.addFile("hello" + i, DOS_EPOCH, asStream("Hello " + i + "!"));
- }
- zipCombiner.close();
- ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (ZipCombiner zipCombiner =
+ new ZipCombiner(OutputMode.DONT_CARE, new CopyEntryFilter(), out)) {
for (int i = 0; i < fileCount; i++) {
- assertEntry(zipInput, "hello" + i, "Hello " + i + "!");
+ zipCombiner.addFile("hello" + i, ZipCombiner.DOS_EPOCH, asStream("Hello " + i + "!"));
}
- assertNull(zipInput.getNextEntry());
- new ZipTester(out.toByteArray()).validate();
}
+ ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
+ for (int i = 0; i < fileCount; i++) {
+ assertEntry(zipInput, "hello" + i, "Hello " + i + "!");
+ }
+ assertNull(zipInput.getNextEntry());
+ new ZipTester(out.toByteArray()).validate();
}
}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataListTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataListTest.java
new file mode 100644
index 0000000000..444b88e058
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataListTest.java
@@ -0,0 +1,117 @@
+// 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.zip;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.NullPointerTester;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@RunWith(JUnit4.class)
+public class ExtraDataListTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test public void testNulls() {
+ NullPointerTester tester = new NullPointerTester();
+ tester.testAllPublicConstructors(ExtraDataList.class);
+ tester.testAllPublicInstanceMethods(new ExtraDataList());
+ }
+
+ @Test public void testConstructFromList() {
+ ExtraData[] extras = new ExtraData[] {
+ new ExtraData((short) 0xcafe, new byte[] { 0x00, 0x11, 0x22 }),
+ new ExtraData((short) 0xbeef, new byte[] { 0x33, 0x44, 0x55 })
+ };
+
+ ExtraDataList extra = new ExtraDataList(extras);
+ // Expect 0xcafe 0x0003 0x00 0x11 0x22, 0xbeef 0x0003 0x33 0x44 0x55 in little endian
+ assertThat(extra.getBytes()).isEqualTo(new byte[] { (byte) 0xfe, (byte) 0xca, 0x03, 0x00,
+ 0x00, 0x11, 0x22, (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33, 0x44, 0x55 });
+ assertThat(extra.contains((short) 0xcafe)).isTrue();
+ assertThat(extra.contains((short) 0xbeef)).isTrue();
+
+ ExtraData cafe = extra.remove((short) 0xcafe);
+ // Expect 0xbeef 0x0003 0x33 0x44 0x55 in little endian
+ assertThat(extra.getBytes()).isEqualTo(new byte[] { (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33,
+ 0x44, 0x55 });
+ assertThat(extra.contains((short) 0xcafe)).isFalse();
+ assertThat(extra.contains((short) 0xbeef)).isTrue();
+
+ extra.add(cafe);
+ // Expect 0xbeef 0x0003 0x33 0x44 0x55, 0xcafe 0x0003 0x00 0x11 0x22 in little endian
+ assertThat(extra.getBytes()).isEqualTo(new byte[] { (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33,
+ 0x44, 0x55, (byte) 0xfe, (byte) 0xca, 0x03, 0x00, 0x00, 0x11, 0x22 });
+ assertThat(extra.contains((short) 0xcafe)).isTrue();
+ assertThat(extra.contains((short) 0xbeef)).isTrue();
+
+ ExtraData beef = extra.get((short) 0xbeef);
+ assertThat(beef.getId()).isEqualTo((short) 0xbeef);
+ }
+
+ @Test public void testConstructFromBuffer() {
+ byte[] buffer = new byte[] { (byte) 0xfe, (byte) 0xca, 0x03, 0x00, 0x00, 0x11, 0x22,
+ (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33, 0x44, 0x55 };
+
+ ExtraDataList extra = new ExtraDataList(buffer);
+ // Expect 0xcafe 0x0003 0x00 0x11 0x22, 0xbeef 0x0003 0x33 0x44 0x55 in little endian
+ assertThat(extra.getBytes()).isEqualTo(new byte[] { (byte) 0xfe, (byte) 0xca, 0x03, 0x00,
+ 0x00, 0x11, 0x22, (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33, 0x44, 0x55 });
+ assertThat(extra.contains((short) 0xcafe)).isTrue();
+ assertThat(extra.contains((short) 0xbeef)).isTrue();
+
+ ExtraData cafe = extra.remove((short) 0xcafe);
+ // Expect 0xbeef 0x0003 0x33 0x44 0x55 in little endian
+ assertThat(extra.getBytes()).isEqualTo(new byte[] { (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33,
+ 0x44, 0x55 });
+ assertThat(extra.contains((short) 0xcafe)).isFalse();
+ assertThat(extra.contains((short) 0xbeef)).isTrue();
+
+ extra.add(cafe);
+ // Expect 0xbeef 0x0003 0x33 0x44 0x55, 0xcafe 0x0003 0x00 0x11 0x22 in little endian
+ assertThat(extra.getBytes()).isEqualTo(new byte[] { (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33,
+ 0x44, 0x55, (byte) 0xfe, (byte) 0xca, 0x03, 0x00, 0x00, 0x11, 0x22 });
+ assertThat(extra.contains((short) 0xcafe)).isTrue();
+ assertThat(extra.contains((short) 0xbeef)).isTrue();
+
+ ExtraData beef = extra.get((short) 0xbeef);
+ assertThat(beef.getId()).isEqualTo((short) 0xbeef);
+ }
+
+ @Test public void testByteStream() throws IOException {
+ byte[] buffer = new byte[] { (byte) 0xfe, (byte) 0xca, 0x03, 0x00, 0x00, 0x11, 0x22,
+ (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33, 0x44, 0x55 };
+
+ ExtraDataList extra = new ExtraDataList(buffer);
+ byte[] bytes = new byte[7];
+ InputStream in = extra.getByteStream();
+ in.read(bytes);
+ // Expect 0xcafe 0x0003 0x00 0x11 0x22 in little endian
+ assertThat(bytes).isEqualTo(new byte[] { (byte) 0xfe, (byte) 0xca, 0x03, 0x00, 0x00, 0x11,
+ 0x22 });
+ in.read(bytes);
+ // Expect 0xbeef 0x0003 0x33 0x44 0x55 in little endian
+ assertThat(bytes).isEqualTo(new byte[] { (byte) 0xef, (byte) 0xbe, 0x03, 0x00, 0x33, 0x44,
+ 0x55 });
+ assertThat(in.read(bytes)).isEqualTo(-1);
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataTest.java
new file mode 100644
index 0000000000..1b250f6e03
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ExtraDataTest.java
@@ -0,0 +1,77 @@
+// 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.zip;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ExtraDataTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test public void testFromData() {
+ short id = (short) 0xcafe;
+ byte[] data = new byte[] { (byte) 0xaa, (byte) 0xbb, (byte) 0xcc };
+ // Contains 1 record: 0xcafe 0x0003 0xaa 0xbb 0xcc in little endian
+ byte[] record = new byte[] { (byte) 0xfe, (byte) 0xca, 0x03, 0x00, (byte) 0xaa, (byte) 0xbb,
+ (byte) 0xcc };
+ ExtraData extra = new ExtraData(id, data);
+ assertThat(extra.getId()).isEqualTo(id);
+ assertThat(extra.getLength()).isEqualTo(record.length);
+ assertThat(extra.getData()).isEqualTo(data);
+ assertThat(extra.getDataLength()).isEqualTo(data.length);
+ assertThat(extra.getBytes()).isEqualTo(record);
+ assertThat(extra.getByte(2)).isEqualTo(record[2]);
+ }
+
+ @Test public void testFromArray() {
+ // Contains 1 record: 0xcafe 0x0004 deadbeef in little endian with 4 bytes padding on the front
+ // and 4 bytes padding on the end
+ byte[] buf = new byte[] { 0x00, 0x11, 0x22, 0x33, (byte) 0xfe, (byte) 0xca, 0x04, 0x00,
+ (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef, (byte) 0xcc, (byte) 0xdd, (byte) 0xee,
+ (byte) 0xff };
+ // record id: cafe
+ short id = (short) 0xcafe;
+ // record payload: deadbeef
+ byte[] data = new byte[] { (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef };
+ // complete record: 0xcafe 0x0004 deadbeef
+ byte[] record = new byte[] { (byte) 0xfe, (byte) 0xca, 0x04, 0x00, (byte) 0xde, (byte) 0xad,
+ (byte) 0xbe, (byte) 0xef };
+ ExtraData extra = new ExtraData(buf, 4);
+ assertThat(extra.getId()).isEqualTo(id);
+ assertThat(extra.getLength()).isEqualTo(record.length);
+ assertThat(extra.getData()).isEqualTo(data);
+ assertThat(extra.getDataLength()).isEqualTo(data.length);
+ assertThat(extra.getBytes()).isEqualTo(record);
+ assertThat(extra.getByte(2)).isEqualTo(record[2]);
+ }
+
+ @Test public void testFromArray_shortHeader() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("incomplete extra data entry in buffer");
+ new ExtraData(new byte[] { (byte) 0xfe, (byte) 0xca, 0x01 }, 0);
+ }
+
+ @Test public void testFromArray_shortData() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("incomplete extra data entry in buffer");
+ new ExtraData(new byte[] { (byte) 0xfe, (byte) 0xca, 0x03, 0x00, 0x00 }, 0);
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileDataTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileDataTest.java
new file mode 100644
index 0000000000..4caa47e768
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileDataTest.java
@@ -0,0 +1,121 @@
+// 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.zip;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.testing.NullPointerTester;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.zip.ZipException;
+
+@RunWith(JUnit4.class)
+public class ZipFileDataTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private ZipFileData data;
+
+ @Before public void setup() {
+ data = new ZipFileData(UTF_8);
+ }
+
+ @Test public void testNulls() {
+ NullPointerTester tester = new NullPointerTester();
+ tester.testAllPublicConstructors(ZipFileData.class);
+ tester.testAllPublicInstanceMethods(data);
+ }
+
+ @Test public void testCharset() {
+ assertThat(new ZipFileData(UTF_8).getCharset()).isEqualTo(UTF_8);
+ }
+
+ @Test public void testComment() throws ZipException {
+ data.setComment("foo");
+ assertThat(data.getComment()).isEqualTo("foo");
+ }
+
+ @Test public void testSetComment_TooLong() throws ZipException {
+ String comment = new String(new byte[0x100ff], UTF_8);
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("File comment too long. Is 65791; max 65535.");
+ data.setComment(comment);
+ }
+
+ @Test public void testSetComment_FromBytes() throws ZipException {
+ String comment = "foo";
+ byte[] bytes = comment.getBytes(UTF_8);
+ data.setComment(bytes);
+ assertThat(data.getComment()).isEqualTo(comment);
+ }
+
+ @Test public void testSetComment_FromBytes_TooLong() throws ZipException {
+ byte[] comment = new byte[0x100ff];
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("File comment too long. Is 65791; max 65535.");
+ data.setComment(comment);
+ }
+
+ @Test public void testZip64Setting() {
+ assertThat(data.isZip64()).isFalse();
+ data.setCentralDirectorySize(0xffffffffL);
+ assertThat(data.isZip64()).isFalse();
+ data.setCentralDirectorySize(0x100000000L);
+ assertThat(data.isZip64()).isTrue();
+
+ data.setZip64(false);
+ assertThat(data.isZip64()).isFalse();
+ data.setCentralDirectoryOffset(0xffffffffL);
+ assertThat(data.isZip64()).isFalse();
+ data.setCentralDirectoryOffset(0x100000000L);
+ assertThat(data.isZip64()).isTrue();
+
+ data.setZip64(false);
+ assertThat(data.isZip64()).isFalse();
+ data.setExpectedEntries(0xffff);
+ assertThat(data.isZip64()).isFalse();
+ data.setExpectedEntries(0x10000L);
+ assertThat(data.isZip64()).isTrue();
+
+ data.setZip64(false);
+ assertThat(data.isZip64()).isFalse();
+ data.setZip64EndOfCentralDirectoryOffset(0);
+ assertThat(data.isZip64()).isTrue();
+
+ data.setZip64(false);
+ assertThat(data.isZip64()).isFalse();
+ ZipFileEntry template = new ZipFileEntry("template");
+ for (int i = 0; i < 0xffff; i++) {
+ ZipFileEntry entry = new ZipFileEntry(template);
+ entry.setName("entry" + i);
+ data.addEntry(entry);
+ }
+ assertThat(data.isZip64()).isFalse();
+ data.addEntry(template);
+ assertThat(data.isZip64()).isTrue();
+ }
+
+ @Test public void testSetZip64SetsMaybeZip64() {
+ assertThat(data.isMaybeZip64()).isFalse();
+ data.setZip64(true);
+ assertThat(data.isMaybeZip64()).isTrue();
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileEntryTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileEntryTest.java
new file mode 100644
index 0000000000..d869d047b6
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipFileEntryTest.java
@@ -0,0 +1,212 @@
+// 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.zip;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.NullPointerTester;
+import com.google.devtools.build.zip.ZipFileEntry.Compression;
+import com.google.devtools.build.zip.ZipFileEntry.Feature;
+import com.google.devtools.build.zip.ZipFileEntry.Flag;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ZipFileEntryTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test public void testNulls() {
+ NullPointerTester tester = new NullPointerTester();
+ tester.testAllPublicConstructors(ZipFileEntry.class);
+ tester.testAllPublicInstanceMethods(new ZipFileEntry("foo"));
+ }
+
+ @Test public void testCrc() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setCrc(32);
+ }
+
+ @Test public void testCrc_Negative() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("invalid entry crc-32");
+ foo.setCrc(-1);
+ }
+
+ @Test public void testCrc_Large() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("invalid entry crc-32");
+ foo.setCrc(0x100000000L);
+ }
+
+ @Test public void testSize() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setSize(32);
+ }
+
+ @Test public void testSize_Negative() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("invalid entry size");
+ foo.setSize(-1);
+ }
+
+ @Test public void testSize_Large() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setSize(0x100000000L);
+ assertThat(foo.getVersion()).isEqualTo(Feature.ZIP64_SIZE.getMinVersion());
+ }
+
+ @Test public void testCompressedSize() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setCompressedSize(32);
+ }
+
+ @Test public void testCompressedSize_Negative() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("invalid entry size");
+ foo.setCompressedSize(-1);
+ }
+
+ @Test public void testCompressedSize_Large() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setCompressedSize(0x100000000L);
+ assertThat(foo.getVersion()).isEqualTo(Feature.ZIP64_CSIZE.getMinVersion());
+ }
+
+ @Test public void testMinVersion() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ assertThat(foo.getVersion()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setVersion((short) 0x14);
+ assertThat(foo.getVersion()).isEqualTo((short) 0x14);
+ }
+
+ @Test public void testMinVersion_MethodUpdated() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ assertThat(foo.getVersion()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setMethod(Compression.DEFLATED);
+ assertThat(foo.getVersion()).isEqualTo(Feature.DEFLATED.getMinVersion());
+ }
+
+ @Test public void testMinVersion_Zip64Updated() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ assertThat(foo.getVersion()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setSize(0xffffffffL);
+ assertThat(foo.getVersion()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setSize(0xfffffffffL);
+ assertThat(foo.getVersion()).isEqualTo(Feature.ZIP64_SIZE.getMinVersion());
+ }
+
+ @Test public void testMinVersion_BelowRequired() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setVersion((short) 0);
+ assertThat(foo.getVersion()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setMethod(Compression.DEFLATED);
+ foo.setVersion(Compression.STORED.getMinVersion());
+ assertThat(foo.getVersion()).isEqualTo(Feature.DEFLATED.getMinVersion());
+ }
+
+ @Test public void testMinVersionNeeded() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setVersionNeeded((short) 0x14);
+ assertThat(foo.getVersionNeeded()).isEqualTo((short) 0x14);
+ }
+
+ @Test public void testMinVersionNeeded_MethodUpdated() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setMethod(Compression.DEFLATED);
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.DEFLATED.getMinVersion());
+ }
+
+ @Test public void testMinVersionNeeded_Zip64Updated() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setSize(0xffffffffL);
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setSize(0xfffffffffL);
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.ZIP64_SIZE.getMinVersion());
+ }
+
+ @Test public void testMinVersionNeeded_BelowRequired() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setVersionNeeded((short) 0);
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.DEFAULT.getMinVersion());
+ foo.setMethod(Compression.DEFLATED);
+ foo.setVersionNeeded(Compression.STORED.getMinVersion());
+ assertThat(foo.getVersionNeeded()).isEqualTo(Feature.DEFLATED.getMinVersion());
+ }
+
+ @Test public void testSetFlag() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setFlag(Flag.DATA_DESCRIPTOR, true);
+ assertThat(foo.getFlags()).isEqualTo((short) 0x08);
+ foo.setFlag(Flag.DATA_DESCRIPTOR, true);
+ assertThat(foo.getFlags()).isEqualTo((short) 0x08);
+ foo.setFlag(Flag.DATA_DESCRIPTOR, false);
+ assertThat(foo.getFlags()).isEqualTo((short) 0x00);
+ foo.setFlag(Flag.DATA_DESCRIPTOR, false);
+ assertThat(foo.getFlags()).isEqualTo((short) 0x00);
+ }
+
+ @Test public void testLocalHeaderOffset() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setLocalHeaderOffset(32);
+ }
+
+ @Test public void testLocalHeaderOffset_Negative() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("invalid local header offset");
+ foo.setLocalHeaderOffset(-1);
+ }
+
+ @Test public void testLocalHeaderOffset_Large() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setLocalHeaderOffset(0x100000000L);
+ assertThat(foo.getVersion()).isEqualTo(Feature.ZIP64_OFFSET.getMinVersion());
+ }
+
+ @Test public void testExtra() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ foo.setExtra(new ExtraDataList(new byte[32]));
+ }
+
+ @Test public void testExtra_Large() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("invalid extra field length");
+ foo.setExtra(new ExtraDataList(new byte[0x10000]));
+ }
+
+ @Test public void testExtraData() {
+ ZipFileEntry foo = new ZipFileEntry("foo");
+ ExtraDataList extra = new ExtraDataList();
+ extra.add(new ExtraData((short) 0xCAFE, new byte[] { 0x01, 0x02 }));
+ extra.add(new ExtraData((short) 0xDEAD, new byte[] { (byte) 0xBE, (byte) 0xEF }));
+ foo.setExtra(extra);
+ // Expect 2 records: 0xCAFE 0x0002 0x01 0x02, 0xDEAD 0x0002 0xBE 0xEF in little endian
+ assertThat(foo.getExtra().getBytes()).isEqualTo(new byte[] {
+ (byte) 0xFE, (byte) 0xCA, 0x02, 0x00, 0x01, 0x02,
+ (byte) 0xAD, (byte) 0xDE, 0x02, 0x00, (byte) 0xBE, (byte) 0xEF });
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipReaderTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipReaderTest.java
new file mode 100644
index 0000000000..dc2652ec03
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipReaderTest.java
@@ -0,0 +1,544 @@
+// 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.zip;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.zip.ZipFileEntry.Compression;
+import com.google.devtools.build.zip.ZipFileEntry.Feature;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipOutputStream;
+
+@RunWith(JUnit4.class)
+public class ZipReaderTest {
+ private void assertDateWithin(Date testDate, Date start, Date end) {
+ if (testDate.before(start) || testDate.after(end)) {
+ fail();
+ }
+ }
+
+ private void assertDateAboutNow(Date testDate) {
+ Date now = new Date();
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(now);
+ cal.add(Calendar.MINUTE, -30);
+ Date start = cal.getTime();
+ cal.add(Calendar.HOUR, 1);
+ Date end = cal.getTime();
+ assertDateWithin(testDate, start, end);
+ }
+
+ @Rule public TemporaryFolder tmp = new TemporaryFolder();
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private File test;
+
+ @Before public void setup() throws IOException {
+ test = tmp.newFile("test.zip");
+ }
+
+ @Test public void testMalformed_Empty() throws IOException {
+ try (FileOutputStream out = new FileOutputStream(test)) {
+ }
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("is malformed. It does not contain an end of central directory record.");
+ new ZipReader(test, UTF_8).close();
+ }
+
+ @Test public void testMalformed_ShorterThanSignature() throws IOException {
+ try (FileOutputStream out = new FileOutputStream(test)) {
+ out.write(new byte[] { 1, 2, 3 });
+ }
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("is malformed. It does not contain an end of central directory record.");
+ new ZipReader(test, UTF_8).close();
+ }
+
+ @Test public void testMalformed_SignatureLength() throws IOException {
+ try (FileOutputStream out = new FileOutputStream(test)) {
+ out.write(new byte[] { 1, 2, 3, 4 });
+ }
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("is malformed. It does not contain an end of central directory record.");
+ new ZipReader(test, UTF_8).close();
+ }
+
+ @Test public void testEmpty() throws IOException {
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.entries()).isEmpty();
+ }
+ }
+
+ @Test public void testFileComment() throws IOException {
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ zout.setComment("test comment");
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.entries()).isEmpty();
+ assertThat(reader.getComment()).isEqualTo("test comment");
+ }
+ }
+
+ @Test public void testFileCommentWithSignature() throws IOException {
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ zout.setComment("test comment\u0050\u004b\u0005\u0006abcdefghijklmnopqrstuvwxyz");
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.entries()).isEmpty();
+ assertThat(reader.getComment())
+ .isEqualTo("test comment\u0050\u004b\u0005\u0006abcdefghijklmnopqrstuvwxyz");
+ }
+ }
+
+ @Test public void testSingleEntry() throws IOException {
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ zout.putNextEntry(new ZipEntry("test"));
+ zout.write("foo".getBytes(UTF_8));
+ zout.closeEntry();
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.entries()).hasSize(1);
+ }
+ }
+
+ @Test public void testMultipleEntries() throws IOException {
+ String[] names = new String[] { "test", "foo", "bar", "baz" };
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ for (String name : names) {
+ zout.putNextEntry(new ZipEntry(name));
+ zout.write(name.getBytes(UTF_8));
+ zout.closeEntry();
+ }
+ }
+
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.entries()).hasSize(names.length);
+ int i = 0;
+ for (ZipFileEntry entry : reader.entries()) {
+ assertThat(entry.getName()).isEqualTo(names[i++]);
+ }
+ assertThat(i).isEqualTo(names.length);
+ }
+ }
+
+ @Test public void testZipEntryFields() throws IOException {
+ CRC32 crc = new CRC32();
+ Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
+ long date = 791784306000L; // 2/3/1995 04:05:06
+ byte[] extra = new ExtraData((short) 0xaa, new byte[] { (byte) 0xbb, (byte) 0xcd }).getBytes();
+ byte[] tmp = new byte[128];
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+
+ ZipEntry foo = new ZipEntry("foo");
+ foo.setComment("foo comment.");
+ foo.setMethod(ZipEntry.DEFLATED);
+ foo.setTime(date);
+ foo.setExtra(extra);
+ zout.putNextEntry(foo);
+ zout.write("foo".getBytes(UTF_8));
+ zout.closeEntry();
+
+ ZipEntry bar = new ZipEntry("bar");
+ bar.setComment("bar comment.");
+ bar.setMethod(ZipEntry.STORED);
+ bar.setSize("bar".length());
+ bar.setCompressedSize("bar".length());
+ crc.reset();
+ crc.update("bar".getBytes(UTF_8));
+ bar.setCrc(crc.getValue());
+ zout.putNextEntry(bar);
+ zout.write("bar".getBytes(UTF_8));
+ zout.closeEntry();
+ }
+
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ ZipFileEntry fooEntry = reader.getEntry("foo");
+ assertThat(fooEntry.getName()).isEqualTo("foo");
+ assertThat(fooEntry.getComment()).isEqualTo("foo comment.");
+ assertThat(fooEntry.getMethod()).isEqualTo(Compression.DEFLATED);
+ assertThat(fooEntry.getVersion()).isEqualTo(Compression.DEFLATED.getMinVersion());
+ assertThat(fooEntry.getTime()).isEqualTo(date);
+ assertThat(fooEntry.getSize()).isEqualTo("foo".length());
+ deflater.reset();
+ deflater.setInput("foo".getBytes(UTF_8));
+ deflater.finish();
+ assertThat(fooEntry.getCompressedSize()).isEqualTo(deflater.deflate(tmp));
+ crc.reset();
+ crc.update("foo".getBytes(UTF_8));
+ assertThat(fooEntry.getCrc()).isEqualTo(crc.getValue());
+ assertThat(fooEntry.getExtra().getBytes()).isEqualTo(extra);
+
+ ZipFileEntry barEntry = reader.getEntry("bar");
+ assertThat(barEntry.getName()).isEqualTo("bar");
+ assertThat(barEntry.getComment()).isEqualTo("bar comment.");
+ assertThat(barEntry.getMethod()).isEqualTo(Compression.STORED);
+ assertThat(barEntry.getVersion()).isEqualTo(Compression.STORED.getMinVersion());
+ assertDateAboutNow(new Date(barEntry.getTime()));
+ assertThat(barEntry.getSize()).isEqualTo("bar".length());
+ assertThat(barEntry.getCompressedSize()).isEqualTo("bar".length());
+ crc.reset();
+ crc.update("bar".getBytes(UTF_8));
+ assertThat(barEntry.getCrc()).isEqualTo(crc.getValue());
+ assertThat(barEntry.getExtra().getBytes()).isEqualTo(new byte[] {});
+ }
+ }
+
+ @Test public void testZipEntryInvalidTime() throws IOException {
+ long date = 312796800000L; // 11/30/1979 00:00:00, which is also 0 in DOS format
+ byte[] extra = new ExtraData((short) 0xaa, new byte[] { (byte) 0xbb, (byte) 0xcd }).getBytes();
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ ZipEntry foo = new ZipEntry("foo");
+ foo.setComment("foo comment.");
+ foo.setMethod(ZipEntry.DEFLATED);
+ foo.setTime(date);
+ foo.setExtra(extra);
+ zout.putNextEntry(foo);
+ zout.write("foo".getBytes(UTF_8));
+ zout.closeEntry();
+ }
+
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ ZipFileEntry fooEntry = reader.getEntry("foo");
+ assertThat(fooEntry.getTime()).isEqualTo(ZipUtil.DOS_EPOCH);
+ }
+ }
+
+ @Test public void testRawFileData() throws IOException {
+ CRC32 crc = new CRC32();
+ Deflater deflator = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ ZipEntry foo = new ZipEntry("foo");
+ foo.setComment("foo comment.");
+ foo.setMethod(ZipEntry.DEFLATED);
+ zout.putNextEntry(foo);
+ zout.write("foo".getBytes(UTF_8));
+ zout.closeEntry();
+
+ ZipEntry bar = new ZipEntry("bar");
+ bar.setComment("bar comment.");
+ bar.setMethod(ZipEntry.STORED);
+ bar.setSize("bar".length());
+ bar.setCompressedSize("bar".length());
+ crc.reset();
+ crc.update("bar".getBytes(UTF_8));
+ bar.setCrc(crc.getValue());
+ zout.putNextEntry(bar);
+ zout.write("bar".getBytes(UTF_8));
+ zout.closeEntry();
+ }
+
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ ZipFileEntry fooEntry = reader.getEntry("foo");
+ InputStream fooIn = reader.getRawInputStream(fooEntry);
+ byte[] fooData = new byte[10];
+ fooIn.read(fooData);
+ byte[] expectedFooData = new byte[10];
+ deflator.reset();
+ deflator.setInput("foo".getBytes(UTF_8));
+ deflator.finish();
+ deflator.deflate(expectedFooData);
+ assertThat(fooData).isEqualTo(expectedFooData);
+
+ ZipFileEntry barEntry = reader.getEntry("bar");
+ InputStream barIn = reader.getRawInputStream(barEntry);
+ byte[] barData = new byte[3];
+ barIn.read(barData);
+ byte[] expectedBarData = "bar".getBytes(UTF_8);
+ assertThat(barData).isEqualTo(expectedBarData);
+
+ assertThat(barIn.read()).isEqualTo(-1);
+ assertThat(barIn.read(barData)).isEqualTo(-1);
+ assertThat(barIn.read(barData, 0, 3)).isEqualTo(-1);
+
+ thrown.expect(IOException.class);
+ thrown.expectMessage("Reset is not supported on this type of stream.");
+ barIn.reset();
+ }
+ }
+
+ @Test public void testFileData() throws IOException {
+ CRC32 crc = new CRC32();
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ ZipEntry foo = new ZipEntry("foo");
+ foo.setComment("foo comment.");
+ foo.setMethod(ZipEntry.DEFLATED);
+ zout.putNextEntry(foo);
+ zout.write("foo".getBytes(UTF_8));
+ zout.closeEntry();
+
+ ZipEntry bar = new ZipEntry("bar");
+ bar.setComment("bar comment.");
+ bar.setMethod(ZipEntry.STORED);
+ bar.setSize("bar".length());
+ bar.setCompressedSize("bar".length());
+ crc.reset();
+ crc.update("bar".getBytes(UTF_8));
+ bar.setCrc(crc.getValue());
+ zout.putNextEntry(bar);
+ zout.write("bar".getBytes(UTF_8));
+ zout.closeEntry();
+ }
+
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ ZipFileEntry fooEntry = reader.getEntry("foo");
+ InputStream fooIn = reader.getInputStream(fooEntry);
+ byte[] fooData = new byte[3];
+ fooIn.read(fooData);
+ byte[] expectedFooData = "foo".getBytes(UTF_8);
+ assertThat(fooData).isEqualTo(expectedFooData);
+
+ assertThat(fooIn.read()).isEqualTo(-1);
+ assertThat(fooIn.read(fooData)).isEqualTo(-1);
+ assertThat(fooIn.read(fooData, 0, 3)).isEqualTo(-1);
+
+ ZipFileEntry barEntry = reader.getEntry("bar");
+ InputStream barIn = reader.getInputStream(barEntry);
+ byte[] barData = new byte[3];
+ barIn.read(barData);
+ byte[] expectedBarData = "bar".getBytes(UTF_8);
+ assertThat(barData).isEqualTo(expectedBarData);
+
+ assertThat(barIn.read()).isEqualTo(-1);
+ assertThat(barIn.read(barData)).isEqualTo(-1);
+ assertThat(barIn.read(barData, 0, 3)).isEqualTo(-1);
+
+ thrown.expect(IOException.class);
+ thrown.expectMessage("Reset is not supported on this type of stream.");
+ barIn.reset();
+ }
+ }
+
+ @Test public void testSimultaneousReads() throws IOException {
+ byte[] expectedFooData = "This if file foo. It contains a foo.".getBytes(UTF_8);
+ byte[] expectedBarData = "This is a different file bar. It contains only a bar."
+ .getBytes(UTF_8);
+ try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
+ ZipEntry foo = new ZipEntry("foo");
+ foo.setComment("foo comment.");
+ foo.setMethod(ZipEntry.DEFLATED);
+ zout.putNextEntry(foo);
+ zout.write(expectedFooData);
+ zout.closeEntry();
+
+ ZipEntry bar = new ZipEntry("bar");
+ bar.setComment("bar comment.");
+ bar.setMethod(ZipEntry.DEFLATED);
+ zout.putNextEntry(bar);
+ zout.write(expectedBarData);
+ zout.closeEntry();
+ }
+
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ ZipFileEntry fooEntry = reader.getEntry("foo");
+ ZipFileEntry barEntry = reader.getEntry("bar");
+ InputStream fooIn = reader.getInputStream(fooEntry);
+ InputStream barIn = reader.getInputStream(barEntry);
+ byte[] fooData = new byte[expectedFooData.length];
+ byte[] barData = new byte[expectedBarData.length];
+ fooIn.read(fooData, 0, 10);
+ barIn.read(barData, 0, 10);
+ fooIn.read(fooData, 10, 10);
+ barIn.read(barData, 10, 10);
+ fooIn.read(fooData, 20, fooData.length - 20);
+ barIn.read(barData, 20, barData.length - 20);
+ assertThat(fooData).isEqualTo(expectedFooData);
+ assertThat(barData).isEqualTo(expectedBarData);
+ }
+ }
+
+ @Test public void testZip64() throws IOException {
+ // Generated with: 'echo "foo" > entry; zip -fz -q out.zip entry'
+ byte[] data = new byte[] {
+ 0x50, 0x4b, 0x03, 0x04, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, (byte) 0x86, (byte) 0xa6,
+ 0x46, (byte) 0xa8, 0x65, 0x32, 0x7e, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x05, 0x00, 0x30, 0x00, 0x65, 0x6e,
+ 0x74, 0x72, 0x79, 0x55, 0x54, 0x09, 0x00, 0x03, (byte) 0xb2, 0x7e, 0x4a, 0x55, (byte) 0xb2,
+ 0x7e, 0x4a, 0x55, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0x46, 0x3a, 0x04, 0x00, 0x04,
+ (byte) 0x88, 0x13, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 0x0a, 0x50,
+ 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, (byte) 0x86,
+ (byte) 0xa6, 0x46, (byte) 0xa8, 0x65, 0x32, 0x7e, 0x04, 0x00, 0x00, 0x00, (byte) 0xff,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, (byte) 0xa0, (byte) 0x81, 0x00, 0x00, 0x00, 0x00, 0x65, 0x6e, 0x74, 0x72,
+ 0x79, 0x55, 0x54, 0x05, 0x00, 0x03, (byte) 0xb2, 0x7e, 0x4a, 0x55, 0x75, 0x78, 0x0b, 0x00,
+ 0x01, 0x04, 0x46, 0x3a, 0x04, 0x00, 0x04, (byte) 0x88, 0x13, 0x00, 0x00, 0x01, 0x00, 0x08,
+ 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x06, 0x06, 0x2c, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x06, 0x07, 0x00, 0x00, 0x00, 0x00, (byte) 0xae,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x05, 0x06,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x57, 0x00, 0x00, 0x00, (byte) 0xff,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00
+ };
+
+ try (FileOutputStream out = new FileOutputStream(test)) {
+ out.write(data);
+ }
+ String foo = "foo\n";
+ byte[] expectedFooData = foo.getBytes(UTF_8);
+ ExtraDataList extras = new ExtraDataList();
+ extras.add(new ExtraData((short) 0x0001, ZipUtil.longToLittleEndian(expectedFooData.length)));
+ byte[] extra = extras.getBytes();
+ CRC32 crc = new CRC32();
+ crc.reset();
+ crc.update(expectedFooData);
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ ZipFileEntry fooEntry = reader.getEntry("entry");
+ InputStream fooIn = reader.getInputStream(fooEntry);
+ byte[] fooData = new byte[expectedFooData.length];
+ fooIn.read(fooData);
+ assertThat(fooData).isEqualTo(expectedFooData);
+ assertThat(fooEntry.getName()).isEqualTo("entry");
+ assertThat(fooEntry.getComment()).isEqualTo("");
+ assertThat(fooEntry.getMethod()).isEqualTo(Compression.STORED);
+ assertThat(fooEntry.getVersionNeeded()).isEqualTo(Feature.ZIP64_SIZE.getMinVersion());
+ assertThat(fooEntry.getSize()).isEqualTo(expectedFooData.length);
+ assertThat(fooEntry.getCompressedSize()).isEqualTo(expectedFooData.length);
+ assertThat(fooEntry.getCrc()).isEqualTo(crc.getValue());
+ assertThat(fooEntry.getExtra().get((short) 0x0001).getBytes()).isEqualTo(extra);
+ }
+ }
+
+ @Test public void testZip64_Potential() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry template = new ZipFileEntry("template");
+ template.setSize(0);
+ template.setCompressedSize(0);
+ template.setCrc(0);
+ template.setTime(ZipUtil.DOS_EPOCH);
+ for (int i = 0; i < 0xffff; i++) {
+ ZipFileEntry entry = new ZipFileEntry(template);
+ entry.setName("entry" + i);
+ writer.putNextEntry(entry);
+ }
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8, true)) {
+ Collection<ZipFileEntry> entries = reader.entries();
+ assertThat(entries).hasSize(0xffff);
+ }
+ }
+
+ @Test public void testZip64_NumFiles() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry template = new ZipFileEntry("template");
+ template.setSize(0);
+ template.setCompressedSize(0);
+ template.setCrc(0);
+ template.setTime(ZipUtil.DOS_EPOCH);
+ for (int i = 0; i < 0x100ff; i++) {
+ ZipFileEntry entry = new ZipFileEntry(template);
+ entry.setName("entry" + i);
+ writer.putNextEntry(entry);
+ }
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8, true)) {
+ Collection<ZipFileEntry> entries = reader.entries();
+ assertThat(entries).hasSize(0x100ff);
+ }
+ }
+
+ @Test public void testZip64_Max32BitSizeFile() throws IOException {
+ File bigFile = tmp.newFile("big");
+ try (RandomAccessFile bigOut = new RandomAccessFile(bigFile, "rw")) {
+ bigOut.setLength(0xffffffffL);
+ }
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry bigEntry = new ZipFileEntry(bigFile.getName());
+ bigEntry.setSize(0xffffffffL);
+ bigEntry.setCompressedSize(0xffffffffL);
+ bigEntry.setCrc(0);
+ bigEntry.setTime(ZipUtil.DOS_EPOCH);
+ writer.putNextEntry(bigEntry);
+ ByteStreams.copy(new BufferedInputStream(new FileInputStream(bigFile)), writer);
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ Collection<ZipFileEntry> entries = reader.entries();
+ assertThat(entries).hasSize(1);
+ ZipFileEntry bigEntry = reader.getEntry(bigFile.getName());
+ assertThat(bigEntry.getSize()).isEqualTo(0xffffffffL);
+ }
+ }
+
+ @Test public void testZip64_Zip64SizeFile() throws IOException {
+ File biggerFile = tmp.newFile("big");
+ try (RandomAccessFile biggerOut = new RandomAccessFile(biggerFile, "rw")) {
+ biggerOut.setLength(0x1000000ffL);
+ }
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry bigEntry = new ZipFileEntry(biggerFile.getName());
+ bigEntry.setSize(0x1000000ffL);
+ bigEntry.setCompressedSize(0x1000000ffL);
+ bigEntry.setCrc(0);
+ bigEntry.setTime(ZipUtil.DOS_EPOCH);
+ writer.putNextEntry(bigEntry);
+ ByteStreams.copy(new BufferedInputStream(new FileInputStream(biggerFile)), writer);
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ Collection<ZipFileEntry> entries = reader.entries();
+ assertThat(entries).hasSize(1);
+ ZipFileEntry bigEntry = reader.getEntry(biggerFile.getName());
+ assertThat(bigEntry.getSize()).isEqualTo(0x1000000ffL);
+ }
+ }
+
+ @Test public void testZip64_FileCount_Zip64Range_ForceZip32() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, false)) {
+ ZipFileEntry template = new ZipFileEntry("template");
+ template.setSize(0);
+ template.setCompressedSize(0);
+ template.setCrc(0);
+ template.setTime(ZipUtil.DOS_EPOCH);
+ for (int i = 0; i < 0x100ff; i++) {
+ ZipFileEntry entry = new ZipFileEntry(template);
+ entry.setName("entry" + i);
+ writer.putNextEntry(entry);
+ }
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.size()).isEqualTo(0x100ff);
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8, true)) {
+ assertThat(reader.size()).isEqualTo(0x00ff);
+ }
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipTests.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipTests.java
new file mode 100644
index 0000000000..2b05b5c2a5
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipTests.java
@@ -0,0 +1,27 @@
+// 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.zip;
+
+
+import com.google.devtools.build.lib.testutil.ClasspathSuite;
+
+import org.junit.runner.RunWith;
+
+/**
+ * A test-suite builder for this package.
+ */
+@RunWith(ClasspathSuite.class)
+public class ZipTests {
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipUtilTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipUtilTest.java
new file mode 100644
index 0000000000..ed10ac234c
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipUtilTest.java
@@ -0,0 +1,180 @@
+// 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.zip;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+@RunWith(JUnit4.class)
+public class ZipUtilTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test public void testShortToLittleEndian() {
+ byte[] bytes = ZipUtil.shortToLittleEndian((short) 4660);
+ assertThat(bytes).isEqualTo(new byte[]{ 0x34, 0x12 });
+ }
+
+ @Test public void testShortToLittleEndian_Signed() {
+ byte[] bytes = ZipUtil.shortToLittleEndian((short) -3532);
+ assertThat(bytes).isEqualTo(new byte[]{ 0x34, (byte) 0xf2 });
+ }
+
+ @Test public void testIntToLittleEndian() {
+ byte[] bytes = ZipUtil.intToLittleEndian(305419896);
+ assertThat(bytes).isEqualTo(new byte[]{ 0x78, 0x56, 0x34, 0x12 });
+ }
+
+ @Test public void testIntToLittleEndian_Signed() {
+ byte[] bytes = ZipUtil.intToLittleEndian(-231451016);
+ assertThat(bytes).isEqualTo(new byte[]{ 0x78, 0x56, 0x34, (byte) 0xf2 });
+ }
+
+ @Test public void testLongToLittleEndian() {
+ byte[] bytes = ZipUtil.longToLittleEndian(305419896);
+ assertThat(bytes).isEqualTo(new byte[]{ 0x78, 0x56, 0x34, 0x12, 0x0, 0x0, 0x0, 0x0 });
+ }
+
+ @Test public void testLongToLittleEndian_Signed() {
+ byte[] bytes = ZipUtil.longToLittleEndian(-231451016);
+ assertThat(bytes).isEqualTo(new byte[]{ 0x78, 0x56, 0x34, (byte) 0xf2,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff });
+ }
+
+ @Test public void testGet16() {
+ short result = ZipUtil.get16(new byte[]{ 0x34, 0x12 }, 0);
+ assertThat(result).isEqualTo((short) 0x1234);
+ assertThat(result).isEqualTo((short) 4660);
+ }
+
+ @Test public void testGet16_Signed() {
+ short result = ZipUtil.get16(new byte[]{ 0x34, (byte) 0xff }, 0);
+ assertThat(result).isEqualTo((short) 0xff34);
+ assertThat(result).isEqualTo((short) -204);
+ }
+
+ @Test public void testGet32() {
+ int result = ZipUtil.get32(new byte[]{ 0x78, 0x56, 0x34, 0x12 }, 0);
+ assertThat(result).isEqualTo(0x12345678);
+ assertThat(result).isEqualTo(305419896);
+ }
+
+ @Test public void testGet32_Short() {
+ int result = ZipUtil.get32(new byte[]{ 0x34, (byte) 0xff, 0x0, 0x0 }, 0);
+ assertThat(result).isEqualTo(0xff34);
+ assertThat(result).isEqualTo(65332);
+ }
+
+ @Test public void testGet32_Signed() {
+ int result = ZipUtil.get32(new byte[]{ 0x34, (byte) 0xff, (byte) 0xff, (byte) 0xff }, 0);
+ assertThat(result).isEqualTo(0xffffff34);
+ assertThat(result).isEqualTo(-204);
+ }
+
+ @Test public void testGetUnsignedShort() {
+ int result = ZipUtil.getUnsignedShort(new byte[]{ 0x34, 0x12 }, 0);
+ assertThat(result).isEqualTo(0x1234);
+ assertThat(result).isEqualTo(4660);
+ }
+
+ @Test public void testGetUnsignedShort_Big() {
+ int result = ZipUtil.getUnsignedShort(new byte[]{ 0x34, (byte) 0xff }, 0);
+ assertThat(result).isEqualTo(0xff34);
+ assertThat(result).isEqualTo(65332);
+ }
+
+ @Test public void testGetUnsignedInt() {
+ long result = ZipUtil.getUnsignedInt(new byte[]{ 0x34, 0x12, 0x0, 0x0 }, 0);
+ assertThat(result).isEqualTo(0x1234);
+ assertThat(result).isEqualTo(4660);
+ }
+
+ @Test public void testGetUnsignedShort_FFFF() {
+ int result = ZipUtil.getUnsignedShort(new byte[]{ (byte) 0xff, (byte) 0xff }, 0);
+ assertThat((short) result).isEqualTo((short) -1);
+ if ((short) result == -1) {
+ System.out.println("-1");
+ }
+ }
+
+ @Test public void testGetUnsignedInt_Big() {
+ long result = ZipUtil.getUnsignedInt(
+ new byte[]{ 0x34, (byte) 0xff, (byte) 0xff, (byte) 0xff }, 0);
+ assertThat(result).isEqualTo(0xffffff34L);
+ assertThat(result).isEqualTo(4294967092L);
+ }
+
+ @Test public void testTimeConversion_DosToUnix() {
+ int dos = (20 << 25) | (2 << 21) | (14 << 16) | (3 << 11) | (7 << 5) | (15 >> 1);
+
+ Calendar time = new GregorianCalendar(2000, Calendar.FEBRUARY, 14, 3, 7, 14);
+ long expectedUnixTime = time.getTimeInMillis();
+ assertThat(ZipUtil.dosToUnixTime(dos)).isEqualTo(expectedUnixTime);
+ }
+
+ @Test public void testTimeConversion_UnixToDos() {
+ Calendar time = new GregorianCalendar(2000, Calendar.FEBRUARY, 14, 3, 7, 14);
+ long unix = time.getTimeInMillis();
+ int expectedDosTime = (20 << 25) | (2 << 21) | (14 << 16) | (3 << 11) | (7 << 5) | (15 >> 1);
+ assertThat(ZipUtil.unixToDosTime(unix)).isEqualTo(expectedDosTime);
+ }
+
+ @Test public void testTimeConversion_UnixToDos_LowBound() {
+ Calendar time = Calendar.getInstance();
+ time.setTimeInMillis(ZipUtil.DOS_EPOCH);
+ time.add(Calendar.SECOND, -1);
+ thrown.expect(IllegalArgumentException.class);
+ ZipUtil.unixToDosTime(time.getTimeInMillis());
+ }
+
+ @Test public void testTimeConversion_UnixToDos_HighBound_Rounding() {
+ Calendar time = Calendar.getInstance();
+ time.setTimeInMillis(ZipUtil.MAX_DOS_DATE);
+ ZipUtil.unixToDosTime(time.getTimeInMillis());
+ }
+
+ @Test public void testTimeConversion_UnixToDos_HighBound() {
+ Calendar time = Calendar.getInstance();
+ time.setTimeInMillis(ZipUtil.MAX_DOS_DATE);
+ time.add(Calendar.SECOND, 1);
+ thrown.expect(IllegalArgumentException.class);
+ ZipUtil.unixToDosTime(time.getTimeInMillis());
+ }
+
+ @Test public void testTimeConversion_UnixToUnix() {
+ Calendar from = new GregorianCalendar(2000, Calendar.FEBRUARY, 14, 3, 7, 15);
+ Calendar to = new GregorianCalendar(2000, Calendar.FEBRUARY, 14, 3, 7, 14);
+ assertThat(ZipUtil.dosToUnixTime(ZipUtil.unixToDosTime(from.getTimeInMillis())))
+ .isEqualTo(to.getTimeInMillis());
+ }
+
+ @Test public void testTimeConversion_DosToDos() {
+ int dos = (20 << 25) | (2 << 21) | (14 << 16) | (3 << 11) | (7 << 5) | (15 >> 1);
+ assertThat(ZipUtil.unixToDosTime(ZipUtil.dosToUnixTime(dos))).isEqualTo(dos);
+ }
+
+ @Test public void testTimeConversion_DosToDos_Zero() {
+ int dos = 0;
+ thrown.expect(IllegalArgumentException.class);
+ assertThat(ZipUtil.unixToDosTime(ZipUtil.dosToUnixTime(dos))).isEqualTo(0);
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipWriterTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipWriterTest.java
new file mode 100644
index 0000000000..71a79725df
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/zip/ZipWriterTest.java
@@ -0,0 +1,493 @@
+// 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.zip;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.common.primitives.Bytes;
+import com.google.devtools.build.zip.ZipFileEntry.Compression;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Random;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+@RunWith(JUnit4.class)
+public class ZipWriterTest {
+ @Rule public TemporaryFolder tmp = new TemporaryFolder();
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private Random rand;
+ private Calendar cal;
+ private CRC32 crc;
+ private Deflater deflater;
+ private File test;
+
+ @Before public void setup() throws IOException {
+ rand = new Random();
+ cal = Calendar.getInstance();
+ cal.clear();
+ cal.set(Calendar.YEAR, rand.nextInt(128) + 1980); // Zip files have 7-bit year resolution.
+ cal.set(Calendar.MONTH, rand.nextInt(12));
+ cal.set(Calendar.DAY_OF_MONTH, rand.nextInt(29));
+ cal.set(Calendar.HOUR_OF_DAY, rand.nextInt(24));
+ cal.set(Calendar.MINUTE, rand.nextInt(60));
+ cal.set(Calendar.SECOND, rand.nextInt(30) * 2); // Zip files have 2 second resolution.
+
+ crc = new CRC32();
+ deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
+ test = tmp.newFile("test.zip");
+ }
+
+ @Test public void testEmpty() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ }
+
+ try (ZipFile zipFile = new ZipFile(test)) {
+ assertThat(zipFile.entries().hasMoreElements()).isFalse();
+ }
+ }
+
+ @Test public void testComment() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ writer.setComment("test comment");
+ }
+
+ try (ZipFile zipFile = new ZipFile(test)) {
+ assertThat(zipFile.entries().hasMoreElements()).isFalse();
+ assertThat(zipFile.getComment()).isEqualTo("test comment");
+ }
+ }
+
+ @Test public void testFileDataBeforeEntry() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ writer.write(new byte[] { 0xf, 0xa, 0xb });
+ fail("Expected ZipException");
+ } catch (ZipException e) {
+ assertThat(e.getMessage()).contains("Cannot write zip contents without first setting a"
+ + " ZipEntry or starting a prefix file.");
+ }
+
+ try (ZipFile zipFile = new ZipFile(test)) {
+ assertThat(zipFile.entries().hasMoreElements()).isFalse();
+ }
+ }
+
+ @Test public void testSingleEntry() throws IOException {
+ ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8);
+ byte[] content = "content".getBytes(UTF_8);
+ crc.update(content);
+ ZipFileEntry entry = new ZipFileEntry("foo");
+ entry.setSize(content.length);
+ entry.setCompressedSize(content.length);
+ entry.setCrc(crc.getValue());
+ entry.setTime(cal.getTimeInMillis());
+
+ writer.putNextEntry(entry);
+ writer.write(content);
+ writer.closeEntry();
+ writer.close();
+
+ byte[] buf = new byte[128];
+ try (ZipFile zipFile = new ZipFile(test)) {
+ ZipEntry foo = zipFile.getEntry("foo");
+ assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED);
+ assertThat(foo.getSize()).isEqualTo(content.length);
+ assertThat(foo.getCompressedSize()).isEqualTo(content.length);
+ assertThat(foo.getCrc()).isEqualTo(crc.getValue());
+ assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis());
+ zipFile.getInputStream(foo).read(buf);
+ assertThat(Bytes.indexOf(buf, content)).isEqualTo(0);
+ }
+ }
+
+ @Test public void testMultipleEntry() throws IOException {
+ ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8);
+ writer.setComment("file comment");
+
+ byte[] fooContent = "content".getBytes(UTF_8);
+ crc.update(fooContent);
+ long fooCrc = crc.getValue();
+ ZipFileEntry rawFoo = new ZipFileEntry("foo");
+ rawFoo.setMethod(Compression.STORED);
+ rawFoo.setSize(fooContent.length);
+ rawFoo.setCompressedSize(fooContent.length);
+ rawFoo.setCrc(crc.getValue());
+ rawFoo.setTime(cal.getTimeInMillis());
+ rawFoo.setComment("foo comment");
+
+ writer.putNextEntry(rawFoo);
+ writer.write(fooContent);
+ writer.closeEntry();
+
+ byte[] barContent = "stuff".getBytes(UTF_8);
+ byte[] deflatedBarContent = new byte[128];
+ crc.reset();
+ crc.update(barContent);
+ long barCrc = crc.getValue();
+ deflater.setInput(barContent);
+ deflater.finish();
+ int deflatedSize = deflater.deflate(deflatedBarContent);
+ ZipFileEntry rawBar = new ZipFileEntry("bar");
+ rawBar.setMethod(Compression.DEFLATED);
+ rawBar.setSize(barContent.length);
+ rawBar.setCompressedSize(deflatedSize);
+ rawBar.setCrc(barCrc);
+ rawBar.setTime(cal.getTimeInMillis());
+
+ writer.putNextEntry(rawBar);
+ writer.write(deflatedBarContent, 0, deflatedSize);
+ writer.closeEntry();
+
+ writer.close();
+
+ byte[] buf = new byte[128];
+ try (ZipFile zipFile = new ZipFile(test)) {
+ assertThat(zipFile.getComment()).isEqualTo("file comment");
+
+ ZipEntry foo = zipFile.getEntry("foo");
+ assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED);
+ assertThat(foo.getSize()).isEqualTo(fooContent.length);
+ assertThat(foo.getCompressedSize()).isEqualTo(fooContent.length);
+ assertThat(foo.getCrc()).isEqualTo(fooCrc);
+ assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis());
+ assertThat(foo.getComment()).isEqualTo("foo comment");
+ zipFile.getInputStream(foo).read(buf);
+ assertThat(Bytes.indexOf(buf, fooContent)).isEqualTo(0);
+
+ ZipEntry bar = zipFile.getEntry("bar");
+ assertThat(bar.getMethod()).isEqualTo(ZipEntry.DEFLATED);
+ assertThat(bar.getSize()).isEqualTo(barContent.length);
+ assertThat(bar.getCompressedSize()).isEqualTo(deflatedSize);
+ assertThat(bar.getCrc()).isEqualTo(barCrc);
+ assertThat(bar.getTime()).isEqualTo(cal.getTimeInMillis());
+ zipFile.getInputStream(bar).read(buf);
+ assertThat(Bytes.indexOf(buf, barContent)).isEqualTo(0);
+ }
+ }
+
+ @Test public void testWrongSizeContent() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ byte[] content = "content".getBytes(UTF_8);
+ crc.update(content);
+ ZipFileEntry entry = new ZipFileEntry("foo");
+ entry.setSize(content.length);
+ entry.setCompressedSize(content.length);
+ entry.setCrc(crc.getValue());
+ entry.setTime(cal.getTimeInMillis());
+
+ writer.putNextEntry(entry);
+ writer.write("some other content".getBytes(UTF_8));
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("Number of bytes written for the entry");
+ writer.closeEntry();
+ }
+ }
+
+ @Test public void testRawZipEntry() throws IOException {
+ ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8);
+ byte[] content = "content".getBytes(UTF_8);
+ crc.update(content);
+ ZipFileEntry entry = new ZipFileEntry("foo");
+ entry.setVersion((short) 1);
+ entry.setVersionNeeded((short) 2);
+ entry.setSize(content.length);
+ entry.setCompressedSize(content.length);
+ entry.setCrc(crc.getValue());
+ entry.setTime(cal.getTimeInMillis());
+ entry.setFlags(ZipUtil.get16(new byte[]{ 0x08, 0x00 }, 0));
+ entry.setInternalAttributes(ZipUtil.get16(new byte[]{ 0x34, 0x12 }, 0));
+ entry.setExternalAttributes(ZipUtil.get32(new byte[]{ 0x0a, 0x09, 0x78, 0x56 }, 0));
+ entry.setLocalHeaderOffset(rand.nextInt(Integer.MAX_VALUE));
+
+ writer.putNextEntry(entry);
+ writer.write(content);
+ writer.closeEntry();
+ writer.close();
+
+ byte[] buf = new byte[128];
+ try (ZipFile zipFile = new ZipFile(test)) {
+ ZipEntry foo = zipFile.getEntry("foo");
+ assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED);
+ assertThat(foo.getSize()).isEqualTo(content.length);
+ assertThat(foo.getCompressedSize()).isEqualTo(content.length);
+ assertThat(foo.getCrc()).isEqualTo(crc.getValue());
+ assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis());
+ zipFile.getInputStream(foo).read(buf);
+ assertThat(Bytes.indexOf(buf, content)).isEqualTo(0);
+ }
+
+ try (ZipReader zipFile = new ZipReader(test)) {
+ ZipFileEntry foo = zipFile.getEntry("foo");
+ // Versions should be increased to minimum required for STORED compression.
+ assertThat(foo.getVersion()).isEqualTo((short) 0xa);
+ assertThat(foo.getVersionNeeded()).isEqualTo((short) 0xa);
+ assertThat(foo.getFlags()).isEqualTo((short) 0); // Data descriptor bit should be cleared.
+ assertThat(foo.getInternalAttributes()).isEqualTo((short) 4660);
+ assertThat(foo.getExternalAttributes()).isEqualTo(1450707210);
+ }
+ }
+
+ @Test public void testPrefixFile() throws IOException, InterruptedException {
+ ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8);
+
+ writer.startPrefixFile();
+ writer.write("#!/bin/bash\necho 'hello world'\n".getBytes(UTF_8));
+ writer.endPrefixFile();
+
+ byte[] content = "content".getBytes(UTF_8);
+ crc.update(content);
+ ZipFileEntry entry = new ZipFileEntry("foo");
+ entry.setSize(content.length);
+ entry.setCompressedSize(content.length);
+ entry.setCrc(crc.getValue());
+ entry.setTime(cal.getTimeInMillis());
+
+ writer.putNextEntry(entry);
+ writer.write(content);
+ writer.closeEntry();
+ writer.close();
+
+ byte[] buf = new byte[128];
+ try (ZipFile zipFile = new ZipFile(test)) {
+ ZipEntry foo = zipFile.getEntry("foo");
+ assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED);
+ assertThat(foo.getSize()).isEqualTo(content.length);
+ assertThat(foo.getCompressedSize()).isEqualTo(content.length);
+ assertThat(foo.getCrc()).isEqualTo(crc.getValue());
+ assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis());
+ zipFile.getInputStream(foo).read(buf);
+ assertThat(Bytes.indexOf(buf, content)).isEqualTo(0);
+ }
+
+ Process pr = new ProcessBuilder("chmod", "750", test.getAbsolutePath()).start();
+ pr.waitFor();
+ pr = new ProcessBuilder(test.getAbsolutePath()).start();
+ pr.getInputStream().read(buf);
+ pr.waitFor();
+ assertThat(Bytes.indexOf(buf, "hello world".getBytes(UTF_8))).isEqualTo(0);
+ }
+
+ @Test public void testPrefixFileAfterZip() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ byte[] content = "content".getBytes(UTF_8);
+ crc.update(content);
+ ZipFileEntry entry = new ZipFileEntry("foo");
+ entry.setSize(content.length);
+ entry.setCompressedSize(content.length);
+ entry.setCrc(crc.getValue());
+ entry.setTime(cal.getTimeInMillis());
+
+ writer.putNextEntry(entry);
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("Cannot add a prefix file after the zip contents have been started.");
+ writer.startPrefixFile();
+ writer.write("#!/bin/bash\necho 'hello world'\n".getBytes(UTF_8));
+ writer.endPrefixFile();
+ }
+ }
+
+ @Test public void testPrefixAfterFinish() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ writer.finish();
+ thrown.expect(IllegalStateException.class);
+ writer.startPrefixFile();
+ writer.write("#!/bin/bash\necho 'hello world'\n".getBytes(UTF_8));
+ writer.endPrefixFile();
+ }
+ }
+
+ @Test public void testPutEntryAfterFinish() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ writer.finish();
+ thrown.expect(IllegalStateException.class);
+ writer.putNextEntry(new ZipFileEntry("foo"));
+ }
+ }
+
+ @Test public void testCloseEntryAfterFinish() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ byte[] content = "content".getBytes(UTF_8);
+ crc.update(content);
+ ZipFileEntry entry = new ZipFileEntry("foo");
+ entry.setSize(content.length);
+ entry.setCompressedSize(content.length);
+ entry.setCrc(crc.getValue());
+ entry.setTime(cal.getTimeInMillis());
+
+ writer.putNextEntry(entry);
+ writer.write(content);
+ writer.finish();
+ thrown.expect(IllegalStateException.class);
+ writer.closeEntry();
+ }
+ }
+
+ @Test public void testFinishAfterFinish() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ writer.finish();
+ thrown.expect(IllegalStateException.class);
+ writer.finish();
+ }
+ }
+
+ @Test public void testWriteAfterFinish() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) {
+ writer.finish();
+ thrown.expect(IllegalStateException.class);
+ writer.write("content".getBytes(UTF_8));
+ }
+ }
+
+ @Test public void testZip64_FileCount_32BitMax() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry template = new ZipFileEntry("template");
+ template.setSize(0);
+ template.setCompressedSize(0);
+ template.setCrc(0);
+ template.setTime(cal.getTimeInMillis());
+ for (int i = 0; i < 0xffff; i++) {
+ ZipFileEntry entry = new ZipFileEntry(template);
+ entry.setName("entry" + i);
+ writer.putNextEntry(entry);
+ }
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8, true)) {
+ assertThat(reader.size()).isEqualTo(0xffff);
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.size()).isEqualTo(0xffff);
+ }
+ }
+
+ @Test public void testZip64_FileCount_Zip64Range() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry template = new ZipFileEntry("template");
+ template.setSize(0);
+ template.setCompressedSize(0);
+ template.setCrc(0);
+ template.setTime(cal.getTimeInMillis());
+ for (int i = 0; i < 0x100ff; i++) {
+ ZipFileEntry entry = new ZipFileEntry(template);
+ entry.setName("entry" + i);
+ writer.putNextEntry(entry);
+ }
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8, true)) {
+ assertThat(reader.size()).isEqualTo(0x100ff);
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.size()).isEqualTo(0x100ff);
+ }
+ }
+
+ @Test public void testZip64_FileCount_Zip64Range_ForceZip32() throws IOException {
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, false)) {
+ ZipFileEntry template = new ZipFileEntry("template");
+ template.setSize(0);
+ template.setCompressedSize(0);
+ template.setCrc(0);
+ template.setTime(cal.getTimeInMillis());
+ for (int i = 0; i < 0x100ff; i++) {
+ ZipFileEntry entry = new ZipFileEntry(template);
+ entry.setName("entry" + i);
+ writer.putNextEntry(entry);
+ }
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8, true)) {
+ assertThat(reader.size()).isEqualTo(0x00ff);
+ }
+ try (ZipReader reader = new ZipReader(test, UTF_8)) {
+ assertThat(reader.size()).isEqualTo(0x100ff);
+ }
+ }
+
+ @Test public void testZip64_FileSize_32BitMax() throws IOException {
+ long size = 0xffffffffL;
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry entry = new ZipFileEntry("big");
+ entry.setCompressedSize(size);
+ entry.setSize(size);
+ entry.setCrc(0);
+ entry.setTime(cal.getTimeInMillis());
+ writer.putNextEntry(entry);
+ byte[] chunk = new byte[1024];
+ for (int i = 0; i < size / chunk.length; i++) {
+ writer.write(chunk);
+ }
+ writer.write(chunk, 0, (int) (size % chunk.length));
+ writer.closeEntry();
+ }
+ try (ZipFile file = new ZipFile(test)) {
+ ZipEntry entry = file.getEntry("big");
+ assertThat(entry.getSize()).isEqualTo(size);
+ assertThat(entry.getCompressedSize()).isEqualTo(size);
+ }
+ }
+
+ @Test public void testZip64_FileSize_Zip64Range() throws IOException {
+ long size = 0x1000000ffL;
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
+ ZipFileEntry entry = new ZipFileEntry("big");
+ entry.setCompressedSize(size);
+ entry.setSize(size);
+ entry.setCrc(0);
+ entry.setTime(cal.getTimeInMillis());
+ writer.putNextEntry(entry);
+ byte[] chunk = new byte[1024];
+ for (int i = 0; i < size / chunk.length; i++) {
+ writer.write(chunk);
+ }
+ writer.write(chunk, 0, (int) (size % chunk.length));
+ writer.closeEntry();
+ }
+ try (ZipFile file = new ZipFile(test)) {
+ ZipEntry entry = file.getEntry("big");
+ assertThat(entry.getSize()).isEqualTo(size);
+ assertThat(entry.getCompressedSize()).isEqualTo(size);
+ }
+ }
+
+ @Test public void testZip64_FileSize_Zip64Range_ForceZip32() throws IOException {
+ long size = 0x1000000ffL;
+ try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, false)) {
+ ZipFileEntry entry = new ZipFileEntry("big");
+ entry.setCompressedSize(size);
+ entry.setSize(size);
+ entry.setCrc(0);
+ entry.setTime(cal.getTimeInMillis());
+ thrown.expect(ZipException.class);
+ thrown.expectMessage("Writing an entry of size");
+ thrown.expectMessage("without Zip64 extensions is not supported.");
+ writer.putNextEntry(entry);
+ }
+ }
+}