apkzlib 7.1.0-alpha10 (#19)

This commit is contained in:
vvb2060 2021-08-27 17:19:05 +08:00 committed by GitHub
parent b24c05c820
commit 898bf7d536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 14436 additions and 8 deletions

1
apkzlib/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

18
apkzlib/build.gradle Normal file
View File

@ -0,0 +1,18 @@
plugins {
id 'java-library'
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
dependencies {
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.69'
implementation 'org.bouncycastle:bcprov-jdk15on:1.69'
api 'com.google.guava:guava:30.1.1-jre'
api 'com.android.tools.build:apksig:7.0.1'
compileOnlyApi 'com.google.auto.value:auto-value-annotations:1.8.2'
annotationProcessor 'com.google.auto.value:auto-value:1.8.2'
}

View File

@ -0,0 +1,86 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.base.Preconditions;
import java.io.IOException;
/**
* Abstract implementation of a {@link CloseableByteSourceFromOutputStreamBuilder} that simplifies
* the implementation of concrete instances. It implements the state machine implied by the
* interface contract and requires subclasses to implement two methods:
* {@link #doWrite(byte[], int, int)} -- that actually does writing and {@link #doBuild()} that
* builds the {@link CloseableByteSource].
*/
abstract class AbstractCloseableByteSourceFromOutputStreamBuilder
extends CloseableByteSourceFromOutputStreamBuilder {
/**
* Array that allows {@link #write(int)} to delegate to {@link #write(byte[], int, int)} without
* having to create an array for each invocation.
*/
private final byte[] tempByte;
/**
* Has the builder been closed? If it has, then {@link #build()} may be called, but none of the
* writing methods can.
*/
private boolean closed;
/**
* Has the builder been built? If this is {@code true} then {@link #closed} is also {@code true}.
*/
private boolean built;
/** Creates a new builder. */
AbstractCloseableByteSourceFromOutputStreamBuilder() {
tempByte = new byte[1];
closed = false;
built = false;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
Preconditions.checkState(!closed);
doWrite(b, off, len);
}
@Override
public void write(int b) throws IOException {
tempByte[0] = (byte) b;
write(tempByte, 0, 1);
}
@Override
public void close() throws IOException {
closed = true;
}
@Override
public CloseableByteSource build() throws IOException {
Preconditions.checkState(!built);
closed = true;
built = true;
return doBuild();
}
/**
* Same as {@link #write(byte[], int, int)}, but with the guarantee that the source has not been
* built and the builder is still open.
*
* @param b see {@link #write(byte[], int, int)}
* @param off see {@link #write(byte[], int, int)}
* @param len see {@link #write(byte[], int, int)}
* @throws IOException see {@link #write(byte[], int, int)}
*/
protected abstract void doWrite(byte[] b, int off, int len) throws IOException;
/**
* Builds the {@link CloseableByteSource} from the written data. This method is at most invoked
* once.
*
* @return the new source that will contain all data written to the builder so far
* @throws IOException failed to create the byte source
*/
protected abstract CloseableByteSource doBuild() throws IOException;
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.io.ByteSource;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
/**
* Interface for a storage that will temporarily save bytes. There are several factory methods to
* create byte sources from several inputs, all of which may be discarded after the byte source has
* been created. The data is saved in the storage and will be kept until the byte source is closed.
*/
public interface ByteStorage extends Closeable {
/**
* Creates a new byte source by fully reading an input stream.
*
* @param stream the input stream
* @return a byte source containing the cached data from the given stream
* @throws IOException failed to read the stream
*/
CloseableByteSource fromStream(InputStream stream) throws IOException;
/**
* Creates a builder that is an output stream and can create a byte source.
*
* @return a builder where data can be written to and a {@link CloseableByteSource} can eventually
* be obtained from
* @throws IOException failed to create the builder; this may happen if the builder require some
* preparation such as temporary storage allocation that may fail
*/
CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException;
/**
* Creates a new byte source from another byte source.
*
* @param source the byte source to copy data from
* @return the tracked byte source
* @throws IOException failed to read data from the byte source
*/
CloseableByteSource fromSource(ByteSource source) throws IOException;
/**
* Obtains the number of bytes currently used.
*
* @return the number of bytes
*/
long getBytesUsed();
/**
* Obtains the maximum number of bytes ever used by this tracker.
*
* @return the number of bytes
*/
long getMaxBytesUsed();
}

View File

@ -0,0 +1,14 @@
package com.android.tools.build.apkzlib.bytestorage;
import java.io.IOException;
/** Factory that creates {@link ByteStorage}. */
public interface ByteStorageFactory {
/**
* Creates a new storage.
*
* @return a storage that should be closed when no longer used.
*/
ByteStorage create() throws IOException;
}

View File

@ -0,0 +1,143 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* Byte storage that breaks byte sources into smaller byte sources. This storage uses another
* storage as a delegate and, when a source is requested, it will allocate one or more sources from
* the delegate to build the requested source.
*/
public class ChunkBasedByteStorage implements ByteStorage {
/** Size of the default chunk size. */
private static final long DEFAULT_CHUNK_SIZE_BYTES = 10 * 1024 * 1024;
/** Maximum size of each chunk. */
private final long maxChunkSize;
/** Byte storage where the data is actually stored. */
private final ByteStorage delegate;
/**
* Creates a new storage breaking sources in chunks with the default maximum size and allocating
* each chunk from {@code delegate}.
*/
ChunkBasedByteStorage(ByteStorage delegate) {
this(DEFAULT_CHUNK_SIZE_BYTES, delegate);
}
/**
* Creates a new storage breaking sources in chunks with the maximum of {@code maxChunkSize} and
* allocating each chunk from {@code delegate}.
*/
ChunkBasedByteStorage(long maxChunkSize, ByteStorage delegate) {
this.maxChunkSize = maxChunkSize;
this.delegate = delegate;
}
/** Obtains the byte storage chunks are allocated from. */
@VisibleForTesting // private otherwise.
public ByteStorage getDelegate() {
return delegate;
}
@Override
public CloseableByteSource fromStream(InputStream stream) throws IOException {
List<CloseableByteSource> sources = new ArrayList<>();
while (true) {
LimitedInputStream limitedInput = new LimitedInputStream(stream, maxChunkSize);
sources.add(delegate.fromStream(limitedInput));
if (limitedInput.isInputFinished()) {
break;
}
}
return new ChunkBasedCloseableByteSource(sources);
}
@Override
public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {
return new AbstractCloseableByteSourceFromOutputStreamBuilder() {
private final List<CloseableByteSource> sources = new ArrayList<>();
@Nullable private CloseableByteSourceFromOutputStreamBuilder currentBuilder = null;
private long written = 0;
@Override
protected void doWrite(byte[] b, int off, int len) throws IOException {
int actualOffset = off;
int remaining = len;
while (remaining > 0) {
// Since we're writing data, make sure we have a builder to create the new source.
if (currentBuilder == null) {
currentBuilder = delegate.makeBuilder();
written = 0;
}
// See how much we can write without exceeding maxChunkSize in the current builder.
int maxWrite = (int) Math.min(maxChunkSize - written, remaining);
currentBuilder.write(b, actualOffset, maxWrite);
written += maxWrite;
remaining -= maxWrite;
actualOffset += maxWrite;
// If we've reached the end of the chunk, create the source for the part we have and reset
// to builder so we start a new one if there is more data.
if (written == maxChunkSize) {
sources.add(currentBuilder.build());
currentBuilder = null;
}
}
}
@Override
protected CloseableByteSource doBuild() throws IOException {
// If we were writing a chunk, close it.
if (currentBuilder != null) {
sources.add(currentBuilder.build());
currentBuilder = null;
}
return new ChunkBasedCloseableByteSource(sources);
}
};
}
@Override
public CloseableByteSource fromSource(ByteSource source) throws IOException {
List<CloseableByteSource> sources = new ArrayList<>();
long end = source.size();
long start = 0;
while (start < end) {
long chunkSize = Math.min(end - start, maxChunkSize);
sources.add(delegate.fromSource(source.slice(start, chunkSize)));
start += chunkSize;
}
return new ChunkBasedCloseableByteSource(sources);
}
@Override
public long getBytesUsed() {
return delegate.getBytesUsed();
}
@Override
public long getMaxBytesUsed() {
return delegate.getMaxBytesUsed();
}
@Override
public void close() throws IOException {
delegate.close();
}
}

View File

@ -0,0 +1,40 @@
package com.android.tools.build.apkzlib.bytestorage;
import java.io.IOException;
import javax.annotation.Nullable;
/**
* {@link ByteStorageFactory} that creates {@link ByteStorage} instances that keep all data in
* memory.
*/
public class ChunkBasedByteStorageFactory implements ByteStorageFactory {
/** Factory to create the delegate storages. */
private final ByteStorageFactory delegate;
/** Maximum size for chunks, if any. */
@Nullable private final Long maxChunkSize;
/** Creates a new factory whose storages are created using delegates from the given factory. */
public ChunkBasedByteStorageFactory(ByteStorageFactory delegate) {
this(delegate, /*maxChunkSize=*/ null);
}
/**
* Creates a new factory whose storages use the given maximum chunk size and are created using
* delegates from the given factory.
*/
public ChunkBasedByteStorageFactory(ByteStorageFactory delegate, @Nullable Long maxChunkSize) {
this.delegate = delegate;
this.maxChunkSize = maxChunkSize;
}
@Override
public ByteStorage create() throws IOException {
if (maxChunkSize == null) {
return new ChunkBasedByteStorage(delegate.create());
} else {
return new ChunkBasedByteStorage(maxChunkSize, delegate.create());
}
}
}

View File

@ -0,0 +1,44 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteSource;
import com.google.common.io.Closer;
import java.io.IOException;
import java.util.List;
/**
* Byte source that has its data spread over several chunks, each with its own {@link
* CloseableByteSource}.
*/
class ChunkBasedCloseableByteSource extends CloseableDelegateByteSource {
/** The sources for data of all the chunks, in order. */
private final ImmutableList<CloseableByteSource> sources;
/** Creates a new source from the given sources. */
ChunkBasedCloseableByteSource(List<CloseableByteSource> sources) throws IOException {
super(ByteSource.concat(sources), sumSizes(sources));
this.sources = ImmutableList.copyOf(sources);
}
/** Computes the size of this source by summing the sizes of all sources. */
private static long sumSizes(List<CloseableByteSource> sources) throws IOException {
long sum = 0;
for (CloseableByteSource source : sources) {
sum += source.size();
}
return sum;
}
@Override
protected synchronized void innerClose() throws IOException {
try (Closer closer = Closer.create()) {
for (CloseableByteSource source : sources) {
closer.register(source);
}
}
}
}

View File

@ -0,0 +1,22 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import java.io.IOException;
import java.io.OutputStream;
/**
* Output stream that creates a {@link CloseableByteSource} from the data that was written to it.
* Calling {@link #close} is optional as {@link #build()} will also close the output stream.
*/
public abstract class CloseableByteSourceFromOutputStreamBuilder extends OutputStream {
/**
* Creates the source from the data that has been written to the stream. No more data can be
* written to the output stream after this method has been called.
*
* @return a source that will provide the data that was written to the stream before this method
* is invoked; where this data is stored is not specified by this interface
* @throws IOException failed to build the byte source
*/
public abstract CloseableByteSource build() throws IOException;
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/** Keeps track of used bytes allowing gauging memory usage. */
public class InMemoryByteStorage implements ByteStorage {
/** Number of bytes currently in use. */
private long bytesUsed;
/** Maximum number of bytes used. */
private long maxBytesUsed;
@Override
public CloseableByteSource fromStream(InputStream stream) throws IOException {
byte[] data = ByteStreams.toByteArray(stream);
updateUsage(data.length);
return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {
@Override
public synchronized void innerClose() throws IOException {
super.innerClose();
updateUsage(-sizeNoException());
}
};
}
@Override
public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
return new AbstractCloseableByteSourceFromOutputStreamBuilder() {
@Override
protected void doWrite(byte[] b, int off, int len) throws IOException {
output.write(b, off, len);
updateUsage(len);
}
@Override
protected CloseableByteSource doBuild() throws IOException {
byte[] data = output.toByteArray();
return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {
@Override
protected synchronized void innerClose() throws IOException {
super.innerClose();
updateUsage(-data.length);
}
};
}
};
}
@Override
public CloseableByteSource fromSource(ByteSource source) throws IOException {
return fromStream(source.openStream());
}
/**
* Updates the memory used by this tracker.
*
* @param delta the number of bytes to add or remove, if negative
*/
private synchronized void updateUsage(long delta) {
bytesUsed += delta;
if (maxBytesUsed < bytesUsed) {
maxBytesUsed = bytesUsed;
}
}
@Override
public synchronized long getBytesUsed() {
return bytesUsed;
}
@Override
public synchronized long getMaxBytesUsed() {
return maxBytesUsed;
}
@Override
public void close() throws IOException {
// Nothing to do on close.
}
}

View File

@ -0,0 +1,15 @@
package com.android.tools.build.apkzlib.bytestorage;
import java.io.IOException;
/**
* {@link ByteStorageFactory} that creates {@link ByteStorage} instances that keep all data in
* memory.
*/
public class InMemoryByteStorageFactory implements ByteStorageFactory {
@Override
public ByteStorage create() throws IOException {
return new InMemoryByteStorage();
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.bytestorage;
import java.io.IOException;
import java.io.InputStream;
/**
* Input stream that reads only a limited number of bytes from another input stream before reporting
* EOF. When closed, this stream will not close the underlying stream.
*
* <p>If the underlying stream does not have enough data, this stream will read all available data
* from the underlying stream.
*/
class LimitedInputStream extends InputStream {
/** Where the data comes from. */
private final InputStream input;
/** How many bytes remain in this stream. */
private long remaining;
/** Has EOF been detected in {@link #input}? */
private boolean eofDetected;
/**
* Creates a new input stream.
*
* @param input where to read data from
* @param maximum the maximum number of bytes to read from {@code input}
*/
LimitedInputStream(InputStream input, long maximum) {
this.input = input;
this.remaining = maximum;
this.eofDetected = false;
}
@Override
public int read() throws IOException {
if (remaining == 0) {
return -1;
}
int r = input.read();
if (r >= 0) {
remaining--;
} else {
eofDetected = true;
}
return r;
}
@Override
public int read(byte[] whereTo, int offset, int length) throws IOException {
if (remaining == 0) {
return -1;
}
int toRead = (int) Math.min(remaining, length);
int r = input.read(whereTo, offset, toRead);
if (r >= 0) {
remaining -= r;
} else {
eofDetected = true;
}
return r;
}
/** Returns {@code true} if EOF has been detected in the {@code input} stream. */
boolean isInputFinished() {
return eofDetected;
}
}

View File

@ -0,0 +1,80 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.io.InputStream;
/**
* Byte source that, until switched, will keep itself in the LRU queue. The byte source will
* automatically remove itself from the queue once closed or moved to disk (see {@link
* #moveToDisk(ByteStorage)}. This source should not be switched explicitly or tracking will not
* work.
*
* <p>The source will consider an access to be opening a stream. Every time a stream is open the
* source will move itself to the top of the LRU list.
*/
class LruTrackedCloseableByteSource extends SwitchableDelegateCloseableByteSource {
/** The tracker being used. */
private final LruTracker<LruTrackedCloseableByteSource> tracker;
/** Are we still tracking usage? */
private boolean tracking;
/** Has the byte source been closed? */
private boolean closed;
/** Creates a new byte source based on the given source and using the provided tracker. */
LruTrackedCloseableByteSource(
CloseableByteSource delegate, LruTracker<LruTrackedCloseableByteSource> tracker)
throws IOException {
super(delegate);
this.tracker = tracker;
tracker.track(this);
tracking = true;
closed = false;
}
@Override
public synchronized InputStream openStream() throws IOException {
Preconditions.checkState(!closed);
if (tracking) {
tracker.access(this);
}
return super.openStream();
}
@Override
protected synchronized void innerClose() throws IOException {
closed = true;
untrack();
super.innerClose();
}
/**
* Marks this source as not being tracked any more. May be called multiple times (only the first
* one will do anything).
*/
private synchronized void untrack() {
if (tracking) {
tracking = false;
tracker.untrack(this);
}
}
/**
* Moves the contents of this source to a storage. This will untrack the source and switch its
* contents to a new delegate provided by {@code diskStorage}.
*/
synchronized void move(ByteStorage diskStorage) throws IOException {
if (closed) {
return;
}
CloseableByteSource diskSource = diskStorage.fromSource(this);
untrack();
switchSource(diskSource);
}
}

View File

@ -0,0 +1,85 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import java.util.TreeSet;
import javax.annotation.Nullable;
/**
* A tracker that keeps a list of the last-recently-used objects of type {@code T}. The tracker
* doesn't define what LRU means, it has a method, {@link #access(Object)} that marks the object as
* being accessed and moves it to the top of the queue.
*
* <p>This implementation is O(log(N)) on all operations.
*
* <p>Implementation note: we don't keep track of time. Instead we use a counter that is incremented
* every time a new access is done or a new object is tracked. Because of this, each access time is
* unique for each object (although it will change after each access).
*/
class LruTracker<T> {
/** Maps each object to its unique access time and vice-versa. */
private final BiMap<T, Integer> objectToAccessTime;
/**
* Ordered set of all object's access times. This set has the same contents as {@code
* objectToAccessTime.value()}. It is sorted from the highest access time (newest) to the lowest
* access time (oldest).
*/
private final TreeSet<Integer> accessTimes;
/** Next access time to use for tracking or accessing. */
private int currentTime;
/** Creates a new tracker without any objects. */
LruTracker() {
currentTime = 1;
objectToAccessTime = HashBiMap.create();
accessTimes = new TreeSet<>((i0, i1) -> i1 - i0);
}
/** Starts tracking an object. This object's will be the most recently used. */
synchronized void track(T object) {
Preconditions.checkState(!objectToAccessTime.containsKey(object));
objectToAccessTime.put(object, currentTime);
accessTimes.add(currentTime);
currentTime++;
}
/** Stops tracking an object. */
synchronized void untrack(T object) {
Preconditions.checkState(objectToAccessTime.containsKey(object));
accessTimes.remove(objectToAccessTime.get(object));
objectToAccessTime.remove(object);
}
/** Marks the given object as having been accessed promoting it as the most recently used. */
synchronized void access(T object) {
untrack(object);
track(object);
}
/**
* Obtains the position of an object in the queue. It will be {@code 0} for the most recently used
* object.
*/
synchronized int positionOf(T object) {
Preconditions.checkState(objectToAccessTime.containsKey(object));
int lastAccess = objectToAccessTime.get(object);
return accessTimes.headSet(lastAccess).size();
}
/**
* Obtains the last element, the one last accessed earliest. Will return empty if there are no
* objects being tracked.
*/
@Nullable
synchronized T last() {
if (accessTimes.isEmpty()) {
return null;
}
return objectToAccessTime.inverse().get(accessTimes.last());
}
}

View File

@ -0,0 +1,167 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteSource;
import java.io.IOException;
import java.io.InputStream;
/**
* Byte storage that keeps data in memory up to a certain size. After that, older sources are moved
* to disk and the newer ones served from memory.
*
* <p>Once unloaded to disk, sources are not reloaded into memory as that would be in direct
* conflict with the filesystem's caching and the costs would probably outweight the benefits.
*
* <p>The maximum memory used by storage is actually larger than the maximum provided. It may exceed
* the limit by the size of one source. That is because sources are always loaded into memory before
* the storage decides to flush them to disk.
*/
public class OverflowToDiskByteStorage implements ByteStorage {
/** Size of the default memory cache. */
private static final long DEFAULT_MEMORY_CACHE_BYTES = 50 * 1024 * 1024;
/** In-memory storage. */
private final InMemoryByteStorage memoryStorage;
/** Disk-based storage. */
@VisibleForTesting // private otherwise.
final TemporaryDirectoryStorage diskStorage;
/** Tracker that keeps all memory sources. */
private final LruTracker<LruTrackedCloseableByteSource> memorySourcesTracker;
/** Maximum amount of data to keep in memory. */
private final long memoryCacheSize;
/** Maximum amount of data used. */
private long maxBytesUsed;
/**
* Creates a new byte storage with the default memory cache using the provided temporary directory
* to write data that overflows the memory size.
*
* @param temporaryDirectoryFactory the factory used to create a temporary directory where to
* overflow to; the created directory will be closed when the {@link
* OverflowToDiskByteStorage} object is closed
* @throws IOException failed to create the temporary directory
*/
public OverflowToDiskByteStorage(TemporaryDirectoryFactory temporaryDirectoryFactory)
throws IOException {
this(DEFAULT_MEMORY_CACHE_BYTES, temporaryDirectoryFactory);
}
/**
* Creates a new byte storage with the given memory cache size using the provided temporary
* directory to write data that overflows the memory size.
*
* @param memoryCacheSize the in-memory cache; a value of {@link 0} will effectively disable
* in-memory caching
* @param temporaryDirectoryFactory the factory used to create a temporary directory where to
* overflow to; the created directory will be closed when the {@link
* OverflowToDiskByteStorage} object is closed
* @throws IOException failed to create the temporary directory
*/
public OverflowToDiskByteStorage(
long memoryCacheSize, TemporaryDirectoryFactory temporaryDirectoryFactory)
throws IOException {
memoryStorage = new InMemoryByteStorage();
diskStorage = new TemporaryDirectoryStorage(temporaryDirectoryFactory);
this.memoryCacheSize = memoryCacheSize;
this.memorySourcesTracker = new LruTracker<>();
}
@Override
public CloseableByteSource fromStream(InputStream stream) throws IOException {
CloseableByteSource memSource =
new LruTrackedCloseableByteSource(memoryStorage.fromStream(stream), memorySourcesTracker);
checkMaxUsage();
reviewSources();
return memSource;
}
@Override
public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {
CloseableByteSourceFromOutputStreamBuilder memBuilder = memoryStorage.makeBuilder();
return new AbstractCloseableByteSourceFromOutputStreamBuilder() {
@Override
protected void doWrite(byte[] b, int off, int len) throws IOException {
memBuilder.write(b, off, len);
}
@Override
protected CloseableByteSource doBuild() throws IOException {
CloseableByteSource memSource =
new LruTrackedCloseableByteSource(memBuilder.build(), memorySourcesTracker);
checkMaxUsage();
reviewSources();
return memSource;
}
};
}
@Override
public CloseableByteSource fromSource(ByteSource source) throws IOException {
CloseableByteSource memSource =
new LruTrackedCloseableByteSource(memoryStorage.fromSource(source), memorySourcesTracker);
checkMaxUsage();
reviewSources();
return memSource;
}
@Override
public synchronized long getBytesUsed() {
return memoryStorage.getBytesUsed() + diskStorage.getBytesUsed();
}
@Override
public synchronized long getMaxBytesUsed() {
return maxBytesUsed;
}
/** Checks if we have reached a new high of data usage and set it. */
private synchronized void checkMaxUsage() {
if (getBytesUsed() > maxBytesUsed) {
maxBytesUsed = getBytesUsed();
}
}
/** Checks if any of the sources needs to be written to disk or loaded into memory. */
private synchronized void reviewSources() throws IOException {
// Move data from memory to disk until we have at most memoryCacheSize bytes in memory.
while (memoryStorage.getBytesUsed() > memoryCacheSize) {
LruTrackedCloseableByteSource last = memorySourcesTracker.last();
if (last != null) {
LruTrackedCloseableByteSource lastSource = last;
lastSource.move(diskStorage);
}
}
}
/** Obtains the number of bytes stored in memory. */
public long getMemoryBytesUsed() {
return memoryStorage.getBytesUsed();
}
/** Obtains the maximum number of bytes ever stored in memory. */
public long getMaxMemoryBytesUsed() {
return memoryStorage.getMaxBytesUsed();
}
/** Obtains the number of bytes stored in disk. */
public long getDiskBytesUsed() {
return diskStorage.getBytesUsed();
}
/** Obtains the maximum number of bytes ever stored in disk. */
public long getMaxDiskBytesUsed() {
return diskStorage.getMaxBytesUsed();
}
@Override
public void close() throws IOException {
memoryStorage.close();
diskStorage.close();
}
}

View File

@ -0,0 +1,50 @@
package com.android.tools.build.apkzlib.bytestorage;
import java.io.IOException;
import javax.annotation.Nullable;
/**
* {@link ByteStorageFactory} that creates instances of {@link ByteStorage} that will keep some data
* in memory and will overflow to disk when necessary.
*/
public class OverflowToDiskByteStorageFactory implements ByteStorageFactory {
/** How much data we want to keep in cache? If {@code null} then we want the default value. */
@Nullable private final Long memoryCacheSizeInBytes;
/** Factory that creates temporary directories. */
private final TemporaryDirectoryFactory temporaryDirectoryFactory;
/**
* Creates a new factory with an optional in-memory size and a temporary directory for overflow.
*
* @param temporaryDirectoryFactory a factory that creates temporary directories that will be used
* for overflow of the {@link ByteStorage} instances created by this factory
*/
public OverflowToDiskByteStorageFactory(TemporaryDirectoryFactory temporaryDirectoryFactory) {
this(null, temporaryDirectoryFactory);
}
/**
* Creates a new factory with an optional in-memory size and a temporary directory for overflow.
*
* @param memoryCacheSizeInBytes how many bytes to keep in memory? If {@code null} then a default
* value will be used
* @param temporaryDirectoryFactory a factory that creates temporary directories that will be used
* for overflow of the {@link ByteStorage} instances created by this factory
*/
public OverflowToDiskByteStorageFactory(
Long memoryCacheSizeInBytes, TemporaryDirectoryFactory temporaryDirectoryFactory) {
this.memoryCacheSizeInBytes = memoryCacheSizeInBytes;
this.temporaryDirectoryFactory = temporaryDirectoryFactory;
}
@Override
public ByteStorage create() throws IOException {
if (memoryCacheSizeInBytes == null) {
return new OverflowToDiskByteStorage(temporaryDirectoryFactory);
} else {
return new OverflowToDiskByteStorage(memoryCacheSizeInBytes, temporaryDirectoryFactory);
}
}
}

View File

@ -0,0 +1,122 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.io.Closer;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Byte source that delegates to another byte source that can be switched dynamically.
*
* <p>This byte source encloses another byte source (the delegate) and allows switching the
* delegate. Switching is done transparently for the user (as long as the new byte source represents
* the same data) maintaining all open streams working, but now streaming from the new source.
*/
class SwitchableDelegateCloseableByteSource extends CloseableByteSource {
/** The current delegate. */
private CloseableByteSource delegate;
/** Has the byte source been closed? */
private boolean closed;
/**
* Streams that have been opened, but not yet closed. These are all the streams that have to be
* switched when we switch delegates.
*/
private final List<SwitchableDelegateInputStream> nonClosedStreams;
/** Creates a new source using {@code source} as delegate. */
SwitchableDelegateCloseableByteSource(CloseableByteSource source) {
this.delegate = source;
nonClosedStreams = new ArrayList<>();
}
@Override
protected synchronized void innerClose() throws IOException {
closed = true;
try (Closer closer = Closer.create()) {
for (SwitchableDelegateInputStream stream : nonClosedStreams) {
closer.register(stream);
}
nonClosedStreams.clear();
}
delegate.close();
}
@Override
public synchronized InputStream openStream() throws IOException {
SwitchableDelegateInputStream stream =
new SwitchableDelegateInputStream(delegate.openStream()) {
// Can't have a lock on the stream while we synchronize the removal of nonClosedStreams
// because it can deadlock when called in parallel with switchSource as the lock order is
// reversed. The lack of synchronization is OK because we don't access any data on the
// stream anyway until super.close() is called.
@SuppressWarnings("UnsynchronizedOverridesSynchronized")
@Override
public void close() throws IOException {
// Remove the stream on close.
synchronized (SwitchableDelegateCloseableByteSource.this) {
nonClosedStreams.remove(this);
}
super.close();
}
};
nonClosedStreams.add(stream);
return stream;
}
/**
* Switches the current source for {@code source}. All streams are kept valid. The current source
* is closed.
*
* <p>If the current source has already been closed, {@code source} will also be closed and
* nothing else is done.
*
* <p>Otherwise, as long as it is possible to open enough input streams from {@code source} to
* replace all current input streams, the source if changed. Any errors while closing input
* streams (which happens during switching -- see {@link
* SwitchableDelegateInputStream#switchStream(InputStream)}) or closing the old source are
* reported as thrown {@code IOException}
*/
synchronized void switchSource(CloseableByteSource source) throws IOException {
if (source == delegate) {
return;
}
if (closed) {
source.close();
return;
}
List<InputStream> switchStreams = new ArrayList<>();
for (int i = 0; i < nonClosedStreams.size(); i++) {
switchStreams.add(source.openStream());
}
CloseableByteSource oldDelegate = delegate;
delegate = source;
// A bit of trickery. We want to call switchStream for all streams. switchStream will
// successfully switch the stream even if it throws an exception (if it does, it means it
// failed to close the old stream). So we want to continue switching and recording all
// exceptions. Closer() has that logic already so we register each stream switch as a close
// operation.
try (Closer closer = Closer.create()) {
for (int i = 0; i < nonClosedStreams.size(); i++) {
SwitchableDelegateInputStream nonClosedStream = nonClosedStreams.get(i);
InputStream switchStream = switchStreams.get(i);
closer.register(() -> nonClosedStream.switchStream(switchStream));
}
closer.register(oldDelegate);
}
}
}

View File

@ -0,0 +1,181 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
/**
* Input stream that delegates to another input stream, but can switch transparently the source
* input stream.
*
* <p>Given a set of input streams that return the same data, this input stream will read from one
* and allow switching to read from other streams continuing from the offset that was initially
* read. The result is only meaningful if all streams read the same data.
*
* <p>This class allows transparently to switch between different implementations of the underlying
* streams (memory, disk, etc.) while transparently providing data to users. It does not support
* marking and it is multi-thread safe.
*/
class SwitchableDelegateInputStream extends InputStream {
/** The input stream that is currently providing data. */
private InputStream delegate;
/**
* Current offset in the input stream. We keep track of this to allow skipping data when switching
* input streams.
*/
private long currentOffset;
/** Have we reached the end of stream? */
@VisibleForTesting // private otherwise.
boolean endOfStreamReached;
/**
* If a switch has occurred, how many bytes still need to be skipped in the input stream to
* continue reading from the same position?
*/
private long needsSkipping;
SwitchableDelegateInputStream(InputStream delegate) {
this.delegate = delegate;
currentOffset = 0;
endOfStreamReached = false;
needsSkipping = 0;
}
/**
* Skips data in the input stream if it has been switched and there is data to skip. Will fail if
* we can't skip all the data.
*/
private void skipDataIfNeeded() throws IOException {
while (needsSkipping > 0) {
long skipped = delegate.skip(needsSkipping);
if (skipped == 0) {
throw new IOException("Skipping InputStream after switching failed");
}
needsSkipping -= skipped;
}
}
/** Same as {@link #increaseOffset(long)}. */
private int increaseOffset(int amount) {
return (int) increaseOffset((long) amount);
}
/**
* Increases the current offset after reading. {@code amount} will indicate how many bytes we have
* read. It {@code -1} then we know we've reached the end of the stream and {@link
* #endOfStreamReached} is set to {@code true}.
*/
private long increaseOffset(long amount) {
if (amount > 0) {
currentOffset += amount;
}
if (amount == -1) {
endOfStreamReached = true;
}
return amount;
}
@Override
public synchronized int read(byte[] b) throws IOException {
if (endOfStreamReached) {
return -1;
}
skipDataIfNeeded();
return increaseOffset(delegate.read(b));
}
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
if (endOfStreamReached) {
return -1;
}
skipDataIfNeeded();
return increaseOffset(delegate.read(b, off, len));
}
@Override
public synchronized int read() throws IOException {
if (endOfStreamReached) {
return -1;
}
skipDataIfNeeded();
int r = delegate.read();
if (r == -1) {
endOfStreamReached = true;
} else {
increaseOffset(1);
}
return r;
}
@Override
public synchronized long skip(long n) throws IOException {
if (endOfStreamReached) {
return 0;
}
skipDataIfNeeded();
return increaseOffset(delegate.skip(n));
}
@Override
public synchronized int available() throws IOException {
if (endOfStreamReached) {
return 0;
}
skipDataIfNeeded();
return delegate.available();
}
@Override
public synchronized void close() throws IOException {
endOfStreamReached = true;
delegate.close();
}
@Override
public void mark(int readlimit) {
// We don't support marking.
}
@Override
public void reset() throws IOException {
throw new IOException("Mark not supported");
}
@Override
public boolean markSupported() {
return false;
}
/**
* Switches the stream used.
*
* <p>The stream that is currently in use and the new stream will be used in further operations.
* If this stream has already reached the end, {@code newStream} will be closed immediately and no
* other action is taken. If the stream has not reached the end, any exception reported is due to
* closing the stream currently in use, the new stream is not affected and this stream can still
* be used to read from {@code newStream}.
*/
synchronized void switchStream(InputStream newStream) throws IOException {
if (newStream == delegate) {
return;
}
try (InputStream oldDelegate = delegate) {
delegate = newStream;
needsSkipping = currentOffset;
}
}
}

View File

@ -0,0 +1,77 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.google.common.annotations.VisibleForTesting;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* A temporary directory is a directory that creates temporary files. Upon close, all temporary
* files are removed. Whether the directory itself is removed is dependent on the actual
* implementation.
*/
public interface TemporaryDirectory extends Closeable {
/**
* Creates a new file in the directory. This method returns a new file that deleted, recreated,
* read and written freely by the caller. No assumptions are made on the contents of this file
* except that it will be deleted it if it still exists when the temporary directory is closed.
*/
File newFile() throws IOException;
/** Obtains the directory, only useful for tests. */
@VisibleForTesting // private otherwise.
File getDirectory();
/**
* Creates a new temporary directory in the system's temporary directory. All files created will
* be created in this directory. The directory will be deleted (as long as all the files in it)
* when closed.
*/
static TemporaryDirectory newSystemTemporaryDirectory() throws IOException {
Path tempDir = Files.createTempDirectory("tempdir_");
TemporaryFile tempDirFile = new TemporaryFile(tempDir.toFile());
return new TemporaryDirectory() {
@Override
public File newFile() throws IOException {
return Files.createTempFile(tempDir, "temp_", ".data").toFile();
}
@Override
public File getDirectory() {
return tempDir.toFile();
}
@Override
public void close() throws IOException {
tempDirFile.close();
}
};
}
/**
* Creates a new temporary directory that uses a fixed directory.
*
* @param directory the directory that will be returned; this directory won't be deleted when the
* {@link TemporaryDirectory} objects are closed
* @return a {@link TemporaryDirectory} that will create files in {@code directory}
*/
static TemporaryDirectory fixed(File directory) {
return new TemporaryDirectory() {
@Override
public File newFile() throws IOException {
return Files.createTempFile(directory.toPath(), "temp_", ".data").toFile();
}
@Override
public File getDirectory() {
return directory;
}
@Override
public void close() throws IOException {}
};
}
}

View File

@ -0,0 +1,31 @@
package com.android.tools.build.apkzlib.bytestorage;
import java.io.File;
import java.io.IOException;
/**
* Factory that creates temporary directories. {@link
* TemporaryDirectory#newSystemTemporaryDirectory()} conforms to this interface.
*/
public interface TemporaryDirectoryFactory {
/**
* Creates a new temporary directory.
*
* @return the new temporary directory that should be closed when finished
* @throws IOException failed to create the temporary directory
*/
TemporaryDirectory make() throws IOException;
/**
* Obtains a factory that creates temporary directories using {@link
* TemporaryDirectory#fixed(File)}.
*
* @param directory the directory where all temporary files will be created
* @return a factory that creates instances of {@link TemporaryDirectory} that creates all files
* inside {@code directory}
*/
static TemporaryDirectoryFactory fixed(File directory) {
return () -> TemporaryDirectory.fixed(directory);
}
}

View File

@ -0,0 +1,102 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Byte storage that keeps all byte sources as files in a temporary directory. Each data stored is
* stored as a new file. The file is deleted as soon as the byte source is closed.
*/
public class TemporaryDirectoryStorage implements ByteStorage {
/** Temporary directory to use. */
@VisibleForTesting // private otherwise.
final TemporaryDirectory temporaryDirectory;
/** Number of bytes currently used. */
private long bytesUsed;
/** Maximum number of bytes used. */
private long maxBytesUsed;
/**
* Creates a new storage using the provided temporary directory.
*
* @param temporaryDirectoryFactory a factory used to create the directory to use for temporary
* files; this directory will be closed when the {@link TemporaryDirectoryStorage} is closed.
* @throws IOException failed to create the temporary directory
*/
public TemporaryDirectoryStorage(TemporaryDirectoryFactory temporaryDirectoryFactory)
throws IOException {
this.temporaryDirectory = temporaryDirectoryFactory.make();
}
@Override
public CloseableByteSource fromStream(InputStream stream) throws IOException {
File temporaryFile = temporaryDirectory.newFile();
try (FileOutputStream output = new FileOutputStream(temporaryFile)) {
ByteStreams.copy(stream, output);
}
long size = temporaryFile.length();
incrementBytesUsed(size);
return new TemporaryFileCloseableByteSource(temporaryFile, () -> incrementBytesUsed(-size));
}
@Override
public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {
File temporaryFile = temporaryDirectory.newFile();
return new AbstractCloseableByteSourceFromOutputStreamBuilder() {
private final FileOutputStream output = new FileOutputStream(temporaryFile);
@Override
protected void doWrite(byte[] b, int off, int len) throws IOException {
output.write(b, off, len);
incrementBytesUsed(len);
}
@Override
protected CloseableByteSource doBuild() throws IOException {
output.close();
long size = temporaryFile.length();
return new TemporaryFileCloseableByteSource(temporaryFile, () -> incrementBytesUsed(-size));
}
};
}
@Override
public CloseableByteSource fromSource(ByteSource source) throws IOException {
try (InputStream stream = source.openStream()) {
return fromStream(stream);
}
}
@Override
public synchronized long getBytesUsed() {
return bytesUsed;
}
@Override
public synchronized long getMaxBytesUsed() {
return maxBytesUsed;
}
/** Increments the byte counter by the given amount (decrements if {@code amount} is negative). */
private synchronized void incrementBytesUsed(long amount) {
bytesUsed += amount;
if (bytesUsed > maxBytesUsed) {
maxBytesUsed = bytesUsed;
}
}
@Override
public void close() throws IOException {
temporaryDirectory.close();
}
}

View File

@ -0,0 +1,64 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.google.common.base.Preconditions;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
/**
* A temporary file or directory. Wraps a file or directory and deletes it (recursively, if it is a
* directory) when closed.
*/
public class TemporaryFile implements Closeable {
/** Has the file or directory represented by {@link #file} been deleted? */
private boolean deleted;
/**
* The file or directory that will be deleted on close. May no longer exist if {@link #deleted} is
* {@code true}.
*/
private final File file;
/**
* Creates a new wrapper around the given file. The file or directory {@code file} will be deleted
* (recursively, if it is a directory) on close.
*/
public TemporaryFile(File file) {
deleted = false;
this.file = file;
}
/** Obtains the file or directory this temporary file refers to. */
public File getFile() {
Preconditions.checkState(!deleted, "File already deleted");
return file;
}
@Override
public void close() throws IOException {
if (deleted) {
return;
}
deleted = true;
deleteFile(file);
}
/** Deletes a file or directory if it exists. */
private void deleteFile(File file) throws IOException {
if (file.isDirectory()) {
File[] contents = file.listFiles();
if (contents != null) {
for (File subFile : contents) {
deleteFile(subFile);
}
}
}
if (file.exists() && !file.delete()) {
throw new IOException("Failed to delete '" + file.getAbsolutePath() + "'");
}
}
}

View File

@ -0,0 +1,37 @@
package com.android.tools.build.apkzlib.bytestorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
/**
* Closeable byte source that uses a temporary file to store its contents. The file is deleted when
* the byte source is closed.
*/
class TemporaryFileCloseableByteSource extends CloseableDelegateByteSource {
/** Temporary file backing the byte source. */
private final TemporaryFile temporaryFile;
/** Callback to notify when the byte source is closed. */
private final Runnable closeCallback;
/**
* Creates a new byte source based on the given file. The provided callback is executed when the
* source is deleted. There is no guarantee about which thread invokes the callback (it is the
* thread that closes the source).
*/
TemporaryFileCloseableByteSource(File file, Runnable closeCallback) {
super(Files.asByteSource(file), file.length());
temporaryFile = new TemporaryFile(file);
this.closeCallback = closeCallback;
}
@Override
protected synchronized void innerClose() throws IOException {
super.innerClose();
temporaryFile.close();
closeCallback.run();
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.sign;
/** Message digest algorithms. */
public enum DigestAlgorithm {
/**
* SHA-1 digest.
*
* <p>Android 2.3 (API Level 9) to 4.2 (API Level 17) (inclusive) do not support SHA-2 JAR
* signatures.
*
* <p>Moreover, platforms prior to API Level 18, without the additional Digest-Algorithms
* attribute, only support SHA or SHA1 algorithm names in .SF and MANIFEST.MF attributes.
*/
SHA1("SHA1", "SHA-1"),
/** SHA-256 digest. */
SHA256("SHA-256", "SHA-256");
/**
* API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and {@link
* SignatureAlgorithm#ECDSA}.
*/
public static final int API_SHA_256_RSA_AND_ECDSA = 18;
/**
* API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s.
*
* <p>Before that, SHA256 can only be used with RSA and ECDSA.
*/
public static final int API_SHA_256_ALL_ALGORITHMS = 21;
/** Name of algorithm for message digest. */
public final String messageDigestName;
/** Name of attribute in signature file with the manifest digest. */
public final String manifestAttributeName;
/** Name of attribute in entry (both manifest and signature file) with the entry's digest. */
public final String entryAttributeName;
/**
* Creates a digest algorithm.
*
* @param attributeName attribute name in the signature file
* @param messageDigestName name of algorithm for message digest
*/
DigestAlgorithm(String attributeName, String messageDigestName) {
this.messageDigestName = messageDigestName;
this.entryAttributeName = attributeName + "-Digest";
this.manifestAttributeName = attributeName + "-Digest-Manifest";
}
}

View File

@ -0,0 +1,222 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.sign;
import com.android.tools.build.apkzlib.utils.CachedSupplier;
import com.android.tools.build.apkzlib.utils.IOExceptionRunnable;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.android.tools.build.apkzlib.zfile.ManifestAttributes;
import com.android.tools.build.apkzlib.zip.StoredEntry;
import com.android.tools.build.apkzlib.zip.ZFile;
import com.android.tools.build.apkzlib.zip.ZFileExtension;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
/**
* Extension to {@link ZFile} that will generate a manifest. The extension will register
* automatically with the {@link ZFile}.
*
* <p>Creating this extension will ensure a manifest for the zip exists. This extension will
* generate a manifest if one does not exist and will update an existing manifest, if one does
* exist. The extension will also provide access to the manifest so that others may update the
* manifest.
*
* <p>Apart from standard manifest elements, this extension does not handle any particular manifest
* features such as signing or adding custom attributes. It simply generates a plain manifest and
* provides infrastructure so that other extensions can add data in the manifest.
*
* <p>The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()}
* notification is received, meaning all manifest manipulation is done in-memory.
*/
public class ManifestGenerationExtension {
/** Name of META-INF directory. */
private static final String META_INF_DIR = "META-INF";
/** Name of the manifest file. */
static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
/** Who should be reported as the manifest builder. */
private final String builtBy;
/** Who should be reported as the manifest creator. */
private final String createdBy;
/** The file this extension is attached to. {@code null} if not yet registered. */
@Nullable private ZFile zFile;
/** The zip file's manifest. */
private final Manifest manifest;
/**
* Byte representation of the manifest. There is no guarantee that two writes of the java's {@code
* Manifest} object will yield the same byte array (there is no guaranteed order of entries in the
* manifest).
*
* <p>Because we need the byte representation of the manifest to be stable if there are no changes
* to the manifest, we cannot rely on {@code Manifest} to generate the byte representation every
* time we need the byte representation.
*
* <p>This cache will ensure that we will request one byte generation from the {@code Manifest}
* and will cache it. All further requests of the manifest's byte representation will receive the
* same byte array.
*/
private final CachedSupplier<byte[]> manifestBytes;
/**
* Has the current manifest been changed and not yet flushed? If {@link #dirty} is {@code true},
* then {@link #manifestBytes} should not be valid. This means that marking the manifest as dirty
* should also invalidate {@link #manifestBytes}. To avoid breaking the invariant, instead of
* setting {@link #dirty}, {@link #markDirty()} should be called.
*/
private boolean dirty;
/** The extension to register with the {@link ZFile}. {@code null} if not registered. */
@Nullable private ZFileExtension extension;
/**
* Creates a new extension. This will not register the extension with the provided {@link ZFile}.
* Until {@link #register(ZFile)} is invoked, this extension is not used.
*
* @param builtBy who built the manifest?
* @param createdBy who created the manifest?
*/
public ManifestGenerationExtension(String builtBy, String createdBy) {
this.builtBy = builtBy;
this.createdBy = createdBy;
manifest = new Manifest();
dirty = false;
manifestBytes =
new CachedSupplier<>(
() -> {
ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
try {
manifest.write(outBytes);
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
return outBytes.toByteArray();
});
}
/**
* Marks the manifest as being dirty, <i>i.e.</i>, its data has changed since it was last read
* and/or written.
*/
private void markDirty() {
dirty = true;
manifestBytes.reset();
}
/**
* Registers the extension with the {@link ZFile} provided in the constructor.
*
* @param zFile the zip file to add the extension to
* @throws IOException failed to analyze the zip
*/
public void register(ZFile zFile) throws IOException {
Preconditions.checkState(extension == null, "register() has already been invoked.");
this.zFile = zFile;
rebuildManifest();
extension =
new ZFileExtension() {
@Nullable
@Override
public IOExceptionRunnable beforeUpdate() {
return ManifestGenerationExtension.this::updateManifest;
}
};
this.zFile.addZFileExtension(extension);
}
/** Rebuilds the zip file's manifest, if it needs changes. */
private void rebuildManifest() throws IOException {
Verify.verifyNotNull(zFile, "zFile == null");
StoredEntry manifestEntry = zFile.get(MANIFEST_NAME);
if (manifestEntry != null) {
/*
* Read the manifest entry in the zip file. Make sure we store these byte sequence
* because writing the manifest may not generate the same byte sequence, which may
* trigger an unnecessary re-sign of the jar.
*/
manifest.clear();
byte[] manifestBytes = manifestEntry.read();
manifest.read(new ByteArrayInputStream(manifestBytes));
this.manifestBytes.precomputed(manifestBytes);
}
Attributes mainAttributes = manifest.getMainAttributes();
String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION);
if (currentVersion == null) {
setMainAttribute(
ManifestAttributes.MANIFEST_VERSION, ManifestAttributes.CURRENT_MANIFEST_VERSION);
} else {
if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) {
throw new IOException("Unsupported manifest version: " + currentVersion + ".");
}
}
/*
* We "blindly" override all other main attributes.
*/
setMainAttribute(ManifestAttributes.BUILT_BY, builtBy);
setMainAttribute(ManifestAttributes.CREATED_BY, createdBy);
}
/**
* Sets the value of a main attribute.
*
* @param attribute the attribute
* @param value the value
*/
private void setMainAttribute(String attribute, String value) {
Attributes mainAttributes = manifest.getMainAttributes();
String current = mainAttributes.getValue(attribute);
if (!value.equals(current)) {
mainAttributes.putValue(attribute, value);
markDirty();
}
}
/**
* Updates the manifest in the zip file, if it has been changed.
*
* @throws IOException failed to update the manifest
*/
private void updateManifest() throws IOException {
Verify.verifyNotNull(zFile, "zFile == null");
if (!dirty) {
return;
}
zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get()));
dirty = false;
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.sign;
import java.security.NoSuchAlgorithmException;
/** Signature algorithm. */
public enum SignatureAlgorithm {
/** RSA algorithm. */
RSA("RSA", 1, "withRSA"),
/** ECDSA algorithm. */
ECDSA("EC", 18, "withECDSA"),
/** DSA algorithm. */
DSA("DSA", 1, "withDSA");
/** Name of the private key as reported by {@code PrivateKey}. */
public final String keyAlgorithm;
/** Minimum SDK version that allows this signature. */
public final int minSdkVersion;
/** Suffix appended to digest algorithm to obtain signature algorithm. */
public final String signatureAlgorithmSuffix;
/**
* Creates a new signature algorithm.
*
* @param keyAlgorithm the name as reported by {@code PrivateKey}
* @param minSdkVersion minimum SDK version that allows this signature
* @param signatureAlgorithmSuffix suffix for signature name with used with a digest
*/
SignatureAlgorithm(String keyAlgorithm, int minSdkVersion, String signatureAlgorithmSuffix) {
this.keyAlgorithm = keyAlgorithm;
this.minSdkVersion = minSdkVersion;
this.signatureAlgorithmSuffix = signatureAlgorithmSuffix;
}
/**
* Obtains the signature algorithm that corresponds to a private key name applicable to a SDK
* version.
*
* @param keyAlgorithm the named referred in the {@code PrivateKey}
* @param minSdkVersion minimum SDK version to run
* @return the algorithm that has {@link #keyAlgorithm} equal to {@code keyAlgorithm}
* @throws NoSuchAlgorithmException if no algorithm was found for the given private key; an
* algorithm was found but is not applicable to the given SDK version
*/
public static SignatureAlgorithm fromKeyAlgorithm(String keyAlgorithm, int minSdkVersion)
throws NoSuchAlgorithmException {
for (SignatureAlgorithm alg : values()) {
if (alg.keyAlgorithm.equalsIgnoreCase(keyAlgorithm)) {
if (alg.minSdkVersion > minSdkVersion) {
throw new NoSuchAlgorithmException(
"Signatures with "
+ keyAlgorithm
+ " keys are not supported on minSdkVersion "
+ minSdkVersion
+ ". They are supported only for minSdkVersion >= "
+ alg.minSdkVersion);
}
return alg;
}
}
throw new NoSuchAlgorithmException("Signing with " + keyAlgorithm + " keys is not supported");
}
/**
* Obtains the name of the signature algorithm when used with a digest algorithm.
*
* @param digestAlgorithm the digest algorithm to use
* @return the name of the signature algorithm
*/
public String signatureAlgorithmName(DigestAlgorithm digestAlgorithm) {
return digestAlgorithm.messageDigestName.replace("-", "") + signatureAlgorithmSuffix;
}
}

View File

@ -0,0 +1,437 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.sign;
import com.android.apksig.ApkSignerEngine;
import com.android.apksig.ApkVerifier;
import com.android.apksig.DefaultApkSignerEngine;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.tools.build.apkzlib.utils.IOExceptionRunnable;
import com.android.tools.build.apkzlib.utils.SigningBlockUtils;
import com.android.tools.build.apkzlib.zip.StoredEntry;
import com.android.tools.build.apkzlib.zip.ZFile;
import com.android.tools.build.apkzlib.zip.ZFileExtension;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Bytes;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/**
* {@link ZFile} extension which signs the APK.
*
* <p>This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK
* Signature Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters
* to this extension's constructor.
*/
public class SigningExtension {
private static final int MAX_READ_CHUNK_SIZE = 65536;
// IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive
// from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.
// This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile
// extension events/callbacks.
//
// The main issue leading to additional complexity in this class is that the current build
// pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that
// matter) for incremental builds. Thus:
// * ZFile extension receives no events for JAR entries already in the APK whereas
// ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this
// class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries
// already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR
// entries which the incremental build session did not touch.
// * The build pipeline expects the APK not to change if no JAR entry was added to it or removed
// from it whereas ApkSignerEngine produces no output only if it has already produced a signed
// APK and no changes have since been made to it. This class addresses this issue by checking
// in its "register" method whether the APK is correctly signed and, only if that's the case,
// doesn't modify the APK unless a JAR entry is added to it or removed from it after
// "register".
/** APK signer which performs most of the heavy lifting. */
private final ApkSignerEngine signer;
/** Names of APK entries which have been processed by {@link #signer}. */
private final Set<String> signerProcessedOutputEntryNames = new HashSet<>();
/** Signing block Id for SDK dependency block. */
static final int DEPENDENCY_INFO_BLOCK_ID = 0x504b4453;
/** SDK dependencies of the APK */
@Nullable private byte[] sdkDependencyData;
/**
* Cached contents of the most recently output APK Signing Block or {@code null} if the block
* hasn't yet been output.
*/
@Nullable private byte[] cachedApkSigningBlock;
/**
* {@code true} if signatures may need to be output, {@code false} if there's no need to output
* signatures. This is used in an optimization where we don't modify the APK if it's already
* signed and if no JAR entries have been added to or removed from the file.
*/
private boolean dirty;
/** The extension registered with the {@link ZFile}. {@code null} if not registered. */
@Nullable private ZFileExtension extension;
/** The file this extension is attached to. {@code null} if not yet registered. */
@Nullable private ZFile zFile;
/** A buffer used to read data from entries to feed to digests */
private final Supplier<byte[]> digestBuffer =
Suppliers.memoize(() -> new byte[MAX_READ_CHUNK_SIZE]);
/** An object that has all necessary information to sign the zip file and verify its signature */
private final SigningOptions options;
public SigningExtension(SigningOptions opts) throws InvalidKeyException {
DefaultApkSignerEngine.SignerConfig signerConfig =
new DefaultApkSignerEngine.SignerConfig.Builder(
"CERT", opts.getKey(), opts.getCertificates())
.build();
signer =
new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), opts.getMinSdkVersion())
.setOtherSignersSignaturesPreserved(false)
.setV1SigningEnabled(opts.isV1SigningEnabled())
.setV2SigningEnabled(opts.isV2SigningEnabled())
.setV3SigningEnabled(false)
.setCreatedBy("1.0 (Android)")
.build();
if (opts.getSdkDependencyData() != null) {
sdkDependencyData = opts.getSdkDependencyData();
}
if (opts.getExecutor() != null) {
signer.setExecutor(opts.getExecutor());
}
this.options = opts;
}
public void register(ZFile zFile) throws NoSuchAlgorithmException, IOException {
Preconditions.checkState(extension == null, "register() already invoked");
this.zFile = zFile;
switch (options.getValidation()) {
case ALWAYS_VALIDATE:
dirty = !isCurrentSignatureAsRequested();
break;
case ASSUME_VALID:
if (options.isV1SigningEnabled()) {
Set<String> entryNames =
ImmutableSet.copyOf(
Iterables.transform(
zFile.entries(), e -> e.getCentralDirectoryHeader().getName()));
StoredEntry manifestEntry = zFile.get(ManifestGenerationExtension.MANIFEST_NAME);
Preconditions.checkNotNull(
manifestEntry,
"No manifest found in apk for incremental build with enabled v1 signature");
signerProcessedOutputEntryNames.addAll(
this.signer.initWith(manifestEntry.read(), entryNames));
}
dirty = false;
break;
case ASSUME_INVALID:
dirty = true;
break;
}
extension =
new ZFileExtension() {
@Override
public IOExceptionRunnable added(StoredEntry entry, @Nullable StoredEntry replaced) {
return () -> onZipEntryOutput(entry);
}
@Override
public IOExceptionRunnable removed(StoredEntry entry) {
String entryName = entry.getCentralDirectoryHeader().getName();
return () -> onZipEntryRemovedFromOutput(entryName);
}
@Override
public IOExceptionRunnable beforeUpdate() throws IOException {
return () -> onOutputZipReadyForUpdate();
}
@Override
public void entriesWritten() throws IOException {
onOutputZipEntriesWritten();
}
@Override
public void closed() {
onOutputClosed();
}
};
this.zFile.addZFileExtension(extension);
}
/**
* Returns {@code true} if the APK's signatures are as requested by parameters to this signing
* extension.
*/
private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {
ApkVerifier.Result result;
try {
result =
new ApkVerifier.Builder(zFile.asDataSource())
.setMinCheckedPlatformVersion(options.getMinSdkVersion())
.build()
.verify();
} catch (ApkFormatException e) {
// Malformed APK
return false;
}
if (!result.isVerified()) {
// Signature(s) did not verify
return false;
}
if ((result.isVerifiedUsingV1Scheme() != options.isV1SigningEnabled())
|| (result.isVerifiedUsingV2Scheme() != options.isV2SigningEnabled())) {
// APK isn't signed with exactly the schemes we want it to be signed
return false;
}
List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();
if (verifiedSignerCerts.size() != 1) {
// APK is not signed by exactly one signer
return false;
}
byte[] expectedEncodedCert;
byte[] actualEncodedCert;
try {
expectedEncodedCert = options.getCertificates().get(0).getEncoded();
actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();
} catch (CertificateEncodingException e) {
// Failed to encode signing certificates
return false;
}
if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {
// APK is signed by a wrong signer
return false;
}
// APK is signed the way we want it to be signed
return true;
}
private void onZipEntryOutput(StoredEntry entry) throws IOException {
setDirty();
String entryName = entry.getCentralDirectoryHeader().getName();
// This event may arrive after the entry has already been deleted. In that case, we don't
// report the addition of the entry to ApkSignerEngine.
if (entry.isDeleted()) {
return;
}
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = signer.outputJarEntry(entryName);
signerProcessedOutputEntryNames.add(entryName);
if (inspectEntryRequest != null) {
try (InputStream inputStream = new BufferedInputStream(entry.open())) {
copyStreamToDataSink(inputStream, inspectEntryRequest.getDataSink());
}
inspectEntryRequest.done();
}
}
private void copyStreamToDataSink(InputStream inputStream, DataSink dataSink) throws IOException {
int bytesRead;
byte[] buffer = digestBuffer.get();
while ((bytesRead = inputStream.read(buffer)) > 0) {
dataSink.consume(buffer, 0, bytesRead);
}
}
private void onZipEntryRemovedFromOutput(String entryName) {
setDirty();
signer.outputJarEntryRemoved(entryName);
signerProcessedOutputEntryNames.remove(entryName);
}
private void onOutputZipReadyForUpdate() throws IOException {
if (!dirty) {
return;
}
// Notify signer engine about ZIP entries that have appeared in the output without the
// engine knowing. Also identify ZIP entries which disappeared from the output without the
// engine knowing.
Set<String> unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames);
for (StoredEntry entry : zFile.entries()) {
String entryName = entry.getCentralDirectoryHeader().getName();
unprocessedRemovedEntryNames.remove(entryName);
if (!signerProcessedOutputEntryNames.contains(entryName)) {
// Signer engine is not yet aware that this entry is in the output
onZipEntryOutput(entry);
}
}
// Notify signer engine about entries which disappeared from the output without the engine
// knowing
for (String entryName : unprocessedRemovedEntryNames) {
onZipEntryRemovedFromOutput(entryName);
}
// Check whether we need to output additional JAR entries which comprise the v1 signature
ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;
try {
addV1SignatureRequest = signer.outputJarEntries();
} catch (Exception e) {
throw new IOException("Failed to generate v1 signature", e);
}
if (addV1SignatureRequest == null) {
return;
}
// We need to output additional JAR entries which comprise the v1 signature
List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =
new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());
// Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first
// entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by
// ManifestGenerationExtension.
for (int i = 0; i < v1SignatureEntries.size(); i++) {
ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);
String name = entry.getName();
if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {
continue;
}
if (i != 0) {
v1SignatureEntries.remove(i);
v1SignatureEntries.add(0, entry);
}
break;
}
// Output the JAR entries comprising the v1 signature
for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {
String name = entry.getName();
byte[] data = entry.getData();
zFile.add(name, new ByteArrayInputStream(data));
}
addV1SignatureRequest.done();
}
private void onOutputZipEntriesWritten() throws IOException {
if (!dirty) {
return;
}
// Check whether we should output an APK Signing Block which contains v2 signatures
byte[] apkSigningBlock;
byte[] centralDirBytes = zFile.getCentralDirectoryBytes();
byte[] eocdBytes = zFile.getEocdBytes();
ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest;
// This event may arrive a second time -- after we write out the APK Signing Block. Thus, we
// cache the block to speed things up. The cached block is invalidated by any changes to the
// file (as reported to this extension).
if (cachedApkSigningBlock != null) {
apkSigningBlock = cachedApkSigningBlock;
addV2SignatureRequest = null;
} else {
DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));
DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));
long zipEntriesSizeBytes =
zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset();
DataSource zipEntries = zFile.asDataSource(0, zipEntriesSizeBytes);
try {
addV2SignatureRequest = signer.outputZipSections2(zipEntries, centralDir, eocd);
} catch (NoSuchAlgorithmException
| InvalidKeyException
| SignatureException
| ApkFormatException
| IOException e) {
throw new IOException("Failed to generate v2 signature", e);
}
if (addV2SignatureRequest != null) {
apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
if (sdkDependencyData != null) {
apkSigningBlock =
SigningBlockUtils.addToSigningBlock(
apkSigningBlock, sdkDependencyData, DEPENDENCY_INFO_BLOCK_ID);
}
apkSigningBlock =
Bytes.concat(
new byte[addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock()],
apkSigningBlock);
} else {
apkSigningBlock = new byte[0];
if (sdkDependencyData != null) {
apkSigningBlock =
SigningBlockUtils.addToSigningBlock(
apkSigningBlock, sdkDependencyData, DEPENDENCY_INFO_BLOCK_ID);
int paddingSize =
ApkSigningBlockUtils.generateApkSigningBlockPadding(
zipEntries, /* apkSigningBlockPaddingSupported */ true)
.getSecond();
apkSigningBlock = Bytes.concat(new byte[paddingSize], apkSigningBlock);
}
}
cachedApkSigningBlock = apkSigningBlock;
}
// Insert the APK Signing Block into the output right before the ZIP Central Directory and
// accordingly update the start offset of ZIP Central Directory in ZIP End of Central
// Directory.
zFile.directWrite(
zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(), apkSigningBlock);
zFile.setExtraDirectoryOffset(apkSigningBlock.length);
if (addV2SignatureRequest != null) {
addV2SignatureRequest.done();
}
}
private void onOutputClosed() {
if (!dirty) {
return;
}
signer.outputDone();
dirty = false;
}
private void setDirty() {
dirty = true;
cachedApkSigningBlock = null;
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.sign;
import com.android.apksig.util.RunnablesExecutor;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/** A class that contains data to initialize SigningExtension. */
@AutoValue
public abstract class SigningOptions {
/** An implementation of builder pattern to create a {@link SigningOptions} object. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setKey(@Nonnull PrivateKey key);
public abstract Builder setCertificates(@Nonnull ImmutableList<X509Certificate> certs);
public abstract Builder setCertificates(X509Certificate... certs);
public abstract Builder setV1SigningEnabled(boolean enabled);
public abstract Builder setV2SigningEnabled(boolean enabled);
public abstract Builder setMinSdkVersion(int version);
public abstract Builder setValidation(@Nonnull Validation validation);
public abstract Builder setExecutor(@Nullable RunnablesExecutor executor);
public abstract Builder setSdkDependencyData(@Nullable byte[] sdkDependencyData);
abstract SigningOptions autoBuild();
public SigningOptions build() {
SigningOptions options = autoBuild();
Preconditions.checkArgument(options.getMinSdkVersion() >= 0, "minSdkVersion < 0");
Preconditions.checkArgument(
!options.getCertificates().isEmpty(),
"There should be at least one certificate in SigningOptions");
return options;
}
}
public static Builder builder() {
return new AutoValue_SigningOptions.Builder()
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setValidation(Validation.ALWAYS_VALIDATE);
}
/** {@link PrivateKey} used to sign the archive. */
public abstract PrivateKey getKey();
/**
* A list of the {@link X509Certificate}s to embed in the signed APKs. The first
* element of the list must be the certificate associated with the private key.
*/
public abstract ImmutableList<X509Certificate> getCertificates();
/** Shows whether signing with JAR Signature Scheme (aka v1 signing) is enabled. */
public abstract boolean isV1SigningEnabled();
/** Shows whether signing with APK Signature Scheme v2 (aka v2 signing) is enabled. */
public abstract boolean isV2SigningEnabled();
/** Minimum SDK version supported. */
public abstract int getMinSdkVersion();
/** Strategy of package signature validation */
public abstract Validation getValidation();
@Nullable
public abstract RunnablesExecutor getExecutor();
/** SDK dependencies of the APK */
@Nullable
public abstract byte[] getSdkDependencyData();
public enum Validation {
/** Always perform signature validation */
ALWAYS_VALIDATE,
/**
* Assume the signature is valid without validation i.e. don't resign if no files changed
*/
ASSUME_VALID,
/** Assume the signature is invalid without validation i.e. unconditionally resign */
ASSUME_INVALID,
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright (C) 2016 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.
*/
/**
* The {@code sign} package provides extensions for the {@code zip} package that allow:
*
* <ul>
* <li>Adding a {@code MANIFEST.MF} file to a zip making a jar.
* <li>Signing a jar.
* <li>Fully signing a jar using v2 apk signature.
* </ul>
*
* <p>Because the {@code zip} package is completely independent of the {@code sign} package, the
* actual coordination between the two is complex. The {@code sign} package works by registering
* extensions with the {@code zip} package. These extensions are notified in changes made in the zip
* and will change the zip file itself.
*
* <p>The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will ensure the zip
* has a manifest file and is, therefore, a valid jar. The {@link
* com.android.apkzlib.sign.SigningExtension} extension will ensure the jar is signed.
*
* <p>The extension mechanism used is the one provided in the {@code zip} package (see {@link
* com.android.apkzlib.zip.ZFile} and {@link com.android.apkzlib.zip.ZFileExtension}. Building the
* zip and then operating the extensions is not done sequentially, as we don't want to build a zip
* and then sign it. We want to build a zip that is automatically signed. Extension are basically
* observers that register on the zip and are notified when things happen in the zip. They will then
* modify the zip accordingly.
*
* <p>The zip file notifies extensions in 4 critical moments: when a file is added or removed from
* the zip, when the zip is about to be flushed to disk and when the zip's entries have been flushed
* but the central directory not. At these moments, the extensions can act to update the zip in any
* way they need.
*
* <p>To see how this works, consider the manifest generation extension: when the extension is
* created, it checks the zip file to see if there is a manifest. If a manifest exists and does not
* need updating, it does not change anything, otherwise it generates a new manifest for the zip
* file. At this point, the extension could write the manifest to the zip, but we opted not to. It
* would be irrelevant anyway as the zip will only be written when flushed.
*
* <p>Now, when the {@code ZFile} notifies the extension that it is about to start writing the zip
* file, the manifest extension, if it has noted that the manifest needs to be rewritten, will --
* before the {@code ZFile} actually writes anything -- modify the zip and add or replace the
* existing manifest file. So, process-wise, the zip is written only once with the correct manifest.
* The flow is as follows (if only the manifest generation extension was added to the {@code
* ZFile}):
*
* <ol>
* <li>{@code ZFile.update()} is called.
* <li>{@code ZFile} calls {@code beforeUpdate()} for all {@code ZFileExtensions} registered, in
* this case, only the instance of the anonymous inner class generated in the {@code
* ManifestGenerationExtension} constructor is invoked.
* <li>{@code ManifestGenerationExtension.updateManifest()} is called.
* <li>If the manifest does not need to be updated, {@code updateManifest()} returns immediately.
* <li>If the manifest needs updating, {@code ZFile.add()} is invoked to add or replace the
* manifest.
* <li>{@code ManifestGenerationExtension.updateManifest()} returns.
* <li>{@code ZFile.update()} continues and writes the zip file, containing the manifest.
* <li>The zip is finally written with an updated manifest.
* </ol>
*
* <p>To generate a signed apk, we need to add a second extension, the {@code SigningExtension}.
* This extension will also register listeners with the {@code ZFile}.
*
* <p>In this case the flow would be (starting a bit earlier for clarity and assuming a package task
* in the build process):
*
* <ol>
* <li>Package task creates a {@code ZFile} on the target apk (or non-existing file, if there is
* no target apk in the output directory).
* <li>Package task configures the {@code ZFile} with alignment rules.
* <li>Package task creates a {@code ManifestGenerationExtension}.
* <li>Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.
* <li>The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid
* manifest. No changes are done to the {@code ZFile}.
* <li>Package task creates a {@code SigningExtension}.
* <li>Package task registers the {@code SigningExtension} with the {@code ZFile}.
* <li>The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile} and
* look at the {@code ZFile} to see if there is a valid signature file.
* <li>If there are changes to the digital signature file needed, these are marked internally in
* the extension. If there are changes needed to the digests, the manifest is updated (by
* calling {@code ManifestGenerationExtension}.<br>
* <em>(note that this point, the apk file, if any existed, has not been touched, the manifest
* is only updated in memory and the digests of all files in the apk, if any, have been
* computed and stored in memory only; the digital signature of the {@code SF} file has not
* been computed.) </em>
* <li>The Package task now adds all files to the {@code ZFile}.
* <li>For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added}
* method of all registered extensions.
* <li>The {@code ManifestGenerationExtension} ignores added invocations.
* <li>The {@code SigningExtension} computes the digest for the added file and stores them in the
* manifest.<br>
* <em>(when all files are added to the apk, all digests are computed and the manifest is
* updated but only in memory; the apk file has not been touched; also note that {@code ZFile}
* has not actually written anything to disk at this point, all files added are kept in
* memory).</em>
* <li>Package task calls {@code ZFile.update()} to update the apk.
* <li>{@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is
* done before anything is written. In this case both the {@code ManifestGenerationExtension}
* and {@code SigningExtension} are invoked.
* <li>The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new
* manifest, unless nothing has changed, in which case it does nothing.
* <li>The {@code SigningExtension} will add the SF file (unless nothing has changed), will
* compute the digital signature of the SF file and write it to the {@code ZFile}.<br>
* <em>(note that the order by which the {@code ManifestGenerationExtension} and {@code
* SigningExtension} are called is non-deterministic; however, this is not a problem because
* the manifest is already computed by the {@code ManifestGenerationExtension} at this time
* and the {@code SigningExtension} will obtain the manifest data from the {@code
* ManifestGenerationExtension} and not from the {@code ZFile}; this means that the {@code SF}
* file may be added to the {@code ZFile} before the {@code MF} file, but that is
* irrelevant.)</em>
* <li>Once both extensions have finished doing the {@code beforeUpdate()} method, the {@code
* ZFile.update()} method continues.
* <li>{@code ZFile.update()} writes all changes and new entries to the zip file.
* <li>{@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all registered
* extensions. {@code SigningExtension} will kick in at this point, if v2 signature has
* changed.
* <li>{@code ZFile} writes the central directory and EOCD.
* <li>{@code ZFile.update()} returns control to the package task.
* <li>The package task finishes.
* </ol>
*
* <em>(*) There is a number of optimizations if we're adding files from another {@code ZFile},
* which is the case when we add the output of aapt to the apk. In particular, files from the aapt
* are ignored if they are already in the apk (same name, same CRC32) and also files copied from the
* aapt's output are not recompressed (the binary compressed data is directly copied to the
* zip).</em>
*
* <p>If there are no changes to the {@code ZFile} made by the package task and the file's manifest
* and v1 signatures are correct, neither the {@code ManifestGenerationExtension} nor the {@code
* SigningExtension} will not do anything on the {@code beforeUpdate()} and the {@code ZFile} won't
* even be open for writing.
*
* <p>This implementation provides perfect incremental updates.
*
* <p>Additionally, by adding/removing extensions we can configure what type of apk we want:
*
* <ul>
* <li>No SigningExtension Aligned, unsigned apk.
* <li>SigningExtension Aligned, signed apk.
* </ul>
*
* So, by configuring which extensions to add, the package task can decide what type of apk we want.
*/
package com.android.apkzlib.sign;

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.utils;
/** Pair implementation to use with the {@code apkzlib} library. */
public class ApkZLibPair<T1, T2> {
/** First value. */
public T1 v1;
/** Second value. */
public T2 v2;
/**
* Creates a new pair.
*
* @param v1 the first value
* @param v2 the second value
*/
public ApkZLibPair(T1 v1, T2 v2) {
this.v1 = v1;
this.v2 = v2;
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.utils;
import com.google.common.base.Objects;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import javax.annotation.Nullable;
/**
* A cache for file contents. The cache allows closing a file and saving in memory its contents (or
* some related information). It can then be used to check if the contents are still valid at some
* later time. Typical usage flow is:
*
* <p>
*
* <pre>{@code
* Object fileRepresentation = // ...
* File toWrite = // ...
* // Write file contents and update in memory representation
* CachedFileContents<Object> contents = new CachedFileContents<Object>(toWrite);
* contents.closed(fileRepresentation);
*
* // Later, when data is needed:
* if (contents.isValid()) {
* fileRepresentation = contents.getCache();
* } else {
* // Re-read the file and recreate the file representation
* }
* }</pre>
*
* @param <T> the type of cached contents
*/
public class CachedFileContents<T> {
/** The file. */
private final File file;
/** Time when last closed (time when {@link #closed(Object)} was invoked). */
private long lastClosed;
/** Size of the file when last closed. */
private long size;
/** Hash of the file when closed. {@code null} if hashing failed for some reason. */
@Nullable private HashCode hash;
/** Cached data associated with the file. */
@Nullable private T cache;
/**
* Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked to
* set the cache.
*
* @param file the file
*/
public CachedFileContents(File file) {
this.file = file;
}
/**
* Should be called when the file's contents are set and the file closed. This will save the cache
* and register the file's timestamp to later detect if it has been modified.
*
* <p>This method can be called as many times as the file has been written.
*
* @param cache an optional cache to save
*/
public void closed(@Nullable T cache) {
this.cache = cache;
lastClosed = file.lastModified();
size = file.length();
hash = hashFile();
}
/**
* Are the cached contents still valid? If this method determines that the file has been modified
* since the last time {@link #closed(Object)} was invoked.
*
* @return are the cached contents still valid? If this method returns {@code false}, the cache is
* cleared
*/
public boolean isValid() {
boolean valid = true;
if (!file.exists()) {
valid = false;
}
if (valid && file.lastModified() != lastClosed) {
valid = false;
}
if (valid && file.length() != size) {
valid = false;
}
if (valid && !Objects.equal(hash, hashFile())) {
valid = false;
}
if (!valid) {
cache = null;
}
return valid;
}
/**
* Obtains the cached data set with {@link #closed(Object)} if the file has not been modified
* since {@link #closed(Object)} was invoked.
*
* @return the last cached data or {@code null} if the file has been modified since {@link
* #closed(Object)} has been invoked
*/
@Nullable
public T getCache() {
return cache;
}
/**
* Computes the hashcode of the cached file.
*
* @return the hash code
*/
@Nullable
private HashCode hashFile() {
try {
return Files.asByteSource(file).hash(Hashing.crc32());
} catch (IOException e) {
return null;
}
}
/**
* Obtains the file used for caching.
*
* @return the file; this file always exists and contains the old (cached) contents of the file
*/
public File getFile() {
return file;
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.utils;
import com.google.common.base.Supplier;
/**
* Supplier that will cache a computed value and always supply the same value. It can be used to
* lazily compute data. For example:
*
* <pre>{@code
* CachedSupplier<Integer> value = new CachedSupplier<>(() -> {
* Integer result;
* // Do some expensive computation.
* return result;
* });
*
* if (a) {
* // We need the result of the expensive computation.
* Integer r = value.get();
* }
*
* if (b) {
* // We also need the result of the expensive computation.
* Integer r = value.get();
* }
*
* // If neither a nor b are true, we avoid doing the computation at all.
* }</pre>
*/
public class CachedSupplier<T> {
/**
* The cached data, {@code null} if computation resulted in {@code null}. It is also {@code null}
* if the cached data has not yet been computed.
*/
private T cached;
/** Is the current data in {@link #cached} valid? */
private boolean valid;
/** Actual supplier of data, if computation is needed. */
private final Supplier<T> supplier;
/** Creates a new supplier. */
public CachedSupplier(Supplier<T> supplier) {
valid = false;
this.supplier = supplier;
}
/**
* Obtains the value.
*
* @return the value, either cached (if one exists) or computed
*/
public synchronized T get() {
if (!valid) {
cached = supplier.get();
valid = true;
}
return cached;
}
/**
* Resets the cache forcing a {@code get()} on the supplier next time {@link #get()} is invoked.
*/
public synchronized void reset() {
cached = null;
valid = false;
}
/**
* In some cases, we may be able to precompute the cache value (or load it from somewhere we had
* previously stored it). This method allows the cache value to be loaded.
*
* <p>If this method is invoked, then an invocation of {@link #get()} will not trigger an
* invocation of the supplier provided in the constructor.
*
* @param t the new cache contents; will replace any currently cache content, if one exists
*/
public synchronized void precomputed(T t) {
cached = t;
valid = true;
}
/**
* Checks if the contents of the cache are valid.
*
* @return are there valid contents in the cache?
*/
public synchronized boolean isValid() {
return valid;
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.utils;
import java.io.IOException;
import javax.annotation.Nullable;
/** Consumer that can throw an {@link IOException}. */
public interface IOExceptionConsumer<T> {
/**
* Performs an operation on the given input.
*
* @param input the input
*/
void accept(@Nullable T input) throws IOException;
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.utils;
import com.google.common.base.Function;
import java.io.IOException;
import javax.annotation.Nullable;
/** Function that can throw an I/O Exception */
public interface IOExceptionFunction<F, T> {
/**
* Applies the function to the given input.
*
* @param input the input
* @return the function result
*/
@Nullable
T apply(@Nullable F input) throws IOException;
/**
* Wraps a function that may throw an IO Exception throwing an {@link IOExceptionWrapper}.
*
* @param f the function
*/
static <F, T> Function<F, T> asFunction(IOExceptionFunction<F, T> f) {
return i -> {
try {
return f.apply(i);
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
};
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.utils;
import java.io.IOException;
/** Runnable that can throw I/O exceptions. */
public interface IOExceptionRunnable {
/**
* Runs the runnable.
*
* @throws IOException failed to run
*/
void run() throws IOException;
/**
* Wraps a runnable that may throw an IO Exception throwing an {@code UncheckedIOException}.
*
* @param r the runnable
*/
static Runnable asRunnable(IOExceptionRunnable r) {
return () -> {
try {
r.run();
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
};
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.utils;
import java.io.IOException;
/**
* Runtime exception used to encapsulate an IO Exception. This is used to allow throwing I/O
* exceptions in functional interfaces that do not allow it and catching the exception afterwards.
*/
public class IOExceptionWrapper extends RuntimeException {
/**
* Creates a new exception.
*
* @param e the I/O exception to encapsulate
*/
public IOExceptionWrapper(IOException e) {
super(e);
}
@Override
public IOException getCause() {
return (IOException) super.getCause();
}
}

View File

@ -0,0 +1,185 @@
/*
* Copyright (C) 2019 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.tools.build.apkzlib.utils;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.apk.ApkUtils.ApkSigningBlock;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.zip.ZipFormatException;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.annotation.Nullable;
/** Generates and appends a new block to APK v2 Signature block. */
public final class SigningBlockUtils {
private static final int MAGIC_NUM_BYTES = 16;
private static final int BLOCK_LENGTH_NUM_BYTES = 8;
static final int SIZE_OF_BLOCK_NUM_BYTES = 8;
static final int BLOCK_ID_NUM_BYTES = 4;
static final int ANDROID_COMMON_PAGE_ALIGNMENT_NUM_BYTES = 4096;
static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
/**
* Generates a new block with the given block value and block id, and appends it to the signing
* block.
*
* @param signingBlock Block containing v2 signature and (optionally) padding block or null.
* @param blockValue byte array containing block value of the new block or null.
* @param blockId block id of the new block.
* @return APK v2 block with signatures and the new block. If {@code blockValue} is null the
* {@code signingBlock} is returned without any modification. If {@code signingBlock} is null,
* a new signature block is created containing the new block and, optionally, padding block.
*/
public static byte[] addToSigningBlock(byte[] signingBlock, byte[] blockValue, int blockId)
throws IOException {
if (blockValue == null || blockValue.length == 0) {
return signingBlock;
}
if (signingBlock == null || signingBlock.length == 0) {
return createSigningBlock(blockValue, blockId);
}
return appendToSigningBlock(signingBlock, blockValue, blockId);
}
/**
* Adds a new block to the signature block and a padding block, if required.
*
* @param signingBlock APK v2 signing block containing : length prefix, signers (can include
* padding block), length postfix and APK sig v2 block magic.
* @param blockValue byte array containing block value of the new block.
* @param blockId block id of the new block.
* @return APK v2 signing block containing : length prefix, signers including the new block (may
* include padding block as well), length postfix and APK sig v2 block magic.
*/
private static byte[] appendToSigningBlock(byte[] signingBlock, byte[] blockValue, int blockId)
throws IOException {
ImmutableList<Pair<byte[], Integer>> entries =
ImmutableList.<Pair<byte[], Integer>>builder()
.addAll(extractAllSigners(DataSources.asDataSource(ByteBuffer.wrap(signingBlock))))
.add(Pair.of(blockValue, blockId))
.build();
return ApkSigningBlockUtils.generateApkSigningBlock(entries);
}
/**
* Generate APK sig v2 block containing a block composed of the provided block value and id, and
* (optionally) padding block.
*/
private static byte[] createSigningBlock(byte[] blockValue, int blockId) {
return ApkSigningBlockUtils.generateApkSigningBlock(
ImmutableList.of(Pair.of(blockValue, blockId)));
}
/**
* Extracts all signing block entries except padding block.
*
* @param signingBlock APK v2 signing block containing: length prefix, signers (can include
* padding block), length postfix and APK sig v2 block magic.
* @return list of block entry value and block entry id pairs.
*/
private static ImmutableList<Pair<byte[], Integer>> extractAllSigners(DataSource signingBlock)
throws IOException {
long wholeBlockSize = signingBlock.size();
// Take the segment of the existing signing block without the length prefix (8 bytes)
// at the beginning and the length and magic (24 bytes) at the end, so it is just the sequence
// of length prefix id value pairs.
DataSource lengthPrefixedIdValuePairsSource =
signingBlock.slice(
SIZE_OF_BLOCK_NUM_BYTES,
wholeBlockSize - 2 * SIZE_OF_BLOCK_NUM_BYTES - MAGIC_NUM_BYTES);
final int lengthAndIdByteCount = BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES;
ByteBuffer lengthAndId = ByteBuffer.allocate(lengthAndIdByteCount).order(LITTLE_ENDIAN);
ImmutableList.Builder<Pair<byte[], Integer>> idValuePairs = ImmutableList.builder();
for (int index = 0; index <= lengthPrefixedIdValuePairsSource.size() - lengthAndIdByteCount; ) {
lengthPrefixedIdValuePairsSource.copyTo(index, lengthAndIdByteCount, lengthAndId);
lengthAndId.flip();
int blockLength = Ints.checkedCast(lengthAndId.getLong());
int id = lengthAndId.getInt();
lengthAndId.clear();
if (id != VERITY_PADDING_BLOCK_ID) {
int blockValueSize = blockLength - BLOCK_ID_NUM_BYTES;
ByteBuffer blockValue = ByteBuffer.allocate(blockValueSize);
lengthPrefixedIdValuePairsSource.copyTo(
index + BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES, blockValueSize, blockValue);
idValuePairs.add(Pair.of(blockValue.array(), id));
}
index += blockLength + BLOCK_LENGTH_NUM_BYTES;
}
return idValuePairs.build();
}
/**
* Extract a block with the given id from the APK. If there is more than one block with the same
* ID, the first block will be returned. If there are no block with the give id, {@code null} will
* be returned.
*
* @param apk APK file
* @param blockId id of the block to be extracted.
*/
@Nullable
public static ByteBuffer extractBlock(File apk, int blockId)
throws IOException, ZipFormatException, ApkSigningBlockNotFoundException {
try (RandomAccessFile file = new RandomAccessFile(apk, "r")) {
DataSource apkDataSource = DataSources.asDataSource(file);
ApkSigningBlock signingBlockInfo =
ApkUtils.findApkSigningBlock(apkDataSource, ApkUtils.findZipSections(apkDataSource));
DataSource wholeV2Block = signingBlockInfo.getContents();
final int lengthAndIdByteCount = BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES;
DataSource signingBlock =
wholeV2Block.slice(
SIZE_OF_BLOCK_NUM_BYTES,
wholeV2Block.size() - SIZE_OF_BLOCK_NUM_BYTES - MAGIC_NUM_BYTES);
ByteBuffer lengthAndId =
ByteBuffer.allocate(lengthAndIdByteCount).order(ByteOrder.LITTLE_ENDIAN);
for (int index = 0; index <= signingBlock.size() - lengthAndIdByteCount; ) {
signingBlock.copyTo(index, lengthAndIdByteCount, lengthAndId);
lengthAndId.flip();
int blockLength = (int) lengthAndId.getLong();
int id = lengthAndId.getInt();
lengthAndId.flip();
if (id == blockId) {
ByteBuffer block = ByteBuffer.allocate(blockLength - BLOCK_ID_NUM_BYTES);
signingBlock.copyTo(
index + lengthAndIdByteCount, blockLength - BLOCK_ID_NUM_BYTES, block);
block.flip();
return block;
}
index += blockLength + BLOCK_LENGTH_NUM_BYTES;
}
return null;
}
}
private SigningBlockUtils() {}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (C) 2017 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.
*/
/** Utilities to work with {@code apkzlib}. */
package com.android.tools.build.apkzlib.utils;

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zfile;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import javax.annotation.Nullable;
/** Creates or updates APKs based on provided entries. */
public interface ApkCreator extends Closeable {
/**
* Copies the content of a Jar/Zip archive into the receiver archive.
*
* <p>An optional predicate allows to selectively choose which files to copy over and an option
* function allows renaming the files as they are copied.
*
* @param zip the zip to copy data from
* @param transform an optional transform to apply to file names before copying them
* @param isIgnored an optional filter or {@code null} to mark which out files should not be
* added, even through they are on the zip; if {@code transform} is specified, then this
* predicate applies after transformation
* @throws IOException I/O error
*/
void writeZip(
File zip, @Nullable Function<String, String> transform, @Nullable Predicate<String> isIgnored)
throws IOException;
/**
* Writes a new {@link File} into the archive. If a file already existed with the given path, it
* should be replaced.
*
* @param inputFile the {@link File} to write.
* @param apkPath the filepath inside the archive.
* @throws IOException I/O error
*/
void writeFile(File inputFile, String apkPath) throws IOException;
/**
* Deletes a file in a given path.
*
* @param apkPath the path to remove
* @throws IOException failed to remove the entry
*/
void deleteFile(String apkPath) throws IOException;
/** Returns true if the APK will be rewritten on close. */
boolean hasPendingChangesWithWait() throws IOException;
}

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zfile;
import com.android.tools.build.apkzlib.sign.SigningOptions;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import java.io.File;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/** Factory that creates instances of {@link ApkCreator}. */
public interface ApkCreatorFactory {
/**
* Creates an {@link ApkCreator} with a given output location, and signing information.
*
* @param creationData the information to create the APK
*/
ApkCreator make(CreationData creationData);
/**
* Data structure with the required information to initiate the creation of an APK. See {@link
* ApkCreatorFactory#make(CreationData)}.
*/
@AutoValue
abstract class CreationData {
/** An implementation of builder pattern to create a {@link CreationData} object. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setApkPath(@Nonnull File apkPath);
public abstract Builder setSigningOptions(@Nonnull SigningOptions signingOptions);
public abstract Builder setBuiltBy(@Nullable String buildBy);
public abstract Builder setCreatedBy(@Nullable String createdBy);
public abstract Builder setNativeLibrariesPackagingMode(
NativeLibrariesPackagingMode packagingMode);
public abstract Builder setNoCompressPredicate(Predicate<String> predicate);
public abstract Builder setIncremental(boolean incremental);
abstract CreationData autoBuild();
public CreationData build() {
CreationData data = autoBuild();
Preconditions.checkArgument(data.getApkPath() != null, "Output apk path is not set");
return data;
}
}
public static Builder builder() {
return new AutoValue_ApkCreatorFactory_CreationData.Builder()
.setBuiltBy(null)
.setCreatedBy(null)
.setNoCompressPredicate(s -> false)
.setIncremental(false);
}
/**
* Obtains the path where the APK should be located. If the path already exists, then the APK
* may be updated instead of re-created.
*
* @return the path that may already exist or not
*/
public abstract File getApkPath();
/**
* Obtains the data used to sign the APK.
*
* @return the SigningOptions
*/
@Nonnull
public abstract Optional<SigningOptions> getSigningOptions();
/**
* Obtains the "built-by" text for the APK.
*
* @return the text or {@code null} if the default should be used
*/
@Nullable
public abstract String getBuiltBy();
/**
* Obtains the "created-by" text for the APK.
*
* @return the text or {@code null} if the default should be used
*/
@Nullable
public abstract String getCreatedBy();
/** Returns the packaging policy that the {@link ApkCreator} should use for native libraries. */
public abstract NativeLibrariesPackagingMode getNativeLibrariesPackagingMode();
/** Returns the predicate to decide which file paths should be uncompressed. */
public abstract Predicate<String> getNoCompressPredicate();
/**
* Returns if this apk build is incremental.
*
* As mentioned in {@link getApkPath} description, we may already have an existing apk in place.
* This is the case when e.g. building APK via build system and this is not the first build.
* In that case the build is called incremental and internal APK data might be reused speeding
* the build up.
*/
public abstract boolean isIncremental();
}
}

View File

@ -0,0 +1,176 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zfile;
import com.android.tools.build.apkzlib.zip.AlignmentRule;
import com.android.tools.build.apkzlib.zip.AlignmentRules;
import com.android.tools.build.apkzlib.zip.StoredEntry;
import com.android.tools.build.apkzlib.zip.ZFile;
import com.android.tools.build.apkzlib.zip.ZFileOptions;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.io.Closer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
/** {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK. */
class ApkZFileCreator implements ApkCreator {
/** Suffix for native libraries. */
private static final String NATIVE_LIBRARIES_SUFFIX = ".so";
/** Shared libraries are alignment at 4096 boundaries. */
private static final AlignmentRule SO_RULE =
AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096);
/** The zip file. */
private final ZFile zip;
/** Has the zip file been closed? */
private boolean closed;
/** Predicate defining which files should not be compressed. */
private final Predicate<String> noCompressPredicate;
/**
* Creates a new creator.
*
* @param creationData the data needed to create the APK
* @param options zip file options
* @throws IOException failed to create the zip
*/
ApkZFileCreator(ApkCreatorFactory.CreationData creationData, ZFileOptions options)
throws IOException {
switch (creationData.getNativeLibrariesPackagingMode()) {
case COMPRESSED:
noCompressPredicate = creationData.getNoCompressPredicate();
break;
case UNCOMPRESSED_AND_ALIGNED:
Predicate<String> baseNoCompressPredicate = creationData.getNoCompressPredicate();
noCompressPredicate =
name -> baseNoCompressPredicate.apply(name) || name.endsWith(NATIVE_LIBRARIES_SUFFIX);
options.setAlignmentRule(AlignmentRules.compose(SO_RULE, options.getAlignmentRule()));
break;
default:
throw new AssertionError();
}
// In case of incremental build we can skip validation since we generated the previous apk and
// we trust ourselves
options.setSkipValidation(creationData.isIncremental());
zip =
ZFiles.apk(
creationData.getApkPath(),
options,
creationData.getSigningOptions(),
creationData.getBuiltBy(),
creationData.getCreatedBy());
closed = false;
}
@Override
public void writeZip(
File zip, @Nullable Function<String, String> transform, @Nullable Predicate<String> isIgnored)
throws IOException {
Preconditions.checkState(!closed, "closed == true");
Preconditions.checkArgument(zip.isFile(), "!zip.isFile()");
Closer closer = Closer.create();
try {
ZFile toMerge = closer.register(ZFile.openReadWrite(zip));
Predicate<String> ignorePredicate;
if (isIgnored == null) {
ignorePredicate = s -> false;
} else {
ignorePredicate = isIgnored;
}
// Files that *must* be uncompressed in the result should not be merged and should be
// added after. This is just very slightly less efficient than ignoring just the ones
// that were compressed and must be uncompressed, but it is a lot simpler :)
Predicate<String> noMergePredicate =
v -> ignorePredicate.apply(v) || noCompressPredicate.apply(v);
this.zip.mergeFrom(toMerge, noMergePredicate);
for (StoredEntry toMergeEntry : toMerge.entries()) {
String path = toMergeEntry.getCentralDirectoryHeader().getName();
if (noCompressPredicate.apply(path) && !ignorePredicate.apply(path)) {
// This entry *must* be uncompressed so it was ignored in the merge and should
// now be added to the apk.
try (InputStream ignoredData = toMergeEntry.open()) {
this.zip.add(path, ignoredData, false);
}
}
}
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
@Override
public void writeFile(File inputFile, String apkPath) throws IOException {
Preconditions.checkState(!closed, "closed == true");
boolean mayCompress = !noCompressPredicate.apply(apkPath);
Closer closer = Closer.create();
try {
FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile));
zip.add(apkPath, inputFileStream, mayCompress);
} catch (IOException e) {
throw closer.rethrow(e, IOException.class);
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
@Override
public void deleteFile(String apkPath) throws IOException {
Preconditions.checkState(!closed, "closed == true");
StoredEntry entry = zip.get(apkPath);
if (entry != null) {
entry.delete();
}
}
@Override
public boolean hasPendingChangesWithWait() throws IOException {
return zip.hasPendingChangesWithWait();
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
zip.close();
closed = true;
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zfile;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.android.tools.build.apkzlib.zip.ZFileOptions;
import java.io.IOException;
/** Creates instances of {@link ApkZFileCreator}. */
public class ApkZFileCreatorFactory implements ApkCreatorFactory {
/** Options for the {@link ZFileOptions} to use in all APKs. */
private final ZFileOptions options;
/**
* Creates a new factory.
*
* @param options the options to use for all instances created
*/
public ApkZFileCreatorFactory(ZFileOptions options) {
this.options = options;
}
@Override
public ApkCreator make(CreationData creationData) {
try {
return new ApkZFileCreator(creationData, options);
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zfile;
/** Java manifest attributes and some default values. */
public interface ManifestAttributes {
/** Manifest attribute with the built by information. */
String BUILT_BY = "Built-By";
/** Manifest attribute with the created by information. */
String CREATED_BY = "Created-By";
/** Manifest attribute with the manifest version. */
String MANIFEST_VERSION = "Manifest-Version";
/** Manifest attribute value with the manifest version. */
String CURRENT_MANIFEST_VERSION = "1.0";
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zfile;
/** Describes how native libs should be packaged. */
public enum NativeLibrariesPackagingMode {
/** Native libs are packaged as any other file. */
COMPRESSED,
/**
* Native libs are packaged uncompressed and page-aligned, so they can be mapped into memory at
* runtime.
*
* <p>Support for this mode was added in Android 23, it only works if the {@code
* extractNativeLibs} attribute is set in the manifest.
*/
UNCOMPRESSED_AND_ALIGNED;
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zfile;
import com.android.tools.build.apkzlib.sign.ManifestGenerationExtension;
import com.android.tools.build.apkzlib.sign.SigningExtension;
import com.android.tools.build.apkzlib.sign.SigningOptions;
import com.android.tools.build.apkzlib.zip.AlignmentRule;
import com.android.tools.build.apkzlib.zip.AlignmentRules;
import com.android.tools.build.apkzlib.zip.ZFile;
import com.android.tools.build.apkzlib.zip.ZFileOptions;
import com.google.common.base.Optional;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.annotation.Nullable;
/** Factory for {@link ZFile}s that are specifically configured to be APKs, AARs, ... */
public class ZFiles {
/** By default all non-compressed files are alignment at 4 byte boundaries.. */
private static final AlignmentRule APK_DEFAULT_RULE = AlignmentRules.constant(4);
/** Default build by string. */
private static final String DEFAULT_BUILD_BY = "Generated-by-ADT";
/** Default created by string. */
private static final String DEFAULT_CREATED_BY = "Generated-by-ADT";
/**
* Creates a new zip file configured as an apk, based on a given file.
*
* @param f the file, if this path does not represent an existing path, will create a {@link
* ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is
* invoked)
* @param options the options to create the {@link ZFile}
* @return the zip file
* @throws IOException failed to create the zip file
*/
public static ZFile apk(File f, ZFileOptions options) throws IOException {
options.setAlignmentRule(AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE));
return ZFile.openReadWrite(f, options);
}
/**
* Creates a new zip file configured as an apk, based on a given file.
*
* @param f the file, if this path does not represent an existing path, will create a {@link
* ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is
* invoked)
* @param options the options to create the {@link ZFile}
* @param signingOptions the options to sign the apk
* @param builtBy who to mark as builder in the manifest
* @param createdBy who to mark as creator in the manifest
* @return the zip file
* @throws IOException failed to create the zip file
*/
public static ZFile apk(
File f,
ZFileOptions options,
Optional<SigningOptions> signingOptions,
@Nullable String builtBy,
@Nullable String createdBy)
throws IOException {
return apk(
f, options, signingOptions, builtBy, createdBy, options.getAlwaysGenerateJarManifest());
}
/**
* Creates a new zip file configured as an apk, based on a given file.
*
* @param f the file, if this path does not represent an existing path, will create a {@link
* ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is
* invoked)
* @param options the options to create the {@link ZFile}
* @param signingOptions the options to sign the apk
* @param builtBy who to mark as builder in the manifest
* @param createdBy who to mark as creator in the manifest
* @param writeManifest a migration parameter that forces keeping (useless) manifest.mf file in
* apk file in order to prevent breaking changes. Clients of the previous interface will still
* get apk with manifest.mf because the flag is true by default
* @return the zip file
* @throws IOException failed to create the zip file
* @deprecated Use ZFileOptions.setAlwaysGenerateJarManifest() instead.
*/
@Deprecated
// This method can be removed once ZFileOptions.getAlwaysGenerateJarManifest() is on Maven.
public static ZFile apk(
File f,
ZFileOptions options,
Optional<SigningOptions> signingOptions,
@Nullable String builtBy,
@Nullable String createdBy,
boolean writeManifest)
throws IOException {
ZFile zfile = apk(f, options);
if ((signingOptions.isPresent() && signingOptions.get().isV1SigningEnabled())
|| writeManifest) {
if (builtBy == null) {
builtBy = DEFAULT_BUILD_BY;
}
if (createdBy == null) {
createdBy = DEFAULT_CREATED_BY;
}
ManifestGenerationExtension manifestExt = new ManifestGenerationExtension(builtBy, createdBy);
manifestExt.register(zfile);
}
if (signingOptions.isPresent()) {
SigningOptions signOptions = signingOptions.get();
try {
new SigningExtension(signOptions).register(zfile);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IOException("Failed to create signature extensions", e);
}
}
return zfile;
}
private ZFiles() {}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (C) 2017 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.
*/
/** The {@code zfile} package contains */
package com.android.tools.build.apkzlib.zfile;

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
/** An alignment rule defines how to a file should be aligned in a zip, based on its name. */
public interface AlignmentRule {
/** Alignment value of files that do not require alignment. */
int NO_ALIGNMENT = 1;
/**
* Obtains the alignment this rule computes for a given path.
*
* @param path the path in the zip file
* @return the alignment value, always greater than {@code 0}; if this rule places no restrictions
* on the provided path, then {@link AlignmentRule#NO_ALIGNMENT} is returned
*/
int alignment(String path);
}

View File

@ -0,0 +1,74 @@
/*
* 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.tools.build.apkzlib.zip;
import com.google.common.base.Preconditions;
/** Factory for instances of {@link AlignmentRule}. */
public final class AlignmentRules {
private AlignmentRules() {}
/**
* A rule that defines a constant alignment for all files.
*
* @param alignment the alignment
* @return the rule
*/
public static AlignmentRule constant(int alignment) {
Preconditions.checkArgument(alignment > 0, "alignment <= 0");
return (String path) -> alignment;
}
/**
* A rule that defines constant alignment for all files with a certain suffix, placing no
* restrictions on other files.
*
* @param suffix the suffix
* @param alignment the alignment for paths that match the provided suffix
* @return the rule
*/
public static AlignmentRule constantForSuffix(String suffix, int alignment) {
Preconditions.checkArgument(!suffix.isEmpty(), "suffix.isEmpty()");
Preconditions.checkArgument(alignment > 0, "alignment <= 0");
return (String path) -> path.endsWith(suffix) ? alignment : AlignmentRule.NO_ALIGNMENT;
}
/**
* A rule that applies other rules in order.
*
* @param rules all rules to be tried; the first rule that does not return {@link
* AlignmentRule#NO_ALIGNMENT} will define the alignment for a path; if there are no rules
* that return a value different from {@link AlignmentRule#NO_ALIGNMENT}, then {@link
* AlignmentRule#NO_ALIGNMENT} is returned
* @return the composition rule
*/
public static AlignmentRule compose(AlignmentRule... rules) {
return (String path) -> {
for (AlignmentRule r : rules) {
int align = r.alignment(path);
if (align != AlignmentRule.NO_ALIGNMENT) {
return align;
}
}
return AlignmentRule.NO_ALIGNMENT;
};
}
}

View File

@ -0,0 +1,482 @@
/*
* 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.bytestorage.ByteStorage;
import com.android.tools.build.apkzlib.utils.CachedSupplier;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.android.tools.build.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.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** 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. */
private final Map<String, StoredEntry> entries;
/** The file where this directory belongs to. */
private final ZFile file;
/** Supplier that provides a byte representation of the central directory. */
private final CachedSupplier<byte[]> bytesSupplier;
/** Verify log for the central directory. */
private final VerifyLog verifyLog;
/**
* Creates a new, empty, central directory, for a given zip file.
*
* @param file the file
*/
CentralDirectory(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
* @param storage the storage used to generate sources with entry data
* @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(ByteBuffer bytes, long count, ZFile file, ByteStorage storage)
throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
Preconditions.checkArgument(count >= 0, "count < 0");
CentralDirectory directory = new CentralDirectory(file);
for (long i = 0; i < count; i++) {
try {
directory.readEntry(bytes, storage);
} 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(Set<StoredEntry> entries, 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
* @param storage the storage used to generate sources to store entry data
* @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(ByteBuffer bytes, ByteStorage storage) 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 = (long) 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,
lastModTime,
lastModDate);
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, storage);
} 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
*/
Map<String, StoredEntry> getEntries() {
return ImmutableMap.copyOf(entries);
}
/**
* Obtains whether the Central Directory contains any files with Zip64 file extensions.
*
* <p>At the present time, files in the Zip64 format are not supported, so this method returns
* false.
*
* @return false, as Zip64 formatted files are not supported
*/
boolean containsZip64Files() {
return false;
}
/**
* 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());
Collections.sort(sorted, 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 IOExceptionWrapper(e);
}
}
}

View File

@ -0,0 +1,414 @@
/*
* 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils;
import com.google.common.base.Verify;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* The Central Directory Header contains information about files stored in the zip. Instances of
* this class contain information for files that already are in the zip and, for which the data was
* read from the Central Directory. But some instances of this class are used for new files. Because
* instances of this class can refer to files not yet on the zip, some of the fields may not be
* filled in, or may be filled in with default values.
*
* <p>Because compression decision is done lazily, some data is stored with futures.
*/
public class CentralDirectoryHeader implements Cloneable {
/**
* Default "version made by" field: upper byte needs to be 0 to set to MS-DOS compatibility. Lower
* byte can be anything, really. We use 18 because aapt uses 17 :)
*/
private static final int DEFAULT_VERSION_MADE_BY = 0x0018;
private static final byte[] EMPTY_COMMENT = new byte[0];
/** Name of the file. */
private final String name;
/** CRC32 of the data. 0 if not yet computed. */
private long crc32;
/** Size of the file uncompressed. 0 if the file has no data. */
private long uncompressedSize;
/** Code of the program that made the zip. We actually don't care about this. */
private long madeBy;
/** General-purpose bit flag. */
private GPFlags gpBit;
/** Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packTime(long)}). */
private long lastModTime;
/** Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packDate(long)}). */
private long lastModDate;
/**
* Extra data field contents. This field follows a specific structure according to the
* specification.
*/
private ExtraField extraField;
/** File comment. */
private byte[] comment;
/** File internal attributes. */
private long internalAttributes;
/** File external attributes. */
private long externalAttributes;
/**
* Offset in the file where the data is located. This will be -1 if the header corresponds to a
* new file that is not yet written in the zip and, therefore, has no written data.
*/
private long offset;
/** Encoded file name. */
private byte[] encodedFileName;
/** Compress information that may not have been computed yet due to lazy compression. */
private final Future<CentralDirectoryHeaderCompressInfo> compressInfo;
/** The file this header belongs to. */
private final ZFile file;
/**
* Creates data for a file.
*
* @param name the file name
* @param encodedFileName the encoded file name, this array will be owned by the header
* @param uncompressedSize the uncompressed file size
* @param compressInfo computation that defines the compression information
* @param flags flags used in the entry
* @param zFile the file this header belongs to
*/
CentralDirectoryHeader(
String name,
byte[] encodedFileName,
long uncompressedSize,
Future<CentralDirectoryHeaderCompressInfo> compressInfo,
GPFlags flags,
ZFile zFile) {
this(
name,
encodedFileName,
uncompressedSize,
compressInfo,
flags,
zFile,
MsDosDateTimeUtils.packCurrentTime(),
MsDosDateTimeUtils.packCurrentDate());
}
CentralDirectoryHeader(
String name,
byte[] encodedFileName,
long uncompressedSize,
Future<CentralDirectoryHeaderCompressInfo> compressInfo,
GPFlags flags,
ZFile zFile,
long currentTime,
long currentDate) {
this.name = name;
this.uncompressedSize = uncompressedSize;
crc32 = 0;
/*
* Set sensible defaults for the rest.
*/
madeBy = DEFAULT_VERSION_MADE_BY;
gpBit = flags;
lastModTime = currentTime;
lastModDate = currentDate;
extraField = ExtraField.EMPTY;
comment = EMPTY_COMMENT;
internalAttributes = 0;
externalAttributes = 0;
offset = -1;
this.encodedFileName = encodedFileName;
this.compressInfo = compressInfo;
file = zFile;
}
/**
* Obtains the name of the file.
*
* @return the name
*/
public String getName() {
return name;
}
/**
* Obtains the size of the uncompressed file.
*
* @return the size of the file
*/
public long getUncompressedSize() {
return uncompressedSize;
}
/**
* Obtains the CRC32 of the data.
*
* @return the CRC32, 0 if not yet computed
*/
public long getCrc32() {
return crc32;
}
/**
* Sets the CRC32 of the data.
*
* @param crc32 the CRC 32
*/
void setCrc32(long crc32) {
this.crc32 = crc32;
}
/**
* Obtains the code of the program that made the zip.
*
* @return the code
*/
public long getMadeBy() {
return madeBy;
}
/**
* Sets the code of the progtram that made the zip.
*
* @param madeBy the code
*/
void setMadeBy(long madeBy) {
this.madeBy = madeBy;
}
/**
* Obtains the general-purpose bit flag.
*
* @return the bit flag
*/
public GPFlags getGpBit() {
return gpBit;
}
/**
* Obtains the last modification time of the entry.
*
* @return the last modification time in MS-DOS format (see {@link
* MsDosDateTimeUtils#packTime(long)})
*/
public long getLastModTime() {
return lastModTime;
}
/**
* Sets the last modification time of the entry.
*
* @param lastModTime the last modification time in MS-DOS format (see {@link
* MsDosDateTimeUtils#packTime(long)})
*/
void setLastModTime(long lastModTime) {
this.lastModTime = lastModTime;
}
/**
* Obtains the last modification date of the entry.
*
* @return the last modification date in MS-DOS format (see {@link
* MsDosDateTimeUtils#packDate(long)})
*/
public long getLastModDate() {
return lastModDate;
}
/**
* Sets the last modification date of the entry.
*
* @param lastModDate the last modification date in MS-DOS format (see {@link
* MsDosDateTimeUtils#packDate(long)})
*/
void setLastModDate(long lastModDate) {
this.lastModDate = lastModDate;
}
/**
* Obtains the data in the extra field.
*
* @return the data (returns an empty array if there is none)
*/
public ExtraField getExtraField() {
return extraField;
}
/**
* Sets the data in the extra field.
*
* @param extraField the data to set
*/
public void setExtraField(ExtraField extraField) {
setExtraFieldNoNotify(extraField);
file.centralDirectoryChanged();
}
/**
* Sets the data in the extra field, but does not notify {@link ZFile}. This method is invoked
* when the {@link ZFile} knows the extra field is being set.
*
* @param extraField the data to set
*/
void setExtraFieldNoNotify(ExtraField extraField) {
this.extraField = extraField;
}
/**
* Obtains the entry's comment.
*
* @return the comment (returns an empty array if there is no comment)
*/
public byte[] getComment() {
return comment;
}
/**
* Sets the entry's comment.
*
* @param comment the comment
*/
void setComment(byte[] comment) {
this.comment = comment;
}
/**
* Obtains the entry's internal attributes.
*
* @return the entry's internal attributes
*/
public long getInternalAttributes() {
return internalAttributes;
}
/**
* Sets the entry's internal attributes.
*
* @param internalAttributes the entry's internal attributes
*/
void setInternalAttributes(long internalAttributes) {
this.internalAttributes = internalAttributes;
}
/**
* Obtains the entry's external attributes.
*
* @return the entry's external attributes
*/
public long getExternalAttributes() {
return externalAttributes;
}
/**
* Sets the entry's external attributes.
*
* @param externalAttributes the entry's external attributes
*/
void setExternalAttributes(long externalAttributes) {
this.externalAttributes = externalAttributes;
}
/**
* Obtains the offset in the zip file where this entry's data is.
*
* @return the offset or {@code -1} if the file has no data in the zip and, therefore, data is
* stored in memory
*/
public long getOffset() {
return offset;
}
/**
* Sets the offset in the zip file where this entry's data is.
*
* @param offset the offset or {@code -1} if the file is new and has no data in the zip yet
*/
void setOffset(long offset) {
this.offset = offset;
}
/**
* Obtains the encoded file name.
*
* @return the encoded file name
*/
public byte[] getEncodedFileName() {
return encodedFileName;
}
/** Resets the deferred CRC flag in the GP flags. */
void resetDeferredCrc() {
/*
* We actually create a new set of flags. Since the only information we care about is the
* UTF-8 encoding, we'll just create a brand new object.
*/
gpBit = GPFlags.make(gpBit.isUtf8FileName());
}
@Override
protected CentralDirectoryHeader clone() throws CloneNotSupportedException {
CentralDirectoryHeader cdr = (CentralDirectoryHeader) super.clone();
cdr.extraField = extraField;
cdr.comment = Arrays.copyOf(comment, comment.length);
cdr.encodedFileName = Arrays.copyOf(encodedFileName, encodedFileName.length);
return cdr;
}
/**
* Obtains the future with the compression information.
*
* @return the information
*/
public Future<CentralDirectoryHeaderCompressInfo> getCompressionInfo() {
return compressInfo;
}
/**
* Equivalent to {@code getCompressionInfo().get()} but masking the possible exceptions and
* guaranteeing non-{@code null} return.
*
* @return the result of the future
* @throws IOException failed to get the information
*/
public CentralDirectoryHeaderCompressInfo getCompressionInfoWithWait() throws IOException {
try {
CentralDirectoryHeaderCompressInfo info = getCompressionInfo().get();
Verify.verifyNotNull(info, "info == null");
return info;
} catch (InterruptedException e) {
throw new IOException("Interrupted while waiting for compression information.", e);
} catch (ExecutionException e) {
throw new IOException("Execution of compression failed.", e);
}
}
}

View File

@ -0,0 +1,110 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
/**
* Information stored in the {@link CentralDirectoryHeader} that is related to compression and may
* need to be computed lazily.
*/
public class CentralDirectoryHeaderCompressInfo {
/** Version of zip file that only supports stored files. */
public static final long VERSION_WITH_STORE_FILES_ONLY = 10L;
/** Version of zip file that only supports directories and deflated files. */
public static final long VERSION_WITH_DIRECTORIES_AND_DEFLATE = 20L;
/** Version of zip file that only supports ZIP64 format extensions */
public static final long VERSION_WITH_ZIP64_EXTENSIONS = 45L;
/** Version of zip file that uses central file encryption and version 2 of the Zip64 EOCD */
public static final long VERSION_WITH_CENTRAL_FILE_ENCRYPTION = 62L;
/** The compression method. */
private final CompressionMethod method;
/** Size of the file compressed. 0 if the file has no data. */
private final long compressedSize;
/** Version needed to extract the zip. */
private final long versionExtract;
/**
* Creates new compression information for the central directory header.
*
* @param method the compression method
* @param compressedSize the compressed size
* @param versionToExtract minimum version to extract (typically {@link
* #VERSION_WITH_STORE_FILES_ONLY} or {@link #VERSION_WITH_DIRECTORIES_AND_DEFLATE})
*/
public CentralDirectoryHeaderCompressInfo(
CompressionMethod method, long compressedSize, long versionToExtract) {
this.method = method;
this.compressedSize = compressedSize;
versionExtract = versionToExtract;
}
/**
* Creates new compression information for the central directory header.
*
* @param header the header this information relates to
* @param method the compression method
* @param compressedSize the compressed size
*/
public CentralDirectoryHeaderCompressInfo(
CentralDirectoryHeader header, CompressionMethod method, long compressedSize) {
this.method = method;
this.compressedSize = compressedSize;
if (header.getName().endsWith("/") || method == CompressionMethod.DEFLATE) {
/*
* Directories and compressed files only in version 2.0.
*/
versionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE;
} else {
versionExtract = VERSION_WITH_STORE_FILES_ONLY;
}
}
/**
* Obtains the compression data size.
*
* @return the compressed data size
*/
public long getCompressedSize() {
return compressedSize;
}
/**
* Obtains the compression method.
*
* @return the compression method
*/
public CompressionMethod getMethod() {
return method;
}
/**
* Obtains the minimum version for extract.
*
* @return the minimum version
*/
long getVersionExtract() {
return versionExtract;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.tools.build.apkzlib.zip;
import javax.annotation.Nullable;
/** Enumeration with all known compression methods. */
public enum CompressionMethod {
/** STORE method: data is stored without any compression. */
STORE(0),
/** DEFLATE method: data is stored compressed using the DEFLATE algorithm. */
DEFLATE(8);
/** Code, within the zip file, that identifies this compression method. */
int methodCode;
/**
* Creates a new compression method.
*
* @param methodCode the code used in the zip file that identifies the compression method
*/
CompressionMethod(int methodCode) {
this.methodCode = methodCode;
}
/**
* Obtains the compression method that corresponds to the provided code.
*
* @param code the code
* @return the method or {@code null} if no method has the provided code
*/
@Nullable
static CompressionMethod fromCode(long code) {
for (CompressionMethod method : values()) {
if (method.methodCode == code) {
return method;
}
}
return null;
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
/** Result of compressing data. */
public class CompressionResult {
/** The compression method used. */
private final CompressionMethod compressionMethod;
/** The resulting data. */
private final CloseableByteSource source;
/**
* Size of the compressed source. Kept because {@code source.size()} can throw {@code
* IOException}.
*/
private final long mSize;
/**
* Creates a new compression result.
*
* @param source the data source
* @param method the compression method
*/
public CompressionResult(CloseableByteSource source, CompressionMethod method, long size) {
compressionMethod = method;
this.source = source;
mSize = size;
}
/**
* Obtains the compression method.
*
* @return the compression method
*/
public CompressionMethod getCompressionMethod() {
return compressionMethod;
}
/**
* Obtains the compressed data.
*
* @return the data, the resulting array should not be modified
*/
public CloseableByteSource getSource() {
return source;
}
/**
* Obtains the size of the compression result.
*
* @return the size
*/
public long getSize() {
return mSize;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.bytestorage.ByteStorage;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.util.concurrent.ListenableFuture;
/**
* A compressor is capable of, well, compressing data. Data is read from an {@code ByteSource}.
* Compressors are asynchronous: compressing results in a {@code ListenableFuture} that will contain
* the compression result.
*/
public interface Compressor {
/**
* Compresses an entry source.
*
* @param source the source to compress
* @param storage a byte storage from where the compressor can obtain byte sources to work
* @return a future that will eventually contain the compression result
*/
ListenableFuture<CompressionResult> compress(CloseableByteSource source, ByteStorage storage);
}

View File

@ -0,0 +1,49 @@
/*
* 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.tools.build.apkzlib.zip;
/**
* Type of data descriptor that an entry has. Data descriptors are used if the CRC and sizing data
* is not known when the data is being written and cannot be placed in the file's local header. In
* those cases, after the file data itself, a data descriptor is placed after the entry's contents.
*
* <p>While the zip specification says the data descriptor should be used but it is optional. We
* record also whether the data descriptor contained the 4-byte signature at the start of the block
* or not.
*/
public enum DataDescriptorType {
/** The entry has no data descriptor. */
NO_DATA_DESCRIPTOR(0),
/** The entry has a data descriptor that does not contain a signature. */
DATA_DESCRIPTOR_WITHOUT_SIGNATURE(12),
/** The entry has a data descriptor that contains a signature. */
DATA_DESCRIPTOR_WITH_SIGNATURE(16);
/** The number of bytes the data descriptor spans. */
public int size;
/**
* Creates a new data descriptor.
*
* @param size the number of bytes the data descriptor spans
*/
DataDescriptorType(int size) {
this.size = size;
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
/** Utilities to encode and decode file names in zips. */
public class EncodeUtils {
/** Utility class: no constructor. */
private EncodeUtils() {
/*
* Nothing to do.
*/
}
/**
* Decodes a file name.
*
* @param bytes the raw data buffer to read from
* @param length the number of bytes in the raw data buffer containing the string to decode
* @param flags the zip entry flags
* @return the decode file name
*/
public static String decode(ByteBuffer bytes, int length, GPFlags flags) throws IOException {
if (bytes.remaining() < length) {
throw new IOException(
"Only "
+ bytes.remaining()
+ " bytes exist in the buffer, but "
+ "length is "
+ length
+ ".");
}
byte[] stringBytes = new byte[length];
bytes.get(stringBytes);
return decode(stringBytes, flags);
}
/**
* Decodes a file name.
*
* @param data the raw data
* @param flags the zip entry flags
* @return the decode file name
*/
public static String decode(byte[] data, GPFlags flags) {
return decode(data, flagsCharset(flags));
}
/**
* Decodes a file name.
*
* @param data the raw data
* @param charset the charset to use
* @return the decode file name
*/
private static String decode(byte[] data, Charset charset) {
try {
return charset
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(data))
.toString();
} catch (CharacterCodingException e) {
// If we're trying to decode ASCII, try UTF-8. Otherwise, revert to the default
// behavior (usually replacing invalid characters).
if (charset.equals(US_ASCII)) {
return decode(data, UTF_8);
} else {
return charset.decode(ByteBuffer.wrap(data)).toString();
}
}
}
/**
* Encodes a file name.
*
* @param name the name to encode
* @param flags the zip entry flags
* @return the encoded file name
*/
public static byte[] encode(String name, GPFlags flags) {
Charset charset = flagsCharset(flags);
ByteBuffer bytes = charset.encode(name);
byte[] result = new byte[bytes.remaining()];
bytes.get(result);
return result;
}
/**
* Obtains the charset to encode and decode zip entries, given a set of flags.
*
* @param flags the flags
* @return the charset to use
*/
private static Charset flagsCharset(GPFlags flags) {
if (flags.isUtf8FileName()) {
return UTF_8;
} else {
return US_ASCII;
}
}
/**
* Checks if some text may be encoded using ASCII.
*
* @param text the text to check
* @return can it be encoded using ASCII?
*/
public static boolean canAsciiEncode(String text) {
return US_ASCII.newEncoder().canEncode(text);
}
}

View File

@ -0,0 +1,277 @@
/*
* 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.utils.CachedSupplier;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
/** End Of Central Directory record in a zip file. */
class Eocd {
/** Max total records that can be specified by the standard EOCD. */
static final long MAX_TOTAL_RECORDS = 0xFFFFL;
/** Max size of the Central Directory that can be specified by the standard EOCD. */
static final long MAX_CD_SIZE = 0xFFFFFFFFL;
/** Max offset of the Central Directory that can be specified by the standard EOCD. */
static final long MAX_CD_OFFSET = 0xFFFFFFFFL;
/** Field in the record: the record signature, fixed at this value by the specification. */
private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06054b50, "EOCD signature");
/**
* Field in the record: the number of the disk where the EOCD is located. It has to be zero
* because we do not support multi-file archives.
*/
private static final ZipField.F2 F_NUMBER_OF_DISK =
new ZipField.F2(F_SIGNATURE.endOffset(), 0, "Number of this disk");
/**
* Field in the record: the number of the disk where the Central Directory starts. Has to be zero
* because we do not support multi-file archives.
*/
private static final ZipField.F2 F_DISK_CD_START =
new ZipField.F2(F_NUMBER_OF_DISK.endOffset(), 0, "Disk where CD starts");
/**
* Field in the record: the number of entries in the Central Directory on this disk. Because we do
* not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}.
*/
private static final ZipField.F2 F_RECORDS_DISK =
new ZipField.F2(
F_DISK_CD_START.endOffset(), "Record on disk count", new ZipFieldInvariantNonNegative());
/**
* Field in the record: the total number of entries in the Central Directory. This value will be
* {@link #MAX_TOTAL_RECORDS} if the file is in the Zip64 format, and the Central Directory holds
* at least {@link #MAX_TOTAL_RECORDS} entries.
*/
private static final ZipField.F2 F_RECORDS_TOTAL =
new ZipField.F2(
F_RECORDS_DISK.endOffset(),
"Total records",
new ZipFieldInvariantNonNegative(),
new ZipFieldInvariantMaxValue(Integer.MAX_VALUE));
/**
* Field in the record: number of bytes of the Central Directory. This is not private because it
* is required in unit tests. This value will be {@link #MAX_CD_SIZE} if the file is in the Zip64
* format, and the Central Directory is at least {@link #MAX_CD_SIZE} bytes.
*/
@VisibleForTesting
static final ZipField.F4 F_CD_SIZE =
new ZipField.F4(
F_RECORDS_TOTAL.endOffset(), "Directory size", new ZipFieldInvariantNonNegative());
/**
* Field in the record: offset, from the archive start, where the Central Directory starts. This
* is not private because it is required in unit tests. This value will be {@link #MAX_CD_OFFSET}
* if the file is in the Zip64 format, and the Central Directory is at least
* {@link #MAX_CD_OFFSET} bytes.
*/
@VisibleForTesting
static final ZipField.F4 F_CD_OFFSET =
new ZipField.F4(
F_CD_SIZE.endOffset(), "Directory offset", new ZipFieldInvariantNonNegative());
/**
* Field in the record: number of bytes of the file comment (located at the end of the EOCD
* record).
*/
private static final ZipField.F2 F_COMMENT_SIZE =
new ZipField.F2(
F_CD_OFFSET.endOffset(), "File comment size", new ZipFieldInvariantNonNegative());
/** Number of entries in the central directory. */
private final long totalRecords;
/** Offset from the beginning of the archive where the Central Directory is located. */
private final long directoryOffset;
/** Number of bytes of the Central Directory. */
private final long directorySize;
/** Contents of the EOCD comment. */
private final byte[] comment;
/** Supplier of the byte representation of the EOCD. */
private final CachedSupplier<byte[]> byteSupplier;
/**
* Creates a new EOCD, reading it from a byte source. This method will parse the byte source and
* obtain the EOCD. It will check that the byte source starts with the EOCD signature.
*
* @param bytes the byte buffer with the EOCD data; when this method finishes, the byte buffer's
* position will have moved to the end of the EOCD
* @throws IOException failed to read information or the EOCD data is corrupt or invalid
*/
Eocd(ByteBuffer bytes) throws IOException {
/*
* Read the EOCD record.
*/
F_SIGNATURE.verify(bytes);
F_NUMBER_OF_DISK.verify(bytes);
F_DISK_CD_START.verify(bytes);
long totalRecords1 = F_RECORDS_DISK.read(bytes);
long totalRecords2 = F_RECORDS_TOTAL.read(bytes);
long directorySize = F_CD_SIZE.read(bytes);
long directoryOffset = F_CD_OFFSET.read(bytes);
int commentSize = Ints.checkedCast(F_COMMENT_SIZE.read(bytes));
/*
* Some sanity checks.
*/
if (totalRecords1 != totalRecords2) {
throw new IOException(
"Zip states records split in multiple disks, which is not " + "supported.");
}
Verify.verify(totalRecords1 <= Integer.MAX_VALUE);
totalRecords = Ints.checkedCast(totalRecords1);
this.directorySize = directorySize;
this.directoryOffset = directoryOffset;
if (bytes.remaining() < commentSize) {
throw new IOException(
"Corrupt EOCD record: not enough data for comment (comment "
+ "size is "
+ commentSize
+ ").");
}
comment = new byte[commentSize];
bytes.get(comment);
byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
* Creates a new EOCD. This is used when generating an EOCD for an Central Directory that has just
* been generated. The EOCD will be generated without any comment.
*
* @param totalRecords total number of records in the directory
* @param directoryOffset offset, since beginning of archive, where the Central Directory is
* located
* @param directorySize number of bytes of the Central Directory
* @param comment the EOCD comment
*/
Eocd(long totalRecords, long directoryOffset, long directorySize, byte[] comment) {
Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0");
Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0");
Preconditions.checkArgument(directorySize >= 0, "directorySize < 0");
this.totalRecords = totalRecords;
this.directoryOffset = directoryOffset;
this.directorySize = directorySize;
this.comment = comment;
byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
* Obtains the number of records in the Central Directory.
*
* @return the number of records
*/
long getTotalRecords() {
return totalRecords;
}
/**
* Obtains the offset since the beginning of the zip archive where the Central Directory is
* located.
*
* @return the offset where the Central Directory is located
*/
long getDirectoryOffset() {
return directoryOffset;
}
/**
* Obtains the size of the Central Directory.
*
* @return the number of bytes that make up the Central Directory
*/
long getDirectorySize() {
return directorySize;
}
/**
* Obtains the size of the EOCD.
*
* @return the size, in bytes, of the EOCD
*/
long getEocdSize() {
return (long) F_COMMENT_SIZE.endOffset() + comment.length;
}
/**
* Generates the EOCD data.
*
* @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
* @throws IOException failed to generate the EOCD data
*/
byte[] toBytes() throws IOException {
return byteSupplier.get();
}
/**
* Obtains the comment in the EOCD.
*
* @return the comment exactly as it is represented in the file (no encoding conversion is
* done)
*/
byte[] getComment() {
byte[] commentCopy = new byte[comment.length];
System.arraycopy(comment, 0, commentCopy, 0, comment.length);
return commentCopy;
}
/**
* Computes the byte representation of the EOCD.
*
* @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
* @throws UncheckedIOException failed to generate the EOCD data
*/
private byte[] computeByteRepresentation() {
ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length);
try {
F_SIGNATURE.write(out);
F_NUMBER_OF_DISK.write(out);
F_DISK_CD_START.write(out);
F_RECORDS_DISK.write(out, totalRecords);
F_RECORDS_TOTAL.write(out, totalRecords);
F_CD_SIZE.write(out, directorySize);
F_CD_OFFSET.write(out, directoryOffset);
F_COMMENT_SIZE.write(out, comment.length);
out.put(comment);
return out.array();
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
}
}

View File

@ -0,0 +1,696 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.annotation.Nullable;
/**
* The collection of all data stored in all End of Central Directory records in the zip file. The
* {@code EOCDGroup} is meant to collect and manage all the information about the {@link Eocd},
* {@link Zip64EocdLocator}, and the {@link Zip64Eocd} in one place.
*/
public class EocdGroup {
/** Minimum size the EOCD can have. */
private static final int MIN_EOCD_SIZE = 22;
/** Maximum size for the EOCD. */
private static final int MAX_EOCD_COMMENT_SIZE = 65535;
/** How many bytes to look back from the end of the file to look for the EOCD signature. */
private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE;
/** Signature of the Zip64 EOCD locator record. */
private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
/** Signature of the EOCD record. */
private static final long EOCD_SIGNATURE = 0x06054b50;
/**
* The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the one
* that exists on disk is no longer valid (because the zip has been changed).
*
* <p>If the EOCD is deleted because the zip has been changed and the old EOCD was no longer
* valid, then {@link #eocdComment} will contain the comment saved from the EOCD.
*/
@Nullable
private FileUseMapEntry<Eocd> eocdEntry;
/**
* The EOCD locator entry. Will be {@code null} if there is no EOCD (because the zip is new),
* the EOCD on disk is no longer valid (because the zip has been changed), or the zip file is not
* in Zip64 format (There are no values in the EOCD that overflow or any files with Zip64
* extended information.)
*
* <p> If this value is {@code nonnull} then the EOCD exists and is in Zip64 format (<i>i.e.</i>
* both {@link #eocdEntry} and {@link #eocd64Entry} will be {@code nonnull}).
*/
@Nullable
private FileUseMapEntry<Zip64EocdLocator> eocd64Locator;
/**
* The Zip64 EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new),
* the EOCD on disk is no longer valid (because the zip has been changed), or the zip file is not
* in Zip64 format (There are no values in the EOCD that overflow or any files with Zip64
* extended information.)
*
* <p> If this value is {@code nonnull} then the EOCD exists and is in Zip64 format (<i>i.e.</i>
* both {@link #eocdEntry} and {@link #eocd64Locator} will be {@code nonnull}).
*/
@Nullable
private FileUseMapEntry<Zip64Eocd> eocd64Entry;
/**
* This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. This
* may happen, for example, if the zip has been changed and the Central Directory and EOCD have
* been deleted (in-memory). In that case, this field will save the comment to place on the EOCD
* once it is created.
*
* <p>This field will only be non-{@code null} if there is no in-memory EOCD structure
* (<i>i.e.</i>, {@link #eocdEntry} is {@code null}, If there is an {@link #eocdEntry}, then the
* comment will be there instead of being in this field.
*/
@Nullable
private byte[] eocdComment;
/**
* This field contains the extensible data sector in the zip's Zip64 EOCD if there is no EOCD
* in-memory. This may happen if the zip has been modified and the Central Directory and EOCD have
* been deleted (in-memory). In that case, this field will save the data sector to place in the
* Zip64 EOCD once it is created.
*
* <p>This field will only be non-{@code null} if there is no in-memory EOCD structure
* (<i>i.e.</i>, {@link #eocdEntry} is {@code null}, If there is an {@link #eocdEntry}, then the
* data sector will be in the {@link #eocd64Entry} instead of being in this field.
*/
@Nullable
private Zip64ExtensibleDataSector eocdDataSector;
/**
* Specifies whether the Zip64 Eocd will be in Version 2 or Version 1 format when it is
* constructed.
*/
private boolean useVersion2Header;
/** The zip file to which this EOCD record belongs. */
private final ZFile file;
/** The in-memory map of the pieces of the zip-file. */
private final FileUseMap map;
/** The zip file's log. */
private final VerifyLog verifyLog;
/**
* Constructs an empty EOCD group, which will have no in-memory EOCD structure.
*
* @param file The zip file to which this EOCD record belongs.
* @param map he in-memory map of the zip file.
*/
EocdGroup(ZFile file, FileUseMap map) {
eocd64Entry = null;
eocd64Locator = null;
eocdEntry = null;
eocdComment = new byte[0];
eocdDataSector = new Zip64ExtensibleDataSector();
this.file = file;
this.map = map;
this.verifyLog = file.getVerifyLog();
useVersion2Header = false;
}
/**
* Attempts to read the EOCD record into the {@link EocdGroup} from disk specified by
* {@link #file}. It will populate the in-memory EOCD structure (<i>i.e.</i> {@link #eocdEntry}),
* including the Zip64 EOCD record and locator if applicable.
*
* @param fileLength The length of the file on disk, used to help find the EOCD record.
* @throws IOException Failed to read the EOCD.
*/
void readRecord(long fileLength) throws IOException {
/*
* Read the last part of the zip into memory. If we don't find the EOCD signature by then,
* the file is corrupt.
*/
int lastToRead = LAST_BYTES_TO_READ;
if (lastToRead > fileLength) {
lastToRead = Ints.checkedCast(fileLength);
}
byte[] last = new byte[lastToRead];
file.directFullyRead(fileLength - lastToRead, last);
/*
* Start endIdx at the first possible location where the signature can be located and then
* move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the
* signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE.
*
* Because the EOCD signature may exist in the file comment, when we find a signature we
* will try to read the Eocd. If we fail, we continue searching for the signature. However,
* we will keep the last exception in case we don't find any signature.
*/
Eocd eocd = null;
int foundEocdSignatureIdx = -1;
IOException errorFindingSignature = null;
long eocdStart = -1;
for (int endIdx = last.length - MIN_EOCD_SIZE;
endIdx >= 0 && foundEocdSignatureIdx == -1;
endIdx--) {
ByteBuffer potentialLocator = ByteBuffer.wrap(last, endIdx, 4);
if (LittleEndianUtils.readUnsigned4Le(potentialLocator) == EOCD_SIGNATURE) {
/*
* We found a signature. Try to read the EOCD record.
*/
foundEocdSignatureIdx = endIdx;
ByteBuffer eocdBytes =
ByteBuffer.wrap(last, foundEocdSignatureIdx, last.length - foundEocdSignatureIdx);
try {
eocd = new Eocd(eocdBytes);
eocdStart = fileLength - lastToRead + foundEocdSignatureIdx;
/*
* Make sure the EOCD takes the whole file up to the end. Log an error if it
* doesn't.
*/
if (eocdStart + eocd.getEocdSize() != fileLength) {
verifyLog.log(
"EOCD starts at "
+ eocdStart
+ " and has "
+ eocd.getEocdSize()
+ " bytes, but file ends at "
+ fileLength
+ ".");
}
} catch (IOException e) {
if (errorFindingSignature != null) {
e.addSuppressed(errorFindingSignature);
}
errorFindingSignature = e;
foundEocdSignatureIdx = -1;
eocd = null;
}
}
}
if (foundEocdSignatureIdx == -1) {
throw new IOException(
"EOCD signature not found in the last " + lastToRead + " bytes of the file.",
errorFindingSignature);
}
Verify.verify(eocdStart >= 0);
eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd);
/*
* Look for the Zip64 central directory locator. If we find it, then this file is a Zip64
* file and we need to read both the Zip64 EOCD locator and Zip64 EOCD
*/
long zip64LocatorStart = eocdStart - Zip64EocdLocator.LOCATOR_SIZE;
if (zip64LocatorStart >= 0) {
byte[] possibleZip64Locator = new byte[Zip64EocdLocator.LOCATOR_SIZE];
file.directFullyRead(zip64LocatorStart, possibleZip64Locator);
if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator))
== ZIP64_EOCD_LOCATOR_SIGNATURE) {
/* found the locator. Read it into memory. */
Zip64EocdLocator locator = new Zip64EocdLocator(ByteBuffer.wrap(possibleZip64Locator));
eocd64Locator = map.add(
zip64LocatorStart, zip64LocatorStart + locator.getSize(), locator);
/* Find the size of the Zip64 EOCD by reading its size field */
byte[] zip64EocdSizeHolder = new byte[8];
file.directFullyRead(
locator.getZ64EocdOffset() + Zip64Eocd.SIZE_OFFSET, zip64EocdSizeHolder);
long zip64EocdSize =
LittleEndianUtils.readUnsigned8Le(ByteBuffer.wrap(zip64EocdSizeHolder))
+ Zip64Eocd.TRUE_SIZE_DIFFERENCE;
/* read the Zip64 EOCD into memory */
byte[] zip64EocdBytes = new byte[Ints.checkedCast(zip64EocdSize)];
file.directFullyRead(locator.getZ64EocdOffset(), zip64EocdBytes);
Zip64Eocd zip64Eocd = new Zip64Eocd(ByteBuffer.wrap(zip64EocdBytes));
useVersion2Header =
zip64Eocd.getVersionToExtract()
>= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION;
long zip64EocdEnd = locator.getZ64EocdOffset() + zip64EocdSize;
if (zip64EocdEnd != zip64LocatorStart) {
String msg =
"Zip64 EOCD record is stored in ["
+ locator.getZ64EocdOffset()
+ " - "
+ zip64EocdEnd
+ "] and EOCD starts at "
+ zip64LocatorStart
+ ".";
/*
* If there is an empty space between the Zip64 EOCD and the EOCD locator, we proceed
* logging an error. If the Zip64 EOCD ends after the start of the EOCD locator (and
* therefore, they overlap), throw an exception.
*/
if (zip64EocdEnd > zip64LocatorStart) {
throw new IOException(msg);
} else {
verifyLog.log(msg);
}
}
eocd64Entry = map.add(
locator.getZ64EocdOffset(), zip64EocdEnd, zip64Eocd);
}
}
}
/**
* Computes the EOCD record from the given Central Directory entry in memory. This will populate
* the EOCD in-memory and possibly the Zip64 EOCD and Locator if applicable.
*
* @param directoryEntry The entry to create the EOCD record from.
* @param extraDirectoryOffset The offset between the last local entry and the Central Directory.
* This will be preserved by the EOCD if the Central Directory is empty.
* @throws IOException Failed to create the EOCD record.
*/
void computeRecord(
@Nullable FileUseMapEntry<CentralDirectory> directoryEntry,
long extraDirectoryOffset) throws IOException {
long dirStart;
long dirSize;
long dirNumEntries;
if (directoryEntry != null) {
dirStart = directoryEntry.getStart();
dirSize = directoryEntry.getSize();
dirNumEntries = directoryEntry.getStore().getEntries().size();
} else {
// if we do not have a directory, then we must leave any required offset.
dirStart = extraDirectoryOffset;
dirSize = 0;
dirNumEntries = 0;
}
/*
* We need a Zip64 EOCD if any value overflows or if Zip64 file extensions are used as stated
* in the Zip Specification.
*/
boolean useZip64Eocd =
dirStart > Eocd.MAX_CD_OFFSET ||
dirSize > Eocd.MAX_CD_SIZE ||
dirNumEntries > Eocd.MAX_TOTAL_RECORDS ||
(directoryEntry != null && directoryEntry.getStore().containsZip64Files());
/* construct the Zip64 EOCD and locator first, as they come before the standard EOCD */
if (useZip64Eocd) {
Verify.verify(eocdDataSector != null);
Zip64Eocd zip64Eocd =
new Zip64Eocd(dirNumEntries, dirStart, dirSize, useVersion2Header, eocdDataSector);
eocdDataSector = null;
byte[] zip64EocdBytes = zip64Eocd.toBytes();
long zip64Offset = map.size();
map.extend(zip64Offset + zip64EocdBytes.length);
eocd64Entry = map.add(zip64Offset, zip64Offset + zip64EocdBytes.length, zip64Eocd);
Zip64EocdLocator locator = new Zip64EocdLocator(eocd64Entry.getStart());
byte[] locatorBytes = locator.toBytes();
long locatorOffset = map.size();
map.extend(locatorOffset + locatorBytes.length);
eocd64Locator = map.add(locatorOffset, locatorOffset + locatorBytes.length, locator);
}
/* add the EOCD to the end of the file */
Verify.verify(eocdComment != null);
Eocd eocd = new Eocd(
Math.min(dirNumEntries, Eocd.MAX_TOTAL_RECORDS),
Math.min(dirStart, Eocd.MAX_CD_OFFSET),
Math.min(dirSize, Eocd.MAX_CD_SIZE),
eocdComment);
eocdComment = null;
byte[] eocdBytes = eocd.toBytes();
long eocdOffset = map.size();
map.extend(eocdOffset + eocdBytes.length);
eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd);
}
/**
* Writes the entire EOCD record to the end of the file. The EOCDGroup must <i>not</i> be empty
* ({@link #isEmpty()}) by being populated by a call to
* {@link #computeRecord(FileUseMapEntry, long)}, and the Central Directory must already be
* written to the file. If the CentralDirectory has not written, then {@link #file} should have
* no entries.
*
* @throws IOException Failed to write the EOCD record.
*/
void appendToFile() throws IOException {
Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
if (eocd64Entry != null) {
Zip64Eocd zip64Eocd = eocd64Entry.getStore();
Preconditions.checkNotNull(zip64Eocd);
Zip64EocdLocator locator = eocd64Locator.getStore();
Preconditions.checkNotNull(locator);
file.directWrite(eocd64Entry.getStart(), zip64Eocd.toBytes());
file.directWrite(eocd64Locator.getStart(), locator.toBytes());
}
Eocd eocd = eocdEntry.getStore();
Preconditions.checkNotNull(eocd, "eocd == null");
byte[] eocdBytes = eocd.toBytes();
long eocdOffset = eocdEntry.getStart();
file.directWrite(eocdOffset, eocdBytes);
}
/**
* Obtains the byte array representation of the EOCD. The EOCD must have already been computed for
* this method to be invoked.
*
* @return The byte representation of the EOCD.
* @throws IOException Failed to obtain the byte representation of the EOCD.
*/
byte[] getEocdBytes() throws IOException {
Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
Eocd eocd = eocdEntry.getStore();
Preconditions.checkNotNull(eocd, "eocd == null");
return eocd.toBytes();
}
/**
* Obtains the byte array representation of the Zip64 EOCD Locator. The EOCD record must already
* have been computed for this method to be invoked.
*
* @return The byte representation of the Zip64 EOCD Locator, or null if the EOCD record is not
* in Zip64 format.
* @throws IOException Failed to obtain the byte representation of the EOCD Locator.
*/
@VisibleForTesting
@Nullable
byte[] getEocdLocatorBytes() throws IOException {
Preconditions.checkNotNull(eocdEntry);
if (eocd64Locator == null) {
return null;
}
return eocd64Locator.getStore().toBytes();
}
/**
* Obtains the byte array representation of the Zip64 EOCD. The EOCD record must already
* have been computed for this method to be invoked.
*
* @return The byte representation of the Zip64 EOCD, or null if the EOCD record is not
* in Zip64 format.
* @throws IOException Failed to obtain the byte representation of the Zip64 EOCD.
*/
@VisibleForTesting
@Nullable
byte[] getZ64EocdBytes() throws IOException {
Preconditions.checkNotNull(eocdEntry);
if (eocd64Entry == null) {
return null;
}
return eocd64Entry.getStore().toBytes();
}
/**
* Checks whether the EOCD record is presently in-memory. (<i>i.e.</i> the EOCD was either read
* from disk and is still valid, or has been computed from the Central Directory).
*
* @return True iff the EOCD record is in-memory.
*/
boolean isEmpty() {
return eocdEntry == null;
}
/**
* Sets whether or not the EOCD record should use the Version 1 or Version 2 of the Zip64 EOCD
* (iff the file needs a Zip64 record). The EOCD record should not be in-memory when trying to set
* this value, and the EOCD will need to be recomputed to have any affect.
*
* @param useVersion2Header True if the Version 2 header is to be used, and false for the Version
* 1 header.
*/
void setUseVersion2Header(boolean useVersion2Header) {
verifyLog.verify(eocdEntry == null, "eocdEntry != null");
this.useVersion2Header = useVersion2Header;
}
/**
* Specifies if the EOCD Group will be using a Version 2 Zip64 EOCD record or a Version 1 record
* if the file needs to be in Zip64 format.
*
* @return True if the Version 2 record will be used, and false if the Version 1 record will be
* used.
*/
boolean usingVersion2Header() {
return useVersion2Header;
}
/**
* Removes the EOCD record from memory.
*/
void deleteRecord() {
if (eocdEntry != null) {
map.remove(eocdEntry);
Eocd eocd = eocdEntry.getStore();
Verify.verify(eocd != null);
eocdComment = eocd.getComment();
eocdEntry = null;
}
if (eocd64Locator != null) {
Verify.verify(eocd64Entry != null);
eocdDataSector = eocd64Entry.getStore().getExtraFields();
map.remove(eocd64Locator);
map.remove(eocd64Entry);
eocd64Locator = null;
eocd64Entry = null;
} else {
eocdDataSector = new Zip64ExtensibleDataSector();
}
}
/**
* Sets the EOCD comment.
*
* @param comment The new comment; no conversion is done, these exact bytes will be placed in the
* EOCD comment.
* @throws IllegalArgumentException If the comment corrupts the ZipFile by having a valid EOCD
* record in it.
*/
void setEocdComment(byte[] comment) {
if (comment.length > MAX_EOCD_COMMENT_SIZE) {
throw new IllegalArgumentException(
"EOCD comment size ("
+ comment.length
+ ") is larger than the maximum allowed ("
+ MAX_EOCD_COMMENT_SIZE
+ ")");
}
// Check if the EOCD signature appears anywhere in the comment we need to check if it
// is valid.
for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) {
// Remember: little endian...
ByteBuffer potentialSignature = ByteBuffer.wrap(comment, i, 4);
try {
if (LittleEndianUtils.readUnsigned4Le(potentialSignature) == EOCD_SIGNATURE) {
// We found a possible EOCD signature at position i. Try to read it.
ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i);
try {
new Eocd(bytes);
// If a valid record is found in the comment then this corrupts the Zip file record
// as we look for the EOCD at the back of the file (where the comment is) first.
throw new IllegalArgumentException(
"Position " + i + " of the comment contains a valid EOCD record.");
} catch (IOException e) {
// Fine, this is an invalid record. Move along...
}
}
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
}
deleteRecord();
eocdComment = new byte[comment.length];
System.arraycopy(comment, 0, eocdComment, 0, comment.length);
}
/**
* Returns the start of the EOCD record location in the file or -1 if the EOCD is not in memory.
*
* @return The start of the record.
*/
long getOffset() {
if (eocdEntry == null) {
return -1;
}
return getRecordStart();
}
/**
* Gets the comment in the EOCD.
*
* @return The comment exactly as it was encoded in the EOCD, no encoding is done.
*/
byte[] getEocdComment() {
if (eocdEntry == null) {
Verify.verify(eocdComment != null);
byte[] eocdCommentCopy = eocdComment.clone();
return eocdCommentCopy;
}
Eocd eocd = eocdEntry.getStore();
Verify.verify(eocd != null);
return eocd.getComment();
}
/**
* Gets the size of the central directory as specified from the EOCD record. The EOCD must be in
* memory before this method is invoked.
*
* @return The directory's size.
*/
long getDirectorySize() {
Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
Eocd eocd = eocdEntry.getStore();
if (eocd64Entry != null && eocd.getDirectorySize() == Eocd.MAX_CD_SIZE) {
return eocd64Entry.getStore().getDirectorySize();
} else {
return eocd.getDirectorySize();
}
}
/**
* Gets the offset of the Central Directory from the start of the archive as specified from the
* EOCD record. The EOCD must be in memory before this method is invoked.
*
* @return The offset of the start of the Central Directory.
*/
long getDirectoryOffset() {
Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
Eocd eocd = eocdEntry.getStore();
if (eocd64Entry != null && eocd.getDirectoryOffset() == Eocd.MAX_CD_OFFSET) {
return eocd64Entry.getStore().getDirectoryOffset();
} else {
return eocd.getDirectoryOffset();
}
}
/**
* Gets the total number of entries in the Central Directory as specified from the EOCD record.
* The EOCD must be in memory before this method is invoked.
*
* @return The total number of records in the Central Directory.
*/
long getTotalDirectoryRecords() {
Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
Eocd eocd = eocdEntry.getStore();
if (eocd64Entry != null && eocd.getTotalRecords() == Eocd.MAX_TOTAL_RECORDS) {
return eocd64Entry.getStore().getTotalRecords();
}
return eocd.getTotalRecords();
}
/**
* Returns the start of the EOCD record from the start of the archive. This will be the same as
* the start of the standard EOCD in a Zip32 file or in a Zip64 file will be the start of the
* Zip64 Eocd record. The EOCD must be in memory for this method to be invoked.
*
* @return The start of the entire EOCD record.
*/
long getRecordStart() {
Verify.verify(eocdEntry != null, "eocdEntry == null");
if (eocd64Entry != null) {
return eocd64Entry.getStart();
}
return eocdEntry.getStart();
}
/**
* Returns the total size of the EOCD record. This will be the same as the standard EOCD size for
* a Zip32 file or in a Zip64 file will be the start of the Zip64 record to the end of the
* standard EOCD. the EOCD must be in memory for this method to be invoked.
*
* @return The total size of the EOCD record.
*/
public long getRecordSize() {
if (eocd64Entry != null) {
Verify.verify(eocdEntry != null);
return eocdEntry.getEnd() - eocd64Entry.getStart();
}
if (eocdEntry == null) {
return -1;
}
return eocdEntry.getSize();
}
/**
* Returns the Zip64 Extensible Data Sector, or {@code null} if the EOCD record is not in the
* Zip64 format. The EOCD must be in memory for this method to be invoked.
*
* @return The Extensible data sector, or {@code null} if none exists.
*/
@Nullable
public Zip64ExtensibleDataSector getExtensibleData() {
Verify.verify(eocdEntry != null);
if (eocd64Entry != null) {
return eocd64Entry.getStore().getExtraFields();
}
return null;
}
}

View File

@ -0,0 +1,385 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* Contains an extra field.
*
* <p>According to the zip specification, the extra field is composed of a sequence of fields. This
* class provides a way to access, parse and modify that information.
*
* <p>The zip specification calls fields to the fields inside the extra field. Because this
* terminology is confusing, we use <i>segment</i> to refer to a part of the extra field. Each
* segment is represented by an instance of {@link Segment} and contains a header ID and data.
*
* <p>Each instance of {@link ExtraField} is immutable. The extra field of a particular entry can be
* changed by creating a new instanceof {@link ExtraField} and pass it to {@link
* StoredEntry#setLocalExtra(ExtraField)}.
*
* <p>Instances of {@link ExtraField} can be created directly from the list of segments in it or
* from the raw byte data. If created from the raw byte data, the data will only be parsed on
* demand. So, if neither {@link #getSegments()} nor {@link #getSingleSegment(int)} is invoked, the
* extra field will not be parsed. This guarantees low performance impact of the using the extra
* field unless its contents are needed.
*/
public class ExtraField {
public static final ExtraField EMPTY = new ExtraField();
/** Header ID for field with zip alignment. */
static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935;
/**
* The field's raw data, if it is known. Either this variable or {@link #segments} must be
* non-{@code null}.
*/
@Nullable private final byte[] rawData;
/**
* The list of field's segments. Will be populated if the extra field is created based on a list
* of segments; will also be populated after parsing if the extra field is created based on the
* raw bytes.
*/
@Nullable private ImmutableList<Segment> segments;
/**
* Creates an extra field based on existing raw data.
*
* @param rawData the raw data; will not be parsed unless needed
*/
public ExtraField(byte[] rawData) {
this.rawData = rawData;
segments = null;
}
/** Creates a new extra field with no segments. */
public ExtraField() {
rawData = null;
segments = ImmutableList.of();
}
/**
* Creates a new extra field with the given segments.
*
* @param segments the segments
*/
public ExtraField(ImmutableList<Segment> segments) {
rawData = null;
this.segments = segments;
}
/**
* Obtains all segments in the extra field.
*
* @return all segments
* @throws IOException failed to parse the extra field
*/
public ImmutableList<Segment> getSegments() throws IOException {
if (segments == null) {
parseSegments();
}
Preconditions.checkNotNull(segments);
return segments;
}
/**
* Obtains the only segment with the provided header ID.
*
* @param headerId the header ID
* @return the segment found or {@code null} if no segment contains the provided header ID
* @throws IOException there is more than one header with the provided header ID
*/
@Nullable
public Segment getSingleSegment(int headerId) throws IOException {
List<Segment> found = new ArrayList<>();
for (Segment s : getSegments()) {
if (s.getHeaderId() == headerId) {
found.add(s);
}
}
if (found.isEmpty()) {
return null;
} else if (found.size() == 1) {
return found.get(0);
} else {
throw new IOException(found.size() + " segments with header ID " + headerId + "found");
}
}
/**
* Parses the raw data and generates all segments in {@link #segments}.
*
* @throws IOException failed to parse the data
*/
private void parseSegments() throws IOException {
Preconditions.checkNotNull(rawData);
Preconditions.checkState(segments == null);
List<Segment> segments = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.wrap(rawData);
while (buffer.remaining() > 0) {
int headerId = LittleEndianUtils.readUnsigned2Le(buffer);
int dataSize = LittleEndianUtils.readUnsigned2Le(buffer);
if (dataSize < 0) {
throw new IOException(
"Invalid data size for extra field segment with header ID "
+ headerId
+ ": "
+ dataSize);
}
byte[] data = new byte[dataSize];
if (buffer.remaining() < dataSize) {
throw new IOException(
"Invalid data size for extra field segment with header ID "
+ headerId
+ ": "
+ dataSize
+ " (only "
+ buffer.remaining()
+ " bytes are available)");
}
buffer.get(data);
SegmentFactory factory = identifySegmentFactory(headerId);
Segment seg = factory.make(headerId, data);
segments.add(seg);
}
this.segments = ImmutableList.copyOf(segments);
}
/**
* Obtains the size of the extra field.
*
* @return the size
*/
public int size() {
if (rawData != null) {
return rawData.length;
} else {
Preconditions.checkNotNull(segments);
int sz = 0;
for (Segment s : segments) {
sz += s.size();
}
return sz;
}
}
/**
* Writes the extra field to the given output buffer.
*
* @param out the output buffer to write the field; exactly {@link #size()} bytes will be written
* @throws IOException failed to write the extra fields
*/
public void write(ByteBuffer out) throws IOException {
if (rawData != null) {
out.put(rawData);
} else {
Preconditions.checkNotNull(segments);
for (Segment s : segments) {
s.write(out);
}
}
}
/**
* Identifies the factory to create the segment with the provided header ID.
*
* @param headerId the header ID
* @return the segmnet factory that creates segments with the given header
*/
private static SegmentFactory identifySegmentFactory(int headerId) {
if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
return AlignmentSegment::new;
}
return RawDataSegment::new;
}
/**
* Field inside the extra field. A segment contains a header ID and data. Specific types of
* segments implement this interface.
*/
public interface Segment {
/**
* Obtains the segment's header ID.
*
* @return the segment's header ID
*/
int getHeaderId();
/**
* Obtains the size of the segment including the header ID.
*
* @return the number of bytes needed to write the segment
*/
int size();
/**
* Writes the segment to a buffer.
*
* @param out the buffer where to write the segment to; exactly {@link #size()} bytes will be
* written
* @throws IOException failed to write segment data
*/
void write(ByteBuffer out) throws IOException;
}
/** Factory that creates a segment. */
interface SegmentFactory {
/**
* Creates a new segment.
*
* @param headerId the header ID
* @param data the segment's data
* @return the created segment
* @throws IOException failed to create the segment from the data
*/
Segment make(int headerId, byte[] data) throws IOException;
}
/**
* Segment of raw data: this class represents a general segment containing an array of bytes as
* data.
*/
public static class RawDataSegment implements Segment {
/** Header ID. */
private final int headerId;
/** Data in the segment. */
private final byte[] data;
/**
* Creates a new raw data segment.
*
* @param headerId the header ID
* @param data the segment data
*/
RawDataSegment(int headerId, byte[] data) {
this.headerId = headerId;
this.data = data;
}
@Override
public int getHeaderId() {
return headerId;
}
@Override
public void write(ByteBuffer out) throws IOException {
LittleEndianUtils.writeUnsigned2Le(out, headerId);
LittleEndianUtils.writeUnsigned2Le(out, data.length);
out.put(data);
}
@Override
public int size() {
return 4 + data.length;
}
}
/**
* Segment with information on an alignment: this segment contains information on how an entry
* should be aligned and contains zero-filled data to force alignment.
*
* <p>An alignment segment contains the header ID, the size of the data, the alignment value and
* zero bytes to pad
*/
public static class AlignmentSegment implements Segment {
/** Minimum size for an alignment segment. */
public static final int MINIMUM_SIZE = 6;
/** The alignment value. */
private int alignment;
/** How many bytes of padding are in this segment? */
private int padding;
/**
* Creates a new alignment segment.
*
* @param alignment the alignment value
* @param totalSize how many bytes should this segment take?
*/
public AlignmentSegment(int alignment, int totalSize) {
Preconditions.checkArgument(alignment > 0, "alignment <= 0");
Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, "totalSize < MINIMUM_SIZE");
/*
* We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment
* value (2 bytes).
*/
this.alignment = alignment;
padding = totalSize - MINIMUM_SIZE;
}
/**
* Creates a new alignment segment from extra data.
*
* @param headerId the header ID
* @param data the segment data
* @throws IOException failed to create the segment from the data
*/
public AlignmentSegment(int headerId, byte[] data) throws IOException {
Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
ByteBuffer dataBuffer = ByteBuffer.wrap(data);
alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer);
if (alignment <= 0) {
throw new IOException("Invalid alignment in alignment field: " + alignment);
}
padding = data.length - 2;
}
@Override
public void write(ByteBuffer out) throws IOException {
LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
LittleEndianUtils.writeUnsigned2Le(out, padding + 2);
LittleEndianUtils.writeUnsigned2Le(out, alignment);
out.put(new byte[padding]);
}
@Override
public int size() {
return padding + 6;
}
@Override
public int getHeaderId() {
return ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID;
}
}
}

View File

@ -0,0 +1,598 @@
/*
* 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.tools.build.apkzlib.zip;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Nullable;
/**
* The file use map keeps track of which parts of the zip file are used which parts are not. It
* essentially maintains an ordered set of entries ({@link FileUseMapEntry}). Each entry either has
* some data (an entry, the Central Directory, the EOCD) or is a free entry.
*
* <p>For example: [0-95, "foo/"][95-260, "xpto"][260-310, free][310-360, Central Directory]
* [360-390,EOCD]
*
* <p>There are a few invariants in this structure:
*
* <ul>
* <li>there are no gaps between map entries;
* <li>the map is fully covered up to its size;
* <li>there are no two free entries next to each other; this is guaranteed by coalescing the
* entries upon removal (see {@link #coalesce(FileUseMapEntry)});
* <li>all free entries have a minimum size defined in the constructor, with the possible
* exception of the last one
* </ul>
*/
class FileUseMap {
/**
* Size of the file according to the map. This should always match the last entry in {@code #map}.
*/
private long size;
/**
* Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #size}. If
* {@link #size} is zero then this set is empty. This is the only situation in which the map will
* be empty.
*/
private final TreeSet<FileUseMapEntry<?>> map;
/**
* Tree with all free blocks ordered by size. This is essentially a view over {@link #map}
* containing only the free blocks, but in a different order.
*/
private final TreeSet<FileUseMapEntry<?>> freeBySize;
private final TreeSet<FileUseMapEntry<?>> freeByStart;
/** If defined, defines the minimum size for a free entry. */
private int mMinFreeSize;
/**
* Creates a new, empty file map.
*
* @param size the size of the file
* @param minFreeSize minimum size of a free entry
*/
FileUseMap(long size, int minFreeSize) {
Preconditions.checkArgument(size >= 0, "size < 0");
Preconditions.checkArgument(minFreeSize >= 0, "minFreeSize < 0");
this.size = size;
map = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
freeBySize = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE);
freeByStart = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
mMinFreeSize = minFreeSize;
if (size > 0) {
internalAdd(FileUseMapEntry.makeFree(0, size));
}
}
/**
* Adds an entry to the internal structures.
*
* @param entry the entry to add
*/
private void internalAdd(FileUseMapEntry<?> entry) {
map.add(entry);
if (entry.isFree()) {
freeBySize.add(entry);
freeByStart.add(entry);
}
}
/**
* Removes an entry from the internal structures.
*
* @param entry the entry to remove
*/
private void internalRemove(FileUseMapEntry<?> entry) {
boolean wasRemoved = map.remove(entry);
Preconditions.checkState(wasRemoved, "entry not in map");
if (entry.isFree()) {
freeBySize.remove(entry);
freeByStart.remove(entry);
}
}
/**
* Adds a new file to the map. The interval specified by {@code entry} must fit inside an empty
* entry in the map. That entry will be replaced by entry and additional free entries will be
* added before and after if needed to make sure no spaces exist on the map.
*
* @param entry the entry to add
*/
private void add(FileUseMapEntry<?> entry) {
Preconditions.checkArgument(entry.getStart() < size, "entry.getStart() >= size");
Preconditions.checkArgument(entry.getEnd() <= size, "entry.getEnd() > size");
Preconditions.checkArgument(!entry.isFree(), "entry.isFree()");
FileUseMapEntry<?> container = findContainer(entry);
Verify.verify(container.isFree(), "!container.isFree()");
Set<FileUseMapEntry<?>> replacements = split(container, entry);
internalRemove(container);
for (FileUseMapEntry<?> r : replacements) {
internalAdd(r);
}
}
/**
* Adds a new file to the map. The interval specified by ({@code start}, {@code end}) must fit
* inside an empty entry in the map. That entry will be replaced by entry and additional free
* entries will be added before and after if needed to make sure no spaces exist on the map.
*
* <p>The entry cannot extend beyong the end of the map. If necessary, extend the map using {@link
* #extend(long)}.
*
* @param start the start of this entry
* @param end the end of the entry
* @param store extra data to store with the entry
* @param <T> the type of data to store in the entry
* @return the new entry
*/
<T> FileUseMapEntry<T> add(long start, long end, T store) {
Preconditions.checkArgument(start >= 0, "start < 0");
Preconditions.checkArgument(end > start, "end < start");
FileUseMapEntry<T> entry = FileUseMapEntry.makeUsed(start, end, store);
add(entry);
return entry;
}
/**
* Removes a file from the map, replacing it with an empty one that is then coalesced with
* neighbors (if the neighbors are free).
*
* @param entry the entry
*/
void remove(FileUseMapEntry<?> entry) {
Preconditions.checkState(map.contains(entry), "!map.contains(entry)");
Preconditions.checkArgument(!entry.isFree(), "entry.isFree()");
internalRemove(entry);
FileUseMapEntry<?> replacement = FileUseMapEntry.makeFree(entry.getStart(), entry.getEnd());
internalAdd(replacement);
coalesce(replacement);
}
/**
* Finds the entry that fully contains the given one. It is assumed that one exists.
*
* @param entry the entry whose container we're looking for
* @return the container
*/
private FileUseMapEntry<?> findContainer(FileUseMapEntry<?> entry) {
FileUseMapEntry container = map.floor(entry);
Verify.verifyNotNull(container);
Verify.verify(container.getStart() <= entry.getStart());
Verify.verify(container.getEnd() >= entry.getEnd());
return container;
}
/**
* Splits a container to add an entry, adding new free entries before and after the provided entry
* if needed.
*
* @param container the container entry, a free entry that is in {@link #map} that that encloses
* {@code entry}
* @param entry the entry that will be used to split {@code container}
* @return a set of non-overlapping entries that completely covers {@code container} and that
* includes {@code entry}
*/
private static Set<FileUseMapEntry<?>> split(
FileUseMapEntry<?> container, FileUseMapEntry<?> entry) {
Preconditions.checkArgument(container.isFree(), "!container.isFree()");
long farStart = container.getStart();
long start = entry.getStart();
long end = entry.getEnd();
long farEnd = container.getEnd();
Verify.verify(farStart <= start, "farStart > start");
Verify.verify(start < end, "start >= end");
Verify.verify(farEnd >= end, "farEnd < end");
Set<FileUseMapEntry<?>> result = Sets.newHashSet();
if (farStart < start) {
result.add(FileUseMapEntry.makeFree(farStart, start));
}
result.add(entry);
if (end < farEnd) {
result.add(FileUseMapEntry.makeFree(end, farEnd));
}
return result;
}
/**
* Coalesces a free entry replacing it and neighboring free entries with a single, larger entry.
* This method does nothing if {@code entry} does not have free neighbors.
*
* @param entry the free entry to coalesce with neighbors
*/
private void coalesce(FileUseMapEntry<?> entry) {
Preconditions.checkArgument(entry.isFree(), "!entry.isFree()");
FileUseMapEntry<?> prevToMerge = null;
long start = entry.getStart();
if (start > 0) {
/*
* See if we have a previous entry to merge with this one.
*/
prevToMerge = map.floor(FileUseMapEntry.makeFree(start - 1, start));
Verify.verifyNotNull(prevToMerge);
if (!prevToMerge.isFree()) {
prevToMerge = null;
}
}
FileUseMapEntry<?> nextToMerge = null;
long end = entry.getEnd();
if (end < size) {
/*
* See if we have a next entry to merge with this one.
*/
nextToMerge = map.ceiling(FileUseMapEntry.makeFree(end, end + 1));
Verify.verifyNotNull(nextToMerge);
if (!nextToMerge.isFree()) {
nextToMerge = null;
}
}
if (prevToMerge == null && nextToMerge == null) {
return;
}
long newStart = start;
if (prevToMerge != null) {
newStart = prevToMerge.getStart();
internalRemove(prevToMerge);
}
long newEnd = end;
if (nextToMerge != null) {
newEnd = nextToMerge.getEnd();
internalRemove(nextToMerge);
}
internalRemove(entry);
internalAdd(FileUseMapEntry.makeFree(newStart, newEnd));
}
/** Truncates map removing the top entry if it is free and reducing the map's size. */
void truncate() {
if (size == 0) {
return;
}
/*
* Find the last entry.
*/
FileUseMapEntry<?> last = map.last();
Verify.verifyNotNull(last, "last == null");
if (last.isFree()) {
internalRemove(last);
size = last.getStart();
}
}
/**
* Obtains the size of the map.
*
* @return the size
*/
long size() {
return size;
}
/**
* Obtains the largest used offset in the map. This will be size of the map after truncation.
*
* @return the size of the file discounting the last block if it is empty
*/
long usedSize() {
if (size == 0) {
return 0;
}
/*
* Find the last entry to see if it is an empty entry. If it is, we need to remove its size
* from the returned value.
*/
FileUseMapEntry<?> last = map.last();
Verify.verifyNotNull(last, "last == null");
if (last.isFree()) {
return last.getStart();
} else {
Verify.verify(last.getEnd() == size);
return size;
}
}
/**
* Extends the map to guarantee it has at least {@code size} bytes. If the current size is as
* large as {@code size}, this method does nothing.
*
* @param size the new size of the map that cannot be smaller that the current size
*/
void extend(long size) {
Preconditions.checkArgument(size >= this.size, "size < size");
if (this.size == size) {
return;
}
FileUseMapEntry<?> newBlock = FileUseMapEntry.makeFree(this.size, size);
internalAdd(newBlock);
this.size = size;
coalesce(newBlock);
}
/**
* Locates a free area in the map with at least {@code size} bytes such that {@code ((start +
* alignOffset) % align == 0} and such that the free space before {@code start} is not smaller
* than the minimum free entry size. This method will follow the algorithm specified by {@code
* alg}.
*
* <p>If no free contiguous block exists in the map that can hold the provided size then the first
* free index at the end of the map is provided. This means that the map may need to be extended
* before data can be added.
*
* @param size the size of the contiguous area requested
* @param alignOffset an offset to which alignment needs to be computed (see method description)
* @param align alignment at the offset (see method description)
* @param alg which algorithm to use
* @return the location of the contiguous area; this may be located at the end of the map
*/
long locateFree(long size, long alignOffset, long align, PositionAlgorithm alg) {
Preconditions.checkArgument(size > 0, "size <= 0");
FileUseMapEntry<?> minimumSizedEntry = FileUseMapEntry.makeFree(0, size);
SortedSet<FileUseMapEntry<?>> matches;
switch (alg) {
case BEST_FIT:
matches = freeBySize.tailSet(minimumSizedEntry);
break;
case FIRST_FIT:
matches = freeByStart;
break;
default:
throw new AssertionError();
}
FileUseMapEntry<?> best = null;
long bestExtraSize = 0;
for (FileUseMapEntry<?> curr : matches) {
/*
* We don't care about blocks that aren't free.
*/
if (!curr.isFree()) {
continue;
}
/*
* Compute any extra size we need in this block to make sure we verify the alignment.
* There must be a better to do this...
*/
long extraSize;
if (align == 0) {
extraSize = 0;
} else {
extraSize = (align - ((curr.getStart() + alignOffset) % align)) % align;
}
/*
* We can't leave than mMinFreeSize before. So if the extraSize is less than
* mMinFreeSize, we have to increase it by 'align' as many times as needed. For
* example, if mMinFreeSize is 20, align 4 and extraSize is 5. We need to increase it
* to 21 (5 + 4 * 4)
*/
if (extraSize > 0 && extraSize < mMinFreeSize) {
int addAlignBlocks = Ints.checkedCast((mMinFreeSize - extraSize + align - 1) / align);
extraSize += addAlignBlocks * align;
}
/*
* We don't care about blocks where we don't fit in.
*/
if (curr.getSize() < (size + extraSize)) {
continue;
}
/*
* We don't care about blocks that leave less than the minimum size after. There are
* two exceptions: (1) this is the last block and (2) the next block is free in which
* case, after coalescing, the free block with have at least the minimum size.
*/
long emptySpaceLeft = curr.getSize() - (size + extraSize);
if (emptySpaceLeft > 0 && emptySpaceLeft < mMinFreeSize) {
FileUseMapEntry<?> next = map.higher(curr);
if (next != null && !next.isFree()) {
continue;
}
}
/*
* We don't care about blocks that are bigger than the best so far (otherwise this
* wouldn't be a best-fit algorithm).
*/
if (best != null && best.getSize() < curr.getSize()) {
continue;
}
best = curr;
bestExtraSize = extraSize;
/*
* If we're doing first fit, we don't want to search for a better one :)
*/
if (alg == PositionAlgorithm.FIRST_FIT) {
break;
}
}
/*
* If no entry that could hold size is found, get the first free byte.
*/
long firstFree = this.size;
if (best == null && !map.isEmpty()) {
FileUseMapEntry<?> last = map.last();
if (last.isFree()) {
firstFree = last.getStart();
}
}
/*
* We're done: either we found something or we didn't, in which the new entry needs to
* be added to the end of the map.
*/
if (best == null) {
long extra = (align - ((firstFree + alignOffset) % align)) % align;
/*
* If adding this entry at the end would create a space smaller than the minimum,
* push it for 'align' bytes forward.
*/
if (extra > 0) {
if (extra < mMinFreeSize) {
extra += align * (((mMinFreeSize - extra) + (align - 1)) / align);
}
}
return firstFree + extra;
} else {
return best.getStart() + bestExtraSize;
}
}
/**
* Obtains all free areas of the map, excluding any trailing free area.
*
* @return all free areas, an empty set if there are no free areas; the areas are returned in file
* order, that is, if area {@code x} starts before area {@code y}, then area {@code x} will be
* stored before area {@code y} in the list
*/
List<FileUseMapEntry<?>> getFreeAreas() {
List<FileUseMapEntry<?>> freeAreas = Lists.newArrayList();
for (FileUseMapEntry<?> area : map) {
if (area.isFree() && area.getEnd() != size) {
freeAreas.add(area);
}
}
return freeAreas;
}
/**
* Obtains the entry that is located before the one provided.
*
* @param entry the map entry to get the previous one for; must belong to the map
* @return the entry before the provided one, {@code null} if {@code entry} is the first entry in
* the map
*/
@Nullable
FileUseMapEntry<?> before(FileUseMapEntry<?> entry) {
Preconditions.checkNotNull(entry, "entry == null");
return map.lower(entry);
}
/**
* Obtains the entry that is located after the one provided.
*
* @param entry the map entry to get the next one for; must belong to the map
* @return the entry after the provided one, {@code null} if {@code entry} is the last entry in
* the map
*/
@Nullable
FileUseMapEntry<?> after(FileUseMapEntry<?> entry) {
Preconditions.checkNotNull(entry, "entry == null");
return map.higher(entry);
}
/**
* Obtains the entry at the given offset.
*
* @param offset the offset to look for
* @return the entry found or {@code null} if there is no entry (not even a free one) at the given
* offset
*/
@Nullable
FileUseMapEntry<?> at(long offset) {
Preconditions.checkArgument(offset >= 0, "offset < 0");
Preconditions.checkArgument(offset < size, "offset >= size");
FileUseMapEntry<?> entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1));
if (entry == null) {
return null;
}
Verify.verify(entry.getStart() <= offset);
Verify.verify(entry.getEnd() > offset);
return entry;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
boolean first = true;
for (FileUseMapEntry<?> entry : map) {
if (first) {
first = false;
} else {
builder.append(", ");
}
builder.append(entry.getStart());
builder.append(" - ");
builder.append(entry.getEnd());
builder.append(": ");
builder.append(entry.getStore());
}
return builder.toString();
}
/** Algorithms used to position entries in blocks. */
public enum PositionAlgorithm {
/** Best fit: finds the smallest free block that can receive the entry. */
BEST_FIT,
/** First fit: finds the first free block that can receive the entry. */
FIRST_FIT
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.tools.build.apkzlib.zip;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.primitives.Ints;
import java.util.Comparator;
import javax.annotation.Nullable;
/**
* Represents an entry in the {@link FileUseMap}. Each entry contains an interval of bytes. The end
* of the interval is exclusive.
*
* <p>Entries can either be free or used. Used entries <em>must</em> store an object. Free entries
* do not store anything.
*
* <p>File map entries are used to keep track of which parts of a file map are used and not.
*
* @param <T> the type of data stored
*/
class FileUseMapEntry<T> {
/** Comparator that compares entries by their start date. */
public static final Comparator<FileUseMapEntry<?>> COMPARE_BY_START =
(o1, o2) -> Ints.saturatedCast(o1.getStart() - o2.getStart());
/** Comparator that compares entries by their size. */
public static final Comparator<FileUseMapEntry<?>> COMPARE_BY_SIZE =
(o1, o2) -> Ints.saturatedCast(o1.getSize() - o2.getSize());
/** The first byte in the entry. */
private final long start;
/** The first byte no longer in the entry. */
private final long end;
/** The stored data. If {@code null} then this entry represents a free entry. */
@Nullable private final T store;
/**
* Creates a new map entry.
*
* @param start the start of the entry
* @param end the end of the entry (first byte no longer in the entry)
* @param store the data to store in the entry or {@code null} if this is a free entry
*/
private FileUseMapEntry(long start, long end, @Nullable T store) {
Preconditions.checkArgument(start >= 0, "start < 0");
Preconditions.checkArgument(end > start, "end <= start");
this.start = start;
this.end = end;
this.store = store;
}
/**
* Creates a new free entry.
*
* @param start the start of the entry
* @param end the end of the entry (first byte no longer in the entry)
* @return the entry
*/
public static FileUseMapEntry<Object> makeFree(long start, long end) {
return new FileUseMapEntry<>(start, end, null);
}
/**
* Creates a new used entry.
*
* @param start the start of the entry
* @param end the end of the entry (first byte no longer in the entry)
* @param store the data to store in the entry
* @param <T> the type of data to store in the entry
* @return the entry
*/
public static <T> FileUseMapEntry<T> makeUsed(long start, long end, T store) {
Preconditions.checkNotNull(store, "store == null");
return new FileUseMapEntry<>(start, end, store);
}
/**
* Obtains the first byte in the entry.
*
* @return the first byte in the entry (if the same value as {@link #getEnd()} then the entry is
* empty and contains no data)
*/
long getStart() {
return start;
}
/**
* Obtains the first byte no longer in the entry.
*
* @return the first byte no longer in the entry
*/
long getEnd() {
return end;
}
/**
* Obtains the size of the entry.
*
* @return the number of bytes contained in the entry
*/
long getSize() {
return end - start;
}
/**
* Determines if this is a free entry.
*
* @return is this entry free?
*/
boolean isFree() {
return store == null;
}
/**
* Obtains the data stored in the entry.
*
* @return the data stored or {@code null} if this entry is a free entry
*/
@Nullable
T getStore() {
return store;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("start", start)
.add("end", end)
.add("store", store)
.toString();
}
}

View File

@ -0,0 +1,159 @@
/*
* 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.tools.build.apkzlib.zip;
import java.io.IOException;
/**
* General purpose bit flags. Contains the encoding of the zip's general purpose bits.
*
* <p>We don't really care about the method bit(s). These are bits 1 and 2. Here are the values:
*
* <ul>
* <li>0 (00): Normal (-en) compression option was used.
* <li>1 (01): Maximum (-exx/-ex) compression option was used.
* <li>2 (10): Fast (-ef) compression option was used.
* <li>3 (11): Super Fast (-es) compression option was used.
* </ul>
*/
class GPFlags {
/** Is the entry encrypted? */
private static final int BIT_ENCRYPTION = 1;
/** Has CRC computation been deferred and, therefore, does a data description block exist? */
private static final int BIT_DEFERRED_CRC = (1 << 3);
/** Is enhanced deflating used? */
private static final int BIT_ENHANCED_DEFLATING = (1 << 4);
/** Does the entry contain patched data? */
private static final int BIT_PATCHED_DATA = (1 << 5);
/** Is strong encryption used? */
private static final int BIT_STRONG_ENCRYPTION = (1 << 6) | (1 << 13);
/**
* If this bit is set the filename and comment fields for this file must be encoded using UTF-8.
*/
private static final int BIT_EFS = (1 << 11);
/** Unused bits. */
private static final int BIT_UNUSED =
(1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 14) | (1 << 15);
/** Bit flag value. */
private final long value;
/** Has the CRC computation beeen deferred? */
private boolean deferredCrc;
/** Is the file name encoded in UTF-8? */
private boolean utf8FileName;
/**
* Creates a new flags object.
*
* @param value the value of the bit mask
*/
private GPFlags(long value) {
this.value = value;
deferredCrc = ((value & BIT_DEFERRED_CRC) != 0);
utf8FileName = ((value & BIT_EFS) != 0);
}
/**
* Obtains the flags value.
*
* @return the value of the bit mask
*/
public long getValue() {
return value;
}
/**
* Is the CRC computation deferred?
*
* @return is the CRC computation deferred?
*/
public boolean isDeferredCrc() {
return deferredCrc;
}
/**
* Is the file name encoded in UTF-8?
*
* @return is the file name encoded in UTF-8?
*/
public boolean isUtf8FileName() {
return utf8FileName;
}
/**
* Creates a new bit mask.
*
* @param utf8Encoding should UTF-8 encoding be used?
* @return the new bit mask
*/
static GPFlags make(boolean utf8Encoding) {
long flags = 0;
if (utf8Encoding) {
flags |= BIT_EFS;
}
return new GPFlags(flags);
}
/**
* Creates the flag information from a byte. This method will also validate that only supported
* options are defined in the flag.
*
* @param bits the bit mask
* @return the created flag information
* @throws IOException unsupported options are used in the bit mask
*/
static GPFlags from(long bits) throws IOException {
if ((bits & BIT_ENCRYPTION) != 0) {
throw new IOException("Zip files with encrypted of entries not supported.");
}
if ((bits & BIT_ENHANCED_DEFLATING) != 0) {
throw new IOException("Enhanced deflating not supported.");
}
if ((bits & BIT_PATCHED_DATA) != 0) {
throw new IOException("Compressed patched data not supported.");
}
if ((bits & BIT_STRONG_ENCRYPTION) != 0) {
throw new IOException("Strong encryption not supported.");
}
if ((bits & BIT_UNUSED) != 0) {
throw new IOException(
"Unused bits set in directory entry. Weird. I don't know what's " + "going on.");
}
if ((bits & 0xffffffff00000000L) != 0) {
throw new IOException("Unsupported bits after 32.");
}
return new GPFlags(bits);
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* Byte source that inflates another byte source. It assumed the inner byte source has deflated
* data.
*/
public class InflaterByteSource extends CloseableByteSource {
/** The stream factory for the deflated data. */
private final CloseableByteSource deflatedSource;
/**
* Creates a new source.
*
* @param byteSource the factory for deflated data
*/
public InflaterByteSource(CloseableByteSource byteSource) {
deflatedSource = byteSource;
}
@Override
public InputStream openStream() throws IOException {
/*
* The extra byte is a dummy byte required by the inflater. Weirdo.
* (see the java.util.Inflater documentation). Looks like a hack...
* "Oh, I need an extra dummy byte to allow for some... err... optimizations..."
*/
ByteArrayInputStream hackByte = new ByteArrayInputStream(new byte[] {0});
return new InflaterInputStream(
new SequenceInputStream(deflatedSource.openStream(), hackByte), new Inflater(true));
}
@Override
public void innerClose() throws IOException {
deflatedSource.close();
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.io.ByteProcessor;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutionException;
/**
* {@code ByteSource} that delegates all operations to another {@code ByteSource}. The other byte
* source, the <em>delegate</em>, may be computed lazily.
*/
public class LazyDelegateByteSource extends CloseableByteSource {
/** Byte source where we delegate operations to. */
private final ListenableFuture<CloseableByteSource> delegate;
/**
* Creates a new byte source that delegates operations to the provided source.
*
* @param delegate the source that will receive all operations
*/
public LazyDelegateByteSource(ListenableFuture<CloseableByteSource> delegate) {
this.delegate = delegate;
}
/**
* Obtains the delegate future.
*
* @return the delegate future, that may be computed or not
*/
public ListenableFuture<CloseableByteSource> getDelegate() {
return delegate;
}
/**
* Obtains the byte source, waiting for the future to be computed.
*
* @return the byte source
* @throws IOException failed to compute the future :)
*/
private CloseableByteSource get() throws IOException {
try {
CloseableByteSource r = delegate.get();
if (r == null) {
throw new IOException("Delegate byte source computation resulted in null.");
}
return r;
} catch (InterruptedException e) {
throw new IOException("Interrupted while waiting for byte source computation.", e);
} catch (ExecutionException e) {
throw new IOException("Failed to compute byte source.", e);
}
}
@Override
public CharSource asCharSource(Charset charset) {
try {
return get().asCharSource(charset);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public InputStream openBufferedStream() throws IOException {
return get().openBufferedStream();
}
@Override
public ByteSource slice(long offset, long length) {
try {
return get().slice(offset, length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isEmpty() throws IOException {
return get().isEmpty();
}
@Override
public long size() throws IOException {
return get().size();
}
@Override
public long copyTo(OutputStream output) throws IOException {
return get().copyTo(output);
}
@Override
public long copyTo(ByteSink sink) throws IOException {
return get().copyTo(sink);
}
@Override
public byte[] read() throws IOException {
return get().read();
}
@Override
public <T> T read(ByteProcessor<T> processor) throws IOException {
return get().read(processor);
}
@Override
public HashCode hash(HashFunction hashFunction) throws IOException {
return get().hash(hashFunction);
}
@Override
public boolean contentEquals(ByteSource other) throws IOException {
return get().contentEquals(other);
}
@Override
public InputStream openStream() throws IOException {
return get().openStream();
}
@Override
public void innerClose() throws IOException {
get().close();
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.io.Closer;
import java.io.Closeable;
import java.io.IOException;
/**
* Container that has two bytes sources: one representing raw data and another processed data. In
* case of compression, the raw data is the compressed data and the processed data is the
* uncompressed data. It is valid for a RaP ("Raw-and-Processed") to contain the same byte sources
* for both processed and raw data.
*/
public class ProcessedAndRawByteSources implements Closeable {
/** The processed byte source. */
private final CloseableByteSource processedSource;
/** The processed raw source. */
private final CloseableByteSource rawSource;
/**
* Creates a new container.
*
* @param processedSource the processed source
* @param rawSource the raw source
*/
public ProcessedAndRawByteSources(
CloseableByteSource processedSource, CloseableByteSource rawSource) {
this.processedSource = processedSource;
this.rawSource = rawSource;
}
/**
* Obtains a byte source that read the processed contents of the entry.
*
* @return a byte source
*/
public CloseableByteSource getProcessedByteSource() {
return processedSource;
}
/**
* Obtains a byte source that reads the raw contents of an entry. This is the data that is
* ultimately stored in the file and, in the case of compressed files, is the same data in the
* source returned by {@link #getProcessedByteSource()}.
*
* @return a byte source
*/
public CloseableByteSource getRawByteSource() {
return rawSource;
}
@Override
public void close() throws IOException {
Closer closer = Closer.create();
closer.register(processedSource);
closer.register(rawSource);
closer.close();
}
}

View File

@ -0,0 +1,775 @@
/*
* 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.bytestorage.ByteStorage;
import com.android.tools.build.apkzlib.bytestorage.CloseableByteSourceFromOutputStreamBuilder;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Verify;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Ints;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Comparator;
import javax.annotation.Nullable;
/**
* A stored entry represents a file in the zip. The entry may or may not be written to the zip file.
*
* <p>Stored entries provide the operations that are related to the files themselves, not to the
* zip. It is through the {@code StoredEntry} class that entries can be deleted ({@link #delete()},
* open ({@link #open()}) or realigned ({@link #realign()}).
*
* <p>Entries are not created directly. They are created using {@link ZFile#add(String, InputStream,
* boolean)} and obtained from the zip file using {@link ZFile#get(String)} or {@link
* ZFile#entries()}.
*
* <p>Most of the data in the an entry is in the Central Directory Header. This includes the name,
* compression method, file compressed and uncompressed sizes, CRC32 checksum, etc. The CDH can be
* obtained using the {@link #getCentralDirectoryHeader()} method.
*/
public class StoredEntry {
/** Comparator that compares instances of {@link StoredEntry} by their names. */
static final Comparator<StoredEntry> COMPARE_BY_NAME =
(o1, o2) -> {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
String name1 = o1.getCentralDirectoryHeader().getName();
String name2 = o2.getCentralDirectoryHeader().getName();
return name1.compareTo(name2);
};
/** Signature of the data descriptor. */
private static final int DATA_DESC_SIGNATURE = 0x08074b50;
/** Local header field: signature. */
private static final ZipField.F4 F_LOCAL_SIGNATURE = new ZipField.F4(0, 0x04034b50, "Signature");
/** Local header field: version to extract, should match the CDH's. */
@VisibleForTesting
static final ZipField.F2 F_VERSION_EXTRACT =
new ZipField.F2(
F_LOCAL_SIGNATURE.endOffset(), "Version to extract", new ZipFieldInvariantNonNegative());
/** Local header field: GP bit flag, should match the CDH's. */
private static final ZipField.F2 F_GP_BIT =
new ZipField.F2(F_VERSION_EXTRACT.endOffset(), "GP bit flag");
/** Local header field: compression method, should match the CDH's. */
private static final ZipField.F2 F_METHOD =
new ZipField.F2(
F_GP_BIT.endOffset(), "Compression method", new ZipFieldInvariantNonNegative());
/** Local header field: last modification time, should match the CDH's. */
private static final ZipField.F2 F_LAST_MOD_TIME =
new ZipField.F2(F_METHOD.endOffset(), "Last modification time");
/** Local header field: last modification time, should match the CDH's. */
private static final ZipField.F2 F_LAST_MOD_DATE =
new ZipField.F2(F_LAST_MOD_TIME.endOffset(), "Last modification date");
/** Local header field: CRC32 checksum, should match the CDH's. 0 if there is no data. */
private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), "CRC32");
/** Local header field: compressed size, size the data takes in the zip file. */
private static final ZipField.F4 F_COMPRESSED_SIZE =
new ZipField.F4(F_CRC32.endOffset(), "Compressed size", new ZipFieldInvariantNonNegative());
/** Local header field: uncompressed size, size the data takes after extraction. */
private static final ZipField.F4 F_UNCOMPRESSED_SIZE =
new ZipField.F4(
F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative());
/** Local header field: length of the file name. */
private static final ZipField.F2 F_FILE_NAME_LENGTH =
new ZipField.F2(
F_UNCOMPRESSED_SIZE.endOffset(), "@File name length", new ZipFieldInvariantNonNegative());
/** Local header filed: length of the extra field. */
private static final ZipField.F2 F_EXTRA_LENGTH =
new ZipField.F2(
F_FILE_NAME_LENGTH.endOffset(), "Extra length", new ZipFieldInvariantNonNegative());
/** Local header size (fixed part, not counting file name or extra field). */
static final int FIXED_LOCAL_FILE_HEADER_SIZE = F_EXTRA_LENGTH.endOffset();
/** Type of entry. */
private final StoredEntryType type;
/** The central directory header with information about the file. */
private final CentralDirectoryHeader cdh;
/** The file this entry is associated with */
private final ZFile file;
/** Has this entry been deleted? */
private boolean deleted;
/** Extra field specified in the local directory. */
private ExtraField localExtra;
/** Type of data descriptor associated with the entry. */
private Supplier<DataDescriptorType> dataDescriptorType;
/**
* Source for this entry's data. If this entry is a directory, this source has to have zero size.
*/
private ProcessedAndRawByteSources source;
/** Verify log for the entry. */
private final VerifyLog verifyLog;
/** Storage used to create buffers when loading storage into memory. */
private final ByteStorage storage;
/**
* Creates a new stored entry.
*
* @param header the header with the entry information; if the header does not contain an offset
* it means that this entry is not yet written in the zip file
* @param file the zip file containing the entry
* @param source the entry's data source; it can be {@code null} only if the source can be read
* from the zip file, that is, if {@code header.getOffset()} is non-negative
* @throws IOException failed to create the entry
*/
StoredEntry(
CentralDirectoryHeader header,
ZFile file,
@Nullable ProcessedAndRawByteSources source,
ByteStorage storage)
throws IOException {
cdh = header;
this.file = file;
deleted = false;
verifyLog = file.makeVerifyLog();
this.storage = storage;
if (header.getOffset() >= 0) {
readLocalHeader();
Preconditions.checkArgument(
source == null, "Source was defined but contents already exist on file.");
/*
* Since the file is already in the zip, dynamically create a source that will read
* the file from the zip when needed. The assignment is not really needed, but we
* would get a warning because of the @NotNull otherwise.
*/
this.source = createSourceFromZip(cdh.getOffset());
} else {
/*
* There is no local extra data for new files.
*/
localExtra = new ExtraField();
Preconditions.checkNotNull(source, "Source was not defined, but contents are not on file.");
this.source = source;
}
/*
* It seems that zip utilities store directories as names ending with "/".
* This seems to be respected by all zip utilities although I could not find there anywhere
* in the specification.
*/
if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) {
type = StoredEntryType.DIRECTORY;
verifyLog.verify(
this.source.getProcessedByteSource().isEmpty(), "Directory source is not empty.");
verifyLog.verify(cdh.getCrc32() == 0, "Directory has CRC32 = %s.", cdh.getCrc32());
verifyLog.verify(
cdh.getUncompressedSize() == 0,
"Directory has uncompressed size = %s.",
cdh.getUncompressedSize());
/*
* Some clever (OMG!) tools, like jar will actually try to compress the directory
* contents and generate a 2 byte compressed data. Of course, the uncompressed size is
* zero and we're just wasting space.
*/
long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize();
verifyLog.verify(
compressedSize == 0 || compressedSize == 2,
"Directory has compressed size = %s.",
compressedSize);
} else {
type = StoredEntryType.FILE;
}
/*
* By default we assume there is no data descriptor unless the CRC is marked as deferred
* in the header's GP Bit.
*/
dataDescriptorType = Suppliers.ofInstance(DataDescriptorType.NO_DATA_DESCRIPTOR);
if (header.getGpBit().isDeferredCrc()) {
/*
* If the deferred CRC bit exists, then we have an extra descriptor field. This extra
* field may have a signature.
*/
Verify.verify(
header.getOffset() >= 0,
"Files that are not on disk cannot have the " + "deferred CRC bit set.");
dataDescriptorType =
Suppliers.memoize(
() -> {
try {
return readDataDescriptorRecord();
} catch (IOException e) {
throw new IOExceptionWrapper(
new IOException("Failed to read data descriptor record.", e));
}
});
}
}
/**
* Obtains the size of the local header of this entry.
*
* @return the local header size in bytes
*/
public int getLocalHeaderSize() {
Preconditions.checkState(!deleted, "deleted");
return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size();
}
/**
* Obtains the size of the whole entry on disk, including local header and data descriptor. This
* method will wait until compression information is complete, if needed.
*
* @return the number of bytes
* @throws IOException failed to get compression information
*/
long getInFileSize() throws IOException {
Preconditions.checkState(!deleted, "deleted");
return cdh.getCompressionInfoWithWait().getCompressedSize()
+ getLocalHeaderSize()
+ dataDescriptorType.get().size;
}
/**
* Obtains a stream that allows reading from the entry.
*
* @return a stream that will return as many bytes as the uncompressed entry size
* @throws IOException failed to open the stream
*/
public InputStream open() throws IOException {
return source.getProcessedByteSource().openStream();
}
/**
* Obtains the contents of the file.
*
* @return a byte array with the contents of the file (uncompressed if the file was compressed)
* @throws IOException failed to read the file
*/
public byte[] read() throws IOException {
try (InputStream is = new BufferedInputStream(open())) {
return ByteStreams.toByteArray(is);
}
}
/**
* Obtains the contents of the file in an existing buffer.
*
* @param bytes buffer to read the file contents in.
* @return the number of bytes read
* @throws IOException failed to read the file.
*/
public int read(byte[] bytes) throws IOException {
if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) {
throw new RuntimeException(
"Buffer to small while reading {}" + getCentralDirectoryHeader().getName());
}
try (InputStream is = new BufferedInputStream(open())) {
return ByteStreams.read(is, bytes, 0, bytes.length);
}
}
/**
* Obtains the type of entry.
*
* @return the type of entry
*/
public StoredEntryType getType() {
Preconditions.checkState(!deleted, "deleted");
return type;
}
/**
* Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. To
* eventually write updates to disk, {@link ZFile#update()} must be called.
*
* @throws IOException failed to delete the entry
* @throws IllegalStateException if the zip file was open in read-only mode
*/
public void delete() throws IOException {
delete(true);
}
/**
* Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. To
* eventually write updates to disk, {@link ZFile#update()} must be called.
*
* @param notify should listeners be notified of the deletion? This will only be {@code false} if
* the entry is being removed as part of a replacement
* @throws IOException failed to delete the entry
* @throws IllegalStateException if the zip file was open in read-only mode
*/
void delete(boolean notify) throws IOException {
Preconditions.checkState(!deleted, "deleted");
file.delete(this, notify);
deleted = true;
source.close();
}
/** Returns {@code true} if this entry has been deleted/replaced. */
public boolean isDeleted() {
return deleted;
}
/**
* Obtains the CDH associated with this entry.
*
* @return the CDH
*/
public CentralDirectoryHeader getCentralDirectoryHeader() {
return cdh;
}
/**
* Reads the file's local header and verifies that it matches the Central Directory Header
* provided in the constructor. This method should only be called if the entry already exists on
* disk; new entries do not have local headers.
*
* <p>This method will define the {@link #localExtra} field that is only defined in the local
* descriptor.
*
* @throws IOException failed to read the local header
*/
private void readLocalHeader() throws IOException {
byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE];
file.directFullyRead(cdh.getOffset(), localHeader);
CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
ByteBuffer bytes = ByteBuffer.wrap(localHeader);
F_LOCAL_SIGNATURE.verify(bytes);
F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog);
F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog);
F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog);
if (file.areTimestampsIgnored()) {
F_LAST_MOD_TIME.skip(bytes);
F_LAST_MOD_DATE.skip(bytes);
} else {
F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog);
F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog);
}
/*
* If CRC-32, compressed size and uncompressed size are deferred, their values in Local
* File Header must be ignored and their actual values must be read from the Data
* Descriptor following the contents of this entry. See readDataDescriptorRecord().
*/
if (cdh.getGpBit().isDeferredCrc()) {
F_CRC32.skip(bytes);
F_COMPRESSED_SIZE.skip(bytes);
F_UNCOMPRESSED_SIZE.skip(bytes);
} else {
F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog);
F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog);
F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog);
}
F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length);
long extraLength = F_EXTRA_LENGTH.read(bytes);
long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset();
byte[] fileNameData = new byte[cdh.getEncodedFileName().length];
file.directFullyRead(fileNameStart, fileNameData);
String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit());
if (!fileName.equals(cdh.getName())) {
verifyLog.log(
String.format(
"Central directory reports file as being named '%s' but local header"
+ "reports file being named '%s'.",
cdh.getName(), fileName));
}
long localExtraStart = fileNameStart + cdh.getEncodedFileName().length;
byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)];
file.directFullyRead(localExtraStart, localExtraRaw);
localExtra = new ExtraField(localExtraRaw);
}
/**
* Reads the data descriptor record. This method can only be invoked once it is established that a
* data descriptor does exist. It will read the data descriptor and check that the data described
* there matches the data provided in the Central Directory.
*
* <p>This method will set the {@link #dataDescriptorType} field to the appropriate type of data
* descriptor record.
*
* @throws IOException failed to read the data descriptor record
*/
private DataDescriptorType readDataDescriptorRecord() throws IOException {
CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
long ddStart =
cdh.getOffset()
+ FIXED_LOCAL_FILE_HEADER_SIZE
+ cdh.getName().length()
+ localExtra.size()
+ compressInfo.getCompressedSize();
byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];
file.directFullyRead(ddStart, ddData);
ByteBuffer ddBytes = ByteBuffer.wrap(ddData);
ZipField.F4 signatureField = new ZipField.F4(0, "Data descriptor signature");
int cpos = ddBytes.position();
long sig = signatureField.read(ddBytes);
DataDescriptorType result;
if (sig == DATA_DESC_SIGNATURE) {
result = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE;
} else {
result = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE;
ddBytes.position(cpos);
}
ZipField.F4 crc32Field = new ZipField.F4(0, "CRC32");
ZipField.F4 compressedField = new ZipField.F4(crc32Field.endOffset(), "Compressed size");
ZipField.F4 uncompressedField =
new ZipField.F4(compressedField.endOffset(), "Uncompressed size");
crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog);
compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog);
uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog);
return result;
}
/**
* Creates a new source that reads data from the zip.
*
* @param zipOffset the offset into the zip file where the data is, must be non-negative
* @throws IOException failed to close the old source
* @return the created source
*/
private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset) throws IOException {
Preconditions.checkArgument(zipOffset >= 0, "zipOffset < 0");
final CentralDirectoryHeaderCompressInfo compressInfo;
try {
compressInfo = cdh.getCompressionInfoWithWait();
} catch (IOException e) {
throw new RuntimeException(
"IOException should never occur here because compression "
+ "information should be immediately available if reading from zip.",
e);
}
/*
* Create a source that will return whatever is on the zip file.
*/
CloseableByteSource rawContents =
new CloseableByteSource() {
@Override
public long size() throws IOException {
return compressInfo.getCompressedSize();
}
@Override
public InputStream openStream() throws IOException {
Preconditions.checkState(!deleted, "deleted");
long dataStart = zipOffset + getLocalHeaderSize();
long dataEnd = dataStart + compressInfo.getCompressedSize();
file.openReadOnlyIfClosed();
return file.directOpen(dataStart, dataEnd);
}
@Override
protected void innerClose() throws IOException {
/*
* Nothing to do here.
*/
}
};
return createSourcesFromRawContents(rawContents);
}
/**
* Creates a {@link ProcessedAndRawByteSources} from the raw data source . The processed source
* will either inflate or do nothing depending on the compression information that, at this point,
* should already be available
*
* @param rawContents the raw data to create the source from
* @return the sources for this entry
*/
private ProcessedAndRawByteSources createSourcesFromRawContents(CloseableByteSource rawContents) {
CentralDirectoryHeaderCompressInfo compressInfo;
try {
compressInfo = cdh.getCompressionInfoWithWait();
} catch (IOException e) {
throw new RuntimeException(
"IOException should never occur here because compression "
+ "information should be immediately available if creating from raw "
+ "contents.",
e);
}
CloseableByteSource contents;
/*
* If the contents are deflated, wrap that source in an inflater source so we get the
* uncompressed data.
*/
if (compressInfo.getMethod() == CompressionMethod.DEFLATE) {
contents = new InflaterByteSource(rawContents);
} else {
contents = rawContents;
}
return new ProcessedAndRawByteSources(contents, rawContents);
}
/**
* Replaces {@link #source} with one that reads file data from the zip file.
*
* @param zipFileOffset the offset in the zip file where data is written; must be non-negative
* @throws IOException failed to replace the source
*/
void replaceSourceFromZip(long zipFileOffset) throws IOException {
Preconditions.checkArgument(zipFileOffset >= 0, "zipFileOffset < 0");
ProcessedAndRawByteSources oldSource = source;
source = createSourceFromZip(zipFileOffset);
cdh.setOffset(zipFileOffset);
oldSource.close();
}
/**
* Loads all data in memory and replaces {@link #source} with one that contains all the data in
* memory.
*
* <p>If the entry's contents are already in memory, this call does nothing.
*
* @throws IOException failed to replace the source
*/
void loadSourceIntoMemory() throws IOException {
if (cdh.getOffset() == -1) {
/*
* No offset in the CDR means data has not been written to disk which, in turn,
* means data is already loaded into memory.
*/
return;
}
CloseableByteSourceFromOutputStreamBuilder rawBuilder = storage.makeBuilder();
try (InputStream input = source.getRawByteSource().openStream()) {
ByteStreams.copy(input, rawBuilder);
}
CloseableByteSource newRaw = rawBuilder.build();
ProcessedAndRawByteSources newSources = createSourcesFromRawContents(newRaw);
try (ProcessedAndRawByteSources oldSource = source) {
source = newSources;
cdh.setOffset(-1);
}
}
/**
* Obtains the source data for this entry. This method can only be called for files, it cannot be
* called for directories.
*
* @return the entry source
*/
ProcessedAndRawByteSources getSource() {
return source;
}
/**
* Obtains the type of data descriptor used in the entry.
*
* @return the type of data descriptor
*/
public DataDescriptorType getDataDescriptorType() {
return dataDescriptorType.get();
}
/**
* Removes the data descriptor, if it has one and resets the data descriptor bit in the central
* directory header.
*
* @return was the data descriptor remove?
*/
boolean removeDataDescriptor() {
if (dataDescriptorType.get() == DataDescriptorType.NO_DATA_DESCRIPTOR) {
return false;
}
dataDescriptorType = Suppliers.ofInstance(DataDescriptorType.NO_DATA_DESCRIPTOR);
cdh.resetDeferredCrc();
return true;
}
/**
* Obtains the local header data.
*
* @param buffer a buffer to write header data to
* @return the header data size
* @throws IOException failed to get header byte data
*/
int toHeaderData(byte[] buffer) throws IOException {
Preconditions.checkArgument(
buffer.length
>= F_EXTRA_LENGTH.endOffset() + cdh.getEncodedFileName().length + localExtra.size(),
"Buffer should be at least the header size");
ByteBuffer out = ByteBuffer.wrap(buffer);
writeData(out);
return out.position();
}
private void writeData(ByteBuffer out) throws IOException {
CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
F_LOCAL_SIGNATURE.write(out);
F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract());
F_GP_BIT.write(out, cdh.getGpBit().getValue());
F_METHOD.write(out, compressInfo.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, cdh.getLastModTime());
F_LAST_MOD_DATE.write(out, cdh.getLastModDate());
}
F_CRC32.write(out, cdh.getCrc32());
F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize());
F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize());
F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length);
F_EXTRA_LENGTH.write(out, localExtra.size());
out.put(cdh.getEncodedFileName());
localExtra.write(out);
}
/**
* Requests that this entry be realigned. If this entry is already aligned according to the rules
* in {@link ZFile} then this method does nothing. Otherwise it will move the file's data into
* memory and place it in a different area of the zip.
*
* @return has this file been changed? Note that if the entry has not yet been written on the
* file, realignment does not count as a change as nothing needs to be updated in the file;
* also, if the entry has been changed, this object may have been marked as deleted and a new
* stored entry may need to be fetched from the file
* @throws IOException failed to realign the entry; the entry may no longer exist in the zip file
*/
public boolean realign() throws IOException {
Preconditions.checkState(!deleted, "Entry has been deleted.");
return file.realign(this);
}
/**
* Obtains the contents of the local extra field.
*
* @return the contents of the local extra field
*/
public ExtraField getLocalExtra() {
return localExtra;
}
/**
* Sets the contents of the local extra field.
*
* @param localExtra the contents of the local extra field
* @throws IOException failed to update the zip file
*/
public void setLocalExtra(ExtraField localExtra) throws IOException {
boolean resized = setLocalExtraNoNotify(localExtra);
file.localHeaderChanged(this, resized);
}
/**
* Sets the contents of the local extra field, does not notify the {@link ZFile} of the change.
* This is used internally when the {@link ZFile} itself wants to change the local extra and
* doesn't need the callback.
*
* @param localExtra the contents of the local extra field
* @return has the local header size changed?
* @throws IOException failed to load the file
*/
boolean setLocalExtraNoNotify(ExtraField localExtra) throws IOException {
boolean sizeChanged;
/*
* Make sure we load into memory.
*
* If we change the size of the local header, the actual start of the file changes
* according to our in-memory structures so, if we don't read the file now, we won't be
* able to load it later :)
*
* But, even if the size doesn't change, we need to read it force the entry to be
* rewritten otherwise the changes in the local header aren't written. Of course this case
* may be optimized with some extra complexity added :)
*/
loadSourceIntoMemory();
if (this.localExtra.size() != localExtra.size()) {
sizeChanged = true;
} else {
sizeChanged = false;
}
this.localExtra = localExtra;
return sizeChanged;
}
/**
* Obtains the verify log for the entry.
*
* @return the verify log
*/
public VerifyLog getVerifyLog() {
return verifyLog;
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.tools.build.apkzlib.zip;
/** Type of stored entry. */
public enum StoredEntryType {
/** Entry is a file. */
FILE,
/** Entry is a directory. */
DIRECTORY
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2017 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.tools.build.apkzlib.zip;
import com.google.common.collect.ImmutableList;
/**
* The verify log contains verification messages. It is used to capture validation issues with a zip
* file or with parts of a zip file.
*/
public interface VerifyLog {
/**
* Logs a message.
*
* @param message the message to verify
*/
void log(String message);
/**
* Obtains all save logged messages.
*
* @return the logged messages
*/
ImmutableList<String> getLogs();
/**
* Performs verification of a non-critical condition, logging a message if the condition is not
* verified.
*
* @param condition the condition
* @param message the message to write if {@code condition} is {@code false}.
* @param args arguments for formatting {@code message} using {@code String.format}
*/
default void verify(boolean condition, String message, Object... args) {
if (!condition) {
log(String.format(message, args));
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2017 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.tools.build.apkzlib.zip;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
/** Factory for verification logs. */
final class VerifyLogs {
private VerifyLogs() {}
/**
* Creates a {@link VerifyLog} that ignores all messages logged.
*
* @return the log
*/
static VerifyLog devNull() {
return new VerifyLog() {
@Override
public void log(String message) {}
@Override
public ImmutableList<String> getLogs() {
return ImmutableList.of();
}
};
}
/**
* Creates a {@link VerifyLog} that stores all log messages.
*
* @return the log
*/
static VerifyLog unlimited() {
return new VerifyLog() {
/** All saved messages. */
private final List<String> messages = new ArrayList<>();
@Override
public void log(String message) {
messages.add(message);
}
@Override
public ImmutableList<String> getLogs() {
return ImmutableList.copyOf(messages);
}
};
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
/*
* 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.utils.IOExceptionRunnable;
import java.io.IOException;
import javax.annotation.Nullable;
/**
* An extension of a {@link ZFile}. Extensions are notified when files are open, updated, closed and
* when files are added or removed from the zip. These notifications are received after the zip has
* been updated in memory for open, when files are added or removed and when the zip has been
* updated on disk or closed.
*
* <p>An extension is also notified before the file is updated, allowing it to modify the file
* before the update happens. If it does, then all extensions are notified of the changes on the zip
* file. Because the order of the notifications is preserved, all extensions are notified in the
* same order. For example, if two extensions E1 and E2 are registered and they both add a file at
* update time, this would be the flow:
*
* <ul>
* <li>E1 receives {@code beforeUpdate} notification.
* <li>E1 adds file F1 to the zip (notifying the addition is suspended because another
* notification is in progress).
* <li>E2 receives {@code beforeUpdate} notification.
* <li>E2 adds file F2 to the zip (notifying the addition is suspended because another
* notification is in progress).
* <li>E1 is notified F1 was added.
* <li>E2 is notified F1 was added.
* <li>E1 is notified F2 was added.
* <li>E2 is notified F2 was added.
* <li>(zip file is updated on disk)
* <li>E1 is notified the zip was updated.
* <li>E2 is notified the zip was updated.
* </ul>
*
* <p>An extension should not modify the zip file when notified of changes. If allowed, this would
* break event notification order in case multiple extensions are registered with the zip file. To
* allow performing changes to the zip file, all notification method return a {@code
* IOExceptionRunnable} that is invoked when {@link ZFile} has finished notifying all extensions.
*/
public abstract class ZFileExtension {
/**
* The zip file has been open and the zip's contents have been read. The default implementation
* does nothing and returns {@code null}.
*
* @return an optional runnable to run when notification of all listeners has ended
* @throws IOException failed to process the event
*/
@Nullable
public IOExceptionRunnable open() throws IOException {
return null;
}
/**
* The zip will be updated. This method allows the extension to register changes to the zip file
* before the file is written. The default implementation does nothing and returns {@code null}.
*
* <p>After this notification is received, the extension will receive further {@link
* #added(StoredEntry, StoredEntry)} and {@link #removed(StoredEntry)} notifications if it or
* other extensions add or remove files before update.
*
* <p>When no more files are updated, the {@link #entriesWritten()} notification is sent.
*
* @return an optional runnable to run when notification of all listeners has ended
* @throws IOException failed to process the event
*/
@Nullable
public IOExceptionRunnable beforeUpdate() throws IOException {
return null;
}
/**
* This notification is sent when all entries have been written in the file but the central
* directory and the EOCD have not yet been written. No entries should be added, removed or
* updated during this notification. If this method forces an update of either the central
* directory or EOCD, then this method will be invoked again for all extensions with the new
* central directory and EOCD.
*
* <p>After this notification, {@link #updated()} is sent.
*
* @throws IOException failed to process the event
*/
public void entriesWritten() throws IOException {}
/**
* The zip file has been updated on disk. The default implementation does nothing.
*
* @throws IOException failed to perform update tasks
*/
public void updated() throws IOException {}
/**
* The zip file has been closed. Note that if {@link ZFile#close()} requires that the zip file be
* updated (because it had in-memory changes), {@link #updated()} will be called before this
* method. The default implementation does nothing.
*/
public void closed() {}
/**
* A new entry has been added to the zip, possibly replacing an entry in there. The default
* implementation does nothing and returns {@code null}.
*
* @param entry the entry that was added
* @param replaced the entry that was replaced, if any
* @return an optional runnable to run when notification of all listeners has ended
*/
@Nullable
public IOExceptionRunnable added(StoredEntry entry, @Nullable StoredEntry replaced) {
return null;
}
/**
* An entry has been removed from the zip. This method is not invoked for entries that have been
* replaced. Those entries are notified using <em>replaced</em> in {@link #added(StoredEntry,
* StoredEntry)}. The default implementation does nothing and returns {@code null}.
*
* @param entry the entry that was deleted
* @return an optional runnable to run when notification of all listeners has ended
*/
@Nullable
public IOExceptionRunnable removed(StoredEntry entry) {
return null;
}
}

View File

@ -0,0 +1,260 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.bytestorage.ByteStorageFactory;
import com.android.tools.build.apkzlib.bytestorage.ChunkBasedByteStorageFactory;
import com.android.tools.build.apkzlib.bytestorage.OverflowToDiskByteStorageFactory;
import com.android.tools.build.apkzlib.bytestorage.TemporaryDirectory;
import com.android.tools.build.apkzlib.zip.compress.DeflateExecutionCompressor;
import com.android.tools.build.apkzlib.zip.utils.ByteTracker;
import com.google.common.base.Supplier;
import java.util.zip.Deflater;
/** Options to create a {@link ZFile}. */
public class ZFileOptions {
/** The storage to use. */
private ByteStorageFactory storageFactory;
/** The compressor to use. */
private Compressor compressor;
/** Should timestamps be zeroed? */
private boolean noTimestamps;
/** The alignment rule to use. */
private AlignmentRule alignmentRule;
/** Should the extra field be used to cover empty space? */
private boolean coverEmptySpaceUsingExtraField;
/** Should files be automatically sorted before update? */
private boolean autoSortFiles;
/**
* Skip expensive validation during {@link ZFile} creation?
*
* <p>During incremental build we are absolutely sure that the zip file is valid, so we do not
* have to spend time verifying different fields (some of these checks are relatively expensive
* and should be skipped if possible for performance)
*/
private boolean skipValidation;
/** Factory creating verification logs to use. */
private Supplier<VerifyLog> verifyLogFactory;
/**
* Whether to always generate the MANIFEST.MF file regardless whether the APK will be signed with
* v1 signing scheme (i.e. jar signing).
*/
private boolean alwaysGenerateJarManifest;
/** Creates a new options object. All options are set to their defaults. */
public ZFileOptions() {
storageFactory =
new ChunkBasedByteStorageFactory(
new OverflowToDiskByteStorageFactory(TemporaryDirectory::newSystemTemporaryDirectory));
compressor = new DeflateExecutionCompressor(Runnable::run, Deflater.DEFAULT_COMPRESSION);
alignmentRule = AlignmentRules.compose();
verifyLogFactory = VerifyLogs::devNull;
// We set this to true because many utilities stream the zip and expect no space between entries
// in the zip file.
coverEmptySpaceUsingExtraField = true;
skipValidation = false;
// True by default for backwards compatibility.
alwaysGenerateJarManifest = true;
}
/**
* Obtains the ZFile's byte storage factory.
*
* @return the factory used to create byte storages used to store data
*/
public ByteStorageFactory getStorageFactory() {
return storageFactory;
}
@Deprecated
public ByteTracker getTracker() {
return new ByteTracker();
}
/**
* Sets the byte storage factory to use.
*
* @param storage the factory to use to create storage for new instances of {@link ZFile} created
* for these options.
*/
public ZFileOptions setStorageFactory(ByteStorageFactory storage) {
this.storageFactory = storage;
return this;
}
/**
* Obtains the compressor to use.
*
* @return the compressor
*/
public Compressor getCompressor() {
return compressor;
}
/**
* Sets the compressor to use.
*
* @param compressor the compressor
*/
public ZFileOptions setCompressor(Compressor compressor) {
this.compressor = compressor;
return this;
}
/**
* Obtains whether timestamps should be zeroed.
*
* @return should timestamps be zeroed?
*/
public boolean getNoTimestamps() {
return noTimestamps;
}
/**
* Sets whether timestamps should be zeroed.
*
* @param noTimestamps should timestamps be zeroed?
*/
public ZFileOptions setNoTimestamps(boolean noTimestamps) {
this.noTimestamps = noTimestamps;
return this;
}
/**
* Obtains the alignment rule.
*
* @return the alignment rule
*/
public AlignmentRule getAlignmentRule() {
return alignmentRule;
}
/**
* Sets the alignment rule.
*
* @param alignmentRule the alignment rule
*/
public ZFileOptions setAlignmentRule(AlignmentRule alignmentRule) {
this.alignmentRule = alignmentRule;
return this;
}
/**
* Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for an
* explanation on using the extra field for covering empty spaces.
*
* @return should the extra field be used to cover empty spaces?
*/
public boolean getCoverEmptySpaceUsingExtraField() {
return coverEmptySpaceUsingExtraField;
}
/**
* Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an
* explanation on using the extra field for covering empty spaces.
*
* @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces?
*/
public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {
this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField;
return this;
}
/**
* Obtains whether files should be automatically sorted before updating the zip file. See {@link
* ZFile} for an explanation on automatic sorting.
*
* @return should the file be automatically sorted?
*/
public boolean getAutoSortFiles() {
return autoSortFiles;
}
/**
* Sets whether files should be automatically sorted before updating the zip file. See {@link
* ZFile} for an explanation on automatic sorting.
*
* @param autoSortFiles should the file be automatically sorted?
*/
public ZFileOptions setAutoSortFiles(boolean autoSortFiles) {
this.autoSortFiles = autoSortFiles;
return this;
}
/**
* Sets the verification log factory.
*
* @param verifyLogFactory verification log factory
*/
public ZFileOptions setVerifyLogFactory(Supplier<VerifyLog> verifyLogFactory) {
this.verifyLogFactory = verifyLogFactory;
return this;
}
/**
* Obtains the verification log factory. By default, the verification log doesn't store anything
* and will always return an empty log.
*
* @return the verification log factory
*/
public Supplier<VerifyLog> getVerifyLogFactory() {
return verifyLogFactory;
}
/**
* Sets whether expensive validation should be skipped during {@link ZFile} creation
*
* @param skipValidation during creation?
*/
public ZFileOptions setSkipValidation(boolean skipValidation) {
this.skipValidation = skipValidation;
return this;
}
/**
* Gets whether expensive validation should be performed during {@link ZFile} creation
*
* @return skip verification during creation?
*/
public boolean getSkipValidation() {
return skipValidation;
}
/**
* Sets whether to always generate the MANIFEST.MF file, regardless whether the APK is signed with
* v1 signing scheme.
*/
public ZFileOptions setAlwaysGenerateJarManifest(boolean alwaysGenerateJarManifest) {
this.alwaysGenerateJarManifest = alwaysGenerateJarManifest;
return this;
}
/** Returns whether the MANIFEST.MF file should always be generated. */
public boolean getAlwaysGenerateJarManifest() {
return alwaysGenerateJarManifest;
}
}

View File

@ -0,0 +1,408 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.utils.CachedSupplier;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Zip64 End of Central Directory record in a zip file.
*/
public class Zip64Eocd {
/**
* Default "version made by" field: upper byte needs to be 0 to set to MS-DOS compatibility. Lower
* byte can be anything, really. We use 0x18 because aapt uses 0x17 :)
*/
private static final int DEFAULT_VERSION_MADE_BY = 0x0018;
/**
* Minimum size that can be stored in the {@link #F_EOCD_SIZE} field of the record.
*/
private static final int MIN_EOCD_SIZE = 44;
/** Field in the record: the record signature, fixed at this value by the specification */
private static final ZipField.F4 F_SIGNATURE =
new ZipField.F4(0, 0x06064b50, "Zip64 EOCD signature");
/**
* Field in the record: the size of the central directory record, not including the first 12
* bytes of data (the signature and this size information). Therefore this variable should be:
*
* <code>size = sizeOfFixedFields + sizeOfVariableData - 12</code>
*
* as specified by the zip specification.
*/
private static final ZipField.F8 F_EOCD_SIZE =
new ZipField.F8(
F_SIGNATURE.endOffset(),
"Zip64 EOCD size",
new ZipFieldInvariantMinValue(MIN_EOCD_SIZE));
/** Field in the record: ID program that made the zip (we don't actually use this). */
private static final ZipField.F2 F_MADE_BY =
new ZipField.F2(F_EOCD_SIZE.endOffset(), "Made by", new ZipFieldInvariantNonNegative());
/**
* Field in the record: Version needed to extract the Zip. We expect this value to be at least
* {@link CentralDirectoryHeaderCompressInfo#VERSION_WITH_ZIP64_EXTENSIONS}. This value also
* determines whether we are using Version 1 or Version 2 of the Zip64 EOCD record.
*/
private static final ZipField.F2 F_VERSION_EXTRACT =
new ZipField.F2(
F_MADE_BY.endOffset(),
"Version to extract",
new ZipFieldInvariantMinValue(
CentralDirectoryHeaderCompressInfo.VERSION_WITH_ZIP64_EXTENSIONS));
/**
* Field in the record: the number of disk where the Zip64 EOCD is located. It must be zero
* as multi-file archives are not supported.
*/
private static final ZipField.F4 F_NUMBER_OF_DISK =
new ZipField.F4(F_VERSION_EXTRACT.endOffset(), 0, "Number of this disk");
/**
* Field in the record: the number of the disk where the central directory resides. This must be
* zero as multi-file archives are not supported.
*/
private static final ZipField.F4 F_DISK_CD_START =
new ZipField.F4(F_NUMBER_OF_DISK.endOffset(), 0, "Disk where CD starts");
/**
* Field in the record: the number of entries in the Central Directory on this disk. Because we do
* not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}
*/
private static final ZipField.F8 F_RECORDS_DISK =
new ZipField.F8(
F_DISK_CD_START.endOffset(),
"Record on disk count",
new ZipFieldInvariantNonNegative());
/** Field in the record: the total number of entries in the Central Directory. */
private static final ZipField.F8 F_RECORDS_TOTAL =
new ZipField.F8(
F_RECORDS_DISK.endOffset(),
"Total records",
new ZipFieldInvariantNonNegative());
/** Field in the record: number of bytes of the Central Directory. */
private static final ZipField.F8 F_CD_SIZE =
new ZipField.F8(
F_RECORDS_TOTAL.endOffset(), "Directory size", new ZipFieldInvariantNonNegative());
/** Field in the record: offset, from the archive start, where the Central Directory starts. */
private static final ZipField.F8 F_CD_OFFSET =
new ZipField.F8(
F_CD_SIZE.endOffset(), "Directory offset", new ZipFieldInvariantNonNegative());
/**
* Field in Version 2 of the record: The compression method used for the Central Directory in the
* given Zip file. Although we do support version 2 of the Zip64 EOCD, we presently do not support
* any compression method, and thus this value must be zero.
*/
private static final ZipField.F2 F_V2_CD_COMPRESSION_METHOD =
new ZipField.F2(
F_CD_OFFSET.endOffset(), 0, "Version 2: Directory Compression method");
/**
* Field in Version 2 of the record: The compressed size of the Central Directory. As Compression
* is not supported for the CD, this value should always be the same as
* {@link #F_V2_CD_UNCOMPRESSED_SIZE}.
*/
private static final ZipField.F8 F_V2_CD_COMPRESSED_SIZE =
new ZipField.F8(
F_V2_CD_COMPRESSION_METHOD.endOffset(),
"Version 2: Directory Compressed Size",
new ZipFieldInvariantNonNegative());
/** Field in Version 2 of the record: The uncompressed size of the Central Directory. */
private static final ZipField.F8 F_V2_CD_UNCOMPRESSED_SIZE =
new ZipField.F8(
F_V2_CD_COMPRESSED_SIZE.endOffset(),
"Version 2: Directory Uncompressed Size",
new ZipFieldInvariantNonNegative());
/**
* Field in Version 2 of the record: The ID for the type of encryption used to encrypt the Central
* directory. Since Central Directory encryption is not supported, this value has to be zero.
*/
private static final ZipField.F2 F_V2_CD_ENCRYPTION_ID =
new ZipField.F2(
F_V2_CD_UNCOMPRESSED_SIZE.endOffset(),
0,
"Version 2: Directory Encryption");
/**
* Field in Version 2 of the record: The length of the encryption key for the encryption of the
* Central Directory given by {@link #F_V2_CD_ENCRYPTION_ID}. Since encryption of the Central
* Directory is not supported, this value has to be zero.
*/
private static final ZipField.F2 F_V2_CD_ENCRYPTION_KEY_LENGTH =
new ZipField.F2(
F_V2_CD_ENCRYPTION_ID.endOffset(),
0,
"Version 2: Directory Encryption key length");
/**
* Field in Version 2 of the record: The flags for the encryption method used on the Central
* Directory. As encryption of the Central Directory is not supported, this value has to be zero.
*/
private static final ZipField.F2 F_V2_CD_ENCRYPTION_FLAGS =
new ZipField.F2(
F_V2_CD_ENCRYPTION_KEY_LENGTH.endOffset(),
0,
"Version 2: Directory Encryption Flags");
/**
* Field in Version 2 of the record: ID of the algorithm used to hash the Central Directory data.
* Hashing of the Central Directory is not supported, so this value has to be zero.
*/
private static final ZipField.F2 F_V2_HASH_ID =
new ZipField.F2(
F_V2_CD_ENCRYPTION_FLAGS.endOffset(),
0,
"Version 2: Hash algorithm ID");
/**
* Field in Version 2 of the record: Length of the data for the hash of the Central Directory.
* Hashing of the Central Directory is not supported, so this value has to be zero.
*/
private static final ZipField.F2 F_V2_HASH_LENGTH =
new ZipField.F2(
F_V2_HASH_ID.endOffset(),
0,
"Version 2: Hash length");
/** The location of the Zip64 size field relative to the start of the Zip64 EOCD. */
public static final int SIZE_OFFSET = F_EOCD_SIZE.offset();
/**
* The difference between the size in the size field and the true size of the Zip64 EOCD. The size
* field in the EOCD does not consider the size field and the identifier field when calculating
* the size of the Zip64 EOCD record.
*/
public static final int TRUE_SIZE_DIFFERENCE = F_EOCD_SIZE.endOffset();
/** Code of the program that made the zip. We actually don't care about this. */
private final long madeBy;
/** Version needed to extract the zip. */
private final long versionToExtract;
/** Number of entries in the Central Directory. */
private final long totalRecords;
/** Offset from the beginning of the archive where the Central Directory is located. */
private final long directoryOffset;
/** Number of bytes of the Central Directory. */
private final long directorySize;
/** The variable extra fields at the end of the Zip64 EOCD (in both Version 1 and 2). */
private final Zip64ExtensibleDataSector extraFields;
/** Supplier of the byte representation of the Zip64 EOCD. */
private final CachedSupplier<byte[]> byteSupplier;
/**
* Creates a Zip64Eocd record from the given information from the central directory record.
*
* @param totalRecords the number of entries in the central directory.
* @param directoryOffset the offset of the central directory from the start of the archive.
* @param directorySize the size (in bytes) of the central directory record.
* @param useVersion2 whether we want to use Version 2 of the Zip64 EOCD.
* @param dataSector the extensible data sector.
*/
Zip64Eocd(
long totalRecords,
long directoryOffset,
long directorySize,
boolean useVersion2,
Zip64ExtensibleDataSector dataSector) {
this.madeBy = DEFAULT_VERSION_MADE_BY;
this.totalRecords = totalRecords;
this.directorySize = directorySize;
this.directoryOffset = directoryOffset;
this.versionToExtract =
useVersion2
? CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION
: CentralDirectoryHeaderCompressInfo.VERSION_WITH_ZIP64_EXTENSIONS;
extraFields = dataSector;
byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
* Creates a Zip64 EOCD from the given byte information. It does verify that the record starts
* with the correct header information.
*
* @param bytes the bytes to be read as a Zip64 EOCD
* @throws IOException the bytes could not be read as a Zip64 EOCD
*/
Zip64Eocd(ByteBuffer bytes) throws IOException {
F_SIGNATURE.verify(bytes);
long eocdSize = F_EOCD_SIZE.read(bytes);
long madeBy = F_MADE_BY.read(bytes);
long versionToExtract = F_VERSION_EXTRACT.read(bytes);
F_NUMBER_OF_DISK.verify(bytes);
F_DISK_CD_START.verify(bytes);
long totalRecords1 = F_RECORDS_DISK.read(bytes);
long totalRecords2 = F_RECORDS_TOTAL.read(bytes);
long directorySize = F_CD_SIZE.read(bytes);
long directoryOffset = F_CD_OFFSET.read(bytes);
long sizeOfFixedFields = F_CD_OFFSET.endOffset();
// sanity checks for Version 1 fields.
if (totalRecords1 != totalRecords2) {
throw new IOException(
"Zip states records split in multiple disks, which is not supported");
}
// read Version 2 fields if necessary
if (versionToExtract
>= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION) {
if (eocdSize < F_V2_HASH_LENGTH.endOffset() - F_EOCD_SIZE.endOffset()) {
throw new IOException(
"Zip states the size of Zip64 EOCD is too small for version 2 format.");
}
F_V2_CD_COMPRESSION_METHOD.verify(bytes);
long compressedSize = F_V2_CD_COMPRESSED_SIZE.read(bytes);
long uncompressedSize = F_V2_CD_UNCOMPRESSED_SIZE.read(bytes);
F_V2_CD_ENCRYPTION_ID.verify(bytes);
F_V2_CD_ENCRYPTION_KEY_LENGTH.verify(bytes);
F_V2_CD_ENCRYPTION_FLAGS.verify(bytes);
F_V2_HASH_ID.verify(bytes);
F_V2_HASH_LENGTH.verify(bytes);
sizeOfFixedFields = F_V2_HASH_LENGTH.endOffset();
// sanity checks for version 2 fields.
if (compressedSize != uncompressedSize) {
throw new IOException(
"Zip states Central Directory Compression is used, which is not supported");
}
directorySize = uncompressedSize;
}
this.madeBy = madeBy;
this.versionToExtract = versionToExtract;
this.totalRecords = totalRecords1;
this.directorySize = directorySize;
this.directoryOffset = directoryOffset;
long extensibleDataSize = eocdSize - (sizeOfFixedFields - F_EOCD_SIZE.endOffset());
if (extensibleDataSize > Integer.MAX_VALUE) {
throw new IOException("Extensible data of size: " + extensibleDataSize + "not supported");
}
byte[] rawData = new byte[Ints.checkedCast(extensibleDataSize)];
bytes.get(rawData);
extraFields = new Zip64ExtensibleDataSector(rawData);
byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
* The size of the fixed field in the Zip64 EOCD. This vaue may be different if we are using a
* version 1 or version 2 record.
*
* @return the size of the fixed fields.
*/
private int sizeOfFixedFields() {
return versionToExtract
>= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION
? F_V2_HASH_LENGTH.endOffset()
: F_CD_OFFSET.endOffset();
}
/**
* Gets the size (in bytes) of the Zip64 EOCD record.
*
* @return the size of the record.
*/
public int size() {
return sizeOfFixedFields() + extraFields.size();
}
public long getTotalRecords() {
return totalRecords;
}
public long getDirectorySize() {
return directorySize;
}
public long getDirectoryOffset() {
return directoryOffset;
}
public Zip64ExtensibleDataSector getExtraFields() {
return extraFields;
}
public long getVersionToExtract() { return versionToExtract; }
/**
* Gets the byte representation of The Zip64 EOCD record.
*
* @return the bytes of the EOCD.
*/
public byte[] toBytes() {
return byteSupplier.get();
}
private byte[] computeByteRepresentation() {
int size = size();
ByteBuffer out = ByteBuffer.allocate(size);
try {
F_SIGNATURE.write(out);
F_EOCD_SIZE.write(out, size - F_EOCD_SIZE.endOffset());
F_MADE_BY.write(out, madeBy);
F_VERSION_EXTRACT.write(out, versionToExtract);
F_NUMBER_OF_DISK.write(out);
F_DISK_CD_START.write(out);
F_RECORDS_DISK.write(out, totalRecords);
F_RECORDS_TOTAL.write(out, totalRecords);
F_CD_SIZE.write(out, directorySize);
F_CD_OFFSET.write(out, directoryOffset);
// write version 2 fields if necessary.
if (versionToExtract
>= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION) {
F_V2_CD_COMPRESSION_METHOD.write(out);
F_V2_CD_COMPRESSED_SIZE.write(out, directorySize);
F_V2_CD_UNCOMPRESSED_SIZE.write(out, directorySize);
F_V2_CD_ENCRYPTION_ID.write(out);
F_V2_CD_ENCRYPTION_KEY_LENGTH.write(out);
F_V2_CD_ENCRYPTION_FLAGS.write(out);
F_V2_HASH_ID.write(out);
F_V2_HASH_LENGTH.write(out);
}
extraFields.write(out);
return out.array();
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.utils.CachedSupplier;
import com.android.tools.build.apkzlib.utils.IOExceptionWrapper;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
/**
* Zip64 End of Central Directory Locator. Used to locate the Zip64 EOCD record in
* the Zip64 format. This will be located right above the standard EOCD record, if it exists.
*/
class Zip64EocdLocator {
/** Field in the record: the record signature, fixed at this value by the specification. */
private static final ZipField.F4 F_SIGNATURE =
new ZipField.F4(0, 0x07064b50, "Zip64 EOCD Locator signature");
/**
* Field in the record: the number of the disk where the Zip64 EOCD is located. This has to be
* zero because multi-file archives are not supported.
*/
private static final ZipField.F4 F_NUMBER_OF_DISK =
new ZipField.F4(F_SIGNATURE.endOffset(), 0, "Number of disk with Zip64 EOCD");
/**
* Field in the record: the location of the zip64 EOCD record on the disk specified by
* {@link #F_NUMBER_OF_DISK}.
*/
private static final ZipField.F8 F_Z64_EOCD_OFFSET =
new ZipField.F8(
F_NUMBER_OF_DISK.endOffset(),
"Offset of Zip64 EOCD",
new ZipFieldInvariantNonNegative());
/**
* Field in the record: the total number of disks in the archive. This has to be zero because
* multi-file archives are not supported.
*/
private static final ZipField.F4 F_TOTAL_NUMBER_OF_DISKS =
new ZipField.F4(
F_Z64_EOCD_OFFSET.endOffset(), 0,"Total number of disks");
public static final int LOCATOR_SIZE = F_TOTAL_NUMBER_OF_DISKS.endOffset();
/**
* Offset from the beginning of the archive to where the Zip64 End of Central Directory record
* is located.
*/
private final long z64EocdOffset;
/** Supplier of the byte representation of the zip64 Eocd Locator. */
private final CachedSupplier<byte[]> byteSupplier;
/**
* Creates a new Zip64 EOCD Locator, reading it from a byte source. This method will parse the
* byte source and obtain the EOCD Locator. It will check that the byte source starts with the
* EOCD Locator signature.
*
* @param bytes the byte buffer with the Locator data; when this method finishes, the byte buffer
* will have its position moved to the end of the Locator (the beginning of the standard EOCD)
* @throws IOException failed to read information or the EOCD data is corrupt or invalid.
*/
Zip64EocdLocator(ByteBuffer bytes) throws IOException {
F_SIGNATURE.verify(bytes);
F_NUMBER_OF_DISK.verify(bytes);
long z64EocdOffset = F_Z64_EOCD_OFFSET.read(bytes);
F_TOTAL_NUMBER_OF_DISKS.verify(bytes);
Verify.verify(z64EocdOffset >= 0);
this.z64EocdOffset = z64EocdOffset;
byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
* Creates a new Zip64 EOCD Locator. This is used when generating an EOCD Locator for a
* Zip64 EOCD that has been generated.
*
* @param z64EocdOffset offset position of the Zip64 EOCD from the beginning of the archive
*/
Zip64EocdLocator(long z64EocdOffset) {
Preconditions.checkArgument(z64EocdOffset >= 0, "z64EocdOffset < 0");
this.z64EocdOffset = z64EocdOffset;
byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
}
/**
* Obtains the offset from the beginning of the archive to where the Zip64 EOCD is located.
*
* @return the Zip64 EOCD offset.
*/
long getZ64EocdOffset() {
return z64EocdOffset;
}
/**
* Obtains the size of the Zip64 EOCD Locator
*
* @return the size, in bytes, of the EOCD Locator.<em> i.e. </em> 20.
*/
long getSize() {
return F_TOTAL_NUMBER_OF_DISKS.endOffset();
}
/**
* Generates the EOCD Locator data.
*
* @return a byte representation of the EOCD Locator that has exactly {@link #getSize()} bytes
* @throws IOException failed to generate the EOCD data.
*/
byte[] toBytes() throws IOException {
return byteSupplier.get();
}
/**
* Computes the byte representation of the EOCD Locator.
*
* @return a byte representation of the Zip64 EOCD Locator that has exactly {@link #getSize()}
* bytes
* @throws UncheckedIOException failed to generate the EOCD Locator data
*/
private byte[] computeByteRepresentation() {
ByteBuffer out = ByteBuffer.allocate(F_TOTAL_NUMBER_OF_DISKS.endOffset());
try {
F_SIGNATURE.write(out);
F_NUMBER_OF_DISK.write(out);
F_Z64_EOCD_OFFSET.write(out, z64EocdOffset);
F_TOTAL_NUMBER_OF_DISKS.write(out);
return out.array();
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
}
}

View File

@ -0,0 +1,219 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* Contains the special purpose data for the Zip64 EOCD record.
*
* <p>According to the zip specification, the Zip64 EOCD is composed of a sequence of zero or more
* Special Purpose Data fields. This class provides a way to access, parse, and modify that
* information.
*
* <p>Each Special Purpose Data is represented by an instance of {@link Z64SpecialPurposeData} and
* contains a header ID and data.
*/
public class Zip64ExtensibleDataSector {
/**
* The extensible data sector's raw data, if it is known. Either this variable or {@link #fields}
* must be non-{@code null}.
*/
@Nullable private final byte[] rawData;
/**
* The list of fields in the data sector. Will be populated if the data sector is created based on
* a list of special purpose data; will also be populated after parsing if the Data Sector is
* created based on the raw bytes.
*/
@Nullable private ImmutableList<Z64SpecialPurposeData> fields;
/**
* Creates a Zip64 Extensible Data Sector based on existing raw data.
*
* @param rawData the raw data; will only be parsed if needed.
*/
public Zip64ExtensibleDataSector(byte[] rawData) {
this.rawData = rawData;
fields = null;
}
/**
* Creates an Extensible Data Sector with no special purpose data.
*/
public Zip64ExtensibleDataSector() {
rawData = null;
fields = ImmutableList.of();
}
/**
* Creates a Zip64 Extensible Data with the given Special purpose data.
*
* @param fields all special purpose data.
*/
public Zip64ExtensibleDataSector(ImmutableList<Z64SpecialPurposeData> fields) {
rawData = null;
this.fields = fields;
}
int size() {
if (rawData != null) {
return rawData.length;
} else {
Preconditions.checkNotNull(fields);
int sumSizes = 0;
for (Z64SpecialPurposeData data : fields){
sumSizes += data.size();
}
return sumSizes;
}
}
void write(ByteBuffer out) throws IOException {
if (rawData != null) {
out.put(rawData);
} else {
Preconditions.checkNotNull(fields);
for (Z64SpecialPurposeData data : fields) {
data.write(out);
}
}
}
public ImmutableList<Z64SpecialPurposeData> getFields() throws IOException {
if (fields == null) {
parseData();
}
Preconditions.checkNotNull(fields);
return fields;
}
private void parseData() throws IOException {
Preconditions.checkNotNull(rawData);
Preconditions.checkState(fields == null);
List<Z64SpecialPurposeData> fields = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.wrap(rawData);
while (buffer.remaining() > 0) {
int headerId = LittleEndianUtils.readUnsigned2Le(buffer);
long dataSize = LittleEndianUtils.readUnsigned4Le(buffer);
byte[] data = new byte[Ints.checkedCast(dataSize)];
if (dataSize < 0) {
throw new IOException(
"Invalid data size for special purpose data with header ID "
+ headerId
+ ": "
+ dataSize);
}
buffer.get(data);
SpecialPurposeDataFactory factory = RawSpecialPurposeData::new;
Z64SpecialPurposeData spd = factory.make(headerId, data);
fields.add(spd);
}
this.fields = ImmutableList.copyOf(fields);
}
public interface Z64SpecialPurposeData {
/** Length of header id and the size length fields that comes before the data */
int PREFIX_LENGTH = 6;
/**
* Obtains the Special purpose data's header id.
*
* @return the data's header id.
*/
int getHeaderId();
/**
* Obtains the size of the data in this special purpose data.
*
* @return the number of bytes needed to write the data.
*/
int size();
/**
* Writes the special purpose data to the buffer.
*
* @param out the buffer where to write the data to; exactly {@link #size()} bytes will be
* written.
* @throws IOException failed to write special purpose data.
*/
void write(ByteBuffer out) throws IOException;
}
public interface SpecialPurposeDataFactory {
/**
* Creates a new special purpose data.
*
* @param headerId the header ID
* @param data the data in the special purpose data
* @return the created special purpose data.
* @throws IOException failed to create the special purpose data from the data given.
*/
Z64SpecialPurposeData make(int headerId, byte[] data) throws IOException;
}
/**
* Special Purpose Data containing raw data: this class represents a general "special purpose
* data" containing an array of bytes as data.
*/
public static class RawSpecialPurposeData implements Z64SpecialPurposeData {
/** Header ID. */
private final int headerId;
/** Data in the segment */
private final byte[] data;
RawSpecialPurposeData(int headerId, byte[] data) {
this.headerId = headerId;
this.data = data;
}
@Override
public int getHeaderId() {
return headerId;
}
@Override
public int size() {
return PREFIX_LENGTH + data.length;
}
@Override
public void write(ByteBuffer out) throws IOException {
LittleEndianUtils.writeUnsigned2Le(out, headerId);
LittleEndianUtils.writeUnsigned4Le(out, data.length);
out.put(data);
}
}
}

View File

@ -0,0 +1,396 @@
/*
* 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.tools.build.apkzlib.zip;
import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The ZipField class represents a field in a record in a zip file. Zip files are made with records
* that have fields. This class makes it easy to read, write and verify field values.
*
* <p>There are three main types of fields: 2-byte, 4-byte, and 8-byte fields. We represent each
* one as a subclass of {@code ZipField}, {@code F2} for the 2-byte field, {@code F4} for the 4-byte
* field and {@code F8} for the 8-byte field. Because Java's {@code int} data type is guaranteed to
* be 4-byte, all methods use Java's native {@link int} as data type.
*
* <p>The {@code F8} subclass is to support the 8-byte fields in the Zip64 specification. Because
* Java's 8-byte {@code long} does not support unsigned types, which reduces the support to 8-byte
* numbers of the form 2^63-1 or less. As {@code F8} fields refer to file sizes, this should be
* sufficient.
*
* <p>For each field we can either read, write or verify. Verification is used for fields whose
* value we know. Some fields, <em>e.g.</em> signature fields, have fixed value. Other fields have
* variable values, but in some situations we know which value they have. For example, the last
* modification time of a file's local header will have to match the value of the file's
* modification time as stored in the central directory.
*
* <p>Because records are compact, <em>i.e.</em> fields are stored sequentially with no empty
* spaces, fields are generally created in the sequence they exist and the end offset of a field is
* used as the offset of the next one. The end of a field can be obtained by invoking {@link
* #endOffset()}. This allows creating fields in sequence without doing offset computation:
*
* <pre>
* ZipField.F2 firstField = new ZipField.F2(0, "First field");
* ZipField.F4 secondField = new ZipField(firstField.endOffset(), "Second field");
* ZipField.F8 thirdField = new ZipField(secondField.endOffset(), "Third field");
* </pre>
*/
abstract class ZipField {
/** Field name. Used for providing (more) useful error messages. */
private final String name;
/** Offset of the file in the record. */
protected final int offset;
/** Size of the field. Only 2, 4, or 8 allowed. */
private final int size;
/** If a fixed value exists for the field, then this attribute will contain that value. */
@Nullable private final Long expected;
/** All invariants that this field must verify. */
private final Set<ZipFieldInvariant> invariants;
/**
* Creates a new field that does not contain a fixed value.
*
* @param offset the field's offset in the record
* @param size the field size
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
ZipField(int offset, int size, String name, ZipFieldInvariant... invariants) {
Preconditions.checkArgument(offset >= 0, "offset >= 0");
Preconditions.checkArgument(
size == 2 || size == 4 || size == 8,
"size != 2 && size != 4 && size != 8");
this.name = name;
this.offset = offset;
this.size = size;
expected = null;
this.invariants = Sets.newHashSet(invariants);
}
/**
* Creates a new field that contains a fixed value.
*
* @param offset the field's offset in the record
* @param size the field size
* @param expected the expected field value
* @param name the field's name
*/
ZipField(int offset, int size, long expected, String name) {
Preconditions.checkArgument(offset >= 0, "offset >= 0");
Preconditions.checkArgument(
size == 2 || size == 4 || size == 8,
"size != 2 && size != 4 && size != 8");
this.name = name;
this.offset = offset;
this.size = size;
this.expected = expected;
invariants = Sets.newHashSet();
}
/**
* Checks whether a value verifies the field's invariants. Nothing happens if the value verifies
* the invariants.
*
* @param value the value
* @throws IOException the invariants are not verified
*/
private void checkVerifiesInvariants(long value) throws IOException {
for (ZipFieldInvariant invariant : invariants) {
if (!invariant.isValid(value)) {
throw new IOException(
"Value "
+ value
+ " of field "
+ name
+ " is invalid "
+ "(fails '"
+ invariant.getName()
+ "').");
}
}
}
/**
* Advances the position in the provided byte buffer by the size of this field.
*
* @param bytes the byte buffer; at the end of the method its position will be greater by the size
* of this field
* @throws IOException failed to advance the buffer
*/
void skip(ByteBuffer bytes) throws IOException {
if (bytes.remaining() < size) {
throw new IOException(
"Cannot skip field "
+ name
+ " because only "
+ bytes.remaining()
+ " remain in the buffer.");
}
bytes.position(bytes.position() + size);
}
/**
* Reads a field value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer will
* be positioned at the first byte after the field
* @return the value of the field
* @throws IOException failed to read the field
*/
long read(ByteBuffer bytes) throws IOException {
if (bytes.remaining() < size) {
throw new IOException(
"Cannot skip field "
+ name
+ " because only "
+ bytes.remaining()
+ " remain in the buffer.");
}
bytes.order(ByteOrder.LITTLE_ENDIAN);
long r;
if (size == 2) {
r = LittleEndianUtils.readUnsigned2Le(bytes);
} else if (size == 4) {
r = LittleEndianUtils.readUnsigned4Le(bytes);
} else {
r = LittleEndianUtils.readUnsigned8Le(bytes);
}
checkVerifiesInvariants(r);
return r;
}
/**
* Verifies that the field at the current buffer position has the expected value. The field must
* have been created with the constructor that defines the expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer will
* be positioned at the first byte after the field
* @throws IOException failed to read the field or the field does not have the expected value
*/
void verify(ByteBuffer bytes) throws IOException {
verify(bytes, null);
}
/**
* Verifies that the field at the current buffer position has the expected value. The field must
* have been created with the constructor that defines the expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer will
* be positioned at the first byte after the field
* @param verifyLog if non-{@code null}, will log the verification error
* @throws IOException failed to read the data or the field does not have the expected value; only
* thrown if {@code verifyLog} is {@code null}
*/
void verify(ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException {
Preconditions.checkState(expected != null, "expected == null");
verify(bytes, expected, verifyLog);
}
/**
* Verifies that the field has an expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer will
* be positioned at the first byte after the field
* @param expected the value we expect the field to have; if this field has invariants, the value
* must verify them
* @throws IOException failed to read the data or the field does not have the expected value
*/
void verify(ByteBuffer bytes, long expected) throws IOException {
verify(bytes, expected, null);
}
/**
* Verifies that the field has an expected value.
*
* @param bytes the byte buffer with the record data; after this method finishes, the buffer will
* be positioned at the first byte after the field
* @param expected the value we expect the field to have; if this field has invariants, the value
* must verify them
* @param verifyLog if non-{@code null}, will log the verification error
* @throws IOException failed to read the data or the field does not have the expected value; only
* thrown if {@code verifyLog} is {@code null}
*/
void verify(ByteBuffer bytes, long expected, @Nullable VerifyLog verifyLog) throws IOException {
checkVerifiesInvariants(expected);
long r = read(bytes);
if (r != expected) {
String error =
String.format(
"Incorrect value for field '%s': value is %s but %s expected.", name, r, expected);
if (verifyLog == null) {
throw new IOException(error);
} else {
verifyLog.log(error);
}
}
}
/**
* Writes the value of the field.
*
* @param output where to write the field; the field will be written at the current position of
* the buffer
* @param value the value to write
* @throws IOException failed to write the value in the stream
*/
void write(ByteBuffer output, long value) throws IOException {
checkVerifiesInvariants(value);
Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
if (size == 2) {
Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value);
LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value));
} else if (size == 4) {
Preconditions.checkArgument(
value <= 0x00000000ffffffffL, "value (%s) > 0x00000000ffffffffL", value);
LittleEndianUtils.writeUnsigned4Le(output, value);
} else {
Verify.verify(size == 8);
LittleEndianUtils.writeUnsigned8Le(output, value);
}
}
/**
* Writes the value of the field. The field must have an expected value set in the constructor.
*
* @param output where to write the field; the field will be written at the current position of
* the buffer
* @throws IOException failed to write the value in the stream
*/
void write(ByteBuffer output) throws IOException {
Preconditions.checkState(expected != null, "expected == null");
write(output, expected);
}
/**
* Obtains the offset at which the field starts.
*
* @return the start offset
*/
int offset() {
return offset;
}
/**
* Obtains the offset at which the field ends. This is the exact offset at which the next field
* starts.
*
* @return the end offset
*/
int endOffset() {
return offset + size;
}
/** Concrete implementation of {@link ZipField} that represents a 2-byte field. */
static class F2 extends ZipField {
/**
* Creates a new field.
*
* @param offset the field's offset in the record
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
F2(int offset, String name, ZipFieldInvariant... invariants) {
super(offset, 2, name, invariants);
}
/**
* Creates a new field that contains a fixed value.
*
* @param offset the field's offset in the record
* @param expected the expected field value
* @param name the field's name
*/
F2(int offset, long expected, String name) {
super(offset, 2, expected, name);
}
}
/** Concrete implementation of {@link ZipField} that represents a 4-byte field. */
static class F4 extends ZipField {
/**
* Creates a new field.
*
* @param offset the field's offset in the record
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
F4(int offset, String name, ZipFieldInvariant... invariants) {
super(offset, 4, name, invariants);
}
/**
* Creates a new field that contains a fixed value.
*
* @param offset the field's offset in the record
* @param expected the expected field value
* @param name the field's name
*/
F4(int offset, long expected, String name) {
super(offset, 4, expected, name);
}
}
/** Concrete implementation of {@link ZipField} that represents a 8-byte field. */
static class F8 extends ZipField {
/**
* Creates a new field
*
* @param offset offset the field's offset in the record
* @param name the field's name
* @param invariants the invariants that must be verified by the field
*/
F8(int offset, String name, ZipFieldInvariant... invariants) {
super(offset, 8, name, invariants);
}
/**
* Creates a new field that contains a fixed value.
*
* @param offset the field's offset in the record
* @param expected the expected field value
* @param name the field's name
*/
F8(int offset, long expected, String name) {
super(offset, 8, expected, name);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.tools.build.apkzlib.zip;
/**
* A field rule defines an invariant (<em>i.e.</em>, a constraint) that has to be verified by a
* field value.
*/
interface ZipFieldInvariant {
/**
* Evalutes the invariant against a value.
*
* @param value the value to check the invariant
* @return is the invariant valid?
*/
boolean isValid(long value);
/**
* Obtains the name of the invariant. Used for information purposes.
*
* @return the name of the invariant
*/
String getName();
}

View File

@ -0,0 +1,43 @@
/*
* 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.tools.build.apkzlib.zip;
/** Invariant checking a zip field does not exceed a threshold. */
class ZipFieldInvariantMaxValue implements ZipFieldInvariant {
/** The maximum value allowed. */
private final long max;
/**
* Creates a new invariant.
*
* @param max the maximum value allowed for the field
*/
ZipFieldInvariantMaxValue(long max) {
this.max = max;
}
@Override
public boolean isValid(long value) {
return value <= max;
}
@Override
public String getName() {
return "Maximum value " + max;
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2018 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.tools.build.apkzlib.zip;
/** Invariant checking a zip field doesn't go below a given value.*/
class ZipFieldInvariantMinValue implements ZipFieldInvariant {
/** The minimum value allowed. */
private final long min;
/**
* Creates a new invariant.
*
* @param min the minimum value allowed for the field
*/
ZipFieldInvariantMinValue(long min) {
this.min = min;
}
@Override
public boolean isValid(long value) {
return value >= min;
}
@Override
public String getName() {
return "Min value " + min;
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.tools.build.apkzlib.zip;
/** Invariant that verifies a field's value is not negative. */
class ZipFieldInvariantNonNegative implements ZipFieldInvariant {
@Override
public boolean isValid(long value) {
return value >= 0;
}
@Override
public String getName() {
return "Is positive";
}
}

View File

@ -0,0 +1,29 @@
/*
* 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.tools.build.apkzlib.zip;
/** The {@code ZipFileState} enumeration holds the state of a {@link ZFile}. */
enum ZipFileState {
/** Zip file is closed. */
CLOSED,
/** File file is open in read-only mode. */
OPEN_RO,
/** File file is open in read-write mode. */
OPEN_RW
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip.compress;
import com.android.tools.build.apkzlib.bytestorage.ByteStorage;
import com.android.tools.build.apkzlib.zip.CompressionResult;
import com.android.tools.build.apkzlib.zip.utils.ByteTracker;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.base.Preconditions;
import java.util.concurrent.Executor;
import java.util.zip.Deflater;
/**
* Compressor that tries both the best and default compression algorithms and picks the default
* unless the best is at least a given percentage smaller.
*/
public class BestAndDefaultDeflateExecutorCompressor extends ExecutorCompressor {
/** Deflater using the default compression level. */
private final DeflateExecutionCompressor defaultDeflater;
/** Deflater using the best compression level. */
private final DeflateExecutionCompressor bestDeflater;
/**
* Minimum best compression size / default compression size ratio needed to pick the default
* compression size.
*/
private final double minRatio;
/**
* Creates a new compressor.
*
* @param executor the executor used to perform compression activities.
* @param minRatio the minimum best compression size / default compression size needed to pick the
* default compression size; if {@code 0.0} then the default compression is always picked, if
* {@code 1.0} then the best compression is always picked unless it produces the exact same
* size as the default compression.
*/
public BestAndDefaultDeflateExecutorCompressor(Executor executor, double minRatio) {
super(executor);
Preconditions.checkArgument(minRatio >= 0.0, "minRatio < 0.0");
Preconditions.checkArgument(minRatio <= 1.0, "minRatio > 1.0");
defaultDeflater = new DeflateExecutionCompressor(executor, Deflater.DEFAULT_COMPRESSION);
bestDeflater = new DeflateExecutionCompressor(executor, Deflater.BEST_COMPRESSION);
this.minRatio = minRatio;
}
@Deprecated
public BestAndDefaultDeflateExecutorCompressor(
Executor executor, ByteTracker tracker, double minRatio) {
this(executor, minRatio);
}
@Override
protected CompressionResult immediateCompress(CloseableByteSource source, ByteStorage storage)
throws Exception {
CompressionResult defaultResult = defaultDeflater.immediateCompress(source, storage);
CompressionResult bestResult = bestDeflater.immediateCompress(source, storage);
double sizeRatio = bestResult.getSize() / (double) defaultResult.getSize();
if (sizeRatio >= minRatio) {
return defaultResult;
} else {
return bestResult;
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip.compress;
import com.android.tools.build.apkzlib.bytestorage.ByteStorage;
import com.android.tools.build.apkzlib.bytestorage.CloseableByteSourceFromOutputStreamBuilder;
import com.android.tools.build.apkzlib.zip.CompressionMethod;
import com.android.tools.build.apkzlib.zip.CompressionResult;
import com.android.tools.build.apkzlib.zip.utils.ByteTracker;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.io.ByteStreams;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
/** Compressor that uses deflate with an executor. */
public class DeflateExecutionCompressor extends ExecutorCompressor {
/** Deflate compression level. */
private final int level;
/**
* Creates a new compressor.
*
* @param executor the executor to run deflation tasks
* @param level the compression level
*/
public DeflateExecutionCompressor(Executor executor, int level) {
super(executor);
this.level = level;
}
@Deprecated
public DeflateExecutionCompressor(Executor executor, ByteTracker tracker, int level) {
this(executor, level);
}
@Override
protected CompressionResult immediateCompress(CloseableByteSource source, ByteStorage storage)
throws Exception {
Deflater deflater = new Deflater(level, true);
CloseableByteSourceFromOutputStreamBuilder resultBuilder = storage.makeBuilder();
try (InputStream inputStream = source.openBufferedStream();
DeflaterOutputStream dos = new DeflaterOutputStream(resultBuilder, deflater)) {
ByteStreams.copy(inputStream, dos);
}
CloseableByteSource result = resultBuilder.build();
if (result.size() >= source.size()) {
result.close();
return new CompressionResult(source, CompressionMethod.STORE, source.size());
} else {
return new CompressionResult(result, CompressionMethod.DEFLATE, result.size());
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip.compress;
import com.android.tools.build.apkzlib.bytestorage.ByteStorage;
import com.android.tools.build.apkzlib.zip.CompressionResult;
import com.android.tools.build.apkzlib.zip.Compressor;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.util.concurrent.Executor;
/**
* A synchronous compressor is a compressor that computes the result of compression immediately and
* never returns an uncomputed future object.
*/
public abstract class ExecutorCompressor implements Compressor {
/** The executor that does the work. */
private final Executor executor;
/**
* Compressor that delegates execution into the given executor.
*
* @param executor the executor that will do the compress
*/
public ExecutorCompressor(Executor executor) {
this.executor = executor;
}
@Override
public ListenableFuture<CompressionResult> compress(
CloseableByteSource source, ByteStorage storage) {
final SettableFuture<CompressionResult> future = SettableFuture.create();
executor.execute(
() -> {
try {
future.set(immediateCompress(source, storage));
} catch (Throwable e) {
future.setException(e);
}
});
return future;
}
/**
* Immediately compresses a source.
*
* @param source the source to compress
* @param storage a byte storage where the compressor can obtain data sources from
* @return the result of compression
* @throws Exception failed to compress
*/
protected abstract CompressionResult immediateCompress(
CloseableByteSource source, ByteStorage storage) throws Exception;
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2017 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.tools.build.apkzlib.zip.compress;
import java.io.IOException;
/** Exception raised by ZFile when encountering unsupported Zip64 format jar files. */
public class Zip64NotSupportedException extends IOException {
public Zip64NotSupportedException(String message) {
super(message);
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (C) 2017 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.
*/
/** Compressors to use with the {@code zip} package. */
package com.android.tools.build.apkzlib.zip.compress;

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip.utils;
/**
* Keeps track of used bytes allowing gauging memory usage.
*
* @deprecated will be removed shortly.
*/
@Deprecated
public class ByteTracker {
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip.utils;
import com.google.common.io.ByteSource;
import java.io.Closeable;
import java.io.IOException;
/**
* Byte source that can be closed. Closing a byte source allows releasing any resources associated
* with it. This should not be confused with closing streams. For example, {@link ByteTracker} uses
* {@code CloseableByteSources} to know when the data associated with the byte source can be
* released.
*/
public abstract class CloseableByteSource extends ByteSource implements Closeable {
/** Has the source been closed? */
private boolean closed;
/** Creates a new byte source. */
public CloseableByteSource() {
closed = false;
}
@Override
public final synchronized void close() throws IOException {
if (closed) {
return;
}
try {
innerClose();
} finally {
closed = true;
}
}
/**
* Closes the by source. This method is only invoked once, even if {@link #close()} is called
* multiple times.
*
* @throws IOException failed to close
*/
protected abstract void innerClose() throws IOException;
}

View File

@ -0,0 +1,159 @@
/*
* Copyright (C) 2016 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.tools.build.apkzlib.zip.utils;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.io.ByteProcessor;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import javax.annotation.Nullable;
/** Closeable byte source that delegates to another byte source. */
public class CloseableDelegateByteSource extends CloseableByteSource {
/** The byte source we delegate all operations to. {@code null} if disposed. */
@Nullable private ByteSource inner;
/**
* Size of the byte source. This is the same as {@code inner.size()} (when {@code inner} is not
* {@code null}), but we keep it separate to avoid calling {@code inner.size()} because it might
* throw {@code IOException}.
*/
private final long mSize;
/**
* Creates a new byte source.
*
* @param inner the inner byte source
* @param size the size of the source
*/
public CloseableDelegateByteSource(ByteSource inner, long size) {
this.inner = inner;
mSize = size;
}
/**
* Obtains the inner byte source. Will throw an exception if the inner by byte source has been
* disposed of.
*
* @return the inner byte source
*/
private synchronized ByteSource get() {
if (inner == null) {
throw new ByteSourceDisposedException();
}
return inner;
}
/** Mark the byte source as disposed. */
@Override
protected synchronized void innerClose() throws IOException {
if (inner == null) {
return;
}
inner = null;
}
/**
* Obtains the size of this byte source. Equivalent to {@link #size()} but not throwing {@code
* IOException}.
*
* @return the size of the byte source
*/
public long sizeNoException() {
return mSize;
}
@Override
public CharSource asCharSource(Charset charset) {
return get().asCharSource(charset);
}
@Override
public InputStream openBufferedStream() throws IOException {
return get().openBufferedStream();
}
@Override
public ByteSource slice(long offset, long length) {
return get().slice(offset, length);
}
@Override
public boolean isEmpty() throws IOException {
return get().isEmpty();
}
@Override
public long size() throws IOException {
return get().size();
}
@Override
public long copyTo(OutputStream output) throws IOException {
return get().copyTo(output);
}
@Override
public long copyTo(ByteSink sink) throws IOException {
return get().copyTo(sink);
}
@Override
public byte[] read() throws IOException {
return get().read();
}
@Override
public <T> T read(ByteProcessor<T> processor) throws IOException {
return get().read(processor);
}
@Override
public HashCode hash(HashFunction hashFunction) throws IOException {
return get().hash(hashFunction);
}
@Override
public boolean contentEquals(ByteSource other) throws IOException {
return get().contentEquals(other);
}
@Override
public InputStream openStream() throws IOException {
return get().openStream();
}
/** Exception thrown when trying to use a byte source that has been disposed. */
private static class ByteSourceDisposedException extends RuntimeException {
/** Creates a new exception. */
private ByteSourceDisposedException() {
super(
"Byte source was created by a ByteTracker and is now disposed. If you see "
+ "this message, then there is a bug.");
}
}
}

View File

@ -0,0 +1,157 @@
/*
* 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.tools.build.apkzlib.zip.utils;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Utilities to read and write 16, 32, and 64 bit integers with support for little-endian encoding,
* as used in zip files. Zip files actually use unsigned data types. We use Java's native (signed)
* data types but will use long (64 bit) to ensure we can fit the whole range for the 16 and 32
* bit fields.
*/
public class LittleEndianUtils {
/** Utility class, no constructor. */
private LittleEndianUtils() {}
/**
* Reads 8 bytes in little-endian format and converts them into a 64-bit value.
*
* @param bytes from where should the bytes be read; the first 8 bytes of the source will be read.
* @return the 64-bit value
* @throws IOException failed to read the value.
*/
public static long readUnsigned8Le(ByteBuffer bytes) throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
if (bytes.remaining() < 8) {
throw new EOFException(
"Not enough data: 8 bytes expected, " + bytes.remaining() + " available.");
}
ByteOrder order = bytes.order();
bytes.order(ByteOrder.LITTLE_ENDIAN);
long r = bytes.getLong();
bytes.order(order);
return r;
}
/**
* Reads 4 bytes in little-endian format and converts them into a 32-bit value.
*
* @param bytes from where should the bytes be read; the first 4 bytes of the source will be read
* @return the 32-bit value
* @throws IOException failed to read the value
*/
public static long readUnsigned4Le(ByteBuffer bytes) throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
if (bytes.remaining() < 4) {
throw new EOFException(
"Not enough data: 4 bytes expected, " + bytes.remaining() + " available.");
}
byte b0 = bytes.get();
byte b1 = bytes.get();
byte b2 = bytes.get();
byte b3 = bytes.get();
long r = (b0 & 0xff) | ((b1 & 0xff) << 8) | ((b2 & 0xff) << 16) | ((b3 & 0xffL) << 24);
Verify.verify(r >= 0);
Verify.verify(r <= 0x00000000ffffffffL);
return r;
}
/**
* Reads 2 bytes in little-endian format and converts them into a 16-bit value.
*
* @param bytes from where should the bytes be read; the first 2 bytes of the source will be read
* @return the 16-bit value
* @throws IOException failed to read the value
*/
public static int readUnsigned2Le(ByteBuffer bytes) throws IOException {
Preconditions.checkNotNull(bytes, "bytes == null");
if (bytes.remaining() < 2) {
throw new EOFException(
"Not enough data: 2 bytes expected, " + bytes.remaining() + " available.");
}
byte b0 = bytes.get();
byte b1 = bytes.get();
int r = (b0 & 0xff) | ((b1 & 0xff) << 8);
Verify.verify(r >= 0);
Verify.verify(r <= 0x0000ffff);
return r;
}
/**
* Writes 8 bytes in little-endian format, converting them from a <em> signed </em> 64-bit value.
*
* @param output the output stream where the bytes will be written.
* @param value the 64-bit value to convert.
* @throws IOException failed to write the value data.
*/
public static void writeUnsigned8Le(ByteBuffer output, long value) throws IOException {
Preconditions.checkNotNull(output, "output == null");
ByteOrder order = output.order();
output.order(ByteOrder.LITTLE_ENDIAN);
output.putLong(value);
output.order(order);
}
/**
* Writes 4 bytes in little-endian format, converting them from a 32-bit value.
*
* @param output the output stream where the bytes will be written
* @param value the 32-bit value to convert
* @throws IOException failed to write the value data
*/
public static void writeUnsigned4Le(ByteBuffer output, long value) throws IOException {
Preconditions.checkNotNull(output, "output == null");
Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
Preconditions.checkArgument(
value <= 0x00000000ffffffffL, "value (%s) > 0x00000000ffffffffL", value);
output.put((byte) (value & 0xff));
output.put((byte) ((value >> 8) & 0xff));
output.put((byte) ((value >> 16) & 0xff));
output.put((byte) ((value >> 24) & 0xff));
}
/**
* Writes 2 bytes in little-endian format, converting them from a 16-bit value.
*
* @param output the output stream where the bytes will be written
* @param value the 16-bit value to convert
* @throws IOException failed to write the value data
*/
public static void writeUnsigned2Le(ByteBuffer output, int value) throws IOException {
Preconditions.checkNotNull(output, "output == null");
Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value);
output.put((byte) (value & 0xff));
output.put((byte) ((value >> 8) & 0xff));
}
}

View File

@ -0,0 +1,105 @@
/*
* 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.tools.build.apkzlib.zip.utils;
import com.google.common.base.Verify;
import java.util.Calendar;
import java.util.Date;
/** Yes. This actually refers to MS-DOS in 2015. That's all I have to say about legacy stuff. */
public class MsDosDateTimeUtils {
/** Utility class: no constructor. */
private MsDosDateTimeUtils() {}
/**
* Packs java time value into an MS-DOS time value.
*
* @param time the time value
* @return the MS-DOS packed time
*/
public static int packTime(long time) {
Calendar c = Calendar.getInstance();
c.setTime(new Date(time));
int seconds = c.get(Calendar.SECOND);
int minutes = c.get(Calendar.MINUTE);
int hours = c.get(Calendar.HOUR_OF_DAY);
/*
* Here is how MS-DOS packs a time value:
* 0-4: seconds (divided by 2 because we only have 5 bits = 32 different numbers)
* 5-10: minutes (6 bits = 64 possible values)
* 11-15: hours (5 bits = 32 possible values)
*
* source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx
*/
return (hours << 11) | (minutes << 5) | (seconds / 2);
}
/**
* Packs the current time value into an MS-DOS time value.
*
* @return the MS-DOS packed time
*/
public static int packCurrentTime() {
return packTime(new Date().getTime());
}
/**
* Packs java time value into an MS-DOS date value.
*
* @param time the time value
* @return the MS-DOS packed date
*/
public static int packDate(long time) {
Calendar c = Calendar.getInstance();
c.setTime(new Date(time));
/*
* Even MS-DOS used 1 for January. Someone wasn't really thinking when they decided on Java
* it would start at 0...
*/
int day = c.get(Calendar.DAY_OF_MONTH);
int month = c.get(Calendar.MONTH) + 1;
/*
* MS-DOS counts years starting from 1980. Since its launch date was in 81, it was obviously
* not necessary to talk about dates earlier than that.
*/
int year = c.get(Calendar.YEAR) - 1980;
Verify.verify(year >= 0 && year < 128);
/*
* Here is how MS-DOS packs a date value:
* 0-4: day (5 bits = 32 values)
* 5-8: month (4 bits = 16 values)
* 9-15: year (7 bits = 128 values)
*
* source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx
*/
return (year << 9) | (month << 5) | day;
}
/**
* Packs the current time value into an MS-DOS date value.
*
* @return the MS-DOS packed date
*/
public static int packCurrentDate() {
return packDate(new Date().getTime());
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.tools.build.apkzlib.zip.utils;
import java.io.IOException;
import java.io.RandomAccessFile;
/** Utility class with utility methods for random access files. */
public final class RandomAccessFileUtils {
private RandomAccessFileUtils() {}
/**
* Reads from an random access file until the provided array is filled. Data is read from the
* current position in the file.
*
* @param raf the file to read data from
* @param data the array that will receive the data
* @throws IOException failed to read the data
*/
public static void fullyRead(RandomAccessFile raf, byte[] data) throws IOException {
int r;
int p = 0;
while ((r = raf.read(data, p, data.length - p)) > 0) {
p += r;
if (p == data.length) {
break;
}
}
if (p < data.length) {
throw new IOException(
"Failed to read "
+ data.length
+ " bytes from file. Only "
+ p
+ " bytes could be read.");
}
}
}

View File

@ -4,12 +4,9 @@ buildscript {
repositories {
google()
mavenCentral()
maven { url "https://jcenter.bintray.com" }
maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.0-alpha10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
}
}
@ -32,8 +29,6 @@ allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jcenter.bintray.com" }
maven { url "https://jitpack.io" }
}
}

View File

@ -13,8 +13,8 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':axmlprinter')
implementation project(':share')
implementation 'commons-io:commons-io:2.10.0'
implementation 'com.android.tools.build:apkzlib:4.2.2'
implementation project(':apkzlib')
implementation 'commons-io:commons-io:2.11.0'
implementation 'com.beust:jcommander:1.81'
}
@ -40,7 +40,7 @@ jar {
from fileTree(dir: "$rootProject.projectDir/out/so")
}
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/*.MF', 'META-INF/*.txt'
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/*.MF', 'META-INF/*.txt', "META-INF/versions/**"
}
tasks.register("buildDebug") {

View File

@ -14,3 +14,4 @@ include ':patch'
include ':axmlprinter'
include ':share'
include ':appstub'
include ':apkzlib'