| // 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.zip; |
| |
| import com.google.devtools.build.zip.ZipFileEntry.Compression; |
| import com.google.devtools.build.zip.ZipFileEntry.Feature; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.charset.Charset; |
| import java.util.EnumSet; |
| import java.util.zip.ZipException; |
| |
| class CentralDirectoryFileHeader { |
| static final int SIGNATURE = 0x02014b50; |
| static final int FIXED_DATA_SIZE = 46; |
| static final int SIGNATURE_OFFSET = 0; |
| static final int VERSION_OFFSET = 4; |
| static final int VERSION_NEEDED_OFFSET = 6; |
| static final int FLAGS_OFFSET = 8; |
| static final int METHOD_OFFSET = 10; |
| static final int MOD_TIME_OFFSET = 12; |
| static final int CRC_OFFSET = 16; |
| static final int COMPRESSED_SIZE_OFFSET = 20; |
| static final int UNCOMPRESSED_SIZE_OFFSET = 24; |
| static final int FILENAME_LENGTH_OFFSET = 28; |
| static final int EXTRA_FIELD_LENGTH_OFFSET = 30; |
| static final int COMMENT_LENGTH_OFFSET = 32; |
| static final int DISK_START_OFFSET = 34; |
| static final int INTERNAL_ATTRIBUTES_OFFSET = 36; |
| static final int EXTERNAL_ATTRIBUTES_OFFSET = 38; |
| static final int LOCAL_HEADER_OFFSET_OFFSET = 42; |
| |
| /** |
| * Reads a {@link ZipFileEntry} from the input stream, using the specified {@link Charset} to |
| * decode the filename and comment. |
| */ |
| static ZipFileEntry read(InputStream in, Charset charset) |
| throws IOException { |
| byte[] fixedSizeData = new byte[FIXED_DATA_SIZE]; |
| |
| if (ZipUtil.readFully(in, fixedSizeData) != FIXED_DATA_SIZE) { |
| throw new ZipException( |
| "Unexpected end of file while reading Central Directory File Header."); |
| } |
| if (!ZipUtil.arrayStartsWith(fixedSizeData, ZipUtil.intToLittleEndian(SIGNATURE))) { |
| throw new ZipException(String.format( |
| "Malformed Central Directory File Header; does not start with %08x", SIGNATURE)); |
| } |
| |
| byte[] name = new byte[ZipUtil.getUnsignedShort(fixedSizeData, FILENAME_LENGTH_OFFSET)]; |
| byte[] extraField = new byte[ |
| ZipUtil.getUnsignedShort(fixedSizeData, EXTRA_FIELD_LENGTH_OFFSET)]; |
| byte[] comment = new byte[ZipUtil.getUnsignedShort(fixedSizeData, COMMENT_LENGTH_OFFSET)]; |
| |
| if (name.length > 0 && ZipUtil.readFully(in, name) != name.length) { |
| throw new ZipException( |
| "Unexpected end of file while reading Central Directory File Header."); |
| } |
| if (extraField.length > 0 && ZipUtil.readFully(in, extraField) != extraField.length) { |
| throw new ZipException( |
| "Unexpected end of file while reading Central Directory File Header."); |
| } |
| if (comment.length > 0 && ZipUtil.readFully(in, comment) != comment.length) { |
| throw new ZipException( |
| "Unexpected end of file while reading Central Directory File Header."); |
| } |
| |
| ExtraDataList extra = new ExtraDataList(extraField); |
| |
| long csize = ZipUtil.getUnsignedInt(fixedSizeData, COMPRESSED_SIZE_OFFSET); |
| long size = ZipUtil.getUnsignedInt(fixedSizeData, UNCOMPRESSED_SIZE_OFFSET); |
| long offset = ZipUtil.getUnsignedInt(fixedSizeData, LOCAL_HEADER_OFFSET_OFFSET); |
| if (csize == 0xffffffffL || size == 0xffffffffL || offset == 0xffffffffL) { |
| ExtraData zip64Extra = extra.get((short) 0x0001); |
| if (zip64Extra != null) { |
| int index = 0; |
| if (size == 0xffffffffL) { |
| size = ZipUtil.getUnsignedLong(zip64Extra.getData(), index); |
| index += 8; |
| } |
| if (csize == 0xffffffffL) { |
| csize = ZipUtil.getUnsignedLong(zip64Extra.getData(), index); |
| index += 8; |
| } |
| if (offset == 0xffffffffL) { |
| offset = ZipUtil.getUnsignedLong(zip64Extra.getData(), index); |
| index += 8; |
| } |
| } |
| } |
| |
| ZipFileEntry entry = new ZipFileEntry(new String(name, charset)); |
| entry.setVersion(ZipUtil.get16(fixedSizeData, VERSION_OFFSET)); |
| entry.setVersionNeeded(ZipUtil.get16(fixedSizeData, VERSION_NEEDED_OFFSET)); |
| entry.setFlags(ZipUtil.get16(fixedSizeData, FLAGS_OFFSET)); |
| entry.setMethod(Compression.fromValue(ZipUtil.get16(fixedSizeData, METHOD_OFFSET))); |
| long time = ZipUtil.dosToUnixTime(ZipUtil.get32(fixedSizeData, MOD_TIME_OFFSET)); |
| entry.setTime(ZipUtil.isValidInDos(time) ? time : ZipUtil.DOS_EPOCH); |
| entry.setCrc(ZipUtil.getUnsignedInt(fixedSizeData, CRC_OFFSET)); |
| entry.setCompressedSize(csize); |
| entry.setSize(size); |
| entry.setInternalAttributes(ZipUtil.get16(fixedSizeData, INTERNAL_ATTRIBUTES_OFFSET)); |
| entry.setExternalAttributes(ZipUtil.get32(fixedSizeData, EXTERNAL_ATTRIBUTES_OFFSET)); |
| entry.setLocalHeaderOffset(offset); |
| entry.setExtra(extra); |
| entry.setComment(new String(comment, charset)); |
| |
| return entry; |
| } |
| |
| /** |
| * Generates the raw byte data of the central directory file header for the ZipEntry. Uses the |
| * specified {@link ZipFileData} to encode the file name and comment. |
| * @throws ZipException |
| */ |
| static byte[] create(ZipFileEntry entry, ZipFileData file, boolean allowZip64) |
| throws ZipException { |
| if (allowZip64) { |
| addZip64Extra(entry); |
| } else { |
| entry.getExtra().remove((short) 0x0001); |
| } |
| byte[] name = file.getBytes(entry.getName()); |
| byte[] extra = entry.getExtra().getBytes(); |
| byte[] comment = entry.getComment() != null |
| ? file.getBytes(entry.getComment()) : new byte[]{}; |
| |
| byte[] buf = new byte[FIXED_DATA_SIZE + name.length + extra.length + comment.length]; |
| |
| fillFixedSizeData(buf, entry, name.length, extra.length, comment.length, allowZip64); |
| System.arraycopy(name, 0, buf, FIXED_DATA_SIZE, name.length); |
| System.arraycopy(extra, 0, buf, FIXED_DATA_SIZE + name.length, extra.length); |
| System.arraycopy(comment, 0, buf, FIXED_DATA_SIZE + name.length + extra.length, |
| comment.length); |
| |
| return buf; |
| } |
| |
| /** |
| * Writes the central directory file header for the ZipEntry to an output stream. Uses the |
| * specified {@link ZipFileData} to encode the file name and comment. |
| */ |
| static int write(ZipFileEntry entry, ZipFileData file, boolean allowZip64, byte[] buf, |
| OutputStream stream) throws IOException { |
| if (buf == null || buf.length < FIXED_DATA_SIZE) { |
| buf = new byte[FIXED_DATA_SIZE]; |
| } |
| |
| ExtraDataList extra = new ExtraDataList(entry.getExtra()); |
| if (allowZip64) { |
| addZip64Extra(entry); |
| } else { |
| extra.remove((short) 0x0001); |
| } |
| |
| extra.remove(ExtraDataList.EXTENDED_TIMESTAMP); |
| extra.remove(ExtraDataList.INFOZIP_UNIX_NEW); |
| |
| byte[] name = entry.getName().getBytes(file.getCharset()); |
| byte[] extraBytes = extra.getBytes(); |
| byte[] comment = entry.getComment() != null |
| ? entry.getComment().getBytes(file.getCharset()) : new byte[]{}; |
| |
| fillFixedSizeData(buf, entry, name.length, extraBytes.length, comment.length, allowZip64); |
| stream.write(buf, 0, FIXED_DATA_SIZE); |
| stream.write(name); |
| stream.write(extraBytes); |
| stream.write(comment); |
| |
| return FIXED_DATA_SIZE + name.length + extraBytes.length + comment.length; |
| } |
| |
| /** |
| * Write the fixed size data portion for the specified ZIP entry to the buffer. |
| * @throws ZipException |
| */ |
| private static void fillFixedSizeData(byte[] buf, ZipFileEntry entry, int nameLength, |
| int extraLength, int commentLength, boolean allowZip64) throws ZipException { |
| if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_CSIZE)) { |
| throw new ZipException(String.format("Writing an entry with compressed size %d without" |
| + " Zip64 extensions is not supported.", entry.getCompressedSize())); |
| } |
| if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_SIZE)) { |
| throw new ZipException(String.format("Writing an entry of size %d without" |
| + " Zip64 extensions is not supported.", entry.getSize())); |
| } |
| if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_OFFSET)) { |
| throw new ZipException(String.format("Writing an entry with local header offset %d without" |
| + " Zip64 extensions is not supported.", entry.getLocalHeaderOffset())); |
| } |
| int csize = (int) (entry.getFeatureSet().contains(Feature.ZIP64_CSIZE) |
| ? -1 : entry.getCompressedSize()); |
| int size = (int) (entry.getFeatureSet().contains(Feature.ZIP64_SIZE) |
| ? -1 : entry.getSize()); |
| int offset = (int) (entry.getFeatureSet().contains(Feature.ZIP64_OFFSET) |
| ? -1 : entry.getLocalHeaderOffset()); |
| ZipUtil.intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE); |
| ZipUtil.shortToLittleEndian(buf, VERSION_OFFSET, entry.getVersion()); |
| ZipUtil.shortToLittleEndian(buf, VERSION_NEEDED_OFFSET, entry.getVersionNeeded()); |
| ZipUtil.shortToLittleEndian(buf, FLAGS_OFFSET, entry.getFlags()); |
| ZipUtil.shortToLittleEndian(buf, METHOD_OFFSET, entry.getMethod().getValue()); |
| ZipUtil.intToLittleEndian(buf, MOD_TIME_OFFSET, ZipUtil.unixToDosTime(entry.getTime())); |
| ZipUtil.intToLittleEndian(buf, CRC_OFFSET, (int) (entry.getCrc() & 0xffffffff)); |
| ZipUtil.intToLittleEndian(buf, COMPRESSED_SIZE_OFFSET, csize); |
| ZipUtil.intToLittleEndian(buf, UNCOMPRESSED_SIZE_OFFSET, size); |
| ZipUtil.shortToLittleEndian(buf, FILENAME_LENGTH_OFFSET, (short) (nameLength & 0xffff)); |
| ZipUtil.shortToLittleEndian(buf, EXTRA_FIELD_LENGTH_OFFSET, (short) (extraLength & 0xffff)); |
| ZipUtil.shortToLittleEndian(buf, COMMENT_LENGTH_OFFSET, (short) (commentLength & 0xffff)); |
| ZipUtil.shortToLittleEndian(buf, DISK_START_OFFSET, (short) 0); |
| ZipUtil.shortToLittleEndian(buf, INTERNAL_ATTRIBUTES_OFFSET, entry.getInternalAttributes()); |
| ZipUtil.intToLittleEndian(buf, EXTERNAL_ATTRIBUTES_OFFSET, entry.getExternalAttributes()); |
| ZipUtil.intToLittleEndian(buf, LOCAL_HEADER_OFFSET_OFFSET, offset); |
| } |
| |
| /** |
| * Update the extra data fields to contain a Zip64 extended information field if required |
| */ |
| private static void addZip64Extra(ZipFileEntry entry) { |
| EnumSet<Feature> features = entry.getFeatureSet(); |
| ExtraDataList extra = entry.getExtra(); |
| int extraSize = 0; |
| if (features.contains(Feature.ZIP64_SIZE)) { |
| extraSize += 8; |
| } |
| if (features.contains(Feature.ZIP64_CSIZE)) { |
| extraSize += 8; |
| } |
| if (features.contains(Feature.ZIP64_OFFSET)) { |
| extraSize += 8; |
| } |
| if (extraSize > 0) { |
| extra.remove((short) 0x0001); |
| byte[] zip64Extra = new byte[ExtraData.FIXED_DATA_SIZE + extraSize]; |
| ZipUtil.shortToLittleEndian(zip64Extra, ExtraData.ID_OFFSET, (short) 0x0001); |
| ZipUtil.shortToLittleEndian(zip64Extra, ExtraData.LENGTH_OFFSET, (short) extraSize); |
| int offset = ExtraData.FIXED_DATA_SIZE; |
| if (features.contains(Feature.ZIP64_SIZE)) { |
| ZipUtil.longToLittleEndian(zip64Extra, offset, entry.getSize()); |
| offset += 8; |
| } |
| if (features.contains(Feature.ZIP64_CSIZE)) { |
| ZipUtil.longToLittleEndian(zip64Extra, offset, entry.getCompressedSize()); |
| offset += 8; |
| } |
| if (features.contains(Feature.ZIP64_OFFSET)) { |
| ZipUtil.longToLittleEndian(zip64Extra, offset, entry.getLocalHeaderOffset()); |
| } |
| extra.add(new ExtraData(zip64Extra, 0)); |
| } |
| } |
| } |