OLD | NEW |
(Empty) | |
| 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 li
brary |
| 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 Andro
id |
| 25 * Package Manager from extracting the library. The native code must be page ali
gned |
| 26 * and uncompressed. The page alignment is implemented by adding a zero filled f
ile |
| 27 * in front of the the native code library. This tool is designed so that runnin
g |
| 28 * SignApk and/or zipalign on the resulting APK does not break the page alignmen
t. |
| 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 alig
nment. |
| 40 // When reordering and verifying the APK they are copied to the end of the f
ile. |
| 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/[^/]*/cr
azy.lib.*[.]so$"); |
| 52 |
| 53 private static boolean isLibraryFilename(String filename) { |
| 54 return sLibraryPattern.matcher(filename).matches() |
| 55 && !sCrazyLinkerPattern.matcher(filename).matches(); |
| 56 } |
| 57 |
| 58 private static boolean isCrazyLibraryFilename(String filename) { |
| 59 return sCrazyLibraryPattern.matcher(filename).matches(); |
| 60 } |
| 61 |
| 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.substr
ing(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 |
| 76 public CountingOutputStream(OutputStream out) { |
| 77 this.mOut = out; |
| 78 } |
| 79 |
| 80 /** Returns the number of bytes written. */ |
| 81 public long getCount() { |
| 82 return mCount; |
| 83 } |
| 84 |
| 85 @Override public void write(byte[] b, int off, int len) throws IOExcepti
on { |
| 86 mOut.write(b, off, len); |
| 87 mCount += len; |
| 88 } |
| 89 |
| 90 @Override public void write(int b) throws IOException { |
| 91 mOut.write(b); |
| 92 mCount++; |
| 93 } |
| 94 |
| 95 @Override public void close() throws IOException { |
| 96 mOut.close(); |
| 97 } |
| 98 |
| 99 @Override public void flush() throws IOException { |
| 100 mOut.flush(); |
| 101 } |
| 102 } |
| 103 |
| 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 manife
st |
| 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 |
| 122 public EntryComparator(boolean rename) { |
| 123 mRename = rename; |
| 124 } |
| 125 |
| 126 @Override |
| 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 |omitMetaF
iles| |
| 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. |
| 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()).match
es()) { |
| 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 */ |
| 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)) % ALIGNM
ENT; |
| 211 headerSize += extraNeeded; |
| 212 |
| 213 if (libNeeded < headerSize + 1) { |
| 214 // The header was bigger than the alignment that we need, add anothe
r 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 i
nflate |
| 244 // it to compute the CRC and size of the output entry. |
| 245 private static JarEntry makeStoredEntry(String name, JarEntry inEntry, JarFi
le 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 |renam
e| is |
| 273 * true then non-empty libraries (*.so) in the input will be renamed by pref
ixing |
| 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 r
easons. |
| 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 "craz
y" |
| 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. I
f |
| 282 * |addAlignment| is true a page alignment file is added, otherwise the "cra
zy" |
| 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 */ |
| 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, ren
ame); |
| 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 tha
t 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 pag
e alignment. " |
| 363 + "Library name: " + name + " Expected alignment
: " |
| 364 + LIBRARY_ALIGNMENT + "Offset: " + offset + " Er
ror: " |
| 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)) % ALIG
NMENT; |
| 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 |
| 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 outpu
t"); |
| 397 System.err.println( |
| 398 " renamealign - rename libraries with \"crazy.\" prefix and ad
d alignment file"); |
| 399 System.err.println(" align - add alignment file"); |
| 400 System.err.println(" reorder - re-creates canonical ordering and c
hecks alignment"); |
| 401 System.exit(2); |
| 402 } |
| 403 |
| 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 alig
nment 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 } |
OLD | NEW |