blob: 1935f1ee920f3d492bdcad81e33caa88a15fe96c [file] [log] [blame]
// 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() {}
}