perf: copy raw data from original apk when saving

This commit is contained in:
Ax333l 2023-03-24 22:04:14 +01:00
parent 2eff5832d7
commit b3ecac25d6
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
9 changed files with 587 additions and 376 deletions

View File

@ -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);

View File

@ -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<String, InputSource> entriesMap = InputSourceUtil.mapZipFileSources(zipFile);
return new APKArchive(entriesMap);
}

View File

@ -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;
}
}

View File

@ -41,23 +41,16 @@ import java.util.zip.ZipInputStream;
return path;
}
public static Map<String, InputSource> mapZipFileSources(ZipFile zipFile){
Map<String, InputSource> results=new LinkedHashMap<>();
Enumeration<? extends ZipEntry> 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<String, InputSource> mapZipFileSources(File zipFile) throws IOException {
ZipDeserializer deserializer = new ZipDeserializer(zipFile);
return deserializer.mapInputSources();
}
public static List<InputSource> listZipFileSources(File zipFile) throws IOException {
ZipDeserializer deserializer = new ZipDeserializer(zipFile);
return deserializer.listInputSources();
}
public static Map<String, ByteInputSource> mapInputStreamAsBuffer(InputStream inputStream) throws IOException {
Map<String, ByteInputSource> 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<InputSource> listZipFileSources(ZipFile zipFile){
List<InputSource> results=new ArrayList<>();
Enumeration<? extends ZipEntry> 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<InputSource> listDirectory(File dir){
List<InputSource> results=new ArrayList<>();
recursiveDirectory(results, dir, dir);

View File

@ -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<XEntry> 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";

View File

@ -85,16 +85,12 @@ public class ZipArchive {
return inputSource;
}
public void addArchive(File archiveFile) throws IOException {
ZipFile zipFile=new ZipFile(archiveFile);
add(zipFile);
List<InputSource> sourceList = InputSourceUtil.listZipFileSources(archiveFile);
this.addAll(sourceList);
}
public void addDirectory(File dir){
addAll(InputSourceUtil.listDirectory(dir));
}
public void add(ZipFile zipFile){
List<InputSource> sourceList = InputSourceUtil.listZipFileSources(zipFile);
this.addAll(sourceList);
}
public void addAll(Collection<? extends InputSource> inputSourceList){
for(InputSource inputSource:inputSourceList){
add(inputSource);

View File

@ -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<InputSource> listInputSources() throws IOException {
inputStream = new FilterInputStreamEx(Channels.newInputStream(channel));
ArrayList<InputSource> 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<String, InputSource> mapInputSources() throws IOException {
final LinkedHashMap<String, InputSource> 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;
}
}
}

View File

@ -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());
}
}

View File

@ -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<InputSource> mSourceList;
private final List<XEntry> mEntries = new ArrayList<>();
private final ZipAlign zipAlign;
private WriteProgress writeProgress;
private WriteInterceptor writeInterceptor;
public ZipSerializer(List<InputSource> sourceList){
public ZipSerializer(List<InputSource> 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);