blob: 31e05023ca1125e64782902ac3944e18e59ac005 [file] [log] [blame]
/*
* 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.fastdeploy;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
/**
* Extremely light-weight APK parser class.
* Aware of Central Directory, Local File Headers and Signature.
* No Zip64 support yet.
*/
public final class ApkArchive {
private static final String TAG = "ApkArchive";
// Central Directory constants.
private static final int EOCD_SIGNATURE = 0x06054b50;
private static final int EOCD_MIN_SIZE = 22;
private static final long EOCD_MAX_SIZE = 65_535L + EOCD_MIN_SIZE;
private static final int CD_ENTRY_HEADER_SIZE_BYTES = 22;
private static final int CD_LOCAL_FILE_HEADER_SIZE_OFFSET = 12;
// Signature constants.
private static final int EOSIGNATURE_SIZE = 24;
public final static class Dump {
final byte[] cd;
final byte[] signature;
Dump(byte[] cd, byte[] signature) {
this.cd = cd;
this.signature = signature;
}
}
final static class Location {
final long offset;
final long size;
public Location(long offset, long size) {
this.offset = offset;
this.size = size;
}
}
private final RandomAccessFile mFile;
private final FileChannel mChannel;
public ApkArchive(File apk) throws IOException {
mFile = new RandomAccessFile(apk, "r");
mChannel = mFile.getChannel();
}
/**
* Extract the APK metadata: content of Central Directory and Signature.
*
* @return raw content from APK representing CD and Signature data.
*/
public Dump extractMetadata() throws IOException {
Location cdLoc = getCDLocation();
byte[] cd = readMetadata(cdLoc);
byte[] signature = null;
Location sigLoc = getSignatureLocation(cdLoc.offset);
if (sigLoc != null) {
signature = readMetadata(sigLoc);
long size = ByteBuffer.wrap(signature).order(ByteOrder.LITTLE_ENDIAN).getLong();
if (sigLoc.size != size) {
Log.e(TAG, "Mismatching signature sizes: " + sigLoc.size + " != " + size);
signature = null;
}
}
return new Dump(cd, signature);
}
private long findEndOfCDRecord() throws IOException {
final long fileSize = mChannel.size();
int sizeToRead = Math.toIntExact(Math.min(fileSize, EOCD_MAX_SIZE));
final long readOffset = fileSize - sizeToRead;
ByteBuffer buffer = mChannel.map(FileChannel.MapMode.READ_ONLY, readOffset,
sizeToRead).order(ByteOrder.LITTLE_ENDIAN);
buffer.position(sizeToRead - EOCD_MIN_SIZE);
while (true) {
int signature = buffer.getInt(); // Read 4 bytes.
if (signature == EOCD_SIGNATURE) {
return readOffset + buffer.position() - 4;
}
if (buffer.position() == 4) {
break;
}
buffer.position(buffer.position() - Integer.BYTES - 1); // Backtrack 5 bytes.
}
return -1L;
}
private Location findCDRecord(ByteBuffer buf) {
if (buf.order() != ByteOrder.LITTLE_ENDIAN) {
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
}
if (buf.remaining() < CD_ENTRY_HEADER_SIZE_BYTES) {
throw new IllegalArgumentException(
"Input too short. Need at least " + CD_ENTRY_HEADER_SIZE_BYTES
+ " bytes, available: " + buf.remaining() + "bytes.");
}
int originalPosition = buf.position();
int recordSignature = buf.getInt();
if (recordSignature != EOCD_SIGNATURE) {
throw new IllegalArgumentException(
"Not a Central Directory record. Signature: 0x"
+ Long.toHexString(recordSignature & 0xffffffffL));
}
buf.position(originalPosition + CD_LOCAL_FILE_HEADER_SIZE_OFFSET);
long size = buf.getInt() & 0xffffffffL;
long offset = buf.getInt() & 0xffffffffL;
return new Location(offset, size);
}
// Retrieve the location of the Central Directory Record.
Location getCDLocation() throws IOException {
long eocdRecord = findEndOfCDRecord();
if (eocdRecord < 0) {
throw new IllegalArgumentException("Unable to find End of Central Directory record.");
}
Location location = findCDRecord(mChannel.map(FileChannel.MapMode.READ_ONLY, eocdRecord,
CD_ENTRY_HEADER_SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN));
if (location == null) {
throw new IllegalArgumentException("Unable to find Central Directory File Header.");
}
return location;
}
// Retrieve the location of the signature block starting from Central
// Directory Record or null if signature is not found.
Location getSignatureLocation(long cdRecordOffset) throws IOException {
long signatureOffset = cdRecordOffset - EOSIGNATURE_SIZE;
if (signatureOffset < 0) {
Log.e(TAG, "Unable to find Signature.");
return null;
}
ByteBuffer signature = mChannel.map(FileChannel.MapMode.READ_ONLY, signatureOffset,
EOSIGNATURE_SIZE).order(ByteOrder.LITTLE_ENDIAN);
long size = signature.getLong();
byte[] sign = new byte[16];
signature.get(sign);
String signAsString = new String(sign);
if (!"APK Sig Block 42".equals(signAsString)) {
Log.e(TAG, "Signature magic does not match: " + signAsString);
return null;
}
long offset = cdRecordOffset - size - 8;
return new Location(offset, size);
}
private byte[] readMetadata(Location loc) throws IOException {
byte[] payload = new byte[(int) loc.size];
ByteBuffer buffer = mChannel.map(FileChannel.MapMode.READ_ONLY, loc.offset, loc.size);
buffer.get(payload);
return payload;
}
}