From 26f00cda4b979d7e74db6872990682335b36612b Mon Sep 17 00:00:00 2001 From: Alex Klyubin Date: Mon, 23 May 2016 14:19:51 -0700 Subject: [PATCH] Store entry alignment information in APK. Data of uncompressed APK entries is often aligned to a multiple of 4 or 4096 in the APK to make it easier to mmap the data. Unfortunately, the current method for achieving alignment suffers from two issues: (1) the way it uses the Local File Header extra field is not compliant with ZIP format (for example, this prevents older versions of Python's zipfile from reading APKs: https://bugs.python.org/issue14315), and (2) it does not store information about the alignment multiple in the APK, making it harder/impossible to preserve the intended alignment when rearranging entries in the APK. This change solves these issues by switching to a different method for aligning data of uncompressed APK entries. Same as before, alignment is achieved using Local File Header entry field. What's different is that alignment is achieved by placing a well-formed extensible data field/block into the extra field. The new field/block contains the alignment multiple (e.g., 4 or 4096) as well as the necessary padding (if any). Compared to the original alignment method, the new method uses 6 more bytes for each uncompressed entry. Bug: 27461702 Change-Id: I8cffbecc50bf634b28fca5bc39eb23f671961cf9 --- .../src/com/android/signapk/SignApk.java | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java index 1df6b80ce2..a7c9fc3f30 100644 --- a/tools/signapk/src/com/android/signapk/SignApk.java +++ b/tools/signapk/src/com/android/signapk/SignApk.java @@ -52,6 +52,7 @@ import java.io.OutputStream; import java.io.PrintStream; import java.lang.reflect.Constructor; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.DigestOutputStream; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; @@ -119,6 +120,19 @@ class SignApk { private static final String OTACERT_NAME = "META-INF/com/android/otacert"; + /** + * Extensible data block/field header ID used for storing information about alignment of + * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section + * 4.5 Extensible data fields. + */ + private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; + + /** + * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed + * entries. + */ + private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; + // bitmasks for which hash algorithms we need the manifest to include. private static final int USE_SHA1 = 1; private static final int USE_SHA256 = 2; @@ -588,28 +602,40 @@ class SignApk { outEntry.setComment(null); outEntry.setExtra(null); - // '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'. + int alignment = getStoredEntryDataAlignment(name, defaultAlignment); + // Alignment of the entry's data is achieved by adding a data block to the entry's Local + // File Header extra field. The data block contains information about the alignment + // value and the necessary padding bytes (0x00) to achieve the alignment. This works + // because the entry's data will be located immediately after the extra field. + // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format + // of the extra field. + + // 'offset' is the offset into the file at which we expect the entry's data to begin. + // This is the value we need to make a multiple of 'alignment'. 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. + // 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. + // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540 + // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619. offset += 4; firstEntry = false; } - int alignment = getStoredEntryDataAlignment(name, defaultAlignment); - 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; + int extraPaddingSizeBytes = 0; + if (alignment > 0) { + long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; + extraPaddingSizeBytes = alignment - (int) (paddingStartOffset % alignment); } + byte[] extra = + new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes]; + ByteBuffer extraBuf = ByteBuffer.wrap(extra); + extraBuf.order(ByteOrder.LITTLE_ENDIAN); + extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID + extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size + extraBuf.putShort((short) alignment); + outEntry.setExtra(extra); + offset += extra.length; out.putNextEntry(outEntry);