| // 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'.\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(); |
| } |
| } |
| } |