// 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 { 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 getOutputFileOrderEntries( JarFile jar, boolean omitMetaFiles, boolean rename) { List entries = new ArrayList(); for (Enumeration 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 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'.\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(); } } }