| // Copyright 2014 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.xcode.zip; |
| |
| import com.google.common.collect.ImmutableMap; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.ReadableByteChannel; |
| import java.nio.channels.SeekableByteChannel; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Map; |
| |
| /** |
| * Utility code for reading information from zip files. |
| */ |
| public final class ZipFiles { |
| /** Read a little-endian integer comprised of {@code count} bytes from the input channel. */ |
| private static int readBytes(int count, ReadableByteChannel input) throws IOException { |
| ByteBuffer buffer = ByteBuffer.allocate(count); |
| if (input.read(buffer) != count) { |
| throw new IOException("could not read expected number of bytes: " + count); |
| } |
| int result = 0; |
| for (int i = count - 1; i >= 0; i--) { |
| result <<= 8; |
| result |= buffer.get(i) & 0xff; |
| } |
| return result; |
| } |
| |
| /** |
| * Returns the external file attributes of each entry as a mapping from the entry name to the |
| * 32-bit value. As long as the attributes are generated by a Unix host, this includes the POSIX |
| * file permissions in the upper two bytes. Entries not generated by a Unix host are not included |
| * in the result. |
| */ |
| public static Map<String, Integer> unixExternalFileAttributes(Path zipFile) throws IOException { |
| // Field descriptions in comments were taken from this document: |
| // http://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT |
| ImmutableMap.Builder<String, Integer> attributes = new ImmutableMap.Builder<>(); |
| try (SeekableByteChannel input = Files.newByteChannel(zipFile)) { |
| // The data we care about is toward the end of the file, after the compressed data for each |
| // file. We begin by looking for the start of the end of central directory record, which is |
| // marked by the signature 0x06054b50 |
| // |
| // This contains the centralDirectoryStartOffset value, which tells us where to seek to find |
| // the first central directory entry. Each such entry is marked by the signature 0x02014b50 |
| // and appear in sequence, one entry for each file in the .zip. |
| // |
| // The central directory entry contains many values, including the file name, the external |
| // file attributes, and the version made by value. If the version made by indicates a Unix |
| // host (0x03??), we include the external file attributes in the returned map. |
| |
| long offset = input.size() - 4; |
| while (offset >= 0) { |
| input.position(offset); |
| int signature = readBytes(4, input); |
| if (signature == 0x06054b50) { |
| break; |
| } else if (signature == 0x06064b50) { |
| throw new IOException("Zip64 format not supported: " + zipFile); |
| } |
| offset--; |
| } |
| if (offset < 0) { |
| throw new IOException(); |
| } |
| |
| // Read end of central directory structure |
| input.position(input.position() |
| + 2 // number of this disk |
| + 2 // number of the disk with the start of the central directory |
| ); |
| int entryCount = readBytes(2, input); |
| input.position(input.position() |
| + 2 // total number of entries in the central directory |
| ); |
| input.position(input.position() |
| + 4 // size of the central directory |
| ); |
| int centralDirectoryStartOffset = readBytes(4, input); |
| if (0xffffffff == centralDirectoryStartOffset) { |
| throw new IOException("Zip64 format not supported."); |
| } |
| |
| input.position(centralDirectoryStartOffset); |
| int entriesFound = 0; |
| |
| // Read each central directory entry |
| while ((entriesFound < entryCount) && (readBytes(4, input) == 0x02014b50)) { |
| int versionMadeBy = readBytes(2, input); |
| input.position(input.position() |
| + 2 // version needed to extract |
| + 2 // general purpose bit flag |
| + 2 // compression method |
| + 2 // last mod file time |
| + 2 // last mod file date |
| + 4 // crc-32 |
| + 4 // compressed size |
| + 4 // uncompressed size |
| ); |
| int filenameLength = readBytes(2, input); |
| int extraFieldLength = readBytes(2, input); |
| int fileCommentLength = readBytes(2, input); |
| input.position(input.position() |
| + 2 // disk number start |
| + 2 // internal file attributes |
| ); |
| int externalFileAttributes = readBytes(4, input); |
| input.position(input.position() |
| + 4 // relative offset of local header |
| ); |
| ByteBuffer filenameBuffer = ByteBuffer.allocate(filenameLength); |
| if (filenameLength != input.read(filenameBuffer)) { |
| throw new IOException( |
| String.format( |
| "Could not read file name (length %d) in central directory record", |
| filenameLength)); |
| } |
| input.position(input.position() + extraFieldLength + fileCommentLength); |
| entriesFound++; |
| if ((versionMadeBy >> 8) == 3) { |
| // Zip made by a Unix host - the external file attributes are POSIX permissions. |
| String filename = new String(filenameBuffer.array(), StandardCharsets.UTF_8); |
| attributes.put(filename, externalFileAttributes); |
| } |
| } |
| if (entriesFound != entryCount) { |
| System.err.printf( |
| "WARNING: Expected %d entries in central directory record in '%s', but found %d\n", |
| entryCount, zipFile, entriesFound); |
| } |
| } |
| return attributes.build(); |
| } |
| |
| private ZipFiles() {} |
| } |