blob: fcb0703aca18cfc7c45b1a324c1e0fb7f2350331 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
/**
* Command line tool used to build APKs which support loading the native code library
* directly from the APK file. To construct the APK we rename the native library by
* adding the prefix "crazy." to the filename. This is done to prevent the Android
* Package Manager from extracting the library. The native code must be page aligned
* and uncompressed. The page alignment is implemented by adding a zero filled file
* in front of the the native code library. This tool is designed so that running
* SignApk and/or zipalign on the resulting APK does not break the page alignment.
* This is achieved by outputing the filenames in the same canonical order used
* by SignApk and adding the same alignment fields added by zipalign.
*/
class RezipApk {
// Alignment to use for non-compressed files (must match zipalign).
private static final int ALIGNMENT = 4;
// Alignment to use for non-compressed *.so files
private static final int LIBRARY_ALIGNMENT = 4096;
// Files matching this pattern are not copied to the output when adding alignment.
// When reordering and verifying the APK they are copied to the end of the file.
private static Pattern sMetaFilePattern =
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" +
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
// Pattern for matching a shared library in the APK
private static Pattern sLibraryPattern = Pattern.compile("^lib/[^/]*/lib.*[.]so$");
// Pattern for match the crazy linker in the APK
private static Pattern sCrazyLinkerPattern =
Pattern.compile("^lib/[^/]*/libchromium_android_linker.so$");
// Pattern for matching a crazy loaded shared library in the APK
private static Pattern sCrazyLibraryPattern =
Pattern.compile("^lib/[^/]*/crazy.lib.*[.]so$");
private static boolean isLibraryFilename(String filename) {
return sLibraryPattern.matcher(filename).matches() &&
!sCrazyLinkerPattern.matcher(filename).matches();
}
private static boolean isCrazyLibraryFilename(String filename) {
return sCrazyLibraryPattern.matcher(filename).matches();
}
private static String renameLibraryForCrazyLinker(String filename) {
int lastSlash = filename.lastIndexOf('/');
// We rename the library, so that the Android Package Manager
// no longer extracts the library.
return filename.substring(0, lastSlash + 1) + "crazy." + filename.substring(lastSlash + 1);
}
/**
* Wraps another output stream, counting the number of bytes written.
*/
private static class CountingOutputStream extends OutputStream {
private long mCount = 0;
private OutputStream mOut;
public CountingOutputStream(OutputStream out) {
this.mOut = out;
}
/** Returns the number of bytes written. */
public long getCount() {
return mCount;
}
@Override public void write(byte[] b, int off, int len) throws IOException {
mOut.write(b, off, len);
mCount += len;
}
@Override public void write(int b) throws IOException {
mOut.write(b);
mCount++;
}
@Override public void close() throws IOException {
mOut.close();
}
@Override public void flush() throws IOException {
mOut.flush();
}
}
private static String outputName(JarEntry entry, boolean rename) {
String inName = entry.getName();
if (rename && entry.getSize() > 0 && isLibraryFilename(inName)) {
return renameLibraryForCrazyLinker(inName);
}
return inName;
}
/**
* Comparator used to sort jar entries from the input file.
* Sorting is done based on the output filename (which maybe renamed).
* Filenames are in natural string order, except that filenames matching
* the meta-file pattern are always after other files. This is so the manifest
* and signature are at the end of the file after any alignment file.
*/
private static class EntryComparator implements Comparator<JarEntry> {
private boolean mRename;
public EntryComparator(boolean rename) {
mRename = rename;
}
@Override
public int compare(JarEntry j1, JarEntry j2) {
String o1 = outputName(j1, mRename);
String o2 = outputName(j2, mRename);
boolean o1Matches = sMetaFilePattern.matcher(o1).matches();
boolean o2Matches = sMetaFilePattern.matcher(o2).matches();
if (o1Matches != o2Matches) {
return o1Matches ? 1 : -1;
} else {
return o1.compareTo(o2);
}
}
}
// Build an ordered list of jar entries. The jar entries from the input are
// sorted based on the output filenames (which maybe renamed). If |omitMetaFiles|
// is true do not include the jar entries for the META-INF files.
// Entries are ordered in the deterministic order used by SignApk.
private static List<JarEntry> getOutputFileOrderEntries(
JarFile jar, boolean omitMetaFiles, boolean rename) {
List<JarEntry> entries = new ArrayList<JarEntry>();
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
if (entry.isDirectory()) {
continue;
}
if (omitMetaFiles &&
sMetaFilePattern.matcher(entry.getName()).matches()) {
continue;
}
entries.add(entry);
}
// We sort the input entries by name. When present META-INF files
// are sorted to the end.
Collections.sort(entries, new EntryComparator(rename));
return entries;
}
/**
* Add a zero filled alignment file at this point in the zip file,
* The added file will be added before |name| and after |prevName|.
* The size of the alignment file is such that the location of the
* file |name| will be on a LIBRARY_ALIGNMENT boundary.
*
* Note this arrangement is devised so that running SignApk and/or zipalign on the resulting
* file will not alter the alignment.
*
* @param offset number of bytes into the output file at this point.
* @param timestamp time in millis since the epoch to include in the header.
* @param name the name of the library filename.
* @param prevName the name of the previous file in the archive (or null).
* @param out jar output stream to write the alignment file to.
*
* @throws IOException if the output file can not be written.
*/
private static void addAlignmentFile(
long offset, long timestamp, String name, String prevName,
JarOutputStream out) throws IOException {
// Compute the start and alignment of the library, as if it was next.
int headerSize = JarFile.LOCHDR + name.length();
long libOffset = offset + headerSize;
int libNeeded = LIBRARY_ALIGNMENT - (int) (libOffset % LIBRARY_ALIGNMENT);
if (libNeeded == LIBRARY_ALIGNMENT) {
// Already aligned, no need to added alignment file.
return;
}
// Check that there is not another file between the library and the
// alignment file.
String alignName = name.substring(0, name.length() - 2) + "align";
if (prevName != null && prevName.compareTo(alignName) >= 0) {
throw new UnsupportedOperationException(
"Unable to insert alignment file, because there is "
+ "another file in front of the file to be aligned. "
+ "Other file: " + prevName + " Alignment file: " + alignName
+ " file: " + name);
}
// Compute the size of the alignment file header.
headerSize = JarFile.LOCHDR + alignName.length();
// We are going to add an alignment file of type STORED. This file
// will itself induce a zipalign alignment adjustment.
int extraNeeded =
(ALIGNMENT - (int) ((offset + headerSize) % ALIGNMENT)) % ALIGNMENT;
headerSize += extraNeeded;
if (libNeeded < headerSize + 1) {
// The header was bigger than the alignment that we need, add another page.
libNeeded += LIBRARY_ALIGNMENT;
}
// Compute the size of the alignment file.
libNeeded -= headerSize;
// Build the header for the alignment file.
byte[] zeroBuffer = new byte[libNeeded];
JarEntry alignEntry = new JarEntry(alignName);
alignEntry.setMethod(JarEntry.STORED);
alignEntry.setSize(libNeeded);
alignEntry.setTime(timestamp);
CRC32 crc = new CRC32();
crc.update(zeroBuffer);
alignEntry.setCrc(crc.getValue());
if (extraNeeded != 0) {
alignEntry.setExtra(new byte[extraNeeded]);
}
// Output the alignment file.
out.putNextEntry(alignEntry);
out.write(zeroBuffer);
out.closeEntry();
out.flush();
}
// Make a JarEntry for the output file which corresponds to the input
// file. The output file will be called |name|. The output file will always
// be uncompressed (STORED). If the input is not STORED it is necessary to inflate
// it to compute the CRC and size of the output entry.
private static JarEntry makeStoredEntry(String name, JarEntry inEntry, JarFile in)
throws IOException {
JarEntry outEntry = new JarEntry(name);
outEntry.setMethod(JarEntry.STORED);
if (inEntry.getMethod() == JarEntry.STORED) {
outEntry.setCrc(inEntry.getCrc());
outEntry.setSize(inEntry.getSize());
} else {
// We are inflating the file. We need to compute the CRC and size.
byte[] buffer = new byte[4096];
CRC32 crc = new CRC32();
int size = 0;
int num;
InputStream data = in.getInputStream(inEntry);
while ((num = data.read(buffer)) > 0) {
crc.update(buffer, 0, num);
size += num;
}
data.close();
outEntry.setCrc(crc.getValue());
outEntry.setSize(size);
}
return outEntry;
}
/**
* Copy the contents of the input APK file to the output APK file. If |rename| is
* true then non-empty libraries (*.so) in the input will be renamed by prefixing
* "crazy.". This is done to prevent the Android Package Manager extracting the
* library. Note the crazy linker itself is not renamed, for bootstrapping reasons.
* Empty libraries are not renamed (they are in the APK to workaround a bug where
* the Android Package Manager fails to delete old versions when upgrading).
* There must be exactly one "crazy" library in the output stream. The "crazy"
* library will be uncompressed and page aligned in the output stream. Page
* alignment is implemented by adding a zero filled file, regular alignment is
* implemented by adding a zero filled extra field to the zip file header. If
* |addAlignment| is true a page alignment file is added, otherwise the "crazy"
* library must already be page aligned. Care is taken so that the output is generated
* in the same way as SignApk. This is important so that running SignApk and
* zipalign on the output does not break the page alignment. The archive may not
* contain a "*.apk" as SignApk has special nested signing logic that we do not
* support.
*
* @param in The input APK File.
* @param out The output APK stream.
* @param countOut Counting output stream (to measure the current offset).
* @param addAlignment Whether to add the alignment file or just check.
* @param rename Whether to rename libraries to be "crazy".
*
* @throws IOException if the output file can not be written.
*/
private static void rezip(
JarFile in, JarOutputStream out, CountingOutputStream countOut,
boolean addAlignment, boolean rename) throws IOException {
List<JarEntry> entries = getOutputFileOrderEntries(in, addAlignment, rename);
long timestamp = System.currentTimeMillis();
byte[] buffer = new byte[4096];
boolean firstEntry = true;
String prevName = null;
int numCrazy = 0;
for (JarEntry inEntry : entries) {
// Rename files, if specied.
String name = outputName(inEntry, rename);
if (name.endsWith(".apk")) {
throw new UnsupportedOperationException(
"Nested APKs are not supported: " + name);
}
// Build the header.
JarEntry outEntry = null;
boolean isCrazy = isCrazyLibraryFilename(name);
if (isCrazy) {
// "crazy" libraries are alway output uncompressed (STORED).
outEntry = makeStoredEntry(name, inEntry, in);
numCrazy++;
if (numCrazy > 1) {
throw new UnsupportedOperationException(
"Found more than one library\n"
+ "Multiple libraries are not supported for APKs that use "
+ "'load_library_from_zip_file'.\n"
+ "See crbug/388223.\n"
+ "Note, check that your build is clean.\n"
+ "An unclean build can incorrectly incorporate old "
+ "libraries in the APK.");
}
} else if (inEntry.getMethod() == JarEntry.STORED) {
// Preserve the STORED method of the input entry.
outEntry = new JarEntry(inEntry);
outEntry.setExtra(null);
} else {
// Create a new entry so that the compressed len is recomputed.
outEntry = new JarEntry(name);
}
outEntry.setTime(timestamp);
// Compute and add alignment
long offset = countOut.getCount();
if (firstEntry) {
// The first entry in a jar file has an extra field of
// four bytes that you can't get rid of; any extra
// data you specify in the JarEntry is appended to
// these forced four bytes. This is JAR_MAGIC in
// JarOutputStream; the bytes are 0xfeca0000.
firstEntry = false;
offset += 4;
}
if (outEntry.getMethod() == JarEntry.STORED) {
if (isCrazy) {
if (addAlignment) {
addAlignmentFile(offset, timestamp, name, prevName, out);
}
// We check that we did indeed get to a page boundary.
offset = countOut.getCount() + JarFile.LOCHDR + name.length();
if ((offset % LIBRARY_ALIGNMENT) != 0) {
throw new AssertionError(
"Library was not page aligned when verifying page alignment. "
+ "Library name: " + name + " Expected alignment: "
+ LIBRARY_ALIGNMENT + "Offset: " + offset + " Error: "
+ (offset % LIBRARY_ALIGNMENT));
}
} else {
// This is equivalent to zipalign.
offset += JarFile.LOCHDR + name.length();
int needed = (ALIGNMENT - (int) (offset % ALIGNMENT)) % ALIGNMENT;
if (needed != 0) {
outEntry.setExtra(new byte[needed]);
}
}
}
out.putNextEntry(outEntry);
// Copy the data from the input to the output
int num;
InputStream data = in.getInputStream(inEntry);
while ((num = data.read(buffer)) > 0) {
out.write(buffer, 0, num);
}
data.close();
out.closeEntry();
out.flush();
prevName = name;
}
if (numCrazy == 0) {
throw new AssertionError("There was no crazy library in the archive");
}
}
private static void usage() {
System.err.println(
"Usage: prealignapk (addalignment|reorder) input.apk output.apk");
System.err.println(
"\"crazy\" libraries are always inflated in the output");
System.err.println(
" renamealign - rename libraries with \"crazy.\" prefix and add alignment file");
System.err.println(
" align - add alignment file");
System.err.println(
" reorder - re-creates canonical ordering and checks alignment");
System.exit(2);
}
public static void main(String[] args) throws IOException {
if (args.length != 3) usage();
boolean addAlignment = false;
boolean rename = false;
if (args[0].equals("renamealign")) {
// Normal case. Before signing we rename the library and add an alignment file.
addAlignment = true;
rename = true;
} else if (args[0].equals("align")) {
// LGPL compliance case. Before signing, we add an alignment file to a
// reconstructed APK which already contains the "crazy" library.
addAlignment = true;
rename = false;
} else if (args[0].equals("reorder")) {
// Normal case. After jarsigning we write the file in the canonical order and check.
addAlignment = false;
} else {
usage();
}
String inputFilename = args[1];
String outputFilename = args[2];
JarFile inputJar = null;
FileOutputStream outputFile = null;
try {
inputJar = new JarFile(new File(inputFilename), true);
outputFile = new FileOutputStream(outputFilename);
CountingOutputStream outCount = new CountingOutputStream(outputFile);
JarOutputStream outputJar = new JarOutputStream(outCount);
// Match the compression level used by SignApk.
outputJar.setLevel(9);
rezip(inputJar, outputJar, outCount, addAlignment, rename);
outputJar.close();
} finally {
if (inputJar != null) inputJar.close();
if (outputFile != null) outputFile.close();
}
}
}