| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 java.util.zip; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.Vector; |
| |
| import org.apache.harmony.archive.internal.nls.Messages; |
| |
| /** |
| * 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>. |
| * |
| * @see ZipEntry |
| * @see ZipFile |
| */ |
| public class ZipOutputStream extends DeflaterOutputStream implements |
| ZipConstants { |
| |
| /** |
| * Indicates deflated entries. |
| */ |
| public static final int DEFLATED = 8; |
| |
| /** |
| * Indicates uncompressed entries. |
| */ |
| public static final int STORED = 0; |
| |
| static final int ZIPDataDescriptorFlag = 8; |
| |
| static final int ZIPLocalHeaderVersionNeeded = 20; |
| |
| private String comment; |
| |
| private final Vector<String> entries = new Vector<String>(); |
| |
| private int compressMethod = DEFLATED; |
| |
| private int compressLevel = Deflater.DEFAULT_COMPRESSION; |
| |
| private ByteArrayOutputStream cDir = new ByteArrayOutputStream(); |
| |
| private ZipEntry currentEntry; |
| |
| private final CRC32 crc = new CRC32(); |
| |
| private int offset = 0, curOffset = 0, nameLength; |
| |
| private byte[] nameBytes; |
| |
| /** |
| * Constructs a new {@code ZipOutputStream} with the specified output |
| * stream. |
| * |
| * @param p1 |
| * the {@code OutputStream} to write the data to. |
| */ |
| public ZipOutputStream(OutputStream p1) { |
| super(p1, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); |
| } |
| |
| /** |
| * Closes the current {@code ZipEntry}, if any, and the underlying output |
| * stream. If the stream is already closed this method does nothing. |
| * |
| * @throws IOException |
| * If an error occurs closing the stream. |
| */ |
| @Override |
| public void close() throws IOException { |
| if (out != null) { |
| finish(); |
| out.close(); |
| out = null; |
| } |
| } |
| |
| /** |
| * Closes the current {@code ZipEntry}. Any entry terminal data is written |
| * to the underlying stream. |
| * |
| * @throws IOException |
| * If an error occurs closing the entry. |
| */ |
| public void closeEntry() throws IOException { |
| if (cDir == null) { |
| throw new IOException(Messages.getString("archive.1E")); //$NON-NLS-1$ |
| } |
| if (currentEntry == null) { |
| return; |
| } |
| if (currentEntry.getMethod() == DEFLATED) { |
| super.finish(); |
| } |
| |
| // Verify values for STORED types |
| if (currentEntry.getMethod() == STORED) { |
| if (crc.getValue() != currentEntry.crc) { |
| throw new ZipException(Messages.getString("archive.20")); //$NON-NLS-1$ |
| } |
| if (currentEntry.size != crc.tbytes) { |
| throw new ZipException(Messages.getString("archive.21")); //$NON-NLS-1$ |
| } |
| } |
| curOffset = LOCHDR; |
| |
| // Write the DataDescriptor |
| if (currentEntry.getMethod() != STORED) { |
| curOffset += EXTHDR; |
| writeLong(out, EXTSIG); |
| writeLong(out, currentEntry.crc = crc.getValue()); |
| writeLong(out, currentEntry.compressedSize = def.getTotalOut()); |
| writeLong(out, currentEntry.size = def.getTotalIn()); |
| } |
| // Update the CentralDirectory |
| writeLong(cDir, CENSIG); |
| writeShort(cDir, ZIPLocalHeaderVersionNeeded); // Version created |
| writeShort(cDir, ZIPLocalHeaderVersionNeeded); // Version to extract |
| writeShort(cDir, currentEntry.getMethod() == STORED ? 0 |
| : ZIPDataDescriptorFlag); |
| writeShort(cDir, currentEntry.getMethod()); |
| writeShort(cDir, currentEntry.time); |
| writeShort(cDir, currentEntry.modDate); |
| writeLong(cDir, crc.getValue()); |
| if (currentEntry.getMethod() == DEFLATED) { |
| curOffset += writeLong(cDir, def.getTotalOut()); |
| writeLong(cDir, def.getTotalIn()); |
| } else { |
| curOffset += writeLong(cDir, crc.tbytes); |
| writeLong(cDir, crc.tbytes); |
| } |
| curOffset += writeShort(cDir, nameLength); |
| if (currentEntry.extra != null) { |
| curOffset += writeShort(cDir, currentEntry.extra.length); |
| } else { |
| writeShort(cDir, 0); |
| } |
| String c; |
| if ((c = currentEntry.getComment()) != null) { |
| writeShort(cDir, c.length()); |
| } else { |
| writeShort(cDir, 0); |
| } |
| writeShort(cDir, 0); // Disk Start |
| writeShort(cDir, 0); // Internal File Attributes |
| writeLong(cDir, 0); // External File Attributes |
| writeLong(cDir, offset); |
| cDir.write(nameBytes); |
| nameBytes = null; |
| if (currentEntry.extra != null) { |
| cDir.write(currentEntry.extra); |
| } |
| offset += curOffset; |
| if (c != null) { |
| cDir.write(c.getBytes()); |
| } |
| currentEntry = null; |
| crc.reset(); |
| def.reset(); |
| done = false; |
| } |
| |
| /** |
| * Indicates that all entries have been written to the stream. Any terminal |
| * information is written to the underlying stream. |
| * |
| * @throws IOException |
| * if an error occurs while terminating the stream. |
| */ |
| @Override |
| public void finish() throws IOException { |
| if (out == null) { |
| throw new IOException(Messages.getString("archive.1E")); //$NON-NLS-1$ |
| } |
| if (cDir == null) { |
| return; |
| } |
| if (entries.size() == 0) { |
| throw new ZipException(Messages.getString("archive.28")); //$NON-NLS-1$; |
| } |
| if (currentEntry != null) { |
| closeEntry(); |
| } |
| int cdirSize = cDir.size(); |
| // Write Central Dir End |
| writeLong(cDir, ENDSIG); |
| writeShort(cDir, 0); // Disk Number |
| writeShort(cDir, 0); // Start Disk |
| writeShort(cDir, entries.size()); // Number of entries |
| 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); |
| } |
| // Write the central dir |
| out.write(cDir.toByteArray()); |
| cDir = null; |
| |
| } |
| |
| /** |
| * Writes entry information to the underlying stream. Data associated with |
| * the entry can then be written using {@code write()}. After data is |
| * written {@code closeEntry()} must be called to complete the writing of |
| * the entry to the underlying stream. |
| * |
| * @param ze |
| * the {@code ZipEntry} to store. |
| * @throws IOException |
| * If an error occurs storing the entry. |
| * @see #write |
| */ |
| public void putNextEntry(ZipEntry ze) throws java.io.IOException { |
| if (currentEntry != null) { |
| closeEntry(); |
| } |
| if (ze.getMethod() == STORED |
| || (compressMethod == STORED && ze.getMethod() == -1)) { |
| if (ze.crc == -1) { |
| /* [MSG "archive.20", "Crc mismatch"] */ |
| throw new ZipException(Messages.getString("archive.20")); //$NON-NLS-1$ |
| } |
| if (ze.size == -1 && ze.compressedSize == -1) { |
| /* [MSG "archive.21", "Size mismatch"] */ |
| throw new ZipException(Messages.getString("archive.21")); //$NON-NLS-1$ |
| } |
| if (ze.size != ze.compressedSize && ze.compressedSize != -1 |
| && ze.size != -1) { |
| /* [MSG "archive.21", "Size mismatch"] */ |
| throw new ZipException(Messages.getString("archive.21")); //$NON-NLS-1$ |
| } |
| } |
| /* [MSG "archive.1E", "Stream is closed"] */ |
| if (cDir == null) { |
| throw new IOException(Messages.getString("archive.1E")); //$NON-NLS-1$ |
| } |
| if (entries.contains(ze.name)) { |
| /* [MSG "archive.29", "Entry already exists: {0}"] */ |
| throw new ZipException(Messages.getString("archive.29", ze.name)); //$NON-NLS-1$ |
| } |
| nameLength = utf8Count(ze.name); |
| if (nameLength > 0xffff) { |
| /* [MSG "archive.2A", "Name too long: {0}"] */ |
| throw new IllegalArgumentException(Messages.getString( |
| "archive.2A", ze.name)); //$NON-NLS-1$ |
| } |
| |
| def.setLevel(compressLevel); |
| currentEntry = ze; |
| entries.add(currentEntry.name); |
| if (currentEntry.getMethod() == -1) { |
| currentEntry.setMethod(compressMethod); |
| } |
| writeLong(out, LOCSIG); // Entry header |
| writeShort(out, ZIPLocalHeaderVersionNeeded); // Extraction version |
| writeShort(out, currentEntry.getMethod() == STORED ? 0 |
| : ZIPDataDescriptorFlag); |
| writeShort(out, currentEntry.getMethod()); |
| 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; |
| } |
| writeLong(out, currentEntry.crc); |
| writeLong(out, currentEntry.size); |
| writeLong(out, currentEntry.size); |
| } else { |
| writeLong(out, 0); |
| writeLong(out, 0); |
| writeLong(out, 0); |
| } |
| writeShort(out, nameLength); |
| if (currentEntry.extra != null) { |
| writeShort(out, currentEntry.extra.length); |
| } else { |
| writeShort(out, 0); |
| } |
| nameBytes = toUTF8Bytes(currentEntry.name, nameLength); |
| out.write(nameBytes); |
| if (currentEntry.extra != null) { |
| out.write(currentEntry.extra); |
| } |
| } |
| |
| /** |
| * Sets the {@code ZipFile} comment associated with the file being written. |
| * |
| * @param comment |
| * the comment associated with the file. |
| */ |
| public void setComment(String comment) { |
| if (comment.length() > 0xFFFF) { |
| throw new IllegalArgumentException(Messages.getString("archive.2B")); //$NON-NLS-1$ |
| } |
| this.comment = comment; |
| } |
| |
| /** |
| * Sets the compression level to be used for writing entry data. This level |
| * may be set on a per entry basis. The level must have a value between -1 |
| * and 8 according to the {@code Deflater} compression level bounds. |
| * |
| * @param level |
| * the compression level (ranging from -1 to 8). |
| * @see Deflater |
| */ |
| public void setLevel(int level) { |
| if (level < Deflater.DEFAULT_COMPRESSION |
| || level > Deflater.BEST_COMPRESSION) { |
| throw new IllegalArgumentException(); |
| } |
| compressLevel = 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. |
| */ |
| public void setMethod(int method) { |
| if (method != STORED && method != DEFLATED) { |
| throw new IllegalArgumentException(); |
| } |
| compressMethod = method; |
| |
| } |
| |
| private long writeLong(OutputStream os, long i) throws java.io.IOException { |
| // Write out the long value as an unsigned int |
| os.write((int) (i & 0xFF)); |
| os.write((int) (i >> 8) & 0xFF); |
| os.write((int) (i >> 16) & 0xFF); |
| os.write((int) (i >> 24) & 0xFF); |
| return i; |
| } |
| |
| private int writeShort(OutputStream os, int i) throws java.io.IOException { |
| os.write(i & 0xFF); |
| os.write((i >> 8) & 0xFF); |
| return i; |
| |
| } |
| |
| /** |
| * Writes data for the current entry to the underlying stream. |
| * |
| * @exception IOException |
| * If an error occurs writing to the stream |
| */ |
| @Override |
| public void write(byte[] buffer, int off, int nbytes) |
| throws java.io.IOException { |
| // avoid int overflow, check null buf |
| if ((off < 0 || (nbytes < 0) || off > buffer.length) |
| || (buffer.length - off < nbytes)) { |
| throw new IndexOutOfBoundsException(); |
| } |
| |
| if (currentEntry == null) { |
| /* [MSG "archive.2C", "No active entry"] */ |
| throw new ZipException(Messages.getString("archive.2C")); //$NON-NLS-1$ |
| } |
| |
| if (currentEntry.getMethod() == STORED) { |
| out.write(buffer, off, nbytes); |
| } else { |
| super.write(buffer, off, nbytes); |
| } |
| crc.update(buffer, off, nbytes); |
| } |
| |
| static int utf8Count(String value) { |
| int total = 0; |
| for (int i = value.length(); --i >= 0;) { |
| char ch = value.charAt(i); |
| if (ch < 0x80) { |
| total++; |
| } else if (ch < 0x800) { |
| total += 2; |
| } else { |
| total += 3; |
| } |
| } |
| return total; |
| } |
| |
| static byte[] toUTF8Bytes(String value, int length) { |
| byte[] result = new byte[length]; |
| int pos = result.length; |
| for (int i = value.length(); --i >= 0;) { |
| char ch = value.charAt(i); |
| if (ch < 0x80) { |
| result[--pos] = (byte) ch; |
| } else if (ch < 0x800) { |
| result[--pos] = (byte) (0x80 | (ch & 0x3f)); |
| result[--pos] = (byte) (0xc0 | (ch >> 6)); |
| } else { |
| result[--pos] = (byte) (0x80 | (ch & 0x3f)); |
| result[--pos] = (byte) (0x80 | ((ch >> 6) & 0x3f)); |
| result[--pos] = (byte) (0xe0 | (ch >> 12)); |
| } |
| } |
| return result; |
| } |
| } |