|  | /* | 
|  | * 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.facebook.buck.timing.Clock; | 
|  | import com.google.common.base.Charsets; | 
|  | import com.google.common.base.Preconditions; | 
|  | import com.google.common.hash.Hasher; | 
|  | import com.google.common.hash.Hashing; | 
|  |  | 
|  | import java.io.ByteArrayOutputStream; | 
|  | import java.io.IOException; | 
|  | import java.io.OutputStream; | 
|  | import java.util.Arrays; | 
|  | import java.util.Calendar; | 
|  | import java.util.zip.Deflater; | 
|  | import java.util.zip.ZipEntry; | 
|  |  | 
|  | /** | 
|  | * A wrapper containing the {@link ZipEntry} and additional book keeping information required to | 
|  | * write the entry to a zip file. | 
|  | */ | 
|  | class EntryAccounting { | 
|  | private static final int DATA_DESCRIPTOR_FLAG = 1 << 3; | 
|  | private static final int UTF8_NAMES_FLAG = 1 << 11; | 
|  | private static final int ARBITRARY_SIZE = 1024; | 
|  | private static final long DOS_EPOCH_START = (1 << 21) | (1 << 16); | 
|  |  | 
|  | private final ZipEntry entry; | 
|  | private final Method method; | 
|  | private Hasher crc = Hashing.crc32().newHasher(); | 
|  | private long offset; | 
|  | /* | 
|  | * General purpose bit flag: | 
|  | *  Bit 00: encrypted file | 
|  | *  Bit 01: compression option | 
|  | *  Bit 02: compression option | 
|  | *  Bit 03: data descriptor | 
|  | *  Bit 04: enhanced deflation | 
|  | *  Bit 05: compressed patched data | 
|  | *  Bit 06: strong encryption | 
|  | *  Bit 07-10: unused | 
|  | *  Bit 11: language encoding | 
|  | *  Bit 12: reserved | 
|  | *  Bit 13: mask header values | 
|  | *  Bit 14-15: reserved | 
|  | * | 
|  | *  The important one is bit 3: the data descriptor. | 
|  | *  Defaults to indicate that names are stored as UTF8. | 
|  | */ | 
|  | private int flags = UTF8_NAMES_FLAG; | 
|  | private final Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true); | 
|  | private final byte[] buffer = new byte[ARBITRARY_SIZE]; | 
|  |  | 
|  | public EntryAccounting(Clock clock, ZipEntry entry, long currentOffset) { | 
|  | this.entry = Preconditions.checkNotNull(entry); | 
|  | this.method = Method.detect(entry.getMethod()); | 
|  | Preconditions.checkNotNull(clock); | 
|  | this.offset = currentOffset; | 
|  |  | 
|  | if (entry.getTime() == -1) { | 
|  | entry.setTime(clock.currentTimeMillis()); | 
|  | } | 
|  |  | 
|  | if (entry instanceof CustomZipEntry) { | 
|  | deflater.setLevel(((CustomZipEntry) entry).getCompressionLevel()); | 
|  | } | 
|  | } | 
|  |  | 
|  | public void updateCrc(byte[] b, int off, int len) { | 
|  | crc = crc.putBytes(b, off, len); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return The time of the entry in DOS format. | 
|  | */ | 
|  | public long getTime() { | 
|  | // It'd be nice to use a Calendar for this, but (and here's the fun bit), that's a Really Bad | 
|  | // Idea since the calendar's internal time representation keeps ticking once set. Instead, do | 
|  | // this long way. | 
|  |  | 
|  | Calendar instance = Calendar.getInstance(); | 
|  | instance.setTimeInMillis(entry.getTime()); | 
|  |  | 
|  | int year = instance.get(Calendar.YEAR); | 
|  |  | 
|  | // The DOS epoch begins in 1980. If the year is before that, then default to the start of the | 
|  | // epoch (the 1st day of the 1st month) | 
|  | if (year < 1980) { | 
|  | return DOS_EPOCH_START; | 
|  | } | 
|  | return (year - 1980) << 25 | | 
|  | (instance.get(Calendar.MONTH) + 1) << 21 | | 
|  | instance.get(Calendar.DAY_OF_MONTH) << 16 | | 
|  | instance.get(Calendar.HOUR_OF_DAY) << 11 | | 
|  | instance.get(Calendar.MINUTE) << 5 | | 
|  | instance.get(Calendar.SECOND) >> 1; | 
|  | } | 
|  |  | 
|  | private boolean isDeflated() { | 
|  | return method == Method.DEFLATE; | 
|  | } | 
|  |  | 
|  | public String getName() { | 
|  | return entry.getName(); | 
|  | } | 
|  |  | 
|  | public long getSize() { | 
|  | return entry.getSize(); | 
|  | } | 
|  |  | 
|  | public long getCompressedSize() { | 
|  | return entry.getCompressedSize(); | 
|  | } | 
|  |  | 
|  | public int getFlags() { | 
|  | return flags; | 
|  | } | 
|  |  | 
|  | public long getOffset() { | 
|  | return offset; | 
|  | } | 
|  |  | 
|  | public void setOffset(long offset) { | 
|  | this.offset = offset; | 
|  | } | 
|  |  | 
|  | public long getCrc() { | 
|  | return entry.getCrc(); | 
|  | } | 
|  |  | 
|  | public void calculateCrc() { | 
|  | entry.setCrc(crc.hash().padToLong()); | 
|  | } | 
|  |  | 
|  | public int getCompressionMethod() { | 
|  | return method.compressionMethod; | 
|  | } | 
|  |  | 
|  | public int getRequiredExtractVersion() { | 
|  | return method.getRequiredExtractVersion(); | 
|  | } | 
|  |  | 
|  | public long writeLocalFileHeader(OutputStream out) throws IOException { | 
|  | if (method == Method.DEFLATE) { | 
|  | flags |= DATA_DESCRIPTOR_FLAG; | 
|  |  | 
|  | // See http://www.pkware.com/documents/casestudies/APPNOTE.TXT (section 4.4.4) | 
|  | // Essentially, we're about to set bits 1 and 2 to indicate to tools such as zipinfo which | 
|  | // level of compression we're using. If we've not set a compression level, then we're using | 
|  | // the default one, which is right. It turns out. For your viewing pleasure: | 
|  | // | 
|  | // +----------+-------+-------+ | 
|  | // | Level    | Bit 1 | Bit 2 | | 
|  | // +----------+-------+-------+ | 
|  | // | Fastest  |   0   |   1   | | 
|  | // | Normal   |   0   |   0   | | 
|  | // | Best     |   1   |   0   | | 
|  | // +----------+-------+-------+ | 
|  | if (entry instanceof CustomZipEntry) { | 
|  | int level = ((CustomZipEntry) entry).getCompressionLevel(); | 
|  | switch (level) { | 
|  | case Deflater.BEST_COMPRESSION: | 
|  | flags |= (1 << 1); | 
|  | break; | 
|  |  | 
|  | case Deflater.BEST_SPEED: | 
|  | flags |= (1 << 2); | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { | 
|  | ByteIo.writeInt(stream, ZipEntry.LOCSIG); | 
|  |  | 
|  | ByteIo.writeShort(stream, getRequiredExtractVersion()); | 
|  | ByteIo.writeShort(stream, flags); | 
|  | ByteIo.writeShort(stream, getCompressionMethod()); | 
|  | ByteIo.writeInt(stream, getTime()); | 
|  |  | 
|  | // In deflate mode, we don't know the size or CRC of the data. | 
|  | if (isDeflated()) { | 
|  | ByteIo.writeInt(stream, 0); | 
|  | ByteIo.writeInt(stream, 0); | 
|  | ByteIo.writeInt(stream, 0); | 
|  | } else { | 
|  | ByteIo.writeInt(stream, entry.getCrc()); | 
|  | ByteIo.writeInt(stream, entry.getSize()); | 
|  | ByteIo.writeInt(stream, entry.getSize()); | 
|  | } | 
|  |  | 
|  | byte[] nameBytes = entry.getName().getBytes(Charsets.UTF_8); | 
|  | ByteIo.writeShort(stream, nameBytes.length); | 
|  | ByteIo.writeShort(stream, 0); | 
|  | stream.write(nameBytes); | 
|  |  | 
|  | byte[] bytes = stream.toByteArray(); | 
|  | out.write(bytes); | 
|  | return bytes.length; | 
|  | } | 
|  | } | 
|  |  | 
|  | private byte[] close() throws IOException { | 
|  | if (!isDeflated()) { | 
|  | return new byte[0]; | 
|  | } | 
|  |  | 
|  | try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { | 
|  | ByteIo.writeInt(out, ZipEntry.EXTSIG); | 
|  | ByteIo.writeInt(out, getCrc()); | 
|  | ByteIo.writeInt(out, getCompressedSize()); | 
|  | ByteIo.writeInt(out, getSize()); | 
|  |  | 
|  | return out.toByteArray(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private int deflate(OutputStream out) throws IOException { | 
|  | int written = deflater.deflate(buffer, 0, buffer.length); | 
|  | if (written > 0) { | 
|  | out.write(Arrays.copyOf(buffer, written)); | 
|  | } | 
|  | return written; | 
|  | } | 
|  |  | 
|  | public long write(OutputStream out, byte[] b, int off, int len) throws IOException { | 
|  | updateCrc(b, off, len); | 
|  |  | 
|  | if (!isDeflated()) { | 
|  | out.write(b, off, len); | 
|  | return len; | 
|  | } | 
|  |  | 
|  | if (len == 0) { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | Preconditions.checkState(!deflater.finished()); | 
|  | deflater.setInput(b, off, len); | 
|  |  | 
|  | while (!deflater.needsInput()) { | 
|  | deflate(out); | 
|  | } | 
|  | return 0; // We calculate how many bytes we write when closing deflated entries. | 
|  | } | 
|  |  | 
|  | public long close(OutputStream out) throws IOException { | 
|  | if (!isDeflated()) { | 
|  | // Nothing left to do. | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | deflater.finish(); | 
|  | while (!deflater.finished()) { | 
|  | deflate(out); | 
|  | } | 
|  | entry.setSize(deflater.getBytesRead()); | 
|  | entry.setCompressedSize(deflater.getBytesWritten()); | 
|  | calculateCrc(); | 
|  |  | 
|  | deflater.end(); | 
|  |  | 
|  | byte[] closeBytes = close(); | 
|  | out.write(closeBytes); | 
|  |  | 
|  | return entry.getCompressedSize() + closeBytes.length; | 
|  | } | 
|  |  | 
|  |  | 
|  | private static enum Method { | 
|  | DEFLATE(ZipEntry.DEFLATED, 20, 8), | 
|  | STORE(ZipEntry.STORED, 10, 0), | 
|  | ; | 
|  |  | 
|  | private final int zipEntryMethod; | 
|  | private final int requiredVersion; | 
|  | private final int compressionMethod; | 
|  |  | 
|  | private Method(int zipEntryMethod, int requiredVersion, int compressionMethod) { | 
|  | this.zipEntryMethod = zipEntryMethod; | 
|  | this.requiredVersion = requiredVersion; | 
|  | this.compressionMethod = compressionMethod; | 
|  | } | 
|  |  | 
|  | public int getRequiredExtractVersion() { | 
|  | return requiredVersion; | 
|  | } | 
|  |  | 
|  | public static Method detect(int fromZipMethod) { | 
|  | if (fromZipMethod == -1) { | 
|  | return DEFLATE; | 
|  | } | 
|  |  | 
|  | for (Method value : values()) { | 
|  | if (value.zipEntryMethod == fromZipMethod) { | 
|  | return value; | 
|  | } | 
|  | } | 
|  |  | 
|  | throw new IllegalArgumentException("Cannot determine zip method from: " + fromZipMethod); | 
|  | } | 
|  | } | 
|  | } |