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 |