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.
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
;
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.
33 // Alignment to use for non-compressed files (must match zipalign).
34 private static final int ALIGNMENT
= 4;
36 // Alignment to use for non-compressed *.so files
37 private static final int LIBRARY_ALIGNMENT
= 4096;
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
) + ")$");
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$");
53 private static boolean isLibraryFilename(String filename
) {
54 return sLibraryPattern
.matcher(filename
).matches()
55 && !sCrazyLinkerPattern
.matcher(filename
).matches();
58 private static boolean isCrazyLibraryFilename(String filename
) {
59 return sCrazyLibraryPattern
.matcher(filename
).matches();
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);
70 * Wraps another output stream, counting the number of bytes written.
72 private static class CountingOutputStream
extends OutputStream
{
73 private long mCount
= 0;
74 private OutputStream mOut
;
76 public CountingOutputStream(OutputStream out
) {
80 /** Returns the number of bytes written. */
81 public long getCount() {
85 @Override public void write(byte[] b
, int off
, int len
) throws IOException
{
86 mOut
.write(b
, off
, len
);
90 @Override public void write(int b
) throws IOException
{
95 @Override public void close() throws IOException
{
99 @Override public void flush() throws IOException
{
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
);
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.
119 private static class EntryComparator
implements Comparator
<JarEntry
> {
120 private boolean mRename
;
122 public EntryComparator(boolean rename
) {
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;
135 return o1
.compareTo(o2
);
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.
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()) {
152 if (omitMetaFiles
&& sMetaFilePattern
.matcher(entry
.getName()).matches()) {
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
));
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.
170 * Note this arrangement is devised so that running SignApk and/or zipalign on the resulting
171 * file will not alter the alignment.
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.
179 * @throws IOException if the output file can not be written.
181 private static void addAlignmentFile(
182 long offset
, long timestamp
, String name
, String prevName
,
183 JarOutputStream out
) throws IOException
{
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.
194 // Check that there is not another file between the library and the
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
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.
210 (ALIGNMENT
- (int) ((offset
+ headerSize
) % ALIGNMENT
)) % ALIGNMENT
;
211 headerSize
+= extraNeeded
;
213 if (libNeeded
< headerSize
+ 1) {
214 // The header was bigger than the alignment that we need, add another page.
215 libNeeded
+= LIBRARY_ALIGNMENT
;
217 // Compute the size of the alignment file.
218 libNeeded
-= headerSize
;
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());
230 if (extraNeeded
!= 0) {
231 alignEntry
.setExtra(new byte[extraNeeded
]);
234 // Output the alignment file.
235 out
.putNextEntry(alignEntry
);
236 out
.write(zeroBuffer
);
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.
245 private static JarEntry
makeStoredEntry(String name
, JarEntry inEntry
, JarFile in
)
247 JarEntry outEntry
= new JarEntry(name
);
248 outEntry
.setMethod(JarEntry
.STORED
);
250 if (inEntry
.getMethod() == JarEntry
.STORED
) {
251 outEntry
.setCrc(inEntry
.getCrc());
252 outEntry
.setSize(inEntry
.getSize());
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();
259 InputStream data
= in
.getInputStream(inEntry
);
260 while ((num
= data
.read(buffer
)) > 0) {
261 crc
.update(buffer
, 0, num
);
265 outEntry
.setCrc(crc
.getValue());
266 outEntry
.setSize(size
);
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
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".
295 * @throws IOException if the output file can not be written.
297 private static void rezip(
298 JarFile in
, JarOutputStream out
, CountingOutputStream countOut
,
299 boolean addAlignment
, boolean rename
) throws IOException
{
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;
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
);
316 JarEntry outEntry
= null;
317 boolean isCrazy
= isCrazyLibraryFilename(name
);
319 // "crazy" libraries are alway output uncompressed (STORED).
320 outEntry
= makeStoredEntry(name
, inEntry
, in
);
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.");
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);
337 // Create a new entry so that the compressed len is recomputed.
338 outEntry
= new JarEntry(name
);
340 outEntry
.setTime(timestamp
);
342 // Compute and add alignment
343 long offset
= countOut
.getCount();
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.
353 if (outEntry
.getMethod() == JarEntry
.STORED
) {
356 addAlignmentFile(offset
, timestamp
, name
, prevName
, out
);
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
));
368 // This is equivalent to zipalign.
369 offset
+= JarFile
.LOCHDR
+ name
.length();
370 int needed
= (ALIGNMENT
- (int) (offset
% ALIGNMENT
)) % ALIGNMENT
;
372 outEntry
.setExtra(new byte[needed
]);
376 out
.putNextEntry(outEntry
);
378 // Copy the data from the input to the output
380 InputStream data
= in
.getInputStream(inEntry
);
381 while ((num
= data
.read(buffer
)) > 0) {
382 out
.write(buffer
, 0, num
);
390 throw new AssertionError("There was no crazy library in the archive");
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");
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");
404 public static void main(String
[] args
) throws IOException
{
405 if (args
.length
!= 3) usage();
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.
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.
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;
425 String inputFilename
= args
[1];
426 String outputFilename
= args
[2];
428 JarFile inputJar
= null;
429 FileOutputStream outputFile
= null;
432 inputJar
= new JarFile(new File(inputFilename
), true);
433 outputFile
= new FileOutputStream(outputFilename
);
435 CountingOutputStream outCount
= new CountingOutputStream(outputFile
);
436 JarOutputStream outputJar
= new JarOutputStream(outCount
);
438 // Match the compression level used by SignApk.
439 outputJar
.setLevel(9);
441 rezip(inputJar
, outputJar
, outCount
, addAlignment
, rename
);
444 if (inputJar
!= null) inputJar
.close();
445 if (outputFile
!= null) outputFile
.close();