make SignApk do zip alignment

When signing an APK, make the SignApk tool align the stored entries to
(by default) 4-byte boundaries.  This obviates the need to run the
separate zipalign tool, which currently does this job.

The alignment byte count can be specified with the -a option.  OTA
package signing (with -w) never does alignment.

The order of files in the output APK is changed so that all stored
files come first in the output, followed by all non-stored files.
This is not expected to have any impact in practice.

Change-Id: Iaeef89b2a7283e25fadb99c0a0f0641f682d76b8
diff --git a/tools/signapk/SignApk.java b/tools/signapk/SignApk.java
index b247072..e661e50 100644
--- a/tools/signapk/SignApk.java
+++ b/tools/signapk/SignApk.java
@@ -461,24 +461,75 @@
      * reduce variation in the output file and make incremental OTAs
      * more efficient.
      */
-    private static void copyFiles(Manifest manifest,
-                                  JarFile in, JarOutputStream out, long timestamp) throws IOException {
+    private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
+                                  long timestamp, int alignment) throws IOException {
         byte[] buffer = new byte[4096];
         int num;
 
         Map<String, Attributes> entries = manifest.getEntries();
         ArrayList<String> names = new ArrayList<String>(entries.keySet());
         Collections.sort(names);
+
+        boolean firstEntry = true;
+        long offset = 0L;
+
+        // We do the copy in two passes -- first copying all the
+        // entries that are STORED, then copying all the entries that
+        // have any other compression flag (which in practice means
+        // DEFLATED).  This groups all the stored entries together at
+        // the start of the file and makes it easier to do alignment
+        // on them (since only stored entries are aligned).
+
         for (String name : names) {
             JarEntry inEntry = in.getJarEntry(name);
             JarEntry outEntry = null;
-            if (inEntry.getMethod() == JarEntry.STORED) {
-                // Preserve the STORED method of the input entry.
-                outEntry = new JarEntry(inEntry);
-            } else {
-                // Create a new entry so that the compressed len is recomputed.
-                outEntry = new JarEntry(name);
+            if (inEntry.getMethod() != JarEntry.STORED) continue;
+            // Preserve the STORED method of the input entry.
+            outEntry = new JarEntry(inEntry);
+            outEntry.setTime(timestamp);
+
+            // 'offset' is the offset into the file at which we expect
+            // the file data to begin.  This is the value we need to
+            // make a multiple of 'alignement'.
+            offset += JarFile.LOCHDR + outEntry.getName().length();
+            if (firstEntry) {
+                // The first entry in a jar file has an extra field of
+                // four bytes that you can't get rid of; any extra
+                // data you specify in the JarEntry is appended to
+                // these forced four bytes.  This is JAR_MAGIC in
+                // JarOutputStream; the bytes are 0xfeca0000.
+                offset += 4;
+                firstEntry = false;
             }
+            if (alignment > 0 && (offset % alignment != 0)) {
+                // Set the "extra data" of the entry to between 1 and
+                // alignment-1 bytes, to make the file data begin at
+                // an aligned offset.
+                int needed = alignment - (int)(offset % alignment);
+                outEntry.setExtra(new byte[needed]);
+                offset += needed;
+            }
+
+            out.putNextEntry(outEntry);
+
+            InputStream data = in.getInputStream(inEntry);
+            while ((num = data.read(buffer)) > 0) {
+                out.write(buffer, 0, num);
+                offset += num;
+            }
+            out.flush();
+        }
+
+        // Copy all the non-STORED entries.  We don't attempt to
+        // maintain the 'offset' variable past this point; we don't do
+        // alignment on these entries.
+
+        for (String name : names) {
+            JarEntry inEntry = in.getJarEntry(name);
+            JarEntry outEntry = null;
+            if (inEntry.getMethod() == JarEntry.STORED) continue;
+            // Create a new entry so that the compressed len is recomputed.
+            outEntry = new JarEntry(name);
             outEntry.setTime(timestamp);
             out.putNextEntry(outEntry);
 
@@ -589,7 +640,7 @@
                 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
 
                 Manifest manifest = addDigestsToManifest(inputJar, hash);
-                copyFiles(manifest, inputJar, outputJar, timestamp);
+                copyFiles(manifest, inputJar, outputJar, timestamp, 0);
                 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
 
                 signFile(manifest, inputJar,
@@ -778,6 +829,7 @@
 
     private static void usage() {
         System.err.println("Usage: signapk [-w] " +
+                           "[-a <alignment>] " +
                            "[-providerClass <className>] " +
                            "publickey.x509[.pem] privatekey.pk8 " +
                            "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
@@ -794,6 +846,7 @@
         boolean signWholeFile = false;
         String providerClass = null;
         String providerArg = null;
+        int alignment = 4;
 
         int argstart = 0;
         while (argstart < args.length && args[argstart].startsWith("-")) {
@@ -806,6 +859,9 @@
                 }
                 providerClass = args[++argstart];
                 ++argstart;
+            } else if ("-a".equals(args[argstart])) {
+                alignment = Integer.parseInt(args[++argstart]);
+                ++argstart;
             } else {
                 usage();
             }
@@ -872,7 +928,7 @@
                 outputJar.setLevel(9);
 
                 Manifest manifest = addDigestsToManifest(inputJar, hashes);
-                copyFiles(manifest, inputJar, outputJar, timestamp);
+                copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
                 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
                 outputJar.close();
             }