1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 import java.io.File; 6 import java.io.FileOutputStream; 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.io.OutputStream; 10 import java.util.ArrayList; 11 import java.util.Collections; 12 import java.util.Comparator; 13 import java.util.Enumeration; 14 import java.util.List; 15 import java.util.jar.JarEntry; 16 import java.util.jar.JarFile; 17 import java.util.jar.JarOutputStream; 18 import java.util.regex.Pattern; 19 import java.util.zip.CRC32; 20 21 /** 22 * Command line tool used to build APKs which support loading the native code library 23 * directly from the APK file. To construct the APK we rename the native library by 24 * adding the prefix "crazy." to the filename. This is done to prevent the Android 25 * Package Manager from extracting the library. The native code must be page aligned 26 * and uncompressed. The page alignment is implemented by adding a zero filled file 27 * in front of the the native code library. This tool is designed so that running 28 * SignApk and/or zipalign on the resulting APK does not break the page alignment. 29 * This is achieved by outputing the filenames in the same canonical order used 30 * by SignApk and adding the same alignment fields added by zipalign. 31 */ 32 class RezipApk { 33 // Alignment to use for non-compressed files (must match zipalign). 34 private static final int ALIGNMENT = 4; 35 36 // Alignment to use for non-compressed *.so files 37 private static final int LIBRARY_ALIGNMENT = 4096; 38 39 // Files matching this pattern are not copied to the output when adding alignment. 40 // When reordering and verifying the APK they are copied to the end of the file. 41 private static Pattern sMetaFilePattern = 42 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" 43 + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 44 45 // Pattern for matching a shared library in the APK 46 private static Pattern sLibraryPattern = Pattern.compile("^lib/[^/]*/lib.*[.]so$"); 47 // Pattern for match the crazy linker in the APK 48 private static Pattern sCrazyLinkerPattern = 49 Pattern.compile("^lib/[^/]*/libchromium_android_linker.so$"); 50 // Pattern for matching a crazy loaded shared library in the APK 51 private static Pattern sCrazyLibraryPattern = Pattern.compile("^lib/[^/]*/crazy.lib.*[.]so$"); 52 isLibraryFilename(String filename)53 private static boolean isLibraryFilename(String filename) { 54 return sLibraryPattern.matcher(filename).matches() 55 && !sCrazyLinkerPattern.matcher(filename).matches(); 56 } 57 isCrazyLibraryFilename(String filename)58 private static boolean isCrazyLibraryFilename(String filename) { 59 return sCrazyLibraryPattern.matcher(filename).matches(); 60 } 61 renameLibraryForCrazyLinker(String filename)62 private static String renameLibraryForCrazyLinker(String filename) { 63 int lastSlash = filename.lastIndexOf('/'); 64 // We rename the library, so that the Android Package Manager 65 // no longer extracts the library. 66 return filename.substring(0, lastSlash + 1) + "crazy." + filename.substring(lastSlash + 1); 67 } 68 69 /** 70 * Wraps another output stream, counting the number of bytes written. 71 */ 72 private static class CountingOutputStream extends OutputStream { 73 private long mCount = 0; 74 private OutputStream mOut; 75 CountingOutputStream(OutputStream out)76 public CountingOutputStream(OutputStream out) { 77 this.mOut = out; 78 } 79 80 /** Returns the number of bytes written. */ getCount()81 public long getCount() { 82 return mCount; 83 } 84 write(byte[] b, int off, int len)85 @Override public void write(byte[] b, int off, int len) throws IOException { 86 mOut.write(b, off, len); 87 mCount += len; 88 } 89 write(int b)90 @Override public void write(int b) throws IOException { 91 mOut.write(b); 92 mCount++; 93 } 94 close()95 @Override public void close() throws IOException { 96 mOut.close(); 97 } 98 flush()99 @Override public void flush() throws IOException { 100 mOut.flush(); 101 } 102 } 103 outputName(JarEntry entry, boolean rename)104 private static String outputName(JarEntry entry, boolean rename) { 105 String inName = entry.getName(); 106 if (rename && entry.getSize() > 0 && isLibraryFilename(inName)) { 107 return renameLibraryForCrazyLinker(inName); 108 } 109 return inName; 110 } 111 112 /** 113 * Comparator used to sort jar entries from the input file. 114 * Sorting is done based on the output filename (which maybe renamed). 115 * Filenames are in natural string order, except that filenames matching 116 * the meta-file pattern are always after other files. This is so the manifest 117 * and signature are at the end of the file after any alignment file. 118 */ 119 private static class EntryComparator implements Comparator<JarEntry> { 120 private boolean mRename; 121 EntryComparator(boolean rename)122 public EntryComparator(boolean rename) { 123 mRename = rename; 124 } 125 126 @Override compare(JarEntry j1, JarEntry j2)127 public int compare(JarEntry j1, JarEntry j2) { 128 String o1 = outputName(j1, mRename); 129 String o2 = outputName(j2, mRename); 130 boolean o1Matches = sMetaFilePattern.matcher(o1).matches(); 131 boolean o2Matches = sMetaFilePattern.matcher(o2).matches(); 132 if (o1Matches != o2Matches) { 133 return o1Matches ? 1 : -1; 134 } else { 135 return o1.compareTo(o2); 136 } 137 } 138 } 139 140 // Build an ordered list of jar entries. The jar entries from the input are 141 // sorted based on the output filenames (which maybe renamed). If |omitMetaFiles| 142 // is true do not include the jar entries for the META-INF files. 143 // Entries are ordered in the deterministic order used by SignApk. getOutputFileOrderEntries( JarFile jar, boolean omitMetaFiles, boolean rename)144 private static List<JarEntry> getOutputFileOrderEntries( 145 JarFile jar, boolean omitMetaFiles, boolean rename) { 146 List<JarEntry> entries = new ArrayList<JarEntry>(); 147 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { 148 JarEntry entry = e.nextElement(); 149 if (entry.isDirectory()) { 150 continue; 151 } 152 if (omitMetaFiles && sMetaFilePattern.matcher(entry.getName()).matches()) { 153 continue; 154 } 155 entries.add(entry); 156 } 157 158 // We sort the input entries by name. When present META-INF files 159 // are sorted to the end. 160 Collections.sort(entries, new EntryComparator(rename)); 161 return entries; 162 } 163 164 /** 165 * Add a zero filled alignment file at this point in the zip file, 166 * The added file will be added before |name| and after |prevName|. 167 * The size of the alignment file is such that the location of the 168 * file |name| will be on a LIBRARY_ALIGNMENT boundary. 169 * 170 * Note this arrangement is devised so that running SignApk and/or zipalign on the resulting 171 * file will not alter the alignment. 172 * 173 * @param offset number of bytes into the output file at this point. 174 * @param timestamp time in millis since the epoch to include in the header. 175 * @param name the name of the library filename. 176 * @param prevName the name of the previous file in the archive (or null). 177 * @param out jar output stream to write the alignment file to. 178 * 179 * @throws IOException if the output file can not be written. 180 */ addAlignmentFile( long offset, long timestamp, String name, String prevName, JarOutputStream out)181 private static void addAlignmentFile( 182 long offset, long timestamp, String name, String prevName, 183 JarOutputStream out) throws IOException { 184 185 // Compute the start and alignment of the library, as if it was next. 186 int headerSize = JarFile.LOCHDR + name.length(); 187 long libOffset = offset + headerSize; 188 int libNeeded = LIBRARY_ALIGNMENT - (int) (libOffset % LIBRARY_ALIGNMENT); 189 if (libNeeded == LIBRARY_ALIGNMENT) { 190 // Already aligned, no need to added alignment file. 191 return; 192 } 193 194 // Check that there is not another file between the library and the 195 // alignment file. 196 String alignName = name.substring(0, name.length() - 2) + "align"; 197 if (prevName != null && prevName.compareTo(alignName) >= 0) { 198 throw new UnsupportedOperationException( 199 "Unable to insert alignment file, because there is " 200 + "another file in front of the file to be aligned. " 201 + "Other file: " + prevName + " Alignment file: " + alignName 202 + " file: " + name); 203 } 204 205 // Compute the size of the alignment file header. 206 headerSize = JarFile.LOCHDR + alignName.length(); 207 // We are going to add an alignment file of type STORED. This file 208 // will itself induce a zipalign alignment adjustment. 209 int extraNeeded = 210 (ALIGNMENT - (int) ((offset + headerSize) % ALIGNMENT)) % ALIGNMENT; 211 headerSize += extraNeeded; 212 213 if (libNeeded < headerSize + 1) { 214 // The header was bigger than the alignment that we need, add another page. 215 libNeeded += LIBRARY_ALIGNMENT; 216 } 217 // Compute the size of the alignment file. 218 libNeeded -= headerSize; 219 220 // Build the header for the alignment file. 221 byte[] zeroBuffer = new byte[libNeeded]; 222 JarEntry alignEntry = new JarEntry(alignName); 223 alignEntry.setMethod(JarEntry.STORED); 224 alignEntry.setSize(libNeeded); 225 alignEntry.setTime(timestamp); 226 CRC32 crc = new CRC32(); 227 crc.update(zeroBuffer); 228 alignEntry.setCrc(crc.getValue()); 229 230 if (extraNeeded != 0) { 231 alignEntry.setExtra(new byte[extraNeeded]); 232 } 233 234 // Output the alignment file. 235 out.putNextEntry(alignEntry); 236 out.write(zeroBuffer); 237 out.closeEntry(); 238 out.flush(); 239 } 240 241 // Make a JarEntry for the output file which corresponds to the input 242 // file. The output file will be called |name|. The output file will always 243 // be uncompressed (STORED). If the input is not STORED it is necessary to inflate 244 // it to compute the CRC and size of the output entry. makeStoredEntry(String name, JarEntry inEntry, JarFile in)245 private static JarEntry makeStoredEntry(String name, JarEntry inEntry, JarFile in) 246 throws IOException { 247 JarEntry outEntry = new JarEntry(name); 248 outEntry.setMethod(JarEntry.STORED); 249 250 if (inEntry.getMethod() == JarEntry.STORED) { 251 outEntry.setCrc(inEntry.getCrc()); 252 outEntry.setSize(inEntry.getSize()); 253 } else { 254 // We are inflating the file. We need to compute the CRC and size. 255 byte[] buffer = new byte[4096]; 256 CRC32 crc = new CRC32(); 257 int size = 0; 258 int num; 259 InputStream data = in.getInputStream(inEntry); 260 while ((num = data.read(buffer)) > 0) { 261 crc.update(buffer, 0, num); 262 size += num; 263 } 264 data.close(); 265 outEntry.setCrc(crc.getValue()); 266 outEntry.setSize(size); 267 } 268 return outEntry; 269 } 270 271 /** 272 * Copy the contents of the input APK file to the output APK file. If |rename| is 273 * true then non-empty libraries (*.so) in the input will be renamed by prefixing 274 * "crazy.". This is done to prevent the Android Package Manager extracting the 275 * library. Note the crazy linker itself is not renamed, for bootstrapping reasons. 276 * Empty libraries are not renamed (they are in the APK to workaround a bug where 277 * the Android Package Manager fails to delete old versions when upgrading). 278 * There must be exactly one "crazy" library in the output stream. The "crazy" 279 * library will be uncompressed and page aligned in the output stream. Page 280 * alignment is implemented by adding a zero filled file, regular alignment is 281 * implemented by adding a zero filled extra field to the zip file header. If 282 * |addAlignment| is true a page alignment file is added, otherwise the "crazy" 283 * library must already be page aligned. Care is taken so that the output is generated 284 * in the same way as SignApk. This is important so that running SignApk and 285 * zipalign on the output does not break the page alignment. The archive may not 286 * contain a "*.apk" as SignApk has special nested signing logic that we do not 287 * support. 288 * 289 * @param in The input APK File. 290 * @param out The output APK stream. 291 * @param countOut Counting output stream (to measure the current offset). 292 * @param addAlignment Whether to add the alignment file or just check. 293 * @param rename Whether to rename libraries to be "crazy". 294 * 295 * @throws IOException if the output file can not be written. 296 */ rezip( JarFile in, JarOutputStream out, CountingOutputStream countOut, boolean addAlignment, boolean rename)297 private static void rezip( 298 JarFile in, JarOutputStream out, CountingOutputStream countOut, 299 boolean addAlignment, boolean rename) throws IOException { 300 301 List<JarEntry> entries = getOutputFileOrderEntries(in, addAlignment, rename); 302 long timestamp = System.currentTimeMillis(); 303 byte[] buffer = new byte[4096]; 304 boolean firstEntry = true; 305 String prevName = null; 306 int numCrazy = 0; 307 for (JarEntry inEntry : entries) { 308 // Rename files, if specied. 309 String name = outputName(inEntry, rename); 310 if (name.endsWith(".apk")) { 311 throw new UnsupportedOperationException( 312 "Nested APKs are not supported: " + name); 313 } 314 315 // Build the header. 316 JarEntry outEntry = null; 317 boolean isCrazy = isCrazyLibraryFilename(name); 318 if (isCrazy) { 319 // "crazy" libraries are alway output uncompressed (STORED). 320 outEntry = makeStoredEntry(name, inEntry, in); 321 numCrazy++; 322 if (numCrazy > 1) { 323 throw new UnsupportedOperationException( 324 "Found more than one library\n" 325 + "Multiple libraries are not supported for APKs that use " 326 + "'load_library_from_zip'.\n" 327 + "See crbug/388223.\n" 328 + "Note, check that your build is clean.\n" 329 + "An unclean build can incorrectly incorporate old " 330 + "libraries in the APK."); 331 } 332 } else if (inEntry.getMethod() == JarEntry.STORED) { 333 // Preserve the STORED method of the input entry. 334 outEntry = new JarEntry(inEntry); 335 outEntry.setExtra(null); 336 } else { 337 // Create a new entry so that the compressed len is recomputed. 338 outEntry = new JarEntry(name); 339 } 340 outEntry.setTime(timestamp); 341 342 // Compute and add alignment 343 long offset = countOut.getCount(); 344 if (firstEntry) { 345 // The first entry in a jar file has an extra field of 346 // four bytes that you can't get rid of; any extra 347 // data you specify in the JarEntry is appended to 348 // these forced four bytes. This is JAR_MAGIC in 349 // JarOutputStream; the bytes are 0xfeca0000. 350 firstEntry = false; 351 offset += 4; 352 } 353 if (outEntry.getMethod() == JarEntry.STORED) { 354 if (isCrazy) { 355 if (addAlignment) { 356 addAlignmentFile(offset, timestamp, name, prevName, out); 357 } 358 // We check that we did indeed get to a page boundary. 359 offset = countOut.getCount() + JarFile.LOCHDR + name.length(); 360 if ((offset % LIBRARY_ALIGNMENT) != 0) { 361 throw new AssertionError( 362 "Library was not page aligned when verifying page alignment. " 363 + "Library name: " + name + " Expected alignment: " 364 + LIBRARY_ALIGNMENT + "Offset: " + offset + " Error: " 365 + (offset % LIBRARY_ALIGNMENT)); 366 } 367 } else { 368 // This is equivalent to zipalign. 369 offset += JarFile.LOCHDR + name.length(); 370 int needed = (ALIGNMENT - (int) (offset % ALIGNMENT)) % ALIGNMENT; 371 if (needed != 0) { 372 outEntry.setExtra(new byte[needed]); 373 } 374 } 375 } 376 out.putNextEntry(outEntry); 377 378 // Copy the data from the input to the output 379 int num; 380 InputStream data = in.getInputStream(inEntry); 381 while ((num = data.read(buffer)) > 0) { 382 out.write(buffer, 0, num); 383 } 384 data.close(); 385 out.closeEntry(); 386 out.flush(); 387 prevName = name; 388 } 389 if (numCrazy == 0) { 390 throw new AssertionError("There was no crazy library in the archive"); 391 } 392 } 393 usage()394 private static void usage() { 395 System.err.println("Usage: prealignapk (addalignment|reorder) input.apk output.apk"); 396 System.err.println("\"crazy\" libraries are always inflated in the output"); 397 System.err.println( 398 " renamealign - rename libraries with \"crazy.\" prefix and add alignment file"); 399 System.err.println(" align - add alignment file"); 400 System.err.println(" reorder - re-creates canonical ordering and checks alignment"); 401 System.exit(2); 402 } 403 main(String[] args)404 public static void main(String[] args) throws IOException { 405 if (args.length != 3) usage(); 406 407 boolean addAlignment = false; 408 boolean rename = false; 409 if (args[0].equals("renamealign")) { 410 // Normal case. Before signing we rename the library and add an alignment file. 411 addAlignment = true; 412 rename = true; 413 } else if (args[0].equals("align")) { 414 // LGPL compliance case. Before signing, we add an alignment file to a 415 // reconstructed APK which already contains the "crazy" library. 416 addAlignment = true; 417 rename = false; 418 } else if (args[0].equals("reorder")) { 419 // Normal case. After jarsigning we write the file in the canonical order and check. 420 addAlignment = false; 421 } else { 422 usage(); 423 } 424 425 String inputFilename = args[1]; 426 String outputFilename = args[2]; 427 428 JarFile inputJar = null; 429 FileOutputStream outputFile = null; 430 431 try { 432 inputJar = new JarFile(new File(inputFilename), true); 433 outputFile = new FileOutputStream(outputFilename); 434 435 CountingOutputStream outCount = new CountingOutputStream(outputFile); 436 JarOutputStream outputJar = new JarOutputStream(outCount); 437 438 // Match the compression level used by SignApk. 439 outputJar.setLevel(9); 440 441 rezip(inputJar, outputJar, outCount, addAlignment, rename); 442 outputJar.close(); 443 } finally { 444 if (inputJar != null) inputJar.close(); 445 if (outputFile != null) outputFile.close(); 446 } 447 } 448 } 449