1. Optimize patch speed
This commit is contained in:
parent
3763f7757f
commit
b9cc00adbc
|
@ -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
|
||||||
|
|
|
@ -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,54 +215,31 @@ 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);
|
||||||
if (maxSdkVersion >= AndroidSdkVersion.N) {
|
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||||
RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
|
FutureTask<ApkSigningBlockUtils.Result> taskV2 = new FutureTask<>(() -> {
|
||||||
// Android P and newer attempts to verify APKs using APK Signature Scheme v3
|
if (maxSdkVersion >= AndroidSdkVersion.N) {
|
||||||
if (maxSdkVersion >= AndroidSdkVersion.P) {
|
// Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P
|
||||||
try {
|
// ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or
|
||||||
ApkSigningBlockUtils.Result v3Result =
|
// APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if
|
||||||
V3SchemeVerifier.verify(
|
// no APK Signature Scheme v3 (or newer scheme) signatures were found.
|
||||||
executor,
|
if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) {
|
||||||
apk,
|
try {
|
||||||
zipSections,
|
RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
|
||||||
Math.max(minSdkVersion, AndroidSdkVersion.P),
|
return V2SchemeVerifier.verify(
|
||||||
maxSdkVersion);
|
executor,
|
||||||
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
|
apk,
|
||||||
result.mergeFrom(v3Result);
|
zipSections,
|
||||||
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
|
supportedSchemeNames,
|
||||||
// v3 signature not required
|
foundApkSigSchemeIds,
|
||||||
}
|
Math.max(minSdkVersion, AndroidSdkVersion.N),
|
||||||
if (result.containsErrors()) {
|
maxSdkVersion);
|
||||||
return result;
|
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
|
||||||
|
// v2 signature not required
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
// 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
|
|
||||||
// APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if
|
|
||||||
// no APK Signature Scheme v3 (or newer scheme) signatures were found.
|
|
||||||
if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) {
|
|
||||||
try {
|
|
||||||
ApkSigningBlockUtils.Result v2Result =
|
|
||||||
V2SchemeVerifier.verify(
|
|
||||||
executor,
|
|
||||||
apk,
|
|
||||||
zipSections,
|
|
||||||
supportedSchemeNames,
|
|
||||||
foundApkSigSchemeIds,
|
|
||||||
Math.max(minSdkVersion, AndroidSdkVersion.N),
|
|
||||||
maxSdkVersion);
|
|
||||||
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
|
|
||||||
result.mergeFrom(v2Result);
|
|
||||||
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
|
|
||||||
// v2 signature not required
|
|
||||||
}
|
|
||||||
if (result.containsErrors()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
||||||
if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
|
FutureTask<V1SchemeVerifier.Result> taskV1 = new FutureTask<>(() -> {
|
||||||
V1SchemeVerifier.Result v1Result =
|
if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
|
||||||
V1SchemeVerifier.verify(
|
return 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;
|
||||||
|
|
|
@ -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)) {
|
|
||||||
|
|
||||||
Optional<V1SchemeVerifier.NamedDigest> extractedDigest =
|
|
||||||
V1SchemeVerifier.getDigestsToVerify(
|
|
||||||
entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE).stream()
|
|
||||||
.filter(d -> d.jcaDigestAlgorithm == alg)
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
extractedDigest.ifPresent(
|
|
||||||
namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 =
|
||||||
|
V1SchemeVerifier.getDigestsToVerify(
|
||||||
|
entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE).stream()
|
||||||
|
.filter(d -> d.jcaDigestAlgorithm.equals(alg))
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
extractedDigest.ifPresent(
|
||||||
|
namedDigest -> mOutputJarEntryDigests.put(entry.getKey(), namedDigest.digest));
|
||||||
|
|
||||||
|
});
|
||||||
return mOutputJarEntryDigests.keySet();
|
return mOutputJarEntryDigests.keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
.setV1SigningEnabled(true)
|
privateKey,
|
||||||
.setV2SigningEnabled(true).build();
|
Collections.singletonList(publicKey))
|
||||||
|
.build());
|
||||||
|
DefaultApkSignerEngine signerEngine = new DefaultApkSignerEngine.Builder(engineSignerConfigs, 19)
|
||||||
|
.setV1SigningEnabled(true)
|
||||||
|
.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();
|
||||||
signer.sign();
|
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();
|
||||||
|
}
|
||||||
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,8 +527,11 @@ public class ApkPatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void emitProgress(int progress) {
|
private void emitProgress(int progress) {
|
||||||
for (Consumer<Integer> consumer : progressListener) {
|
if(lastProgress < progress) {
|
||||||
consumer.accept(progress);
|
lastProgress = progress;
|
||||||
|
for (Consumer<Integer> consumer : progressListener) {
|
||||||
|
consumer.accept(progress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 (entryMap.containsKey(inEntry.getName())) {
|
if (removePredict != null && removePredict.apply(inEntry.getName())) {
|
||||||
ZipEntrySource source = entryMap.get(inEntry.getName());
|
continue;
|
||||||
ZioEntry zioEntry = new ZioEntry(inEntry.getName());
|
}
|
||||||
|
taskBundle.submitTask(()->{
|
||||||
|
if (entryMap.containsKey(inEntry.getName())) {
|
||||||
|
ZipEntrySource source = entryMap.get(inEntry.getName());
|
||||||
|
ZioEntry zioEntry = new ZioEntry(inEntry.getName());
|
||||||
|
zioEntry.setCompression(source.getCompressionMethod());
|
||||||
|
try (InputStream inputStream = source.getDataStream()) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
ByteStreams.copy(inputStream, zioEntry.getOutputStream());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
replacedFileSet.put(inEntry.getName(), true);
|
||||||
|
return zioEntry;
|
||||||
|
} else {
|
||||||
|
originEntryName.put(inEntry.getName(), true);
|
||||||
|
return inEntry;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (String name : difference) {
|
||||||
|
taskBundle.submitTask(()-> {
|
||||||
|
ZipEntrySource source = entryMap.get(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;
|
||||||
replacedFileSet.add(inEntry.getName());
|
});
|
||||||
} else {
|
|
||||||
zipOutput.write(inEntry);
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
if (index % reportInterval == 0) {
|
|
||||||
progressCallback.accept((int) (index * 95.0 / size));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sets.SetView<String> difference = Sets.difference(entryMap.keySet(), replacedFileSet);
|
|
||||||
index = 0;
|
|
||||||
for (String name : difference) {
|
|
||||||
ZipEntrySource source = entryMap.get(name);
|
|
||||||
ZioEntry zioEntry = new ZioEntry(name);
|
|
||||||
zioEntry.setCompression(source.getCompressionMethod());
|
|
||||||
try (InputStream inputStream = source.getDataStream()) {
|
|
||||||
Streams.pipeAll(inputStream, zioEntry.getOutputStream());
|
|
||||||
}
|
|
||||||
zipOutput.write(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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,17 +290,18 @@ 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) {
|
||||||
int numRead = din.read(tmpdata, count, size - count);
|
int numRead = din.read(tmpdata, count, size - count);
|
||||||
if (numRead < 0)
|
if (numRead < 0)
|
||||||
throw new IllegalStateException(String.format("Read failed, expecting %d bytes, got %d instead", size,
|
throw new IllegalStateException(String.format("Read failed, expecting %d bytes, got %d instead", size,
|
||||||
count));
|
count));
|
||||||
count += numRead;
|
count += numRead;
|
||||||
|
}
|
||||||
|
return tmpdata;
|
||||||
}
|
}
|
||||||
return tmpdata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns an input stream for reading the entry's data.
|
// Returns an input stream for reading the entry's data.
|
||||||
|
@ -300,11 +309,12 @@ 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 = ((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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue