zipalign-java/ZipAlign.java

823 lines
22 KiB
Java

/*
* Copyright (C) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
import org.apache.commons.io.IOUtils;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* ZipAlign.
* <p>
* This file is ported from <a href=
* "https://android.googlesource.com/platform/build/+/master/tools/zipalign/"
* >AOSP's ZipAlign</a> tool.
* </p>
* <p>
* <h1>Quote from original README</h1>
* </p>
* <p>
*
* <pre>
* The purpose of zipalign is to ensure that all uncompressed data starts
* with a particular alignment relative to the start of the file. This
* allows those portions to be accessed directly with mmap() even if they
* contain binary data with alignment restrictions.
*
* Some data needs to be word-aligned for easy access, others might benefit
* from being page-aligned. The adjustment is made by altering the size of
* the "extra" field in the zip Local File Header sections. Existing data
* in the "extra" fields may be altered by this process.
*
* Compressed data isn't very useful until it's uncompressed, so there's no
* need to adjust its alignment.
*
* Alterations to the archive, such as renaming or deleting entries, will
* potentially disrupt the alignment of the modified entry and all later
* entries. Files added to an "aligned" archive will not be aligned.
* </pre>
*
* </p>
* <p>
* <h1>Notes</h1>
* </p>
* <p>
* <ul>
* <li>The tool modifies the "extra" field of all entries which are not
* compressed ({@link ZipEntry#STORED}).</li>
*
* <li>Only the "extra" fields in local file headers are modified. The ones in
* central directory are not touched.</li>
* </ul>
* </p>
* <p>
* See <a href="http://en.wikipedia.org/wiki/Zip_(file_format)">Zip (file
* format) - Wikipedia</a> for further information..
* </p>
*
* @author Hai Bison
* @since v1.6.9 beta
*/
class ZipAlign {
/**
* The minimum size of a ZIP entry's header.
*/
public static final int ZIP_ENTRY_HEADER_LEN = 30;
/**
* Default version to work with ZIP files.
*/
public static final int ZIP_ENTRY_VERSION = 20;
/**
* The offset of extra field length in a ZIP entry's header.
*/
public static final int ZIP_ENTRY_OFFSET_EXTRA_LEN = 28;
/**
* The size of field extra length, in a ZIP entry's header.
*/
public static final int ZIP_ENTRY_FIELD_EXTRA_LEN_SIZE = 2;
/**
* @see <a
* href="https://android.googlesource.com/platform/build/+/master/tools/zipalign/ZipEntry.h">ZipEntry.h</a>
*/
public static final int ZIP_ENTRY_USES_DATA_DESCR = 0x0008;
/**
* @see <a
* href="https://android.googlesource.com/platform/build/+/master/tools/zipalign/ZipEntry.h">ZipEntry.h</a>
*/
public static final int ZIP_ENTRY_DATA_DESCRIPTOR_LEN = 16;
/**
* Default alignment value.
* <p>
* See <a
* href="http://developer.android.com/tools/help/zipalign.html">zipalign
* </a>.
* </p>
*/
public static final int DEFAULT_ALIGNMENT = 4;
/**
* Used to append to newly aligned APK's file name.
*/
public static final String ALIGNED = "ALIGNED";
/**
* Private helper class.
*
* @author Hai Bison
* @since v1.6.9 beta
*/
private static class XEntry {
public final ZipEntry entry;
public final long headerOffset;
public final int flags;
public final int padding;
/**
* Creates new instance.
*
* @param entry the entry.
* @param headerOffset the offset of the header.
* @param flags the flags.
* @param padding the padding of the "extra" field.
*/
public XEntry(ZipEntry entry, long headerOffset, int flags, int padding) {
this.entry = entry;
this.headerOffset = headerOffset;
this.flags = flags;
this.padding = padding;
}// XEntry()
}// XEntry
/**
* Extended class of {@link FilterOutputStream}, which has some helper
* methods for writing data to ZIP stream.
*
* @author Hai Bison
* @since v1.6.9 beta
*/
private static class FilterOutputStreamEx extends FilterOutputStream {
private long totalWritten = 0;
/**
* Creates new instance.
*
* @param out {@link OutputStream}.
*/
public FilterOutputStreamEx(OutputStream out) {
super(out);
}// FilterOutputStreamEx()
@Override
public void write(byte[] b) throws IOException {
out.write(b);
totalWritten += b.length;
}// write()
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
totalWritten += len;
}// write()
@Override
public void write(int b) throws IOException {
out.write(b);
totalWritten += 1;
}// write()
@Override
public void close() throws IOException {
// l("\t\tclose() >> totalWritten = %,d", totalWritten);
super.close();
}// close()
/**
* Writes a 32-bit int to the output stream in little-endian byte order.
*
* @param v the data to write.
* @throws IOException
*/
public void writeInt(long v) throws IOException {
write((int) ((v >>> 0) & 0xff));
write((int) ((v >>> 8) & 0xff));
write((int) ((v >>> 16) & 0xff));
write((int) ((v >>> 24) & 0xff));
}// writeInt()
/**
* Writes a 16-bit short to the output stream in little-endian byte
* order.
*
* @param v the data to write.
* @throws IOException
*/
public void writeShort(int v) throws IOException {
write((v >>> 0) & 0xff);
write((v >>> 8) & 0xff);
}// writeShort()
}// FilterOutputStreamEx
/**
* To align ZIP files :-)
*
* @author Hai Bison
* @since v1.6.9 beta
*/
public static class ZipAligner {
private final File mInputFile;
private final int mAlignment;
private final File mOutputFile;
private final List<XEntry> mXEntries = new ArrayList<XEntry>();
private ZipFile mZipFile;
private RandomAccessFile mRafInput;
private FilterOutputStreamEx mOutputStream;
private long mInputFileOffset = 0;
private int mTotalPadding = 0;
/**
* Creates new instance with alignment value of
* {@link ZipAlign#DEFAULT_ALIGNMENT}.
*
* @param input the input file.
* @param output the output file.
*/
public ZipAligner(File input, File output) {
this(input, DEFAULT_ALIGNMENT, output);
}
/**
* Creates new instance.
*
* @param input the input file.
* @param alignment the alignment, {@link ZipAlign#DEFAULT_ALIGNMENT} is
* highly recommended.
* @param output the output file.
*/
public ZipAligner(File input, int alignment, File output) {
mInputFile = input;
mAlignment = alignment;
mOutputFile = output;
}
public void run() {
try {
mZipFile = new ZipFile(mInputFile);
mRafInput = new RandomAccessFile(mInputFile, "r");
mOutputStream = new FilterOutputStreamEx(new BufferedOutputStream(
new FileOutputStream(mOutputFile),
Files.FILE_BUFFER
));
copyAllEntries();
buildCentralDirectory();
} catch (Exception exception) {
throw new RuntimeException(exception);
} finally {
IOUtils.closeQuietly(mZipFile);
IOUtils.closeQuietly(mRafInput);
IOUtils.closeQuietly(mOutputStream);
}
}
/**
* Copies all entries, aligning them if needed.
* <p>
* This takes 80% of total.
* </p>
*
* @throws IOException
*/
private void copyAllEntries() throws IOException {
final int entryCount = mZipFile.size();
if (entryCount == 0) {
// sendNotification(MSG_INFO, mProgress += 80);
return;
}
final float progress = 80f / entryCount;
final Enumeration<? extends ZipEntry> entries = mZipFile.entries();
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
int flags = entry.getMethod() == ZipEntry.STORED ? 0 : 1 << 3;
flags |= 1 << 11;
final long outputEntryHeaderOffset = mOutputStream.totalWritten;
// if (Sys.DEBUG)
// L.d("\t\toutputEntryHeaderOffset = %,d",
// outputEntryHeaderOffset);
final int inputEntryHeaderSize = ZIP_ENTRY_HEADER_LEN
+ (entry.getExtra() != null ? entry.getExtra().length
: 0)
+ entry.getName().getBytes(Texts.UTF8).length;
final long inputEntryDataOffset = mInputFileOffset
+ inputEntryHeaderSize;
// sendNotification(
// MSG_INFO,
// Texts.NULL,
// String.format("%,15d %s", inputEntryDataOffset,
// entry.getName()));
final int padding;
if (entry.getMethod() != ZipEntry.STORED) {
/*
* The entry is compressed, copy it without padding.
*/
padding = 0;
} else {
/*
* Copy the entry, adjusting as required. We assume that the
* file position in the new file will be equal to the file
* position in the original.
*/
long newOffset = inputEntryDataOffset + mTotalPadding;
// if (Sys.DEBUG)
// L.d("\t\t\tnewOffset = %,d", newOffset);
padding = (int) ((mAlignment - (newOffset % mAlignment)) % mAlignment);
mTotalPadding += padding;
}
final XEntry xentry = new XEntry(entry,
outputEntryHeaderOffset, flags, padding);
mXEntries.add(xentry);
// if (Sys.DEBUG)
// L.d("\t'%s' >> header = %,d, padding = %,d",
// entry.getName(), inputEntryHeaderSize, padding);
/*
* Modify the original header, add padding to `extra` field and
* copy it to output.
*/
byte[] extra = entry.getExtra();
if (extra == null) {
extra = new byte[padding];
Arrays.fill(extra, (byte) 0);
} else {
byte[] newExtra = new byte[extra.length + padding];
System.arraycopy(extra, 0, newExtra, 0, extra.length);
Arrays.fill(newExtra, extra.length, newExtra.length,
(byte) 0);
extra = newExtra;
}
entry.setExtra(extra);
/*
* Now write the header to output.
*/
mOutputStream.writeInt(ZipOutputStream.LOCSIG);
mOutputStream.writeShort(ZIP_ENTRY_VERSION);
mOutputStream.writeShort(flags);
mOutputStream.writeShort(entry.getMethod());
int modDate;
int time;
GregorianCalendar cal = new GregorianCalendar();
cal.setTime(new Date(entry.getTime()));
int year = cal.get(Calendar.YEAR);
if (year < 1980) {
modDate = 0x21;
time = 0;
} else {
modDate = cal.get(Calendar.DATE);
modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate;
modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate;
time = cal.get(Calendar.SECOND) >> 1;
time = (cal.get(Calendar.MINUTE) << 5) | time;
time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time;
}
mOutputStream.writeShort(time);
mOutputStream.writeShort(modDate);
mOutputStream.writeInt(entry.getCrc());
mOutputStream.writeInt(entry.getCompressedSize());
mOutputStream.writeInt(entry.getSize());
mOutputStream
.writeShort(entry.getName().getBytes(Texts.UTF8).length);
mOutputStream.writeShort(entry.getExtra().length);
mOutputStream.write(entry.getName().getBytes(Texts.UTF8));
mOutputStream.write(entry.getExtra(), 0,
entry.getExtra().length);
/*
* Copy raw data.
*/
mInputFileOffset += inputEntryHeaderSize;
final long sizeToCopy;
if ((flags & ZIP_ENTRY_USES_DATA_DESCR) != 0)
sizeToCopy = (entry.isDirectory() ? 0 : entry
.getCompressedSize())
+ ZIP_ENTRY_DATA_DESCRIPTOR_LEN;
else
sizeToCopy = entry.isDirectory() ? 0 : entry
.getCompressedSize();
if (sizeToCopy > 0) {
mRafInput.seek(mInputFileOffset);
long totalSizeCopied = 0;
final byte[] buf = new byte[Files.FILE_BUFFER];
while (totalSizeCopied < sizeToCopy) {
int read = mRafInput.read(
buf,
0,
(int) Math.min(Files.FILE_BUFFER, sizeToCopy
- totalSizeCopied));
if (read <= 0)
break;
mOutputStream.write(buf, 0, read);
totalSizeCopied += read;
}// while
}// if
mInputFileOffset += sizeToCopy;
// if (padding == 0)
// sendNotification(MSG_INFO, mProgress += progress,
// Texts.NULL, String.format(" (%s, %s)\n",
// Messages.getString(R.string.compressed),
// Messages.getString(R.string.passed)));
// else
// sendNotification(
// MSG_INFO,
// mProgress += progress,
// Texts.NULL,
// String.format(" (%s, %s)\n",
// Messages.getString(R.string.aligned),
// Texts.sizeToStr(padding)));
}// while
}// copyAllEntries()
/**
* Builds central directory.
* <p>
* This takes 10% of total.
* </p>
*
* @throws IOException
*/
private void buildCentralDirectory() throws IOException {
final long centralDirOffset = mOutputStream.totalWritten;
// L.d("\tWriting Central Directory at %,d", centralDirOffset);
for (XEntry xentry : mXEntries) {
/*
* Write entry.
*/
final ZipEntry entry = xentry.entry;
int modDate;
int time;
GregorianCalendar cal = new GregorianCalendar();
cal.setTime(new Date(entry.getTime()));
int year = cal.get(Calendar.YEAR);
if (year < 1980) {
modDate = 0x21;
time = 0;
} else {
modDate = cal.get(Calendar.DATE);
modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate;
modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate;
time = cal.get(Calendar.SECOND) >> 1;
time = (cal.get(Calendar.MINUTE) << 5) | time;
time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time;
}
mOutputStream.writeInt(ZipFile.CENSIG); // CEN header signature
mOutputStream.writeShort(ZIP_ENTRY_VERSION); // version made by
mOutputStream.writeShort(ZIP_ENTRY_VERSION); // version needed
// to
// extract
mOutputStream.writeShort(xentry.flags); // general purpose bit
// flag
mOutputStream.writeShort(entry.getMethod()); // compression
// method
mOutputStream.writeShort(time);
mOutputStream.writeShort(modDate);
mOutputStream.writeInt(entry.getCrc()); // crc-32
mOutputStream.writeInt(entry.getCompressedSize()); // compressed
// size
mOutputStream.writeInt(entry.getSize()); // uncompressed size
final byte[] nameBytes = entry.getName().getBytes(Texts.UTF8);
mOutputStream.writeShort(nameBytes.length);
mOutputStream.writeShort(entry.getExtra() != null ? entry
.getExtra().length - xentry.padding : 0);
final byte[] commentBytes;
if (entry.getComment() != null) {
commentBytes = entry.getComment().getBytes(Texts.UTF8);
mOutputStream.writeShort(Math.min(commentBytes.length,
0xffff));
} else {
commentBytes = null;
mOutputStream.writeShort(0);
}
mOutputStream.writeShort(0); // starting disk number
mOutputStream.writeShort(0); // internal file attributes
// (unused)
mOutputStream.writeInt(0); // external file attributes (unused)
mOutputStream.writeInt(xentry.headerOffset); // relative offset
// of
// local
// header
mOutputStream.write(nameBytes);
if (entry.getExtra() != null)
mOutputStream.write(entry.getExtra(), 0,
entry.getExtra().length - xentry.padding);
if (commentBytes != null)
mOutputStream.write(commentBytes, 0,
Math.min(commentBytes.length, 0xffff));
}// for xentry
// sendNotification(MSG_INFO, mProgress += 5);
/*
* Write the end of central directory.
*/
final long centralDirSize = mOutputStream.totalWritten
- centralDirOffset;
// L.d("\tWriting End of Central Directory, its size = %,d",
// centralDirSize);
final int entryCount = mXEntries.size();
mOutputStream.writeInt(ZipFile.ENDSIG); // END record signature
mOutputStream.writeShort(0); // number of this disk
mOutputStream.writeShort(0); // central directory start disk
mOutputStream.writeShort(entryCount); // number of directory entries
// on
// disk
mOutputStream.writeShort(entryCount); // total number of directory
// entries
mOutputStream.writeInt(centralDirSize); // length of central
// directory
mOutputStream.writeInt(centralDirOffset); // offset of central
// directory
if (mZipFile.getComment() != null) { // zip file comment
final byte[] bytes = mZipFile.getComment().getBytes(Texts.UTF8);
mOutputStream.writeShort(bytes.length);
mOutputStream.write(bytes);
} else {
mOutputStream.writeShort(0);
}
mOutputStream.flush();
// sendNotification(MSG_INFO, mProgress += 5);
}
}
/**
* The ZIP alignment verifier.
*
* @author Hai Bison
* @since v1.6.9 beta
*/
public static class ZipAlignmentVerifier {
private final File mInputFile;
private final int mAlignment;
private ZipFile mZipFile;
private RandomAccessFile mRafInput;
/**
* 0 >> 100
*/
private double mProgress = 0;
private boolean mFoundBad = false;
/**
* Creates new instance with alignment value of
* {@link ZipAlign#DEFAULT_ALIGNMENT}.
*
* @param inputFile the input file to verify.
*/
public ZipAlignmentVerifier(File inputFile) {
this(inputFile, DEFAULT_ALIGNMENT);
}// ZipAlignmentVerifier()
/**
* Creates new instance.
*
* @param inputFile the input file.
* @param alignment the alignment, {@link ZipAlign#DEFAULT_ALIGNMENT} is
* highly recommended.
*/
public ZipAlignmentVerifier(File inputFile, int alignment) {
mInputFile = inputFile;
mAlignment = alignment;
}// ZipAlignmentVerifier()
public void run() {
// L.d("%s >> starting", ZipAlignmentVerifier.class.getSimpleName());
try {
openFiles();
verify();
} catch (Exception e) {
mFoundBad = true;
// sendNotification(
// MSG_ERROR,
// Texts.NULL,
// Messages.getString(R.string.pmsg_error_details,
// e.getMessage(), L.printStackTrace(e)));
} finally {
try {
closeFiles();
} catch (Exception e) {
mFoundBad = true;
// sendNotification(
// MSG_ERROR,
// Texts.NULL,
// Messages.getString(R.string.pmsg_error_details,
// e.getMessage(), L.printStackTrace(e)));
}
}
// sendNotification(MSG_ERROR, Texts.NULL,
// Messages.getString(R.string.cancelled));
//
// sendNotification(MSG_DONE);
//
// L.d("%s >> finishing", ZipAlignmentVerifier.class.getSimpleName());
}// run()
/**
* Opens files.
* <p>
* This takes 5% of total.
* </p>
*
* @throws IOException
*/
private void openFiles() throws IOException {
// sendNotification(MSG_INFO, Texts.NULL, String.format("%s\n\n",
// Messages.getString(
// R.string.pmsg_verifying_alignment_of_apk,
// mInputFile.getName(), mAlignment)));
mZipFile = new ZipFile(mInputFile);
mRafInput = new RandomAccessFile(mInputFile, "r");
// sendNotification(MSG_INFO, mProgress = 5);
}// openFiles()
/**
* Verifies input file.
* <p>
* This takes 90% of total.
* </p>
*
* @throws IOException
*/
private void verify() throws IOException {
final int entryCount = mZipFile.size();
if (entryCount == 0) {
// sendNotification(MSG_INFO, mProgress += 90);
return;
}
final Enumeration<? extends ZipEntry> entries = mZipFile.entries();
final float progress = 90f / entryCount;
long dataOffset = 0;
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
mRafInput.seek(dataOffset + ZIP_ENTRY_OFFSET_EXTRA_LEN);
final byte[] buf = new byte[ZIP_ENTRY_FIELD_EXTRA_LEN_SIZE];
if (mRafInput.read(buf) != buf.length) {
mFoundBad = true;
throw new IOException("Reading extra field length failed");
}
/*
* Fetches unsigned 16-bit value from byte array at specified
* offset. The bytes are assumed to be in Intel (little-endian)
* byte order.
*/
final int extraLen = (buf[0] & 0xff) | ((buf[1] & 0xff) << 8);
final int headerSize = ZIP_ENTRY_HEADER_LEN + extraLen
+ entry.getName().getBytes(Texts.UTF8).length;
if (entry.getMethod() != ZipEntry.STORED) {
/*
* The entry is compressed.
*/
// sendNotification(
// MSG_INFO,
// mProgress += progress,
// Texts.NULL,
// String.format("%,15d %s (%s - %s)\n", dataOffset
// + headerSize, entry.getName(),
// Messages.getString(R.string.ok),
// Messages.getString(R.string.compressed)));
} else {
/*
* The entry is not compressed.
*/
if ((dataOffset + headerSize) % mAlignment != 0) {
// sendNotification(
// MSG_INFO,
// mProgress += progress,
// Texts.NULL,
// String.format(
// "%,15d %s (%s - %s)\n",
// dataOffset + headerSize,
// entry.getName(),
// Messages.getString(R.string.BAD),
// Texts.sizeToStr((dataOffset + headerSize)
// % mAlignment)));
mFoundBad = true;
} else {
// sendNotification(
// MSG_INFO,
// mProgress += progress,
// Texts.NULL,
// String.format("%,15d %s (%s)\n", dataOffset
// + headerSize, entry.getName(),
// Messages.getString(R.string.ok)));
}
}
int flags = entry.getMethod() == ZipEntry.STORED ? 0 : 1 << 3;
flags |= 1 << 11;
final long dataSize;
if ((flags & ZIP_ENTRY_USES_DATA_DESCR) != 0)
dataSize = (entry.isDirectory() ? 0 : entry
.getCompressedSize())
+ ZIP_ENTRY_DATA_DESCRIPTOR_LEN;
else
dataSize = entry.isDirectory() ? 0 : entry
.getCompressedSize();
// if (Sys.DEBUG)
// L.d("size = %,8d, compressed = %,8d, crc32 = %08x, data mHeaderOffset = %,8d >> %,8d"
// + " >> Entry '%s'", entry.getSize(),
// entry.getCompressedSize(), entry.getCrc(),
// dataOffset, dataOffset + headerSize,
// entry.getName());
dataOffset += headerSize + dataSize;
}// while
}// verify()
/**
* Closes source files.
* <p>
* This takes 5% of total.
* </p>
*
* @throws IOException
*/
private void closeFiles() throws IOException {
mZipFile.close();
mRafInput.close();
// sendNotification(
// MSG_INFO,
// mProgress = 100,
// Texts.NULL,
// String.format(
// "\n%s",
// mFoundBad ? Messages
// .getString(R.string.verification_failed)
// : Messages
// .getString(R.string.verification_succesful)));
}// closeFiles()
}
static class Texts {
public static final String UTF8 = "UTF-8";
}
static class Files {
/**
* File handling buffer (reading, writing...). {@code 32 KiB}.
*/
public static final int FILE_BUFFER = 32 * 1024;
}
}