| // Copyright 2015 The Bazel Authors. 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 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(); |
| } |
| } |
| } |