summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--luni/src/main/java/java/util/zip/InflaterInputStream.java9
-rw-r--r--luni/src/main/java/java/util/zip/ZipEntry.java54
-rw-r--r--luni/src/main/java/java/util/zip/ZipFile.java148
-rw-r--r--luni/src/main/java/java/util/zip/ZipInputStream.java19
-rw-r--r--luni/src/main/java/java/util/zip/ZipOutputStream.java147
-rw-r--r--luni/src/test/java/libcore/java/util/zip/ZipFileTest.java339
6 files changed, 471 insertions, 245 deletions
diff --git a/luni/src/main/java/java/util/zip/InflaterInputStream.java b/luni/src/main/java/java/util/zip/InflaterInputStream.java
index 397637e..371c80a 100644
--- a/luni/src/main/java/java/util/zip/InflaterInputStream.java
+++ b/luni/src/main/java/java/util/zip/InflaterInputStream.java
@@ -189,13 +189,8 @@ public class InflaterInputStream extends FilterInputStream {
protected void fill() throws IOException {
checkClosed();
if (nativeEndBufSize > 0) {
- ZipFile.RAFStream is = (ZipFile.RAFStream)in;
- synchronized (is.mSharedRaf) {
- long len = is.mLength - is.mOffset;
- if (len > nativeEndBufSize) len = nativeEndBufSize;
- int cnt = inf.setFileInput(is.mSharedRaf.getFD(), is.mOffset, nativeEndBufSize);
- is.skip(cnt);
- }
+ ZipFile.RAFStream is = (ZipFile.RAFStream) in;
+ len = is.fill(inf, nativeEndBufSize);
} else {
if ((len = in.read(buf)) > 0) {
inf.setInput(buf, 0, len);
diff --git a/luni/src/main/java/java/util/zip/ZipEntry.java b/luni/src/main/java/java/util/zip/ZipEntry.java
index 3e58727..c313666 100644
--- a/luni/src/main/java/java/util/zip/ZipEntry.java
+++ b/luni/src/main/java/java/util/zip/ZipEntry.java
@@ -40,11 +40,17 @@ import libcore.io.HeapBufferIterator;
* @see ZipOutputStream
*/
public class ZipEntry implements ZipConstants, Cloneable {
- String name, comment;
+ String name;
+ String comment;
- long compressedSize = -1, crc = -1, size = -1;
+ long crc = -1; // Needs to be a long to distinguish -1 ("not set") from the 0xffffffff CRC32.
- int compressionMethod = -1, time = -1, modDate = -1;
+ long compressedSize = -1;
+ long size = -1;
+
+ int compressionMethod = -1;
+ int time = -1;
+ int modDate = -1;
byte[] extra;
@@ -80,11 +86,8 @@ public class ZipEntry implements ZipConstants, Cloneable {
}
/**
- * Gets the comment for this {@code ZipEntry}.
- *
- * @return the comment for this {@code ZipEntry}, or {@code null} if there
- * is no comment. If we're reading an archive with
- * {@code ZipInputStream} the comment is not available.
+ * Returns the comment for this {@code ZipEntry}, or {@code null} if there is no comment.
+ * If we're reading an archive with {@code ZipInputStream} the comment is not available.
*/
public String getComment() {
return comment;
@@ -178,13 +181,17 @@ public class ZipEntry implements ZipConstants, Cloneable {
/**
* Sets the comment for this {@code ZipEntry}.
- *
- * @param comment
- * the comment for this entry.
+ * @throws IllegalArgumentException if the comment is longer than 64 KiB.
*/
public void setComment(String comment) {
- if (comment != null && comment.length() > 0xffff) {
- throw new IllegalArgumentException("Comment too long: " + comment.length());
+ if (comment == null) {
+ this.comment = null;
+ return;
+ }
+
+ byte[] commentBytes = comment.getBytes(Charsets.UTF_8);
+ if (commentBytes.length > 0xffff) {
+ throw new IllegalArgumentException("Comment too long: " + commentBytes.length);
}
this.comment = comment;
}
@@ -221,7 +228,7 @@ public class ZipEntry implements ZipConstants, Cloneable {
* @param data
* a byte array containing the extra information.
* @throws IllegalArgumentException
- * when the length of data is greater than 0xFFFF bytes.
+ * when the length of data is greater than 64 KiB.
*/
public void setExtra(byte[] data) {
if (data != null && data.length > 0xffff) {
@@ -231,11 +238,12 @@ public class ZipEntry implements ZipConstants, Cloneable {
}
/**
- * Sets the compression method for this {@code ZipEntry}.
- *
- * @param value
- * the compression method, either {@code DEFLATED} or {@code
- * STORED}.
+ * Sets the compression method for this entry to either {@code DEFLATED} or {@code STORED}.
+ * The default is {@code DEFLATED}, which will cause the size, compressed size, and CRC to be
+ * set automatically, and the entry's data to be compressed. If you switch to {@code STORED}
+ * note that you'll have to set the size (or compressed size; they must be the same, but it's
+ * okay to only set one) and CRC yourself because they must appear <i>before</i> the user data
+ * in the resulting zip file. See {@link #setSize} and {@link #setCrc}.
* @throws IllegalArgumentException
* when value is not {@code DEFLATED} or {@code STORED}.
*/
@@ -369,7 +377,7 @@ public class ZipEntry implements ZipConstants, Cloneable {
nameLength = it.readShort();
int extraLength = it.readShort();
- int commentLength = it.readShort();
+ int commentByteCount = it.readShort();
// This is a 32-bit value in the file, but a 64-bit field in this object.
it.seek(42);
@@ -381,9 +389,9 @@ public class ZipEntry implements ZipConstants, Cloneable {
// The RI has always assumed UTF-8. (If GPBF_UTF8_FLAG isn't set, the encoding is
// actually IBM-437.)
- if (commentLength > 0) {
- byte[] commentBytes = new byte[commentLength];
- Streams.readFully(in, commentBytes, 0, commentLength);
+ if (commentByteCount > 0) {
+ byte[] commentBytes = new byte[commentByteCount];
+ Streams.readFully(in, commentBytes, 0, commentByteCount);
comment = new String(commentBytes, 0, commentBytes.length, Charsets.UTF_8);
}
diff --git a/luni/src/main/java/java/util/zip/ZipFile.java b/luni/src/main/java/java/util/zip/ZipFile.java
index 83b1992..6e8b516 100644
--- a/luni/src/main/java/java/util/zip/ZipFile.java
+++ b/luni/src/main/java/java/util/zip/ZipFile.java
@@ -34,18 +34,17 @@ import libcore.io.HeapBufferIterator;
import libcore.io.Streams;
/**
- * This class provides random read access to a <i>ZIP-archive</i> file.
- * <p>
- * While {@code ZipInputStream} provides stream based read access to a
- * <i>ZIP-archive</i>, this class implements more efficient (file based) access
- * and makes use of the <i>central directory</i> within a <i>ZIP-archive</i>.
- * <p>
- * Use {@code ZipOutputStream} if you want to create an archive.
- * <p>
- * A temporary ZIP file can be marked for automatic deletion upon closing it.
+ * This class provides random read access to a <i>ZIP-archive</i> file. You pay more to read
+ * the zip file's central directory up front (from the constructor), but if you're using
+ * {@link #getEntry} to look up multiple files by name, you get the benefit of this index.
*
- * @see ZipEntry
- * @see ZipOutputStream
+ * <p>If you only want to iterate through all the files (using {@link #entries}, you should
+ * consider {@link ZipInputStream}, which provides stream-like read access to a zip file and
+ * has a lower up-front cost, and doesn't require an in-memory index. The savings could be
+ * particularly large if your zip file has many entries and you only require a few of them.
+ *
+ * <p>If you want to create a zip file, use {@link ZipOutputStream}. There is no API for updating
+ * an existing zip file.
*/
public class ZipFile implements ZipConstants {
/**
@@ -70,7 +69,7 @@ public class ZipFile implements ZipConstants {
static final int GPBF_UTF8_FLAG = 1 << 11;
/**
- * Open ZIP file for read.
+ * Open ZIP file for reading.
*/
public static final int OPEN_READ = 1;
@@ -79,9 +78,9 @@ public class ZipFile implements ZipConstants {
*/
public static final int OPEN_DELETE = 4;
- private final String fileName;
+ private final String mFilename;
- private File fileToDeleteOnClose;
+ private File mFileToDeleteOnClose;
private RandomAccessFile mRaf;
@@ -90,61 +89,51 @@ public class ZipFile implements ZipConstants {
private final CloseGuard guard = CloseGuard.get();
/**
- * Constructs a new {@code ZipFile} with the specified file.
- *
- * @param file
- * the file to read from.
- * @throws ZipException
- * if a ZIP error occurs.
- * @throws IOException
- * if an {@code IOException} occurs.
+ * Constructs a new {@code ZipFile} allowing read access to the contents of the given file.
+ * @throws ZipException if a ZIP error occurs.
+ * @throws IOException if an {@code IOException} occurs.
*/
public ZipFile(File file) throws ZipException, IOException {
this(file, OPEN_READ);
}
/**
- * Opens a file as <i>ZIP-archive</i>. "mode" must be {@code OPEN_READ} or
- * {@code OPEN_DELETE} . The latter sets the "delete on exit" flag through a
- * file.
+ * Constructs a new {@code ZipFile} allowing read access to the contents of the given file.
+ * @throws IOException if an IOException occurs.
+ */
+ public ZipFile(String name) throws IOException {
+ this(new File(name), OPEN_READ);
+ }
+
+ /**
+ * Constructs a new {@code ZipFile} allowing access to the given file.
+ * The {@code mode} must be either {@code OPEN_READ} or {@code OPEN_READ|OPEN_DELETE}.
*
- * @param file
- * the ZIP file to read.
- * @param mode
- * the mode of the file open operation.
- * @throws IOException
- * if an {@code IOException} occurs.
+ * <p>If the {@code OPEN_DELETE} flag is supplied, the file will be deleted at or before the
+ * time that the {@code ZipFile} is closed (the contents will remain accessible until
+ * this {@code ZipFile} is closed); it also calls {@code File.deleteOnExit}.
+ *
+ * @throws IOException if an {@code IOException} occurs.
*/
public ZipFile(File file, int mode) throws IOException {
- fileName = file.getPath();
+ mFilename = file.getPath();
if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE)) {
throw new IllegalArgumentException("Bad mode: " + mode);
}
if ((mode & OPEN_DELETE) != 0) {
- fileToDeleteOnClose = file; // file.deleteOnExit();
+ mFileToDeleteOnClose = file;
+ mFileToDeleteOnClose.deleteOnExit();
} else {
- fileToDeleteOnClose = null;
+ mFileToDeleteOnClose = null;
}
- mRaf = new RandomAccessFile(fileName, "r");
+ mRaf = new RandomAccessFile(mFilename, "r");
readCentralDir();
guard.open("close");
}
- /**
- * Opens a ZIP archived file.
- *
- * @param name
- * the name of the ZIP file.
- * @throws IOException
- * if an IOException occurs.
- */
- public ZipFile(String name) throws IOException {
- this(new File(name), OPEN_READ);
- }
-
@Override protected void finalize() throws IOException {
try {
if (guard != null) {
@@ -174,9 +163,9 @@ public class ZipFile implements ZipConstants {
mRaf = null;
raf.close();
}
- if (fileToDeleteOnClose != null) {
- fileToDeleteOnClose.delete();
- fileToDeleteOnClose = null;
+ if (mFileToDeleteOnClose != null) {
+ mFileToDeleteOnClose.delete();
+ mFileToDeleteOnClose = null;
}
}
}
@@ -257,19 +246,19 @@ public class ZipFile implements ZipConstants {
// position of the entry's local header. At position 28 we find the
// length of the extra data. In some cases this length differs from
// the one coming in the central header.
- RAFStream rafstrm = new RAFStream(raf, entry.mLocalHeaderRelOffset + 28);
- DataInputStream is = new DataInputStream(rafstrm);
+ RAFStream rafStream = new RAFStream(raf, entry.mLocalHeaderRelOffset + 28);
+ DataInputStream is = new DataInputStream(rafStream);
int localExtraLenOrWhatever = Short.reverseBytes(is.readShort());
is.close();
// Skip the name and this "extra" data or whatever it is:
- rafstrm.skip(entry.nameLength + localExtraLenOrWhatever);
- rafstrm.mLength = rafstrm.mOffset + entry.compressedSize;
+ rafStream.skip(entry.nameLength + localExtraLenOrWhatever);
+ rafStream.mLength = rafStream.mOffset + entry.compressedSize;
if (entry.compressionMethod == ZipEntry.DEFLATED) {
int bufSize = Math.max(1024, (int)Math.min(entry.getSize(), 65535L));
- return new ZipInflaterInputStream(rafstrm, new Inflater(true), bufSize, entry);
+ return new ZipInflaterInputStream(rafStream, new Inflater(true), bufSize, entry);
} else {
- return rafstrm;
+ return rafStream;
}
}
}
@@ -280,7 +269,7 @@ public class ZipFile implements ZipConstants {
* @return the file name of this {@code ZipFile}.
*/
public String getName() {
- return fileName;
+ return mFilename;
}
/**
@@ -351,18 +340,21 @@ public class ZipFile implements ZipConstants {
int numEntries = it.readShort() & 0xffff;
int totalNumEntries = it.readShort() & 0xffff;
it.skip(4); // Ignore centralDirSize.
- int centralDirOffset = it.readInt();
+ long centralDirOffset = ((long) it.readInt()) & 0xffffffffL;
if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) {
throw new ZipException("spanned archives not supported");
}
// Seek to the first CDE and read all entries.
- RAFStream rafs = new RAFStream(mRaf, centralDirOffset);
- BufferedInputStream bin = new BufferedInputStream(rafs, 4096);
+ // We have to do this now (from the constructor) rather than lazily because the
+ // public API doesn't allow us to throw IOException except from the constructor
+ // or from getInputStream.
+ RAFStream rafStream = new RAFStream(mRaf, centralDirOffset);
+ BufferedInputStream bufferedStream = new BufferedInputStream(rafStream, 4096);
byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry.
for (int i = 0; i < numEntries; ++i) {
- ZipEntry newEntry = new ZipEntry(hdrBuf, bin);
+ ZipEntry newEntry = new ZipEntry(hdrBuf, bufferedStream);
String entryName = newEntry.getName();
if (mEntries.put(entryName, newEntry) != null) {
throw new ZipException("Duplicate entry name: " + entryName);
@@ -379,14 +371,13 @@ public class ZipFile implements ZipConstants {
* <p>We could support mark/reset, but we don't currently need them.
*/
static class RAFStream extends InputStream {
+ private final RandomAccessFile mSharedRaf;
+ private long mLength;
+ private long mOffset;
- RandomAccessFile mSharedRaf;
- long mOffset;
- long mLength;
-
- public RAFStream(RandomAccessFile raf, long pos) throws IOException {
+ public RAFStream(RandomAccessFile raf, long offset) throws IOException {
mSharedRaf = raf;
- mOffset = pos;
+ mOffset = offset;
mLength = raf.length();
}
@@ -414,28 +405,34 @@ public class ZipFile implements ZipConstants {
}
}
- @Override
- public long skip(long byteCount) throws IOException {
+ @Override public long skip(long byteCount) throws IOException {
if (byteCount > mLength - mOffset) {
byteCount = mLength - mOffset;
}
mOffset += byteCount;
return byteCount;
}
+
+ public int fill(Inflater inflater, int nativeEndBufSize) throws IOException {
+ synchronized (mSharedRaf) {
+ int len = Math.min((int) (mLength - mOffset), nativeEndBufSize);
+ int cnt = inflater.setFileInput(mSharedRaf.getFD(), mOffset, nativeEndBufSize);
+ skip(cnt);
+ return len;
+ }
+ }
}
static class ZipInflaterInputStream extends InflaterInputStream {
-
- ZipEntry entry;
- long bytesRead = 0;
+ private final ZipEntry entry;
+ private long bytesRead = 0;
public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
super(is, inf, bsize);
this.entry = entry;
}
- @Override
- public int read(byte[] buffer, int off, int nbytes) throws IOException {
+ @Override public int read(byte[] buffer, int off, int nbytes) throws IOException {
int i = super.read(buffer, off, nbytes);
if (i != -1) {
bytesRead += i;
@@ -443,8 +440,7 @@ public class ZipFile implements ZipConstants {
return i;
}
- @Override
- public int available() throws IOException {
+ @Override public int available() throws IOException {
if (closed) {
// Our superclass will throw an exception, but there's a jtreg test that
// explicitly checks that the InputStream returned from ZipFile.getInputStream
diff --git a/luni/src/main/java/java/util/zip/ZipInputStream.java b/luni/src/main/java/java/util/zip/ZipInputStream.java
index e7c4566..57d9034 100644
--- a/luni/src/main/java/java/util/zip/ZipInputStream.java
+++ b/luni/src/main/java/java/util/zip/ZipInputStream.java
@@ -34,14 +34,15 @@ import libcore.io.Streams;
*
* <p>A ZIP archive is a collection of (possibly) compressed files.
* When reading from a {@code ZipInputStream}, you retrieve the
- * entry's metadata with {@code getNextEntry} before you can read the userdata.
+ * entry's metadata with {@code getNextEntry} (which returns a {@link ZipEntry}
+ * before you can read the userdata.
*
* <p>Although {@code InflaterInputStream} can only read compressed ZIP archive
* entries, this class can read non-compressed entries as well.
*
- * <p>Use {@code ZipFile} if you can access the archive as a file directly,
- * especially if you want random access to entries, rather than needing to
- * iterate over all entries.
+ * <p>Use {@link ZipFile} if you need random access to entries by name, but use this class
+ * if you just want to iterate over all entries (and remember that iteration is better
+ * than lookup by name in the case where you're only looking for one file).
*
* <h3>Example</h3>
* <p>Using {@code ZipInputStream} is a little more complicated than {@link GZIPInputStream}
@@ -67,9 +68,6 @@ import libcore.io.Streams;
* zis.close();
* }
* </pre>
- *
- * @see ZipEntry
- * @see ZipFile
*/
public class ZipInputStream extends InflaterInputStream implements ZipConstants {
private static final int ZIPLocalHeaderVersionNeeded = 20;
@@ -213,13 +211,10 @@ public class ZipInputStream extends InflaterInputStream implements ZipConstants
}
/**
- * Reads the next entry from this {@code ZipInputStream} or {@code null} if
+ * Returns the next entry from this {@code ZipInputStream} or {@code null} if
* no more entries are present.
*
- * @return the next {@code ZipEntry} contained in the input stream.
- * @throws IOException
- * if an {@code IOException} occurs.
- * @see ZipEntry
+ * @throws IOException if an {@code IOException} occurs.
*/
public ZipEntry getNextEntry() throws IOException {
closeEntry();
diff --git a/luni/src/main/java/java/util/zip/ZipOutputStream.java b/luni/src/main/java/java/util/zip/ZipOutputStream.java
index 77a993b..9f7a4ad 100644
--- a/luni/src/main/java/java/util/zip/ZipOutputStream.java
+++ b/luni/src/main/java/java/util/zip/ZipOutputStream.java
@@ -23,20 +23,19 @@ import java.io.OutputStream;
import java.nio.charset.Charsets;
import java.util.Arrays;
import java.util.HashSet;
+import libcore.util.EmptyArray;
/**
* This class provides an implementation of {@code FilterOutputStream} that
* compresses data entries into a <i>ZIP-archive</i> output stream.
- * <p>
- * {@code ZipOutputStream} is used to write {@code ZipEntries} to the underlying
- * stream. Output from {@code ZipOutputStream} conforms to the {@code ZipFile}
- * file format.
- * <p>
- * While {@code DeflaterOutputStream} can write a compressed <i>ZIP-archive</i>
- * entry, this extension can write uncompressed entries as well. In this case
- * special rules apply, for this purpose refer to the <a
- * href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">file format
- * specification</a>.
+ *
+ * <p>{@code ZipOutputStream} is used to write {@link ZipEntry}s to the underlying
+ * stream. Output from {@code ZipOutputStream} can be read using {@link ZipFile}
+ * or {@link ZipInputStream}.
+ *
+ * <p>While {@code DeflaterOutputStream} can write a compressed <i>ZIP-archive</i>
+ * entry, this extension can write uncompressed entries as well.
+ * Use {@link ZipEntry#setMethod} or {@link #setMethod} with the {@link ZipEntry#STORED} flag.
*
* <h3>Example</h3>
* <p>Using {@code ZipOutputStream} is a little more complicated than {@link GZIPOutputStream}
@@ -58,9 +57,6 @@ import java.util.HashSet;
* zos.close();
* }
* </pre>
- *
- * @see ZipEntry
- * @see ZipFile
*/
public class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
@@ -76,13 +72,13 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
private static final int ZIPLocalHeaderVersionNeeded = 20;
- private String comment;
+ private byte[] commentBytes = EmptyArray.BYTE;
private final HashSet<String> entries = new HashSet<String>();
- private int compressMethod = DEFLATED;
+ private int defaultCompressionMethod = DEFLATED;
- private int compressLevel = Deflater.DEFAULT_COMPRESSION;
+ private int compressionLevel = Deflater.DEFAULT_COMPRESSION;
private ByteArrayOutputStream cDir = new ByteArrayOutputStream();
@@ -95,14 +91,11 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
private byte[] nameBytes;
/**
- * Constructs a new {@code ZipOutputStream} with the specified output
- * stream.
- *
- * @param p1
- * the {@code OutputStream} to write the data to.
+ * Constructs a new {@code ZipOutputStream} that writes a zip file
+ * to the given {@code OutputStream}.
*/
- public ZipOutputStream(OutputStream p1) {
- super(p1, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
+ public ZipOutputStream(OutputStream os) {
+ super(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
}
/**
@@ -131,7 +124,7 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
* If an error occurs closing the entry.
*/
public void closeEntry() throws IOException {
- checkClosed();
+ checkOpen();
if (currentEntry == null) {
return;
}
@@ -186,12 +179,13 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
} else {
writeShort(cDir, 0);
}
- String c;
- if ((c = currentEntry.getComment()) != null) {
- writeShort(cDir, c.length());
- } else {
- writeShort(cDir, 0);
+
+ String comment = currentEntry.getComment();
+ byte[] commentBytes = EmptyArray.BYTE;
+ if (comment != null) {
+ commentBytes = comment.getBytes(Charsets.UTF_8);
}
+ writeShort(cDir, commentBytes.length); // Comment length.
writeShort(cDir, 0); // Disk Start
writeShort(cDir, 0); // Internal File Attributes
writeLong(cDir, 0); // External File Attributes
@@ -202,8 +196,8 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
cDir.write(currentEntry.extra);
}
offset += curOffset;
- if (c != null) {
- cDir.write(c.getBytes());
+ if (commentBytes.length > 0) {
+ cDir.write(commentBytes);
}
currentEntry = null;
crc.reset();
@@ -220,7 +214,7 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
*/
@Override
public void finish() throws IOException {
- // TODO: is there a bug here? why not checkClosed?
+ // TODO: is there a bug here? why not checkOpen?
if (out == null) {
throw new IOException("Stream is closed");
}
@@ -242,11 +236,9 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
writeShort(cDir, entries.size()); // Number of entries
writeLong(cDir, cdirSize); // Size of central dir
writeLong(cDir, offset); // Offset of central dir
- if (comment != null) {
- writeShort(cDir, comment.length());
- cDir.write(comment.getBytes());
- } else {
- writeShort(cDir, 0);
+ writeShort(cDir, commentBytes.length);
+ if (commentBytes.length > 0) {
+ cDir.write(commentBytes);
}
// Write the central directory.
cDir.writeTo(out);
@@ -269,18 +261,33 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
if (currentEntry != null) {
closeEntry();
}
- if (ze.getMethod() == STORED || (compressMethod == STORED && ze.getMethod() == -1)) {
- if (ze.crc == -1) {
- throw new ZipException("CRC mismatch");
+
+ // Did this ZipEntry specify a method, or should we use the default?
+ int method = ze.getMethod();
+ if (method == -1) {
+ method = defaultCompressionMethod;
+ }
+
+ // If the method is STORED, check that the ZipEntry was configured appropriately.
+ if (method == STORED) {
+ if (ze.getCompressedSize() == -1) {
+ ze.setCompressedSize(ze.getSize());
+ } else if (ze.getSize() == -1) {
+ ze.setSize(ze.getCompressedSize());
}
- if (ze.size == -1 && ze.compressedSize == -1) {
- throw new ZipException("Size mismatch");
+ if (ze.getCrc() == -1) {
+ throw new ZipException("STORED entry missing CRC");
}
- if (ze.size != ze.compressedSize && ze.compressedSize != -1 && ze.size != -1) {
- throw new ZipException("Size mismatch");
+ if (ze.getSize() == -1) {
+ throw new ZipException("STORED entry missing size");
+ }
+ if (ze.size != ze.compressedSize) {
+ throw new ZipException("STORED entry size/compressed size mismatch");
}
}
- checkClosed();
+
+ checkOpen();
+
if (entries.contains(ze.name)) {
throw new ZipException("Entry already exists: " + ze.name);
}
@@ -294,35 +301,29 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
throw new IllegalArgumentException("Name too long: " + nameLength + " UTF-8 bytes");
}
- def.setLevel(compressLevel);
+ def.setLevel(compressionLevel);
+ ze.setMethod(method);
+
currentEntry = ze;
entries.add(currentEntry.name);
- if (currentEntry.getMethod() == -1) {
- currentEntry.setMethod(compressMethod);
- }
// Local file header.
// http://www.pkware.com/documents/casestudies/APPNOTE.TXT
- int flags = currentEntry.getMethod() == STORED ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG;
+ int flags = (method == STORED) ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG;
// Java always outputs UTF-8 filenames. (Before Java 7, the RI didn't set this flag and used
// modified UTF-8. From Java 7, it sets this flag and uses normal UTF-8.)
flags |= ZipFile.GPBF_UTF8_FLAG;
writeLong(out, LOCSIG); // Entry header
writeShort(out, ZIPLocalHeaderVersionNeeded); // Extraction version
writeShort(out, flags);
- writeShort(out, currentEntry.getMethod());
+ writeShort(out, method);
if (currentEntry.getTime() == -1) {
currentEntry.setTime(System.currentTimeMillis());
}
writeShort(out, currentEntry.time);
writeShort(out, currentEntry.modDate);
- if (currentEntry.getMethod() == STORED) {
- if (currentEntry.size == -1) {
- currentEntry.size = currentEntry.compressedSize;
- } else if (currentEntry.compressedSize == -1) {
- currentEntry.compressedSize = currentEntry.size;
- }
+ if (method == STORED) {
writeLong(out, currentEntry.crc);
writeLong(out, currentEntry.size);
writeLong(out, currentEntry.size);
@@ -344,16 +345,20 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
}
/**
- * Sets the {@code ZipFile} comment associated with the file being written.
- *
- * @param comment
- * the comment associated with the file.
+ * Sets the comment associated with the file being written.
+ * @throws IllegalArgumentException if the comment is longer than 64 KiB.
*/
public void setComment(String comment) {
- if (comment.length() > 0xFFFF) {
- throw new IllegalArgumentException("Comment too long: " + comment.length() + " characters");
+ if (comment == null) {
+ this.commentBytes = null;
+ return;
}
- this.comment = comment;
+
+ byte[] newCommentBytes = comment.getBytes(Charsets.UTF_8);
+ if (newCommentBytes.length > 0xffff) {
+ throw new IllegalArgumentException("Comment too long: " + newCommentBytes.length + " bytes");
+ }
+ this.commentBytes = newCommentBytes;
}
/**
@@ -369,22 +374,18 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) {
throw new IllegalArgumentException("Bad level: " + level);
}
- compressLevel = level;
+ compressionLevel = level;
}
/**
- * Sets the compression method to be used when compressing entry data.
- * method must be one of {@code STORED} (for no compression) or {@code
- * DEFLATED}.
- *
- * @param method
- * the compression method to use.
+ * Sets the default compression method to be used when a {@code ZipEntry} doesn't
+ * explicitly specify a method. See {@link ZipEntry#setMethod} for more details.
*/
public void setMethod(int method) {
if (method != STORED && method != DEFLATED) {
throw new IllegalArgumentException("Bad method: " + method);
}
- compressMethod = method;
+ defaultCompressionMethod = method;
}
private long writeLong(OutputStream os, long i) throws IOException {
@@ -423,7 +424,7 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant
crc.update(buffer, offset, byteCount);
}
- private void checkClosed() throws IOException {
+ private void checkOpen() throws IOException {
if (cDir == null) {
throw new IOException("Stream is closed");
}
diff --git a/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java b/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java
index afceaba..49dc050 100644
--- a/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java
+++ b/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java
@@ -26,6 +26,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Random;
+import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
@@ -35,7 +36,6 @@ import junit.framework.TestCase;
import libcore.io.IoUtils;
public final class ZipFileTest extends TestCase {
-
/**
* Exercise Inflater's ability to refill the zlib's input buffer. As of this
* writing, this buffer's max size is 64KiB compressed bytes. We'll write a
@@ -45,7 +45,7 @@ public final class ZipFileTest extends TestCase {
public void testInflatingFilesRequiringZipRefill() throws IOException {
int originalSize = 1024 * 1024;
byte[] readBuffer = new byte[8192];
- ZipFile zipFile = new ZipFile(createZipFile(originalSize));
+ ZipFile zipFile = new ZipFile(createZipFile(1, originalSize));
for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
ZipEntry zipEntry = e.nextElement();
assertTrue("This test needs >64 KiB of compressed data to exercise Inflater",
@@ -121,72 +121,303 @@ public final class ZipFileTest extends TestCase {
public void testInflatingStreamsRequiringZipRefill() throws IOException {
int originalSize = 1024 * 1024;
byte[] readBuffer = new byte[8192];
- ZipInputStream in = new ZipInputStream(new FileInputStream(createZipFile(originalSize)));
+ ZipInputStream in = new ZipInputStream(new FileInputStream(createZipFile(1, originalSize)));
while (in.getNextEntry() != null) {
while (in.read(readBuffer, 0, readBuffer.length) != -1) {}
}
in.close();
}
+ public void testZipFileWithLotsOfEntries() throws IOException {
+ int expectedEntryCount = 64*1024 - 1;
+ File f = createZipFile(expectedEntryCount, 0);
+ ZipFile zipFile = new ZipFile(f);
+ int entryCount = 0;
+ for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
+ ZipEntry zipEntry = e.nextElement();
+ ++entryCount;
+ }
+ assertEquals(expectedEntryCount, entryCount);
+ zipFile.close();
+ }
+
+ // http://code.google.com/p/android/issues/detail?id=36187
+ public void testZipFileLargerThan2GiB() throws IOException {
+ if (false) { // TODO: this test requires too much time and too much disk space!
+ File f = createZipFile(1024, 3*1024*1024);
+ ZipFile zipFile = new ZipFile(f);
+ int entryCount = 0;
+ for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
+ ZipEntry zipEntry = e.nextElement();
+ ++entryCount;
+ }
+ assertEquals(1024, entryCount);
+ zipFile.close();
+ }
+ }
+
+ public void testZip64Support() throws IOException {
+ try {
+ createZipFile(64*1024, 0);
+ fail(); // Make this test more like testHugeZipFile when we have Zip64 support.
+ } catch (ZipException expected) {
+ }
+ }
+
/**
- * Compresses a single random file into a .zip archive.
+ * Compresses the given number of files, each of the given size, into a .zip archive.
*/
- private File createZipFile(int uncompressedSize) throws IOException {
+ private File createZipFile(int entryCount, int entrySize) throws IOException {
+ File result = createTemporaryZipFile();
+
+ byte[] writeBuffer = new byte[8192];
+ Random random = new Random();
+
+ ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(result)));
+ for (int entry = 0; entry < entryCount; ++entry) {
+ ZipEntry ze = new ZipEntry(Integer.toHexString(entry));
+ out.putNextEntry(ze);
+
+ for (int i = 0; i < entrySize; i += writeBuffer.length) {
+ random.nextBytes(writeBuffer);
+ int byteCount = Math.min(writeBuffer.length, entrySize - i);
+ out.write(writeBuffer, 0, byteCount);
+ }
+
+ out.closeEntry();
+ }
+
+ out.close();
+ return result;
+ }
+
+ private File createTemporaryZipFile() throws IOException {
File result = File.createTempFile("ZipFileTest", "zip");
result.deleteOnExit();
+ return result;
+ }
- ZipOutputStream out = new ZipOutputStream(new FileOutputStream(result));
- ZipEntry entry = new ZipEntry("random");
- out.putNextEntry(entry);
+ private ZipOutputStream createZipOutputStream(File f) throws IOException {
+ return new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
+ }
- byte[] writeBuffer = new byte[8192];
- Random random = new Random();
- for (int i = 0; i < uncompressedSize; i += writeBuffer.length) {
- random.nextBytes(writeBuffer);
- out.write(writeBuffer, 0, Math.min(writeBuffer.length, uncompressedSize - i));
+ public void testSTORED() throws IOException {
+ ZipOutputStream out = createZipOutputStream(createTemporaryZipFile());
+ CRC32 crc = new CRC32();
+
+ // Missing CRC, size, and compressed size => failure.
+ try {
+ ZipEntry ze = new ZipEntry("a");
+ ze.setMethod(ZipEntry.STORED);
+ out.putNextEntry(ze);
+ fail();
+ } catch (ZipException expected) {
+ }
+
+ // Missing CRC and compressed size => failure.
+ try {
+ ZipEntry ze = new ZipEntry("a");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setSize(0);
+ out.putNextEntry(ze);
+ fail();
+ } catch (ZipException expected) {
}
+ // Missing CRC and size => failure.
+ try {
+ ZipEntry ze = new ZipEntry("a");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setSize(0);
+ ze.setCompressedSize(0);
+ out.putNextEntry(ze);
+ fail();
+ } catch (ZipException expected) {
+ }
+
+ // Missing size and compressed size => failure.
+ try {
+ ZipEntry ze = new ZipEntry("a");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setCrc(crc.getValue());
+ out.putNextEntry(ze);
+ fail();
+ } catch (ZipException expected) {
+ }
+
+ // Missing size is copied from compressed size.
+ {
+ ZipEntry ze = new ZipEntry("okay1");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setCrc(crc.getValue());
+
+ assertEquals(-1, ze.getSize());
+ assertEquals(-1, ze.getCompressedSize());
+
+ ze.setCompressedSize(0);
+
+ assertEquals(-1, ze.getSize());
+ assertEquals(0, ze.getCompressedSize());
+
+ out.putNextEntry(ze);
+
+ assertEquals(0, ze.getSize());
+ assertEquals(0, ze.getCompressedSize());
+ }
+
+ // Missing compressed size is copied from size.
+ {
+ ZipEntry ze = new ZipEntry("okay2");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setCrc(crc.getValue());
+
+ assertEquals(-1, ze.getSize());
+ assertEquals(-1, ze.getCompressedSize());
+
+ ze.setSize(0);
+
+ assertEquals(0, ze.getSize());
+ assertEquals(-1, ze.getCompressedSize());
+
+ out.putNextEntry(ze);
+
+ assertEquals(0, ze.getSize());
+ assertEquals(0, ze.getCompressedSize());
+ }
+
+ // Mismatched size and compressed size => failure.
+ try {
+ ZipEntry ze = new ZipEntry("a");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setCrc(crc.getValue());
+ ze.setCompressedSize(1);
+ ze.setSize(0);
+ out.putNextEntry(ze);
+ fail();
+ } catch (ZipException expected) {
+ }
+
+ // Everything present => success.
+ ZipEntry ze = new ZipEntry("okay");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setCrc(crc.getValue());
+ ze.setSize(0);
+ ze.setCompressedSize(0);
+ out.putNextEntry(ze);
+
+ out.close();
+ }
+
+ private String makeString(int count, String ch) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; ++i) {
+ sb.append(ch);
+ }
+ return sb.toString();
+ }
+
+ public void testComment() throws Exception {
+ String expectedFileComment = "1 \u0666 2";
+ String expectedEntryComment = "a \u0666 b";
+
+ File file = createTemporaryZipFile();
+ ZipOutputStream out = createZipOutputStream(file);
+
+ // Is file comment length checking done on bytes or characters? (Should be bytes.)
+ out.setComment(null);
+ out.setComment(makeString(0xffff, "a"));
+ try {
+ out.setComment(makeString(0xffff + 1, "a"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ try {
+ out.setComment(makeString(0xffff, "\u0666"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+
+ ZipEntry ze = new ZipEntry("a");
+
+ // Is entry comment length checking done on bytes or characters? (Should be bytes.)
+ ze.setComment(null);
+ ze.setComment(makeString(0xffff, "a"));
+ try {
+ ze.setComment(makeString(0xffff + 1, "a"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ try {
+ ze.setComment(makeString(0xffff, "\u0666"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+
+ ze.setComment(expectedEntryComment);
+ out.putNextEntry(ze);
out.closeEntry();
+
+ out.setComment(expectedFileComment);
out.close();
- return result;
- }
-
- public void testHugeZipFile() throws IOException {
- int expectedEntryCount = 64*1024 - 1;
- File f = createHugeZipFile(expectedEntryCount);
- ZipFile zipFile = new ZipFile(f);
- int entryCount = 0;
- for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
- ZipEntry zipEntry = e.nextElement();
- ++entryCount;
- }
- assertEquals(expectedEntryCount, entryCount);
- zipFile.close();
- }
-
- public void testZip64Support() throws IOException {
- try {
- createHugeZipFile(64*1024);
- fail(); // Make this test more like testHugeZipFile when we have Zip64 support.
- } catch (ZipException expected) {
- }
- }
-
- /**
- * Compresses the given number of empty files into a .zip archive.
- */
- private File createHugeZipFile(int count) throws IOException {
- File result = File.createTempFile("ZipFileTest", "zip");
- result.deleteOnExit();
-
- ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(result)));
- for (int i = 0; i < count; ++i) {
- ZipEntry entry = new ZipEntry(Integer.toHexString(i));
- out.putNextEntry(entry);
- out.closeEntry();
- }
-
- out.close();
- return result;
- }
+
+ ZipFile zipFile = new ZipFile(file);
+ // TODO: there's currently no API for reading the file comment --- strings(1) the file?
+ assertEquals(expectedEntryComment, zipFile.getEntry("a").getComment());
+ zipFile.close();
+ }
+
+ public void testNameLengthChecks() throws IOException {
+ // Is entry name length checking done on bytes or characters?
+ // Really it should be bytes, but the RI only checks characters at construction time.
+ // Android does the same, because it's cheap...
+ try {
+ new ZipEntry((String) null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ new ZipEntry(makeString(0xffff, "a"));
+ try {
+ new ZipEntry(makeString(0xffff + 1, "a"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+
+ // ...but Android won't let you create a zip file with a truncated name.
+ ZipOutputStream out = createZipOutputStream(createTemporaryZipFile());
+ ZipEntry ze = new ZipEntry(makeString(0xffff, "\u0666"));
+ try {
+ out.putNextEntry(ze);
+ fail(); // The RI fails this test; it just checks the character count at construction time.
+ } catch (IllegalArgumentException expected) {
+ }
+ out.closeEntry();
+ out.putNextEntry(new ZipEntry("okay")); // ZipOutputStream.close throws if you add nothing!
+ out.close();
+ }
+
+ public void testCrc() throws IOException {
+ ZipEntry ze = new ZipEntry("test");
+ ze.setMethod(ZipEntry.STORED);
+ ze.setSize(4);
+
+ // setCrc takes a long, not an int, so -1 isn't a valid CRC32 (because it's 64 bits).
+ try {
+ ze.setCrc(-1);
+ } catch (IllegalArgumentException expected) {
+ }
+
+ // You can set the CRC32 to 0xffffffff if you're slightly more careful though...
+ ze.setCrc(0xffffffffL);
+ assertEquals(0xffffffffL, ze.getCrc());
+
+ // And it actually works, even though we use -1L to mean "no CRC set"...
+ ZipOutputStream out = createZipOutputStream(createTemporaryZipFile());
+ out.putNextEntry(ze);
+ out.write(-1);
+ out.write(-1);
+ out.write(-1);
+ out.write(-1);
+ out.closeEntry();
+ out.close();
+ }
}