1. Optimize patch speed

This commit is contained in:
ZaneYork 2021-02-10 10:25:01 +08:00
parent 3763f7757f
commit b9cc00adbc
13 changed files with 378 additions and 194 deletions

View File

@ -138,6 +138,7 @@
-keep class pxb.android.** { *; } -keep class pxb.android.** { *; }
-keep class net.fornwall.apksigner.** { *; } -keep class net.fornwall.apksigner.** { *; }
-keep class com.android.apksig.** { *; } -keep class com.android.apksig.** { *; }
-keep class com.fansin.message.tool.core.BufferedRandomAccessFile { *; }
#Warning:org.bouncycastle.jce.provider.X509LDAPCertStoreSpi: can't find referenced class javax.naming.NamingEnumeration #Warning:org.bouncycastle.jce.provider.X509LDAPCertStoreSpi: can't find referenced class javax.naming.NamingEnumeration
-dontwarn javax.naming.** -dontwarn javax.naming.**
@ -162,27 +163,27 @@ public static java.lang.String TABLENAME;
# If you do NOT use RxJava: # If you do NOT use RxJava:
-dontwarn rx.** -dontwarn rx.**
-keep class com.sun.org.apache.xml.internal.utils.PrefixResolver -dontwarn com.sun.org.apache.xml.internal.utils.PrefixResolver
-keep class java.rmi.Remote -dontwarn java.rmi.Remote
-keep class java.rmi.server.* -dontwarn java.rmi.server.*
-keep class javax.annotation.processing.AbstractProcessor -dontwarn javax.annotation.processing.AbstractProcessor
-keep class javax.el.* -dontwarn javax.el.*
-keep class javax.servlet.http.* -dontwarn javax.servlet.http.*
-keep class javax.servlet.jsp.el.VariableResolver -dontwarn javax.servlet.jsp.el.VariableResolver
-keep class javax.servlet.jsp.* -dontwarn javax.servlet.jsp.*
-keep class javax.servlet.jsp.tagext.* -dontwarn javax.servlet.jsp.tagext.*
-keep class javax.servlet.* -dontwarn javax.servlet.*
-keep class javax.swing.JTree -dontwarn javax.swing.JTree
-keep class javax.swing.tree.TreeNode -dontwarn javax.swing.tree.TreeNode
-keep class lombok.core.configuration.ConfigurationKey -dontwarn lombok.core.configuration.ConfigurationKey
-keep class org.apache.tools.ant.Task -dontwarn org.apache.tools.ant.Task
-keep class org.apache.tools.ant.taskdefs.MatchingTask -dontwarn org.apache.tools.ant.taskdefs.MatchingTask
-keep class org.apache.xml.utils.PrefixResolver -dontwarn org.apache.xml.utils.PrefixResolver
-keep class org.jaxen.dom.* -dontwarn org.jaxen.dom.*
-keep class org.jaxen.dom4j.Dom4jXPath -dontwarn org.jaxen.dom4j.Dom4jXPath
-keep class org.jaxen.jdom.JDOMXPath -dontwarn org.jaxen.jdom.JDOMXPath
-keep class org.jaxen.* -dontwarn org.jaxen.*
-keep class org.jdom.output.XMLOutputter -dontwarn org.jdom.output.XMLOutputter
-keep class org.python.core.PyObject -dontwarn org.python.core.PyObject
-keep class org.python.util.PythonInterpreter -dontwarn org.python.util.PythonInterpreter
-keep class org.zeroturnaround.javarebel.ClassEventListener -dontwarn org.zeroturnaround.javarebel.ClassEventListener

View File

@ -20,17 +20,17 @@ import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils; import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.AndroidBinXmlParser; import com.android.apksig.internal.apk.AndroidBinXmlParser;
import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier; import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources; import com.android.apksig.util.DataSources;
import com.android.apksig.util.RunnablesExecutor; import com.android.apksig.util.RunnablesExecutor;
import com.android.apksig.zip.ZipFormatException; import com.android.apksig.zip.ZipFormatException;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -47,6 +47,10 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
/** /**
* APK signature verifier which mimics the behavior of the Android platform. * APK signature verifier which mimics the behavior of the Android platform.
@ -211,36 +215,17 @@ public class ApkVerifier {
// include v2 and/or v3 signatures. If none is found, it falls back to JAR signature // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature
// verification. If the signature is found but does not verify, the APK is rejected. // verification. If the signature is found but does not verify, the APK is rejected.
Set<Integer> foundApkSigSchemeIds = new HashSet<>(2); Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
FutureTask<ApkSigningBlockUtils.Result> taskV2 = new FutureTask<>(() -> {
if (maxSdkVersion >= AndroidSdkVersion.N) { if (maxSdkVersion >= AndroidSdkVersion.N) {
RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
// Android P and newer attempts to verify APKs using APK Signature Scheme v3
if (maxSdkVersion >= AndroidSdkVersion.P) {
try {
ApkSigningBlockUtils.Result v3Result =
V3SchemeVerifier.verify(
executor,
apk,
zipSections,
Math.max(minSdkVersion, AndroidSdkVersion.P),
maxSdkVersion);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
result.mergeFrom(v3Result);
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
// v3 signature not required
}
if (result.containsErrors()) {
return result;
}
}
// Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P
// ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or
// APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if
// no APK Signature Scheme v3 (or newer scheme) signatures were found. // no APK Signature Scheme v3 (or newer scheme) signatures were found.
if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) { if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) {
try { try {
ApkSigningBlockUtils.Result v2Result = RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
V2SchemeVerifier.verify( return V2SchemeVerifier.verify(
executor, executor,
apk, apk,
zipSections, zipSections,
@ -248,17 +233,13 @@ public class ApkVerifier {
foundApkSigSchemeIds, foundApkSigSchemeIds,
Math.max(minSdkVersion, AndroidSdkVersion.N), Math.max(minSdkVersion, AndroidSdkVersion.N),
maxSdkVersion); maxSdkVersion);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
result.mergeFrom(v2Result);
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
// v2 signature not required // v2 signature not required
} }
if (result.containsErrors()) {
return result;
} }
} }
} return null;
});
// Android O and newer requires that APKs targeting security sandbox version 2 and higher // Android O and newer requires that APKs targeting security sandbox version 2 and higher
// are signed using APK Signature Scheme v2 or newer. // are signed using APK Signature Scheme v2 or newer.
if (maxSdkVersion >= AndroidSdkVersion.O) { if (maxSdkVersion >= AndroidSdkVersion.O) {
@ -280,16 +261,27 @@ public class ApkVerifier {
// ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures. // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
// Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
// scheme) signatures were found. // scheme) signatures were found.
FutureTask<V1SchemeVerifier.Result> taskV1 = new FutureTask<>(() -> {
if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) { if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
V1SchemeVerifier.Result v1Result = return V1SchemeVerifier.verify(
V1SchemeVerifier.verify(
apk, apk,
zipSections, zipSections,
supportedSchemeNames, supportedSchemeNames,
foundApkSigSchemeIds, foundApkSigSchemeIds,
minSdkVersion, minSdkVersion,
maxSdkVersion); maxSdkVersion);
result.mergeFrom(v1Result); }
return null;
});
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
executorService.submit(taskV1);
executorService.submit(taskV2);
executorService.shutdown();
try {
result.mergeFrom(taskV1.get());
result.mergeFrom(taskV2.get());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
} }
if (result.containsErrors()) { if (result.containsErrors()) {
return result; return result;
@ -632,6 +624,9 @@ public class ApkVerifier {
} }
private void mergeFrom(V1SchemeVerifier.Result source) { private void mergeFrom(V1SchemeVerifier.Result source) {
if(source == null) {
return;
}
mVerifiedUsingV1Scheme = source.verified; mVerifiedUsingV1Scheme = source.verified;
mErrors.addAll(source.getErrors()); mErrors.addAll(source.getErrors());
mWarnings.addAll(source.getWarnings()); mWarnings.addAll(source.getWarnings());
@ -644,6 +639,9 @@ public class ApkVerifier {
} }
private void mergeFrom(ApkSigningBlockUtils.Result source) { private void mergeFrom(ApkSigningBlockUtils.Result source) {
if(source == null) {
return;
}
switch (source.signatureSchemeVersion) { switch (source.signatureSchemeVersion) {
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
mVerifiedUsingV2Scheme = source.verified; mVerifiedUsingV2Scheme = source.verified;

View File

@ -32,8 +32,8 @@ import com.android.apksig.internal.util.TeeDataSink;
import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSinks; import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor; import com.android.apksig.util.RunnablesExecutor;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -53,7 +53,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.StreamSupport; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* Default implementation of {@link ApkSignerEngine}. * Default implementation of {@link ApkSignerEngine}.
@ -432,21 +433,25 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections = Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult); V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm); String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
for (Map.Entry<String, ManifestParser.Section> entry: sections.getSecond().entrySet()) { Stream<Map.Entry<String, ManifestParser.Section>> entryStream;
String entryName = entry.getKey(); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) && entryStream = sections.getSecond().entrySet().parallelStream();
isDebuggable(entryName)) { }
else {
entryStream = sections.getSecond().entrySet().stream();
}
entryStream.filter(entry->V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) &&
isDebuggable(entry.getKey()) && entryNames.contains(entry.getKey())).forEach(entry->{
Optional<V1SchemeVerifier.NamedDigest> extractedDigest = Optional<V1SchemeVerifier.NamedDigest> extractedDigest =
V1SchemeVerifier.getDigestsToVerify( V1SchemeVerifier.getDigestsToVerify(
entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE).stream() entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE).stream()
.filter(d -> d.jcaDigestAlgorithm == alg) .filter(d -> d.jcaDigestAlgorithm.equals(alg))
.findFirst(); .findFirst();
extractedDigest.ifPresent( extractedDigest.ifPresent(
namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest)); namedDigest -> mOutputJarEntryDigests.put(entry.getKey(), namedDigest.digest));
}
} });
return mOutputJarEntryDigests.keySet(); return mOutputJarEntryDigests.keySet();
} }

View File

@ -50,14 +50,15 @@ public class ChainedDataSource implements DataSource {
} }
for (DataSource src : mSources) { for (DataSource src : mSources) {
long srcSize = src.size();
// Offset is beyond the current source. Skip. // Offset is beyond the current source. Skip.
if (offset >= src.size()) { if (offset >= srcSize) {
offset -= src.size(); offset -= srcSize;
continue; continue;
} }
// If the remaining is enough, finish it. // If the remaining is enough, finish it.
long remaining = src.size() - offset; long remaining = srcSize - offset;
if (remaining >= size) { if (remaining >= size) {
src.feed(offset, size, sink); src.feed(offset, size, sink);
break; break;

View File

@ -12,6 +12,9 @@ import android.util.Log;
import com.android.apksig.ApkSigner; import com.android.apksig.ApkSigner;
import com.android.apksig.ApkVerifier; import com.android.apksig.ApkVerifier;
import com.android.apksig.DefaultApkSignerEngine;
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSources;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker; import com.google.common.base.Ticker;
@ -23,6 +26,7 @@ import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants; import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.constant.DialogAction; import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.constant.ManifestPatchConstants; import com.zane.smapiinstaller.constant.ManifestPatchConstants;
import com.zane.smapiinstaller.dto.Tuple2;
import com.zane.smapiinstaller.entity.ApkFilesManifest; import com.zane.smapiinstaller.entity.ApkFilesManifest;
import com.zane.smapiinstaller.entity.ManifestEntry; import com.zane.smapiinstaller.entity.ManifestEntry;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
@ -39,6 +43,7 @@ import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.RandomAccessFile;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@ -46,6 +51,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@ -79,8 +85,12 @@ public class ApkPatcher {
private final AtomicInteger switchAction = new AtomicInteger(); private final AtomicInteger switchAction = new AtomicInteger();
private Tuple2<byte[], Set<String>> originSignInfo = null;
private final List<Consumer<Integer>> progressListener = new ArrayList<>(); private final List<Consumer<Integer>> progressListener = new ArrayList<>();
private int lastProgress = -1;
private final Stopwatch stopwatch = Stopwatch.createUnstarted(new Ticker() { private final Stopwatch stopwatch = Stopwatch.createUnstarted(new Ticker() {
@Override @Override
public long read() { public long read() {
@ -89,6 +99,7 @@ public class ApkPatcher {
}); });
public ApkPatcher(Context context) { public ApkPatcher(Context context) {
this.lastProgress = -1;
this.context = context; this.context = context;
} }
@ -120,14 +131,6 @@ public class ApkPatcher {
File apkFile = new File(sourceDir); File apkFile = new File(sourceDir);
String stadewValleyBasePath = FileUtils.getStadewValleyBasePath(); String stadewValleyBasePath = FileUtils.getStadewValleyBasePath();
File dest = new File(stadewValleyBasePath + "/SMAPI Installer/");
if (!dest.exists()) {
if (!dest.mkdir()) {
errorMessage.set(String.format(context.getString(R.string.error_failed_to_create_file), dest.getAbsolutePath()));
return null;
}
}
File distFile = new File(dest, apkFile.getName());
if (advancedStage == 0) { if (advancedStage == 0) {
AtomicInteger count = new AtomicInteger(); AtomicInteger count = new AtomicInteger();
ZipUtil.unpack(apkFile, new File(stadewValleyBasePath + "/StardewValley/"), name -> { ZipUtil.unpack(apkFile, new File(stadewValleyBasePath + "/StardewValley/"), name -> {
@ -140,7 +143,7 @@ public class ApkPatcher {
} }
return null; return null;
}); });
return distFile.getAbsolutePath(); return apkFile.getAbsolutePath();
} else if (advancedStage == 1) { } else if (advancedStage == 1) {
File contentFolder = new File(stadewValleyBasePath + "/StardewValley/Content"); File contentFolder = new File(stadewValleyBasePath + "/StardewValley/Content");
if (contentFolder.exists()) { if (contentFolder.exists()) {
@ -151,16 +154,11 @@ public class ApkPatcher {
} else { } else {
extract(0); extract(0);
} }
ZipUtils.removeEntries(sourceDir, "assets/Content", distFile.getAbsolutePath(), (progress) -> emitProgress((int) (progress * 0.05))); return apkFile.getAbsolutePath();
} else {
Files.copy(apkFile, distFile);
} }
emitProgress(5); emitProgress(5);
return distFile.getAbsolutePath(); return apkFile.getAbsolutePath();
} catch (PackageManager.NameNotFoundException ignored) { } catch (PackageManager.NameNotFoundException ignored) {
} catch (IOException e) {
Log.e(TAG, "Extract error", e);
errorMessage.set(e.getLocalizedMessage());
return null; return null;
} }
} }
@ -172,10 +170,11 @@ public class ApkPatcher {
* 将指定APK文件重新打包添加SMAPI修改AndroidManifest.xml同时验证版本是否正确 * 将指定APK文件重新打包添加SMAPI修改AndroidManifest.xml同时验证版本是否正确
* *
* @param apkPath APK文件路径 * @param apkPath APK文件路径
* @param targetFile 目标文件
* @param isAdvanced 是否高级模式 * @param isAdvanced 是否高级模式
* @return 是否成功打包 * @return 是否成功打包
*/ */
public boolean patch(String apkPath, boolean isAdvanced) { public boolean patch(String apkPath, File targetFile, boolean isAdvanced) {
if (apkPath == null) { if (apkPath == null) {
return false; return false;
} }
@ -205,17 +204,13 @@ public class ApkPatcher {
.filter(Objects::nonNull).flatMap(Stream::of).distinct().collect(Collectors.toList()); .filter(Objects::nonNull).flatMap(Stream::of).distinct().collect(Collectors.toList());
entries.add(new ZipUtils.ZipEntrySource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED)); entries.add(new ZipUtils.ZipEntrySource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED));
emitProgress(10); emitProgress(10);
String patchedFilename = apkPath + ".patched";
File patchedFile = new File(patchedFilename);
int baseProgress = 10; int baseProgress = 10;
stopwatch.reset(); stopwatch.reset();
stopwatch.start(); stopwatch.start();
ZipUtils.addOrReplaceEntries(apkPath, entries, patchedFilename, originSignInfo = ZipUtils.addOrReplaceEntries(apkPath, entries, targetFile.getAbsolutePath(),
isAdvanced ? (entryName) -> entryName.startsWith("assets/Content") : null,
(progress) -> emitProgress((int) (baseProgress + (progress / 100.0) * 35))); (progress) -> emitProgress((int) (baseProgress + (progress / 100.0) * 35)));
stopwatch.stop(); stopwatch.stop();
emitProgress(45);
FileUtils.forceDelete(file);
FileUtils.moveFile(patchedFile, file);
emitProgress(46); emitProgress(46);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
@ -403,21 +398,29 @@ public class ApkPatcher {
String alias = ks.aliases().nextElement(); String alias = ks.aliases().nextElement();
X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias); X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias);
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray()); PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray());
ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder("debug", privateKey, Collections.singletonList(publicKey)).build();
emitProgress(49); emitProgress(49);
File outputFile = new File(signApkPath); File outputFile = new File(signApkPath);
ApkSigner signer = new ApkSigner.Builder(Collections.singletonList(signerConfig)) List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs = Collections.singletonList(
.setInputApk(new File(apkPath)) new DefaultApkSignerEngine.SignerConfig.Builder(
.setOutputApk(outputFile) "debug",
privateKey,
Collections.singletonList(publicKey))
.build());
DefaultApkSignerEngine signerEngine = new DefaultApkSignerEngine.Builder(engineSignerConfigs, 19)
.setV1SigningEnabled(true) .setV1SigningEnabled(true)
.setV2SigningEnabled(true).build(); .setV2SigningEnabled(true)
.setV3SigningEnabled(false)
.build();
if(originSignInfo != null && originSignInfo.getFirst() != null) {
signerEngine.initWith(originSignInfo.getFirst(), originSignInfo.getSecond());
}
long zipOpElapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); long zipOpElapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
stopwatch.reset(); stopwatch.reset();
Thread thread = new Thread(() -> { Thread thread = new Thread(() -> {
stopwatch.start(); stopwatch.start();
while (true) { while (true) {
try { try {
Thread.sleep(20); Thread.sleep(200);
long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
double progress = elapsed * 0.98 / zipOpElapsed; double progress = elapsed * 0.98 / zipOpElapsed;
if (progress < 1.0) { if (progress < 1.0) {
@ -429,7 +432,13 @@ public class ApkPatcher {
} }
}); });
thread.start(); thread.start();
try(RandomAccessFile inputApkFile = new RandomAccessFile(apkPath, "r")) {
ApkSigner signer = new ApkSigner.Builder(signerEngine)
.setInputApk(DataSources.asDataSource(inputApkFile, 0, inputApkFile.length()))
.setOutputApk(outputFile)
.build();
signer.sign(); signer.sign();
}
FileUtils.forceDelete(new File(apkPath)); FileUtils.forceDelete(new File(apkPath));
ApkVerifier.Result result = new ApkVerifier.Builder(outputFile).build().verify(); ApkVerifier.Result result = new ApkVerifier.Builder(outputFile).build().verify();
if (thread.isAlive() && !thread.isInterrupted()) { if (thread.isAlive() && !thread.isInterrupted()) {
@ -518,10 +527,13 @@ public class ApkPatcher {
} }
private void emitProgress(int progress) { private void emitProgress(int progress) {
if(lastProgress < progress) {
lastProgress = progress;
for (Consumer<Integer> consumer : progressListener) { for (Consumer<Integer> consumer : progressListener) {
consumer.accept(progress); consumer.accept(progress);
} }
} }
}
public void registerProgressListener(Consumer<Integer> listener) { public void registerProgressListener(Consumer<Integer> listener) {
progressListener.add(listener); progressListener.add(listener);

View File

@ -39,11 +39,13 @@ import com.zane.smapiinstaller.utils.ZipUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.ZipUtil;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.channels.Channels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -298,8 +300,8 @@ public class CommonLogic {
if (entry.isXALZ()) { if (entry.isXALZ()) {
bytes = ZipUtils.decompressXALZ(bytes); bytes = ZipUtils.decompressXALZ(bytes);
} }
try (FileOutputStream stream = FileUtils.openOutputStream(targetFile)) { try (FileOutputStream outputStream = FileUtils.openOutputStream(targetFile)) {
stream.write(bytes); ByteStreams.copy(Channels.newChannel(new ByteArrayInputStream(bytes)), outputStream.getChannel());
} catch (IOException ignore) { } catch (IOException ignore) {
} }
} else { } else {
@ -312,7 +314,7 @@ public class CommonLogic {
if (entry.isExternal() && apkFilesManifest != null) { if (entry.isExternal() && apkFilesManifest != null) {
byte[] bytes = FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath()); byte[] bytes = FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath());
try (FileOutputStream outputStream = new FileOutputStream(targetFile)) { try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
outputStream.write(bytes); ByteStreams.copy(Channels.newChannel(new ByteArrayInputStream(bytes)), outputStream.getChannel());
} catch (IOException ignored) { } catch (IOException ignored) {
} }
} else { } else {
@ -358,7 +360,7 @@ public class CommonLogic {
} }
} }
try (FileOutputStream outputStream = new FileOutputStream(targetFile)) { try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
ByteStreams.copy(inputStream, outputStream); ByteStreams.copy(Channels.newChannel(inputStream), outputStream.getChannel());
} }
} catch (IOException e) { } catch (IOException e) {
Log.e("COMMON", "Copy Error", e); Log.e("COMMON", "Copy Error", e);

View File

@ -119,7 +119,15 @@ public class InstallFragment extends Fragment {
patcher.registerProgressListener((progress) -> DialogUtils.setProgressDialogState(binding.getRoot(), dialog, null, progress)); patcher.registerProgressListener((progress) -> DialogUtils.setProgressDialogState(binding.getRoot(), dialog, null, progress));
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.extracting_package, null); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.extracting_package, null);
String path = patcher.extract(isAdv ? 1 : -1); String path = patcher.extract(isAdv ? 1 : -1);
if (path == null) { String stadewValleyBasePath = FileUtils.getStadewValleyBasePath();
File dest = new File(stadewValleyBasePath + "/SMAPI Installer/");
boolean failed = path == null;
if (!dest.exists()) {
if (!dest.mkdir()) {
failed = true;
}
}
if (failed) {
DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found))); DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found)));
return; return;
} }
@ -132,7 +140,8 @@ public class InstallFragment extends Fragment {
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, 6); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, 6);
modAssetsManager.installDefaultMods(); modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8);
if (!patcher.patch(path, isAdv)) { File targetApk = new File(dest, "base.apk");
if (!patcher.patch(path, targetApk, isAdv)) {
int target = patcher.getSwitchAction().getAndSet(0); int target = patcher.getSwitchAction().getAndSet(0);
if (target == R.string.menu_download) { if (target == R.string.menu_download) {
DialogUtils.showConfirmDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)), R.string.menu_download, R.string.cancel, (d, which) -> { DialogUtils.showConfirmDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)), R.string.menu_download, R.string.cancel, (d, which) -> {
@ -147,7 +156,7 @@ public class InstallFragment extends Fragment {
return; return;
} }
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null);
String signPath = patcher.sign(path); String signPath = patcher.sign(targetApk.getAbsolutePath());
if (signPath == null) { if (signPath == null) {
DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game))); DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return; return;

View File

@ -0,0 +1,88 @@
package com.zane.smapiinstaller.utils;
import android.os.Build;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
/**
* @author Zane
*/
public class MultiprocessingUtil {
public final static ExecutorService EXECUTOR_SERVICE = getExecutorService();
public static ExecutorService getExecutorService() {
int processors = Runtime.getRuntime().availableProcessors();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new ForkJoinPool(processors, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
return new ThreadPoolExecutor(processors, processors,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().build());
}
public static <T> TaskBundle<T> newTaskBundle(Consumer<T> onResult) {
LinkedBlockingQueue<T> futureResults = new LinkedBlockingQueue<>();
AtomicLong taskCount = new AtomicLong(1);
CountDownLatch doneLatch = new CountDownLatch(1);
EXECUTOR_SERVICE.submit(() -> {
while (true) {
try {
T result = futureResults.poll(50, TimeUnit.MILLISECONDS);
if (result != null) {
onResult.accept(result);
} else {
if (taskCount.get() == 0) {
if (futureResults.isEmpty()) {
doneLatch.countDown();
return;
}
}
}
} catch (InterruptedException ignored) {
doneLatch.countDown();
return;
}
}
});
return new TaskBundle<>(taskCount, doneLatch, futureResults);
}
@Data
@AllArgsConstructor
public static class TaskBundle<T> {
private AtomicLong taskCount;
private CountDownLatch doneLatch;
private LinkedBlockingQueue<T> futureResults;
public void submitTask(Supplier<T> task) {
EXECUTOR_SERVICE.submit(() -> {
taskCount.incrementAndGet();
try {
futureResults.add(task.get());
} finally {
taskCount.decrementAndGet();
}
});
}
@SneakyThrows
public void join() {
taskCount.decrementAndGet();
doneLatch.await();
}
}
}

View File

@ -3,6 +3,8 @@ package com.zane.smapiinstaller.utils;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.zane.smapiinstaller.dto.Tuple2;
import net.fornwall.apksigner.zipio.ZioEntry; import net.fornwall.apksigner.zipio.ZioEntry;
import net.fornwall.apksigner.zipio.ZipInput; import net.fornwall.apksigner.zipio.ZipInput;
@ -10,7 +12,6 @@ import net.fornwall.apksigner.zipio.ZipOutput;
import net.jpountz.lz4.LZ4Factory; import net.jpountz.lz4.LZ4Factory;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.io.Streams;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
@ -18,9 +19,12 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -36,7 +40,7 @@ public class ZipUtils {
public static byte[] decompressXALZ(byte[] bytes) { public static byte[] decompressXALZ(byte[] bytes) {
if (bytes == null) { if (bytes == null) {
return bytes; return new byte[0];
} }
if (FILE_HEADER_XALZ.equals(new String(ByteUtils.subArray(bytes, 0, 4), StandardCharsets.ISO_8859_1))) { if (FILE_HEADER_XALZ.equals(new String(ByteUtils.subArray(bytes, 0, 4), StandardCharsets.ISO_8859_1))) {
byte[] length = ByteUtils.subArray(bytes, 8, 12); byte[] length = ByteUtils.subArray(bytes, 8, 12);
@ -46,52 +50,96 @@ public class ZipUtils {
return bytes; return bytes;
} }
public static void addOrReplaceEntries(String inputZipFilename, List<ZipEntrySource> entrySources, String outputZipFilename, Consumer<Integer> progressCallback) throws IOException { public static Tuple2<byte[], Set<String>> addOrReplaceEntries(String inputZipFilename, List<ZipEntrySource> entrySources, String outputZipFilename, Function<String, Boolean> removePredict, Consumer<Integer> progressCallback) throws IOException {
File inFile = new File(inputZipFilename).getCanonicalFile(); File inFile = new File(inputZipFilename).getCanonicalFile();
File outFile = new File(outputZipFilename).getCanonicalFile(); File outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile)) { if (inFile.equals(outFile)) {
throw new IllegalArgumentException("Input and output files are the same"); throw new IllegalArgumentException("Input and output files are the same");
} }
ImmutableMap<String, ZipEntrySource> entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath); ImmutableMap<String, ZipEntrySource> entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath);
byte[] originManifest = null;
ConcurrentHashMap<String, Boolean> originEntryName = new ConcurrentHashMap<>();
try (ZipInput input = new ZipInput(inputZipFilename)) { try (ZipInput input = new ZipInput(inputZipFilename)) {
int size = input.entries.values().size(); int size = input.entries.values().size();
int index = 0; AtomicLong count = new AtomicLong();
int reportInterval = size / 100; int reportInterval = size / 100;
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) { try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
HashSet<String> replacedFileSet = new HashSet<>(entryMap.size()); ConcurrentHashMap<String, Boolean> replacedFileSet = new ConcurrentHashMap<>(entryMap.size());
MultiprocessingUtil.TaskBundle<ZioEntry> taskBundle = MultiprocessingUtil.newTaskBundle((zioEntry) -> {
try {
zipOutput.write(zioEntry);
long index = count.incrementAndGet();
if (index % reportInterval == 0) {
progressCallback.accept((int) (index * 95.0 / size));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
ZioEntry manifest = input.entries.get("META-INF/MANIFEST.MF");
if(manifest != null) {
originManifest = manifest.getData();
}
for (ZioEntry inEntry : input.entries.values()) { for (ZioEntry inEntry : input.entries.values()) {
if (removePredict != null && removePredict.apply(inEntry.getName())) {
continue;
}
taskBundle.submitTask(()->{
if (entryMap.containsKey(inEntry.getName())) { if (entryMap.containsKey(inEntry.getName())) {
ZipEntrySource source = entryMap.get(inEntry.getName()); ZipEntrySource source = entryMap.get(inEntry.getName());
ZioEntry zioEntry = new ZioEntry(inEntry.getName()); ZioEntry zioEntry = new ZioEntry(inEntry.getName());
zioEntry.setCompression(source.getCompressionMethod()); zioEntry.setCompression(source.getCompressionMethod());
try (InputStream inputStream = source.getDataStream()) { try (InputStream inputStream = source.getDataStream()) {
Streams.pipeAll(inputStream, zioEntry.getOutputStream()); if (inputStream != null) {
ByteStreams.copy(inputStream, zioEntry.getOutputStream());
} }
zipOutput.write(zioEntry); } catch (IOException e) {
replacedFileSet.add(inEntry.getName()); throw new RuntimeException(e);
}
replacedFileSet.put(inEntry.getName(), true);
return zioEntry;
} else { } else {
zipOutput.write(inEntry); originEntryName.put(inEntry.getName(), true);
return inEntry;
} }
index++; });
if (index % reportInterval == 0) {
progressCallback.accept((int) (index * 95.0 / size));
} }
taskBundle.join();
Sets.SetView<String> difference = Sets.difference(entryMap.keySet(), replacedFileSet.keySet());
count.set(0);
taskBundle = MultiprocessingUtil.newTaskBundle((zioEntry) -> {
try {
zipOutput.write(zioEntry);
long index = count.incrementAndGet();
progressCallback.accept(95 + (int) (index * 5.0 / difference.size()));
} catch (IOException e) {
throw new RuntimeException(e);
} }
Sets.SetView<String> difference = Sets.difference(entryMap.keySet(), replacedFileSet); });
index = 0;
for (String name : difference) { for (String name : difference) {
taskBundle.submitTask(()-> {
ZipEntrySource source = entryMap.get(name); ZipEntrySource source = entryMap.get(name);
ZioEntry zioEntry = new ZioEntry(name); ZioEntry zioEntry = new ZioEntry(name);
zioEntry.setCompression(source.getCompressionMethod()); zioEntry.setCompression(source.getCompressionMethod());
try (InputStream inputStream = source.getDataStream()) { try (InputStream inputStream = source.getDataStream()) {
Streams.pipeAll(inputStream, zioEntry.getOutputStream()); ByteStreams.copy(inputStream, zioEntry.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
} }
zipOutput.write(zioEntry); return zioEntry;
progressCallback.accept(95 + (int) (index * 5.0 / difference.size())); });
} }
taskBundle.join();
progressCallback.accept(100); progressCallback.accept(100);
} }
} }
catch (RuntimeException e) {
if(e.getCause() != null && e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
throw e;
}
return new Tuple2<>(originManifest, originEntryName.keySet());
} }
public static void removeEntries(String inputZipFilename, String prefix, String outputZipFilename, Consumer<Integer> progressCallback) throws IOException { public static void removeEntries(String inputZipFilename, String prefix, String outputZipFilename, Consumer<Integer> progressCallback) throws IOException {
@ -134,11 +182,8 @@ public class ZipUtils {
} }
private InputStream getDataStream() { private InputStream getDataStream() {
// Optimize: read only once
if (dataSupplier != null) { if (dataSupplier != null) {
InputStream bytes = dataSupplier.get(); return dataSupplier.get();
dataSupplier = null;
return bytes;
} }
return null; return null;
} }

View File

@ -15,8 +15,10 @@
*/ */
package net.fornwall.apksigner.zipio; package net.fornwall.apksigner.zipio;
import com.google.common.io.ByteSource;
import com.google.common.io.FileBackedOutputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -50,6 +52,7 @@ public final class ZioEntry implements Cloneable {
private int localHeaderOffset; private int localHeaderOffset;
private long dataPosition = -1; private long dataPosition = -1;
private byte[] data = null; private byte[] data = null;
private ByteSource dataSource = null;
private ZioEntryOutputStream entryOut = null; private ZioEntryOutputStream entryOut = null;
private static byte[] alignBytes = new byte[4]; private static byte[] alignBytes = new byte[4];
@ -207,8 +210,9 @@ public final class ZioEntry implements Cloneable {
if (entryOut != null) { if (entryOut != null) {
entryOut.close(); entryOut.close();
size = entryOut.getSize(); size = entryOut.getSize();
data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray(); // data = ((FileBackedOutputStream) entryOut.wrapped).toByteArray();
compressedSize = data.length; dataSource = ((FileBackedOutputStream) entryOut.wrapped).asByteSource();
compressedSize = (int) dataSource.size();
crc32 = entryOut.getCRC(); crc32 = entryOut.getCRC();
} }
@ -252,6 +256,10 @@ public final class ZioEntry implements Cloneable {
if (data != null) { if (data != null) {
output.writeBytes(data); output.writeBytes(data);
} else if(dataSource != null){
try (InputStream inputStream = dataSource.openStream()){
output.pipeStream(inputStream);
}
} else { } else {
zipInput.seek(dataPosition); zipInput.seek(dataPosition);
@ -282,7 +290,7 @@ public final class ZioEntry implements Cloneable {
byte[] tmpdata = new byte[size]; byte[] tmpdata = new byte[size];
InputStream din = getInputStream(); try(InputStream din = getInputStream()) {
int count = 0; int count = 0;
while (count != size) { while (count != size) {
@ -294,17 +302,19 @@ public final class ZioEntry implements Cloneable {
} }
return tmpdata; return tmpdata;
} }
}
// Returns an input stream for reading the entry's data. // Returns an input stream for reading the entry's data.
public InputStream getInputStream() throws IOException { public InputStream getInputStream() throws IOException {
if (entryOut != null) { if (entryOut != null) {
entryOut.close(); entryOut.close();
size = entryOut.getSize(); size = entryOut.getSize();
data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray(); // data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray();
dataSource = ((FileBackedOutputStream) entryOut.wrapped).asByteSource();
compressedSize = data.length; compressedSize = data.length;
crc32 = entryOut.getCRC(); crc32 = entryOut.getCRC();
entryOut = null; entryOut = null;
InputStream rawis = new ByteArrayInputStream(data); InputStream rawis = dataSource.openStream();
if (compression == 0) if (compression == 0)
return rawis; return rawis;
else { else {
@ -332,7 +342,8 @@ public final class ZioEntry implements Cloneable {
// Returns an output stream for writing an entry's data. // Returns an output stream for writing an entry's data.
public OutputStream getOutputStream() { public OutputStream getOutputStream() {
entryOut = new ZioEntryOutputStream(compression, new ByteArrayOutputStream()); // use FileBackedOutputStream to reduce memory consumption
entryOut = new ZioEntryOutputStream(compression, new FileBackedOutputStream(128 * 1024, true));
return entryOut; return entryOut;
} }

View File

@ -32,7 +32,7 @@ final class ZioEntryOutputStream extends OutputStream {
public ZioEntryOutputStream(int compression, OutputStream wrapped) { public ZioEntryOutputStream(int compression, OutputStream wrapped) {
this.wrapped = wrapped; this.wrapped = wrapped;
downstream = (compression == 0) ? wrapped : new DeflaterOutputStream(wrapped, new Deflater( downstream = (compression == 0) ? wrapped : new DeflaterOutputStream(wrapped, new Deflater(
Deflater.BEST_COMPRESSION, true)); compression, true));
} }
@Override @Override

View File

@ -1,6 +1,7 @@
package net.fornwall.apksigner.zipio; package net.fornwall.apksigner.zipio;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -34,8 +35,11 @@ public final class ZipInput implements AutoCloseable {
public Manifest getManifest() throws IOException { public Manifest getManifest() throws IOException {
if (manifest == null) { if (manifest == null) {
ZioEntry e = entries.get("META-INF/MANIFEST.MF"); ZioEntry e = entries.get("META-INF/MANIFEST.MF");
if (e != null) if (e != null) {
manifest = new Manifest(e.getInputStream()); try(InputStream inputStream = e.getInputStream()) {
manifest = new Manifest(inputStream);
}
}
} }
return manifest; return manifest;
} }

View File

@ -15,7 +15,10 @@
*/ */
package net.fornwall.apksigner.zipio; package net.fornwall.apksigner.zipio;
import com.google.common.io.ByteStreams;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -79,6 +82,11 @@ public final class ZipOutput implements AutoCloseable {
filePointer += value.length; filePointer += value.length;
} }
public void pipeStream(InputStream inputStream) throws IOException {
long length = ByteStreams.copy(inputStream, out);
filePointer += length;
}
public void writeBytes(byte[] value, int offset, int length) throws IOException { public void writeBytes(byte[] value, int offset, int length) throws IOException {
out.write(value, offset, length); out.write(value, offset, length);
filePointer += length; filePointer += length;