blob: 44389c16861eb0107fd9a7ff3e1e7afbddbaa08c [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.apkzlib.zip;
import com.android.apkzlib.utils.CachedSupplier;
import com.android.apkzlib.zip.utils.MsDosDateTimeUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
/**
* Representation of the central directory of a zip archive.
*/
class CentralDirectory {
/**
* Field in the central directory with the central directory signature.
*/
private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x02014b50, "Signature");
/**
* Field in the central directory with the "made by" code.
*/
private static final ZipField.F2 F_MADE_BY = new ZipField.F2(F_SIGNATURE.endOffset(),
"Made by", new ZipFieldInvariantNonNegative());
/**
* Field in the central directory with the minimum version required to extract the entry.
*/
@VisibleForTesting
static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(),
"Version to extract", new ZipFieldInvariantNonNegative());
/**
* Field in the central directory with the GP bit flag.
*/
private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(),
"GP bit");
/**
* Field in the central directory with the code of the compression method. See
* {@link CompressionMethod#fromCode(long)}.
*/
private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), "Method");
/**
* Field in the central directory with the last modification time in MS-DOS format (see
* {@link MsDosDateTimeUtils#packTime(long)}).
*/
private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(),
"Last modification time");
/**
* Field in the central directory with the last modification date in MS-DOS format. See
* {@link MsDosDateTimeUtils#packDate(long)}.
*/
private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(),
"Last modification date");
/**
* Field in the central directory with the CRC32 checksum of the entry. This will be zero for
* directories and files with no content.
*/
private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(),
"CRC32");
/**
* Field in the central directory with the entry's compressed size, <em>i.e.</em>, the file on
* the archive. This will be the same as the uncompressed size if the method is
* {@link CompressionMethod#STORE}.
*/
private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(),
"Compressed size", new ZipFieldInvariantNonNegative());
/**
* Field in the central directory with the entry's uncompressed size, <em>i.e.</em>, the size
* the file will have when extracted from the zip. This will be zero for directories and empty
* files and will be the same as the compressed size if the method is
* {@link CompressionMethod#STORE}.
*/
private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4(
F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative());
/**
* Field in the central directory with the length of the file name. The file name is stored
* after the offset field ({@link #F_OFFSET}). The number of characters in the file name are
* stored in this field.
*/
private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2(
F_UNCOMPRESSED_SIZE.endOffset(), "File name length",
new ZipFieldInvariantNonNegative());
/**
* Field in the central directory with the length of the extra field. The extra field is
* stored after the file name ({@link #F_FILE_NAME_LENGTH}). The contents of this field are
* partially defined in the zip specification but we do not parse it.
*/
private static final ZipField.F2 F_EXTRA_FIELD_LENGTH = new ZipField.F2(
F_FILE_NAME_LENGTH.endOffset(), "Extra field length",
new ZipFieldInvariantNonNegative());
/**
* Field in the central directory with the length of the comment. The comment is stored after
* the extra field ({@link #F_EXTRA_FIELD_LENGTH}). We do not parse the comment.
*/
private static final ZipField.F2 F_COMMENT_LENGTH = new ZipField.F2(
F_EXTRA_FIELD_LENGTH.endOffset(), "Comment length", new ZipFieldInvariantNonNegative());
/**
* Number of the disk where the central directory starts. Because we do not support multi-file
* archives, this field has to have value {@code 0}.
*/
private static final ZipField.F2 F_DISK_NUMBER_START = new ZipField.F2(
F_COMMENT_LENGTH.endOffset(), 0, "Disk start");
/**
* Internal attributes. This field can only contain one bit set, the {@link #ASCII_BIT}.
*/
private static final ZipField.F2 F_INTERNAL_ATTRIBUTES = new ZipField.F2(
F_DISK_NUMBER_START.endOffset(), "Int attributes");
/**
* External attributes. This field is ignored.
*/
private static final ZipField.F4 F_EXTERNAL_ATTRIBUTES = new ZipField.F4(
F_INTERNAL_ATTRIBUTES.endOffset(), "Ext attributes");
/**
* Offset into the archive where the entry starts. This is the offset to the local header
* (see {@link StoredEntry} for information on the local header), not to the file data itself.
* The file data, if there is any, will be stored after the local header.
*/
private static final ZipField.F4 F_OFFSET = new ZipField.F4(F_EXTERNAL_ATTRIBUTES.endOffset(),
"Offset", new ZipFieldInvariantNonNegative());
/**
* Maximum supported version to extract.
*/
private static final int MAX_VERSION_TO_EXTRACT = 20;
/**
* Bit that can be set on the internal attributes stating that the file is an ASCII file. We
* don't do anything with this information, but we check that nothing unexpected appears in the
* internal attributes.
*/
private static final int ASCII_BIT = 1;
/**
* Contains all entries in the directory mapped from their names.
*/
@Nonnull
private final Map<String, StoredEntry> entries;
/**
* The file where this directory belongs to.
*/
@Nonnull
private final ZFile file;
/**
* Supplier that provides a byte representation of the central directory.
*/
@Nonnull
private final CachedSupplier<byte[]> bytesSupplier;
/**
* Verify log for the central directory.
*/
@Nonnull
private final VerifyLog verifyLog;
/**
* Creates a new, empty, central directory, for a given zip file.
*
* @param file the file
*/
CentralDirectory(@Nonnull ZFile file) {
entries = Maps.newHashMap();
this.file = file;
bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation);
verifyLog = file.getVerifyLog();
}
/**
* Reads the central directory data from a zip file, parses it, and creates the in-memory
* structure representing the directory.
*
* @param bytes the data of the central directory; the directory is read from the buffer's
* current position; when this method terminates, the buffer's position is the first byte
* after the directory
* @param count the number of entries expected in the central directory (usually read from the
* {@link Eocd}).
* @param file the zip file this central directory belongs to
* @return the central directory
* @throws IOException failed to read data from the zip, or the central directory is corrupted
* or has unsupported features
*/
static CentralDirectory makeFromData(@Nonnull ByteBuffer bytes, int count, @Nonnull ZFile file)
throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
Preconditions.checkArgument(count >= 0, "count < 0");
CentralDirectory directory = new CentralDirectory(file);
for (int i = 0; i < count; i++) {
try {
directory.readEntry(bytes);
} catch (IOException e) {
throw new IOException(
"Failed to read directory entry index "
+ i
+ " (total "
+ "directory bytes read: "
+ bytes.position()
+ ").",
e);
}
}
return directory;
}
/**
* Creates a new central directory from the entries. This is used to build a new central
* directory from entries in the zip file.
*
* @param entries the entries in the zip file
* @param file the zip file itself
* @return the created central directory
*/
static CentralDirectory makeFromEntries(
@Nonnull Set<StoredEntry> entries,
@Nonnull ZFile file) {
CentralDirectory directory = new CentralDirectory(file);
for (StoredEntry entry : entries) {
CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader();
Preconditions.checkArgument(
!directory.entries.containsKey(cdr.getName()),
"Duplicate filename");
directory.entries.put(cdr.getName(), entry);
}
return directory;
}
/**
* Reads the next entry from the central directory and adds it to {@link #entries}.
*
* @param bytes the central directory's data, positioned starting at the beginning of the next
* entry to read; when finished, the buffer's position will be at the first byte after the
* entry
* @throws IOException failed to read the directory entry, either because of an I/O error,
* because it is corrupt or contains unsupported features
*/
private void readEntry(@Nonnull ByteBuffer bytes) throws IOException {
F_SIGNATURE.verify(bytes);
long madeBy = F_MADE_BY.read(bytes);
long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes);
verifyLog.verify(
versionNeededToExtract <= MAX_VERSION_TO_EXTRACT,
"Ignored unknown version needed to extract in zip directory entry: %s.",
versionNeededToExtract);
long gpBit = F_GP_BIT.read(bytes);
GPFlags flags = GPFlags.from(gpBit);
long methodCode = F_METHOD.read(bytes);
CompressionMethod method = CompressionMethod.fromCode(methodCode);
verifyLog.verify(method != null, "Unknown method in zip directory entry: %s.", methodCode);
long lastModTime;
long lastModDate;
if (file.areTimestampsIgnored()) {
lastModTime = 0;
lastModDate = 0;
F_LAST_MOD_TIME.skip(bytes);
F_LAST_MOD_DATE.skip(bytes);
} else {
lastModTime = F_LAST_MOD_TIME.read(bytes);
lastModDate = F_LAST_MOD_DATE.read(bytes);
}
long crc32 = F_CRC32.read(bytes);
long compressedSize = F_COMPRESSED_SIZE.read(bytes);
long uncompressedSize = F_UNCOMPRESSED_SIZE.read(bytes);
int fileNameLength = Ints.checkedCast(F_FILE_NAME_LENGTH.read(bytes));
int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes));
int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes));
F_DISK_NUMBER_START.verify(bytes, verifyLog);
long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes);
verifyLog.verify(
(internalAttributes & ~ASCII_BIT) == 0,
"Ignored invalid internal attributes: %s.",
internalAttributes);
long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes);
long entryOffset = F_OFFSET.read(bytes);
long remainingSize = fileNameLength + extraFieldLength + fileCommentLength;
if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) {
throw new IOException(
"Directory entry should have "
+ remainingSize
+ " bytes remaining (name = "
+ fileNameLength
+ ", extra = "
+ extraFieldLength
+ ", comment = "
+ fileCommentLength
+ "), but it has "
+ bytes.remaining()
+ ".");
}
byte[] encodedFileName = new byte[fileNameLength];
bytes.get(encodedFileName);
String fileName = EncodeUtils.decode(encodedFileName, flags);
byte[] extraField = new byte[extraFieldLength];
bytes.get(extraField);
byte[] fileCommentField = new byte[fileCommentLength];
bytes.get(fileCommentField);
/*
* Tricky: to create a CentralDirectoryHeader we need the future that will hold the result
* of the compress information. But, to actually create the result of the compress
* information we need the CentralDirectoryHeader
*/
ListenableFuture<CentralDirectoryHeaderCompressInfo> compressInfo =
Futures.immediateFuture(
new CentralDirectoryHeaderCompressInfo(
method,
compressedSize,
versionNeededToExtract));
CentralDirectoryHeader centralDirectoryHeader =
new CentralDirectoryHeader(
fileName, encodedFileName, uncompressedSize, compressInfo, flags, file);
centralDirectoryHeader.setMadeBy(madeBy);
centralDirectoryHeader.setLastModTime(lastModTime);
centralDirectoryHeader.setLastModDate(lastModDate);
centralDirectoryHeader.setCrc32(crc32);
centralDirectoryHeader.setInternalAttributes(internalAttributes);
centralDirectoryHeader.setExternalAttributes(externalAttributes);
centralDirectoryHeader.setOffset(entryOffset);
centralDirectoryHeader.setExtraFieldNoNotify(new ExtraField(extraField));
centralDirectoryHeader.setComment(fileCommentField);
StoredEntry entry;
try {
entry = new StoredEntry(centralDirectoryHeader, file, null);
} catch (IOException e) {
throw new IOException("Failed to read stored entry '" + fileName + "'.", e);
}
if (entries.containsKey(fileName)) {
verifyLog.log("File file contains duplicate file '" + fileName + "'.");
}
entries.put(fileName, entry);
}
/**
* Obtains all the entries in the central directory.
*
* @return all entries on a non-modifiable map
*/
@Nonnull
Map<String, StoredEntry> getEntries() {
return ImmutableMap.copyOf(entries);
}
/**
* Obtains the byte representation of the central directory.
*
* @return a byte array containing the whole central directory
* @throws IOException failed to write the byte array
*/
byte[] toBytes() throws IOException {
return bytesSupplier.get();
}
/**
* Computes the byte representation of the central directory.
*
* @return a byte array containing the whole central directory
* @throws UncheckedIOException failed to write the byte array
*/
private byte[] computeByteRepresentation() {
List<StoredEntry> sorted = Lists.newArrayList(entries.values());
sorted.sort(StoredEntry.COMPARE_BY_NAME);
CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()];
CentralDirectoryHeaderCompressInfo[] compressInfos =
new CentralDirectoryHeaderCompressInfo[entries.size()];
byte[][] encodedFileNames = new byte[entries.size()][];
byte[][] extraFields = new byte[entries.size()][];
byte[][] comments = new byte[entries.size()][];
try {
/*
* First collect all the data and compute the total size of the central directory.
*/
int idx = 0;
int total = 0;
for (StoredEntry entry : sorted) {
cdhs[idx] = entry.getCentralDirectoryHeader();
compressInfos[idx] = cdhs[idx].getCompressionInfoWithWait();
encodedFileNames[idx] = cdhs[idx].getEncodedFileName();
extraFields[idx] = new byte[cdhs[idx].getExtraField().size()];
cdhs[idx].getExtraField().write(ByteBuffer.wrap(extraFields[idx]));
comments[idx] = cdhs[idx].getComment();
total += F_OFFSET.endOffset() + encodedFileNames[idx].length
+ extraFields[idx].length + comments[idx].length;
idx++;
}
ByteBuffer out = ByteBuffer.allocate(total);
for (idx = 0; idx < entries.size(); idx++) {
F_SIGNATURE.write(out);
F_MADE_BY.write(out, cdhs[idx].getMadeBy());
F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract());
F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue());
F_METHOD.write(out, compressInfos[idx].getMethod().methodCode);
if (file.areTimestampsIgnored()) {
F_LAST_MOD_TIME.write(out, 0);
F_LAST_MOD_DATE.write(out, 0);
} else {
F_LAST_MOD_TIME.write(out, cdhs[idx].getLastModTime());
F_LAST_MOD_DATE.write(out, cdhs[idx].getLastModDate());
}
F_CRC32.write(out, cdhs[idx].getCrc32());
F_COMPRESSED_SIZE.write(out, compressInfos[idx].getCompressedSize());
F_UNCOMPRESSED_SIZE.write(out, cdhs[idx].getUncompressedSize());
F_FILE_NAME_LENGTH.write(out, cdhs[idx].getEncodedFileName().length);
F_EXTRA_FIELD_LENGTH.write(out, cdhs[idx].getExtraField().size());
F_COMMENT_LENGTH.write(out, cdhs[idx].getComment().length);
F_DISK_NUMBER_START.write(out);
F_INTERNAL_ATTRIBUTES.write(out, cdhs[idx].getInternalAttributes());
F_EXTERNAL_ATTRIBUTES.write(out, cdhs[idx].getExternalAttributes());
F_OFFSET.write(out, cdhs[idx].getOffset());
out.put(encodedFileNames[idx]);
out.put(extraFields[idx]);
out.put(comments[idx]);
}
return out.array();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}