| /* |
| * 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; |
| private long externalAttributes = 0; |
| |
| /* |
| * 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 = entry; |
| this.method = Method.detect(entry.getMethod()); |
| this.offset = currentOffset; |
| |
| if (entry.getTime() == -1) { |
| entry.setTime(clock.currentTimeMillis()); |
| } |
| |
| if (entry instanceof CustomZipEntry) { |
| deflater.setLevel(((CustomZipEntry) entry).getCompressionLevel()); |
| externalAttributes = ((CustomZipEntry) entry).getExternalAttributes(); |
| } |
| } |
| |
| 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 getExternalAttributes() { |
| return externalAttributes; |
| } |
| |
| 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); |
| } |
| } |
| |
| } |