/* * Copyright 2013-present Facebook, Inc. * * 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.facebook.buck.zip; import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipException; /** * An implementation of an {@link OutputStream} that will zip output. Note that, just as with {@link * java.util.zip.ZipOutputStream}, no implementation of this is thread-safe. */ public class CustomZipOutputStream extends OutputStream { protected static interface Impl { /** * Called by {@link CustomZipOutputStream#putNextEntry(ZipEntry)} and used by impls to put the * next entry into the zip file. It is guaranteed that the {@code entry} won't be null and the * stream will be open. It is also guaranteed that there's no current entry open. * * @param entry The {@link ZipEntry} to write. */ void actuallyPutNextEntry(ZipEntry entry) throws IOException; /** * Called by {@link CustomZipOutputStream#close()} and used by impls to close the delegate * stream. This method will be called at most once in the lifecycle of the * CustomZipOutputStream. */ void actuallyCloseEntry() throws IOException; /** * Called by {@link CustomZipOutputStream#write(byte[], int, int)} only once it is known that * the stream has not been closed, and that a {@link ZipEntry} has already been put on the * stream and not closed. */ void actuallyWrite(byte b[], int off, int len) throws IOException; void actuallyClose() throws IOException; } private final Impl impl; private State state; private boolean entryOpen; protected CustomZipOutputStream(Impl impl) { this.impl = impl; this.state = State.CLEAN; } public final void putNextEntry(ZipEntry entry) throws IOException { Preconditions.checkState(state != State.CLOSED, "Stream has been closed."); state = State.OPEN; closeEntry(); validateEntry(entry); impl.actuallyPutNextEntry(entry); entryOpen = true; } private void validateEntry(ZipEntry entry) { if (entry.getMethod() == ZipEntry.STORED) { Preconditions.checkState( entry.getCompressedSize() == entry.getSize(), "STORED entry where compressed != uncompressed size"); } } public final void closeEntry() throws IOException { Preconditions.checkState(state != State.CLOSED, "Stream has been closed"); if (!entryOpen) { return; // As ZipOutputStream does. } entryOpen = false; impl.actuallyCloseEntry(); } @Override public final void write(byte[] b, int off, int len) throws IOException { Preconditions.checkState(state != State.CLOSED, "Stream has been closed."); if (!entryOpen) { // Same exception as Java's ZipOutputStream. throw new ZipException("no current ZIP entry"); } impl.actuallyWrite(b, off, len); } // javadocs taken from OutputStream and amended to make it clear what we're doing here. /** * Writes the specified byte to this output stream. Specifically one byte is written to the output * stream. The byte to be written is the eight low-order bits of the argument <code>b</code>. The * 24 high-order bits of <code>b</code> are ignored. * * @param b the <code>byte</code>. * @exception IOException if an I/O error occurs. In particular, an <code>IOException</code> may * be thrown if the output stream has been closed. */ @Override public void write(int b) throws IOException { byte[] buf = new byte[1]; buf[0] = (byte) (b & 0xff); write(buf, 0, 1); } public void writeEntry(String name, InputStream contents) throws IOException { try { putNextEntry(new CustomZipEntry(name)); ByteStreams.copy(contents, this); closeEntry(); } finally { contents.close(); } } @Override public final void close() throws IOException { if (state == State.CLOSED) { return; // no-op to call close again. } try { closeEntry(); impl.actuallyClose(); } finally { state = State.CLOSED; } } /** * State of a {@link com.facebook.buck.zip.CustomZipOutputStream}. Certain operations are only * available when the stream is in a particular state. */ private static enum State { CLEAN, // Open but no data written. OPEN, // Open and data written. CLOSED, // Just as it says on the tin. } }