diff --git a/src/main/java/com/reandroid/apk/ApkModule.java b/src/main/java/com/reandroid/apk/ApkModule.java index 5181403..16f92ca 100644 --- a/src/main/java/com/reandroid/apk/ApkModule.java +++ b/src/main/java/com/reandroid/apk/ApkModule.java @@ -268,7 +268,7 @@ public class ApkModule implements ApkFile { if(manifest!=null){ manifest.setSort(0); } - ZipSerializer serializer=new ZipSerializer(archive.listInputSources()); + ZipSerializer serializer=new ZipSerializer(archive.listInputSources(), new ZipAlign()); serializer.setWriteProgress(progress); serializer.setWriteInterceptor(interceptor); serializer.writeZip(file); diff --git a/src/main/java/com/reandroid/archive/APKArchive.java b/src/main/java/com/reandroid/archive/APKArchive.java index 537051b..87abca4 100644 --- a/src/main/java/com/reandroid/archive/APKArchive.java +++ b/src/main/java/com/reandroid/archive/APKArchive.java @@ -16,6 +16,7 @@ package com.reandroid.archive; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -35,17 +36,14 @@ public class APKArchive extends ZipArchive { sortApkFiles(new ArrayList<>(listInputSources())); } public long writeApk(File outApk) throws IOException{ - ZipSerializer serializer=new ZipSerializer(listInputSources()); + ZipSerializer serializer=new ZipSerializer(listInputSources(), new ZipAlign()); return serializer.writeZip(outApk); } public long writeApk(OutputStream outputStream) throws IOException{ - ZipSerializer serializer=new ZipSerializer(listInputSources()); + ZipSerializer serializer=new ZipSerializer(listInputSources(), new ZipAlign()); return serializer.writeZip(outputStream); } public static APKArchive loadZippedApk(File zipFile) throws IOException { - return loadZippedApk(new ZipFile(zipFile)); - } - public static APKArchive loadZippedApk(ZipFile zipFile) { Map entriesMap = InputSourceUtil.mapZipFileSources(zipFile); return new APKArchive(entriesMap); } diff --git a/src/main/java/com/reandroid/archive/InputSource.java b/src/main/java/com/reandroid/archive/InputSource.java index 407a001..02135d2 100644 --- a/src/main/java/com/reandroid/archive/InputSource.java +++ b/src/main/java/com/reandroid/archive/InputSource.java @@ -18,8 +18,7 @@ package com.reandroid.archive; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; +import java.util.zip.*; public abstract class InputSource { private final String name; @@ -28,67 +27,119 @@ public abstract class InputSource { private long mLength; private int method = ZipEntry.DEFLATED; private int sort; - public InputSource(String name){ + + public InputSource(String name) { this.name = name; this.alias = InputSourceUtil.sanitize(name); } - public void disposeInputSource(){ + + public void disposeInputSource() { } + public int getSort() { return sort; } + public void setSort(int sort) { this.sort = sort; } + public int getMethod() { return method; } + public void setMethod(int method) { this.method = method; } - public String getAlias(){ - if(alias!=null){ + public String getAlias() { + if (alias != null) { return alias; } return getName(); } + public void setAlias(String alias) { this.alias = alias; } + public void close(InputStream inputStream) throws IOException { inputStream.close(); } + public long write(OutputStream outputStream) throws IOException { return write(outputStream, openStream()); } - private long write(OutputStream outputStream, InputStream inputStream) throws IOException { - long result=0; - byte[] buffer=new byte[10240]; + + protected final long write(OutputStream outputStream, InputStream inputStream) throws IOException { + long result = 0; + byte[] buffer = new byte[10240]; int len; - while ((len=inputStream.read(buffer))>0){ + while ((len = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, len); - result+=len; + result += len; } close(inputStream); return result; } - public String getName(){ + + public static class WriteCompressedResult { + public long crc; + public long compressedSize; + public long uncompressedSize; + + WriteCompressedResult(long crc, long compressedSize, long uncompressedSize) { + this.crc = crc; + this.compressedSize = compressedSize; + this.uncompressedSize = uncompressedSize; + } + } + + public WriteCompressedResult writeCompressed(OutputStream outputStream) throws IOException { + CRC32 checksum = null; + Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + DeflaterOutputStream deflatedStream = new DeflaterOutputStream(outputStream, deflater); + + OutputStream targetStream; + if (mCrc == 0) { + checksum = new CRC32(); + targetStream = new CheckedOutputStream(deflatedStream, checksum); + } else { + targetStream = deflatedStream; + } + + write(targetStream); + targetStream.flush(); + deflatedStream.finish(); + + mLength = deflater.getBytesRead(); + if (checksum != null) { + mCrc = checksum.getValue(); + } + + return new WriteCompressedResult(mCrc, deflater.getBytesWritten(), mLength); + } + + public String getName() { return name; } - public long getLength() throws IOException{ - if(mLength==0){ + + public long getLength() throws IOException { + if (mLength == 0) { calculateCrc(); } return mLength; } - public long getCrc() throws IOException{ - if(mCrc==0){ + + public long getCrc() throws IOException { + if (mCrc == 0) { calculateCrc(); } return mCrc; } + public abstract InputStream openStream() throws IOException; + @Override public boolean equals(Object o) { if (this == o) { @@ -100,26 +151,29 @@ public abstract class InputSource { InputSource that = (InputSource) o; return getName().equals(that.getName()); } + @Override public int hashCode() { return getName().hashCode(); } + @Override - public String toString(){ - return getClass().getSimpleName()+": "+getName(); + public String toString() { + return getClass().getSimpleName() + ": " + getName(); } + private void calculateCrc() throws IOException { - InputStream inputStream=openStream(); - long length=0; + InputStream inputStream = openStream(); + long length = 0; CRC32 crc = new CRC32(); int bytesRead; - byte[] buffer = new byte[1024*64]; - while((bytesRead = inputStream.read(buffer)) != -1) { + byte[] buffer = new byte[1024 * 64]; + while ((bytesRead = inputStream.read(buffer)) != -1) { crc.update(buffer, 0, bytesRead); - length+=bytesRead; + length += bytesRead; } close(inputStream); - mCrc=crc.getValue(); - mLength=length; + mCrc = crc.getValue(); + mLength = length; } } diff --git a/src/main/java/com/reandroid/archive/InputSourceUtil.java b/src/main/java/com/reandroid/archive/InputSourceUtil.java index 49d283e..a8cbb16 100644 --- a/src/main/java/com/reandroid/archive/InputSourceUtil.java +++ b/src/main/java/com/reandroid/archive/InputSourceUtil.java @@ -41,23 +41,16 @@ import java.util.zip.ZipInputStream; return path; } - public static Map mapZipFileSources(ZipFile zipFile){ - Map results=new LinkedHashMap<>(); - Enumeration entriesEnum = zipFile.entries(); - int i=0; - while (entriesEnum.hasMoreElements()){ - ZipEntry zipEntry = entriesEnum.nextElement(); - if(zipEntry.isDirectory()){ - continue; - } - ZipEntrySource source=new ZipEntrySource(zipFile, zipEntry); - source.setSort(i); - source.setMethod(zipEntry.getMethod()); - results.put(source.getName(), source); - i++; - } - return results; + public static Map mapZipFileSources(File zipFile) throws IOException { + ZipDeserializer deserializer = new ZipDeserializer(zipFile); + return deserializer.mapInputSources(); } + + public static List listZipFileSources(File zipFile) throws IOException { + ZipDeserializer deserializer = new ZipDeserializer(zipFile); + return deserializer.listInputSources(); + } + public static Map mapInputStreamAsBuffer(InputStream inputStream) throws IOException { Map results = new LinkedHashMap<>(); ZipInputStream zin = new ZipInputStream(inputStream); @@ -88,21 +81,7 @@ import java.util.zip.ZipInputStream; outputStream.close(); return outputStream.toByteArray(); } - public static List listZipFileSources(ZipFile zipFile){ - List results=new ArrayList<>(); - Enumeration entriesEnum = zipFile.entries(); - int i=0; - while (entriesEnum.hasMoreElements()){ - ZipEntry zipEntry = entriesEnum.nextElement(); - if(zipEntry.isDirectory()){ - continue; - } - ZipEntrySource source=new ZipEntrySource(zipFile, zipEntry); - source.setSort(i); - results.add(source); - } - return results; - } + public static List listDirectory(File dir){ List results=new ArrayList<>(); recursiveDirectory(results, dir, dir); diff --git a/src/main/java/com/reandroid/archive/ZipAlign.java b/src/main/java/com/reandroid/archive/ZipAlign.java index 8b687e0..760ae5c 100644 --- a/src/main/java/com/reandroid/archive/ZipAlign.java +++ b/src/main/java/com/reandroid/archive/ZipAlign.java @@ -5,299 +5,58 @@ package com.reandroid.archive; -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.charset.StandardCharsets; -import java.util.ArrayList; +import java.io.*; 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; - public class ZipAlign { - private static final int ZIP_ENTRY_HEADER_LEN = 30; - private static final int ZIP_ENTRY_VERSION = 20; - private static final int ZIP_ENTRY_USES_DATA_DESCR = 0x0008; - private static final int ZIP_ENTRY_DATA_DESCRIPTOR_LEN = 16; private static final int ALIGNMENT_4 = 4; private static final int ALIGNMENT_PAGE = 4096; + private final int mAlignment; - private static class XEntry { - public final ZipEntry entry; - public final long headerOffset; - public final int flags; - public final int padding; - - public XEntry(ZipEntry entry, long headerOffset, int flags, int padding) { - this.entry = entry; - this.headerOffset = headerOffset; - this.flags = flags; - this.padding = padding; - } + public ZipAlign(int mAlignment) { + this.mAlignment = mAlignment; } - - - private static class FilterOutputStreamEx extends FilterOutputStream { - private long totalWritten = 0; - public FilterOutputStreamEx(OutputStream out) { - super(out); - } - @Override - public void write(byte[] b) throws IOException { - out.write(b); - totalWritten += b.length; - } - @Override - public void write(byte[] b, int off, int len) throws IOException { - out.write(b, off, len); - totalWritten += len; - } - @Override - public void write(int b) throws IOException { - out.write(b); - totalWritten += 1; - } - @Override - public void close() throws IOException { - super.close(); - } - public void writeInt(long v) throws IOException { - write((int) (v & 0xff)); - write((int) ((v >>> 8) & 0xff)); - write((int) ((v >>> 16) & 0xff)); - write((int) ((v >>> 24) & 0xff)); - } - public void writeShort(int v) throws IOException { - write((v) & 0xff); - write((v >>> 8) & 0xff); - } + public ZipAlign() { + this(ALIGNMENT_4); } - private File mInputFile; - private int mAlignment; - private File mOutputFile; - private ZipFile mZipFile; - private RandomAccessFile mRafInput; - private FilterOutputStreamEx mOutputStream; - private final List mXEntries = new ArrayList<>(); - private long mInputFileOffset = 0; - private int mTotalPadding = 0; - public void zipAlign(File input, File output) throws IOException { - zipAlign(input, output, ALIGNMENT_4); - } - public void zipAlign(File input, File output, int alignment) throws IOException { - mInputFile = input; - mAlignment = alignment; - mOutputFile = output; - openFiles(); - copyAllEntries(); - buildCentralDirectory(); - closeFiles(); - } - private void openFiles() throws IOException { - mZipFile = new ZipFile(mInputFile); - mRafInput = new RandomAccessFile(mInputFile, "r"); - mOutputStream = new FilterOutputStreamEx(new BufferedOutputStream(new FileOutputStream(mOutputFile), 32 * 1024)); - } - private void copyAllEntries() throws IOException { - final int entryCount = mZipFile.size(); - if (entryCount == 0) { - return; - } - final Enumeration entries = mZipFile.entries(); - while (entries.hasMoreElements()) { - final ZipEntry entry = (ZipEntry) entries.nextElement(); - final String name = entry.getName(); - - int flags = entry.getMethod() == ZipEntry.STORED ? 0 : 1 << 3; - flags |= 1 << 11; - - final long outputEntryHeaderOffset = mOutputStream.totalWritten; - - final int inputEntryHeaderSize = ZIP_ENTRY_HEADER_LEN + (entry.getExtra() != null ? entry.getExtra().length : 0) - + name.getBytes(StandardCharsets.UTF_8).length; - final long inputEntryDataOffset = mInputFileOffset + inputEntryHeaderSize; - - final int padding; - - if (entry.getMethod() != ZipEntry.STORED) { - padding = 0; - } else { - int alignment = mAlignment; - if (name.startsWith("lib/") && name.endsWith(".so")) { - alignment = ALIGNMENT_PAGE; - } - long newOffset = inputEntryDataOffset + mTotalPadding; - padding = (int) ((alignment - (newOffset % alignment)) % alignment); - mTotalPadding += padding; - } - - final XEntry xentry = new XEntry(entry, outputEntryHeaderOffset, flags, padding); - mXEntries.add(xentry); - byte[] extra = entry.getExtra(); - if (extra == null) { - extra = new byte[padding]; - } 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); - 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(StandardCharsets.UTF_8).length); - mOutputStream.writeShort(entry.getExtra().length); - mOutputStream.write(entry.getName().getBytes(StandardCharsets.UTF_8)); - mOutputStream.write(entry.getExtra(), 0, entry.getExtra().length); - - 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[32 * 1024]; - while (totalSizeCopied < sizeToCopy) { - int read = mRafInput.read(buf, 0, (int) Math.min(32 * 1024, sizeToCopy - totalSizeCopied)); - if (read <= 0) { - break; - } - mOutputStream.write(buf, 0, read); - totalSizeCopied += read; - } - } - - mInputFileOffset += sizeToCopy; - } - } - - private void buildCentralDirectory() throws IOException { - final long centralDirOffset = mOutputStream.totalWritten; - final int entryCount = mXEntries.size(); - for (int i = 0; i < entryCount; i++) { - XEntry xentry = mXEntries.get(i); - 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(StandardCharsets.UTF_8); - 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(StandardCharsets.UTF_8); - 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)); - } - } - final long centralDirSize = mOutputStream.totalWritten - centralDirOffset; - - 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 - mOutputStream.writeShort(0); - mOutputStream.flush(); - } - - private void closeFiles() throws IOException { + ZipDeserializer zipDeserializer = new ZipDeserializer(input); try { - mZipFile.close(); + ZipSerializer zipSerializer = new ZipSerializer(zipDeserializer.listInputSources(), this); + zipSerializer.writeZip(output); } finally { - try { - mRafInput.close(); - } finally { - mOutputStream.close(); + zipDeserializer.closeChannel(); + } + } + + public int alignEntry(ZipEntry entry, long dataOffset) { + String name = entry.getName(); + final int padding; + + if (entry.getMethod() != ZipEntry.STORED) { + padding = 0; + } else { + int alignment = mAlignment; + if (name.startsWith("lib/") && name.endsWith(".so")) { + alignment = ALIGNMENT_PAGE; } + padding = (int) ((alignment - (dataOffset % alignment)) % alignment); } + byte[] extra = entry.getExtra(); + if (extra == null) { + extra = new byte[padding]; + } 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); + + return padding; } public static void align4(File inFile) throws IOException{ @@ -314,12 +73,12 @@ public class ZipAlign { tmp.renameTo(inFile); } public static void align(File inFile, File outFile, int alignment) throws IOException{ - ZipAlign zipAlign=new ZipAlign(); + ZipAlign zipAlign=new ZipAlign(alignment); File dir=outFile.getParentFile(); if(dir!=null && !dir.exists()){ dir.mkdirs(); } - zipAlign.zipAlign(inFile, outFile, alignment); + zipAlign.zipAlign(inFile, outFile); } private static File toTmpFile(File file){ String name=file.getName()+".align.tmp"; diff --git a/src/main/java/com/reandroid/archive/ZipArchive.java b/src/main/java/com/reandroid/archive/ZipArchive.java index 8f8b544..22d3710 100644 --- a/src/main/java/com/reandroid/archive/ZipArchive.java +++ b/src/main/java/com/reandroid/archive/ZipArchive.java @@ -85,16 +85,12 @@ public class ZipArchive { return inputSource; } public void addArchive(File archiveFile) throws IOException { - ZipFile zipFile=new ZipFile(archiveFile); - add(zipFile); + List sourceList = InputSourceUtil.listZipFileSources(archiveFile); + this.addAll(sourceList); } public void addDirectory(File dir){ addAll(InputSourceUtil.listDirectory(dir)); } - public void add(ZipFile zipFile){ - List sourceList = InputSourceUtil.listZipFileSources(zipFile); - this.addAll(sourceList); - } public void addAll(Collection inputSourceList){ for(InputSource inputSource:inputSourceList){ add(inputSource); diff --git a/src/main/java/com/reandroid/archive/ZipDeserializer.java b/src/main/java/com/reandroid/archive/ZipDeserializer.java new file mode 100644 index 0000000..a5a60c0 --- /dev/null +++ b/src/main/java/com/reandroid/archive/ZipDeserializer.java @@ -0,0 +1,153 @@ +package com.reandroid.archive; + +import java.io.*; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; + +public class ZipDeserializer { + private final FileChannel channel; + private FilterInputStreamEx inputStream; + + public ZipDeserializer(File file) throws IOException { + this.channel = FileChannel.open(file.toPath(), StandardOpenOption.READ); + } + + public static long dataOffset(long headerOffset, ZipEntry entry) { + final byte[] extra = entry.getExtra(); + return headerOffset + ZipEntry.LOCHDR + (extra != null ? extra.length : 0) + entry.getName().getBytes(StandardCharsets.UTF_8).length; + } + + public void closeChannel() throws IOException { + channel.close(); + } + + public List listInputSources() throws IOException { + inputStream = new FilterInputStreamEx(Channels.newInputStream(channel)); + ArrayList list = new ArrayList<>(); + channel.position(channel.size() - ZipEntry.ENDHDR); + + int commentSize = 0; + while (inputStream.readInt() != ZipEntry.ENDSIG) { + channel.position(channel.position() - 5); + if (commentSize > 65535) { + throw new IOException("Could not find central directory"); + } + commentSize++; + } + + skip(ZipEntry.ENDTOT - 4); + int numRecords = inputStream.readShort(); // number of entries + skip(4); // size of central directory + long centralDirectoryOffset = inputStream.readInt(); // central directory offset. + + channel.position(centralDirectoryOffset); + int sort = 0; + for (int i = 0; i < numRecords; i++) { + ZipEntrySource inputSource = readCentralDirRecord(); + if (inputSource != null) { + inputSource.setSort(sort); + list.add(inputSource); + sort += 1; + } + } + return list; + } + + public Map mapInputSources() throws IOException { + final LinkedHashMap map = new LinkedHashMap<>(); + for (InputSource inputSource : listInputSources()) { + map.put(inputSource.getName(), inputSource); + } + return map; + } + + private void validateSignature(long actual, long expected, String recordType) throws IOException { + if (actual != expected) { + throw new IOException("Invalid " + recordType + " signature."); + } + } + + private ZipEntrySource readCentralDirRecord() throws IOException { + validateSignature(inputStream.readInt(), ZipEntry.CENSIG, "central directory"); + skip(6); // versions and general purpose bit flag. + int method = inputStream.readShort(); // compression method + skip(4); // modification date/time + long crc = inputStream.readInt(); // crc-32 + long compressedSize = inputStream.readInt(); // compressed size + long uncompressedSize = inputStream.readInt(); // uncompressed size + int nameLength = inputStream.readShort(); // name length + int extraLength = inputStream.readShort(); // extra field length (central directory) + int commentLength = inputStream.readShort(); // comment length + skip(8); // attributes and disk number. + long headerOffset = inputStream.readInt(); // location of the entry + String name = new String(inputStream.readBytes(nameLength)); // name + byte[] extra = readLocalExtraField(headerOffset, nameLength); // extra (from local entry) + skip(extraLength); // skip the extra field of the central directory. + String comment = new String(inputStream.readBytes(commentLength)); // comment + + ZipEntry entry = new ZipEntry(name); + entry.setExtra(extra); + entry.setComment(comment); + entry.setMethod(method); + entry.setCompressedSize(compressedSize); + entry.setSize(uncompressedSize); + entry.setCrc(crc); + if (entry.isDirectory()) { + return null; + } + + return new ZipEntrySource(entry, headerOffset, channel); + } + + private byte[] readLocalExtraField(long headerOffset, int nameLength) throws IOException { + long previousPosition = channel.position(); + byte[] extra; + try { + channel.position(headerOffset); + validateSignature(inputStream.readInt(), ZipEntry.LOCSIG, "local file"); + + // skip to and read the length of the extra field. + skip(ZipEntry.LOCEXT - 4); + final int extraLength = inputStream.readShort(); + + skip(nameLength); + extra = inputStream.readBytes(extraLength); + } finally { + channel.position(previousPosition); + } + return extra; + } + private void skip(long n) throws IOException { + channel.position(channel.position() + n); + } + + private static class FilterInputStreamEx extends FilterInputStream { + public FilterInputStreamEx(InputStream inputStream) { + super(inputStream); + } + + public long readInt() throws IOException { + final byte[] bytes = readBytes(4); + return ((long) (0xff & bytes[3]) << 24) | ((0xff & bytes[2]) << 16) | + ((0xff & bytes[1]) << 8) | (0xff & bytes[0]); + } + + public int readShort() throws IOException { + final byte[] bytes = readBytes(2); + return (((0xff & bytes[1]) << 8) | (bytes[0] & 0xff)); + } + + public byte[] readBytes(int n) throws IOException { + final byte[] bytes = new byte[n]; + read(bytes); + return bytes; + } + } +} diff --git a/src/main/java/com/reandroid/archive/ZipEntrySource.java b/src/main/java/com/reandroid/archive/ZipEntrySource.java index 41bc096..8cec8a4 100644 --- a/src/main/java/com/reandroid/archive/ZipEntrySource.java +++ b/src/main/java/com/reandroid/archive/ZipEntrySource.java @@ -15,22 +15,89 @@ */ package com.reandroid.archive; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.WritableByteChannel; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; public class ZipEntrySource extends InputSource { - private final ZipFile zipFile; private final ZipEntry zipEntry; - public ZipEntrySource(ZipFile zipFile, ZipEntry zipEntry){ + private final FileChannel channel; + private final long headerOffset; + private long dataOffset; + + public ZipEntrySource(ZipEntry zipEntry, long headerOffset, FileChannel channel) { super(zipEntry.getName()); - this.zipFile=zipFile; - this.zipEntry=zipEntry; + this.zipEntry = zipEntry; super.setMethod(zipEntry.getMethod()); + this.channel = channel; + this.headerOffset = headerOffset; + this.dataOffset = ZipDeserializer.dataOffset(headerOffset, zipEntry); } + + @Override + public long getLength() { + return zipEntry.getSize(); + } + + @Override + public long getCrc() { + return zipEntry.getCrc(); + } + + private InputStream rawStream() { + return new InputStream() { + private int cursor = 0; + @Override + public int read(byte[] b, int off, int len) throws IOException { + long remaining = zipEntry.getCompressedSize() - cursor; + + if (len == 0) { + return 0; + } + if (len < 0 || off < 0 || (len + off) < b.length) { + throw new IOException("Invalid length/offset."); + } + if (remaining == 0) { + return -1; + } + + len = (int) Math.min(remaining, len); + int n = channel.read(ByteBuffer.wrap(b).position(off).limit(off + len), dataOffset + cursor); + cursor += n; + return n; + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + int n = read(b); + if (n == 1) + return b[0] & 0xff; + return -1; + } + }; + } + @Override public InputStream openStream() throws IOException { - return zipFile.getInputStream(zipEntry); + InputStream stream = rawStream(); + switch (zipEntry.getMethod()) { + case ZipEntry.DEFLATED: + return new InflaterInputStream(stream, new Inflater(true)); + case ZipEntry.STORED: + return stream; + default: + throw new IOException("Unsupported compression method: " + zipEntry.getMethod()); + } + } + + public WriteCompressedResult writeCompressed(OutputStream outputStream) throws IOException { + write(outputStream, rawStream()); + return new WriteCompressedResult(getCrc(), zipEntry.getCompressedSize(), getLength()); } } diff --git a/src/main/java/com/reandroid/archive/ZipSerializer.java b/src/main/java/com/reandroid/archive/ZipSerializer.java index f462ecb..9fe73ae 100644 --- a/src/main/java/com/reandroid/archive/ZipSerializer.java +++ b/src/main/java/com/reandroid/archive/ZipSerializer.java @@ -12,20 +12,81 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * + * + * This class contains code from "apksigner" and I couldn't find the + * original repo/author to credit here. */ package com.reandroid.archive; import java.io.*; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.*; + + public class ZipSerializer { + public static final int ZIP_ENTRY_VERSION = 20; + public static final int ZIP_ENTRY_USES_DATA_DESCR = 0x0008; + + private static class XEntry { + public final ZipEntry entry; + public final long headerOffset; + public final int flags; + public final int padding; + + public XEntry(ZipEntry entry, long headerOffset, int flags, int padding) { + this.entry = entry; + this.headerOffset = headerOffset; + this.flags = flags; + this.padding = padding; + } + } + + private static class FilterOutputStreamEx extends FilterOutputStream { + private long totalWritten = 0; + public FilterOutputStreamEx(OutputStream out) { + super(out); + } + @Override + public void write(byte[] b) throws IOException { + out.write(b); + totalWritten += b.length; + } + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + totalWritten += len; + } + @Override + public void write(int b) throws IOException { + out.write(b); + totalWritten += 1; + } + @Override + public void close() throws IOException { + super.close(); + } + public void writeInt(long v) throws IOException { + write((int) (v & 0xff)); + write((int) ((v >>> 8) & 0xff)); + write((int) ((v >>> 16) & 0xff)); + write((int) ((v >>> 24) & 0xff)); + } + public void writeShort(int v) throws IOException { + write((v) & 0xff); + write((v >>> 8) & 0xff); + } + } -public class ZipSerializer { private final List mSourceList; + private final List mEntries = new ArrayList<>(); + private final ZipAlign zipAlign; private WriteProgress writeProgress; private WriteInterceptor writeInterceptor; - public ZipSerializer(List sourceList){ + public ZipSerializer(List sourceList, ZipAlign zipAlign){ this.mSourceList=sourceList; + this.zipAlign = zipAlign; } public void setWriteInterceptor(WriteInterceptor writeInterceptor) { @@ -40,9 +101,9 @@ public class ZipSerializer { dir.mkdirs(); } File tmp=toTmpFile(outZip); - FileOutputStream fileOutputStream=new FileOutputStream(tmp); - long length= writeZip(fileOutputStream); - fileOutputStream.close(); + OutputStream outputStream=new BufferedOutputStream(new FileOutputStream(tmp), 32 * 1024); + long length= writeZip(outputStream); + outputStream.close(); outZip.delete(); tmp.renameTo(outZip); return length; @@ -53,41 +114,185 @@ public class ZipSerializer { return new File(dir, name); } public long writeZip(OutputStream outputStream) throws IOException{ - long length=0; WriteProgress progress=writeProgress; - ZipOutputStream zipOutputStream=new ZipOutputStream(outputStream); + FilterOutputStreamEx zipOutputStream=new FilterOutputStreamEx(outputStream); for(InputSource inputSource:mSourceList){ inputSource = interceptWrite(inputSource); if(inputSource==null){ continue; } if(progress!=null){ - progress.onCompressFile(inputSource.getAlias(), inputSource.getMethod(), length); + progress.onCompressFile(inputSource.getAlias(), inputSource.getMethod(), zipOutputStream.totalWritten); } - length+=write(zipOutputStream, inputSource); - zipOutputStream.closeEntry(); + write(zipOutputStream, inputSource); inputSource.disposeInputSource(); } + buildCentralDirectory(zipOutputStream); zipOutputStream.close(); - return length; + return zipOutputStream.totalWritten; } - private long write(ZipOutputStream zipOutputStream, InputSource inputSource) throws IOException{ - ZipEntry zipEntry=createZipEntry(inputSource); - zipOutputStream.putNextEntry(zipEntry); - return inputSource.write(zipOutputStream); + + private void writeDescriptor(FilterOutputStreamEx mOutputStream, ZipEntry entry) throws IOException{ + if (entry.getCompressedSize() != -1) { + mOutputStream.writeInt(entry.getCrc()); + mOutputStream.writeInt(entry.getCompressedSize()); + mOutputStream.writeInt(entry.getSize()); + } else { + mOutputStream.write(new byte[12]); + } + } + + private void write(FilterOutputStreamEx mOutputStream, InputSource inputSource) throws IOException{ + final ZipEntry entry=createZipEntry(inputSource); + + int flags = 1 << 11; + if (entry.getMethod() != ZipEntry.STORED) { + flags |= ZIP_ENTRY_USES_DATA_DESCR; + } + + final long outputEntryHeaderOffset = mOutputStream.totalWritten; + final long outputEntryDataOffset = ZipDeserializer.dataOffset(outputEntryHeaderOffset, entry); + int padding = 0; + if (zipAlign != null) { + padding = zipAlign.alignEntry(entry, outputEntryDataOffset); + } + if (entry.getExtra() == null) { + entry.setExtra(new byte[0]); + } + + final XEntry xentry = new XEntry(entry, outputEntryHeaderOffset, flags, padding); + mEntries.add(xentry); + + 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); + + writeDescriptor(mOutputStream, entry); + + mOutputStream.writeShort(entry.getName().getBytes(StandardCharsets.UTF_8).length); + mOutputStream.writeShort(entry.getExtra().length); + mOutputStream.write(entry.getName().getBytes(StandardCharsets.UTF_8)); + mOutputStream.write(entry.getExtra()); + + if (entry.getMethod() == ZipEntry.STORED) { + inputSource.write(mOutputStream); + } else { + InputSource.WriteCompressedResult result = inputSource.writeCompressed(mOutputStream); + entry.setCompressedSize(result.compressedSize); + entry.setSize(result.uncompressedSize); + entry.setCrc(result.crc); + } + + if ((flags & ZIP_ENTRY_USES_DATA_DESCR) != 0) { + mOutputStream.writeInt(ZipEntry.EXTSIG); + writeDescriptor(mOutputStream, entry); + } } private ZipEntry createZipEntry(InputSource inputSource) throws IOException { String name=inputSource.getAlias(); ZipEntry zipEntry=new ZipEntry(name); + int method = inputSource.getMethod(); zipEntry.setMethod(method); - if(method==ZipEntry.STORED){ + if (method == ZipEntry.STORED) { + long length = inputSource.getLength(); zipEntry.setCrc(inputSource.getCrc()); - zipEntry.setSize(inputSource.getLength()); + zipEntry.setSize(length); + zipEntry.setCompressedSize(length); } return zipEntry; } - private InputSource interceptWrite(InputSource inputSource){ + private void buildCentralDirectory(FilterOutputStreamEx mOutputStream) throws IOException { + final long centralDirOffset = mOutputStream.totalWritten; + final int entryCount = mEntries.size(); + for (int i = 0; i < entryCount; i++) { + XEntry xentry = mEntries.get(i); + 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(StandardCharsets.UTF_8); + 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(StandardCharsets.UTF_8); + 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)); + } + } + final long centralDirSize = mOutputStream.totalWritten - centralDirOffset; + + 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 + mOutputStream.writeShort(0); + mOutputStream.flush(); + } + + private InputSource interceptWrite(InputSource inputSource){ WriteInterceptor interceptor=writeInterceptor; if(interceptor!=null){ return interceptor.onWriteArchive(inputSource);