blob: 81bcc5343059cca6cf276e66821baf0bbbe105b8 [file] [log] [blame]
/*
* 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);
}
}
}