• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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