// 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.android.ziputils; import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENOFF; import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDOFF; import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIZ; import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSUB; import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDTOT; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** * API for writing to a zip archive. This does not currently perform compression, * but merely provides the facilities for creating a zip archive. The client must * ensure that file content is written conforming to the created headers. */ public class ZipOut { /** * Central directory output buffer block size */ private static final int DIR_BLOCK_SIZE = 1024 * 1024; private final String filename; // To ensure good performance when writing to a remote file system // we need to use asynchronous output. However, because we may want this // to run on and off devices, we can't depend on java.nio.AsynchronousFileChannel (JDK 1.7). // Instead, we use a FileChannel (JDK 1.4), and use a single-thread pool, // to execute writes serially, but non-blocking from our client's // point-of-view. All data written, are assumed to remain unchanged until the write is complete. // ZipIn is designed to not reuse internal buffers, to make direct data transfer safe. // This optimizes the common cases where input is processed serially. private final FileChannel fileChannel; private final ExecutorService executor; private final List<Future<?>> futures; private final List<CentralDirectory> centralDirectory; private CentralDirectory current; private int fileOffset = 0; private int entryCount = 0; private boolean finished = false; private final boolean verbose = false; /** * Creates a {@code ZipOut} for writing to file, with the given (nick)name. * * @param channel File channel open for output. * @param filename File name or nickname. * @throws java.io.IOException */ public ZipOut(FileChannel channel, String filename) throws IOException { this.executor = Executors.newSingleThreadExecutor(); this.futures = new ArrayList<>(); this.fileChannel = channel; this.filename = filename; centralDirectory = new ArrayList<>(); fileOffset = (int) fileChannel.position(); } /** * Returns a writable copy of the given * {@link com.google.devtools.build.android.ziputils.DirectoryEntry}, backed by an internal * direct byte buffer, allocated over storage for this files central directory. The file offset * is set, and must not be changed. The variable entry data (filename, extra data and comment), * must not be changed (in a way that changes the total size of the directory entry). * @param entry directory entry to copy. * @return a writable directory entry view, over the provided byte buffer. */ public DirectoryEntry nextEntry(DirectoryEntry entry) { entryCount++; int size = entry.getSize(); if (current == null || current.buffer.remaining() < size) { ByteBuffer buffer = ByteBuffer.allocateDirect(DIR_BLOCK_SIZE); current = CentralDirectory.viewOf(buffer); centralDirectory.add(current); } return current.nextEntry(entry).set(CENOFF, fileOffset); } /** * Writes content to the current entry. Content is written as-is, and the client is responsible * for compression, consistent with the storage method set in the current directory entry. The * client must first write an appropriate local file header, and if necessary, complete the entry * with a data descriptor. If the header indicates that the content is compressed, the client is * responsible for compressing the data before writing. * * <p>Data is written serially, but asynchronously, to the output file. The client must not change * the underlying data after it has been scheduled for writing. Usually, a client will release * any references to the data, so that storage may be eligible for GC, once the write operation * has completed. It's safe to pass references to views of data obtained from a {#link ZipIn}, * object, because {@code ZipIn} doesn't reuse internal buffers. * * @param content */ public synchronized void write(ByteBuffer content) { fileOffset += content.remaining(); futures.add(executor.submit(new OutputTask(content))); } /** * Writes a {@link com.google.devtools.build.android.ziputils.View} to the current entry. * Used to write a {@link com.google.devtools.build.android.ziputils.LocalFileHeader} * before the content, and if needed, a * {@link com.google.devtools.build.android.ziputils.DataDescriptor} after the content. * <P> * See also {@link #write(java.nio.ByteBuffer)}. * </P> * * @param view the view to write as part of the current entry. * @throws java.io.IOException */ public void write(View<?> view) throws IOException { view.at(fileOffset).buffer.rewind(); write(view.buffer); } /** * Returns the file position for the next write operation. Because writes are asynchronous, this * may not be the actual position of the underlying file channel. * @return the file position position for the next write operation. */ public int fileOffset() { return fileOffset; } /** * Writes out the central directory. This doesn't close the output file. */ public void finish() throws IOException { if (finished) { return; } finished = true; int cdOffset = fileOffset; for (CentralDirectory cd : centralDirectory) { //cd.finish().buffer.rewind(); cd.buffer.flip(); write(cd.buffer); } int size = fileOffset - cdOffset; verbose("ZipOut finishing: " + filename); verbose("-- CDIR: " + cdOffset + " count: " + entryCount); verbose("-- EOCD: " + fileOffset + " size: " + size); EndOfCentralDirectory eocd = EndOfCentralDirectory.allocate(null) .set(ENDSUB, (short) entryCount) .set(ENDTOT, (short) entryCount) .set(ENDSIZ, size) .set(ENDOFF, cdOffset) .at(fileOffset); eocd.buffer.rewind(); write(eocd.buffer); verbose("-- size: " + fileOffset); } /** * Closes the output file. If this object has not been finished yet, this method will call * {@link #finish()} before closing the output channel. * * @throws java.io.IOException */ public void close() throws IOException { if (!finished) { finish(); } try { executor.shutdown(); executor.awaitTermination(30, TimeUnit.SECONDS); for (Future<?> f : futures) { try { f.get(); } catch (ExecutionException ex) { throw new IOException(ex.getCause().getMessage()); } } } catch (InterruptedException ex) { executor.shutdownNow(); Thread.currentThread().interrupt(); } fileChannel.close(); } /** * Helper class to write asynchronously to the output channel. */ private class OutputTask implements Runnable { final ByteBuffer buffer; public OutputTask(ByteBuffer buffer) { this.buffer = buffer; } @Override public void run() { try { fileChannel.write(buffer); } catch (IOException ex) { throw new IllegalStateException("Unexpected IOException writing to output channel"); } } } private void verbose(String msg) { if (verbose) { System.out.println(msg); } } }