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 net.fornwall.apksigner.** { *; }
-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
-dontwarn javax.naming.**
@ -162,27 +163,27 @@ public static java.lang.String TABLENAME;
# If you do NOT use RxJava:
-dontwarn rx.**
-keep class com.sun.org.apache.xml.internal.utils.PrefixResolver
-keep class java.rmi.Remote
-keep class java.rmi.server.*
-keep class javax.annotation.processing.AbstractProcessor
-keep class javax.el.*
-keep class javax.servlet.http.*
-keep class javax.servlet.jsp.el.VariableResolver
-keep class javax.servlet.jsp.*
-keep class javax.servlet.jsp.tagext.*
-keep class javax.servlet.*
-keep class javax.swing.JTree
-keep class javax.swing.tree.TreeNode
-keep class lombok.core.configuration.ConfigurationKey
-keep class org.apache.tools.ant.Task
-keep class org.apache.tools.ant.taskdefs.MatchingTask
-keep class org.apache.xml.utils.PrefixResolver
-keep class org.jaxen.dom.*
-keep class org.jaxen.dom4j.Dom4jXPath
-keep class org.jaxen.jdom.JDOMXPath
-keep class org.jaxen.*
-keep class org.jdom.output.XMLOutputter
-keep class org.python.core.PyObject
-keep class org.python.util.PythonInterpreter
-keep class org.zeroturnaround.javarebel.ClassEventListener
-dontwarn com.sun.org.apache.xml.internal.utils.PrefixResolver
-dontwarn java.rmi.Remote
-dontwarn java.rmi.server.*
-dontwarn javax.annotation.processing.AbstractProcessor
-dontwarn javax.el.*
-dontwarn javax.servlet.http.*
-dontwarn javax.servlet.jsp.el.VariableResolver
-dontwarn javax.servlet.jsp.*
-dontwarn javax.servlet.jsp.tagext.*
-dontwarn javax.servlet.*
-dontwarn javax.swing.JTree
-dontwarn javax.swing.tree.TreeNode
-dontwarn lombok.core.configuration.ConfigurationKey
-dontwarn org.apache.tools.ant.Task
-dontwarn org.apache.tools.ant.taskdefs.MatchingTask
-dontwarn org.apache.xml.utils.PrefixResolver
-dontwarn org.jaxen.dom.*
-dontwarn org.jaxen.dom4j.Dom4jXPath
-dontwarn org.jaxen.jdom.JDOMXPath
-dontwarn org.jaxen.*
-dontwarn org.jdom.output.XMLOutputter
-dontwarn org.python.core.PyObject
-dontwarn org.python.util.PythonInterpreter
-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.internal.apk.AndroidBinXmlParser;
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.SignatureAlgorithm;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
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.zip.CentralDirectoryRecord;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.util.RunnablesExecutor;
import com.android.apksig.zip.ZipFormatException;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
@ -47,6 +47,10 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.
@ -211,36 +215,17 @@ public class ApkVerifier {
// 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.
Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
FutureTask<ApkSigningBlockUtils.Result> taskV2 = new FutureTask<>(() -> {
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
// 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(
RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
return V2SchemeVerifier.verify(
executor,
apk,
zipSections,
@ -248,17 +233,13 @@ public class ApkVerifier {
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;
}
}
}
return null;
});
// Android O and newer requires that APKs targeting security sandbox version 2 and higher
// are signed using APK Signature Scheme v2 or newer.
if (maxSdkVersion >= AndroidSdkVersion.O) {
@ -280,16 +261,27 @@ public class ApkVerifier {
// 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
// scheme) signatures were found.
FutureTask<V1SchemeVerifier.Result> taskV1 = new FutureTask<>(() -> {
if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
V1SchemeVerifier.Result v1Result =
V1SchemeVerifier.verify(
return V1SchemeVerifier.verify(
apk,
zipSections,
supportedSchemeNames,
foundApkSigSchemeIds,
minSdkVersion,
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()) {
return result;
@ -632,6 +624,9 @@ public class ApkVerifier {
}
private void mergeFrom(V1SchemeVerifier.Result source) {
if(source == null) {
return;
}
mVerifiedUsingV1Scheme = source.verified;
mErrors.addAll(source.getErrors());
mWarnings.addAll(source.getWarnings());
@ -644,6 +639,9 @@ public class ApkVerifier {
}
private void mergeFrom(ApkSigningBlockUtils.Result source) {
if(source == null) {
return;
}
switch (source.signatureSchemeVersion) {
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
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.DataSinks;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -53,7 +53,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.StreamSupport;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Default implementation of {@link ApkSignerEngine}.
@ -432,21 +433,25 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
for (Map.Entry<String, ManifestParser.Section> entry: sections.getSecond().entrySet()) {
String entryName = entry.getKey();
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) &&
isDebuggable(entryName)) {
Stream<Map.Entry<String, ManifestParser.Section>> entryStream;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
entryStream = sections.getSecond().entrySet().parallelStream();
}
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 == alg)
.filter(d -> d.jcaDigestAlgorithm.equals(alg))
.findFirst();
extractedDigest.ifPresent(
namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest));
}
}
namedDigest -> mOutputJarEntryDigests.put(entry.getKey(), namedDigest.digest));
});
return mOutputJarEntryDigests.keySet();
}

View File

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

View File

@ -12,6 +12,9 @@ import android.util.Log;
import com.android.apksig.ApkSigner;
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.google.common.base.Stopwatch;
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.DialogAction;
import com.zane.smapiinstaller.constant.ManifestPatchConstants;
import com.zane.smapiinstaller.dto.Tuple2;
import com.zane.smapiinstaller.entity.ApkFilesManifest;
import com.zane.smapiinstaller.entity.ManifestEntry;
import com.zane.smapiinstaller.utils.DialogUtils;
@ -39,6 +43,7 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
@ -46,6 +51,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@ -79,8 +85,12 @@ public class ApkPatcher {
private final AtomicInteger switchAction = new AtomicInteger();
private Tuple2<byte[], Set<String>> originSignInfo = null;
private final List<Consumer<Integer>> progressListener = new ArrayList<>();
private int lastProgress = -1;
private final Stopwatch stopwatch = Stopwatch.createUnstarted(new Ticker() {
@Override
public long read() {
@ -89,6 +99,7 @@ public class ApkPatcher {
});
public ApkPatcher(Context context) {
this.lastProgress = -1;
this.context = context;
}
@ -120,14 +131,6 @@ public class ApkPatcher {
File apkFile = new File(sourceDir);
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) {
AtomicInteger count = new AtomicInteger();
ZipUtil.unpack(apkFile, new File(stadewValleyBasePath + "/StardewValley/"), name -> {
@ -140,7 +143,7 @@ public class ApkPatcher {
}
return null;
});
return distFile.getAbsolutePath();
return apkFile.getAbsolutePath();
} else if (advancedStage == 1) {
File contentFolder = new File(stadewValleyBasePath + "/StardewValley/Content");
if (contentFolder.exists()) {
@ -151,16 +154,11 @@ public class ApkPatcher {
} else {
extract(0);
}
ZipUtils.removeEntries(sourceDir, "assets/Content", distFile.getAbsolutePath(), (progress) -> emitProgress((int) (progress * 0.05)));
} else {
Files.copy(apkFile, distFile);
return apkFile.getAbsolutePath();
}
emitProgress(5);
return distFile.getAbsolutePath();
return apkFile.getAbsolutePath();
} catch (PackageManager.NameNotFoundException ignored) {
} catch (IOException e) {
Log.e(TAG, "Extract error", e);
errorMessage.set(e.getLocalizedMessage());
return null;
}
}
@ -172,10 +170,11 @@ public class ApkPatcher {
* 将指定APK文件重新打包添加SMAPI修改AndroidManifest.xml同时验证版本是否正确
*
* @param apkPath APK文件路径
* @param targetFile 目标文件
* @param isAdvanced 是否高级模式
* @return 是否成功打包
*/
public boolean patch(String apkPath, boolean isAdvanced) {
public boolean patch(String apkPath, File targetFile, boolean isAdvanced) {
if (apkPath == null) {
return false;
}
@ -205,17 +204,13 @@ public class ApkPatcher {
.filter(Objects::nonNull).flatMap(Stream::of).distinct().collect(Collectors.toList());
entries.add(new ZipUtils.ZipEntrySource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED));
emitProgress(10);
String patchedFilename = apkPath + ".patched";
File patchedFile = new File(patchedFilename);
int baseProgress = 10;
stopwatch.reset();
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)));
stopwatch.stop();
emitProgress(45);
FileUtils.forceDelete(file);
FileUtils.moveFile(patchedFile, file);
emitProgress(46);
return true;
} catch (Exception e) {
@ -403,21 +398,29 @@ public class ApkPatcher {
String alias = ks.aliases().nextElement();
X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias);
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray());
ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder("debug", privateKey, Collections.singletonList(publicKey)).build();
emitProgress(49);
File outputFile = new File(signApkPath);
ApkSigner signer = new ApkSigner.Builder(Collections.singletonList(signerConfig))
.setInputApk(new File(apkPath))
.setOutputApk(outputFile)
List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs = Collections.singletonList(
new DefaultApkSignerEngine.SignerConfig.Builder(
"debug",
privateKey,
Collections.singletonList(publicKey))
.build());
DefaultApkSignerEngine signerEngine = new DefaultApkSignerEngine.Builder(engineSignerConfigs, 19)
.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);
stopwatch.reset();
Thread thread = new Thread(() -> {
stopwatch.start();
while (true) {
try {
Thread.sleep(20);
Thread.sleep(200);
long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
double progress = elapsed * 0.98 / zipOpElapsed;
if (progress < 1.0) {
@ -429,7 +432,13 @@ public class ApkPatcher {
}
});
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();
}
FileUtils.forceDelete(new File(apkPath));
ApkVerifier.Result result = new ApkVerifier.Builder(outputFile).build().verify();
if (thread.isAlive() && !thread.isInterrupted()) {
@ -518,10 +527,13 @@ public class ApkPatcher {
}
private void emitProgress(int progress) {
if(lastProgress < progress) {
lastProgress = progress;
for (Consumer<Integer> consumer : progressListener) {
consumer.accept(progress);
}
}
}
public void registerProgressListener(Consumer<Integer> 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.zeroturnaround.zip.ZipUtil;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -298,8 +300,8 @@ public class CommonLogic {
if (entry.isXALZ()) {
bytes = ZipUtils.decompressXALZ(bytes);
}
try (FileOutputStream stream = FileUtils.openOutputStream(targetFile)) {
stream.write(bytes);
try (FileOutputStream outputStream = FileUtils.openOutputStream(targetFile)) {
ByteStreams.copy(Channels.newChannel(new ByteArrayInputStream(bytes)), outputStream.getChannel());
} catch (IOException ignore) {
}
} else {
@ -312,7 +314,7 @@ public class CommonLogic {
if (entry.isExternal() && apkFilesManifest != null) {
byte[] bytes = FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath());
try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
outputStream.write(bytes);
ByteStreams.copy(Channels.newChannel(new ByteArrayInputStream(bytes)), outputStream.getChannel());
} catch (IOException ignored) {
}
} else {
@ -358,7 +360,7 @@ public class CommonLogic {
}
}
try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
ByteStreams.copy(inputStream, outputStream);
ByteStreams.copy(Channels.newChannel(inputStream), outputStream.getChannel());
}
} catch (IOException 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));
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.extracting_package, null);
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)));
return;
}
@ -132,7 +140,8 @@ public class InstallFragment extends Fragment {
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, 6);
modAssetsManager.installDefaultMods();
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);
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) -> {
@ -147,7 +156,7 @@ public class InstallFragment extends Fragment {
return;
}
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null);
String signPath = patcher.sign(path);
String signPath = patcher.sign(targetApk.getAbsolutePath());
if (signPath == null) {
DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
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.Maps;
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.ZipInput;
@ -10,7 +12,6 @@ import net.fornwall.apksigner.zipio.ZipOutput;
import net.jpountz.lz4.LZ4Factory;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.io.Streams;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -18,9 +19,12 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
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.Function;
import java.util.function.Supplier;
import lombok.AllArgsConstructor;
@ -36,7 +40,7 @@ public class ZipUtils {
public static byte[] decompressXALZ(byte[] bytes) {
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))) {
byte[] length = ByteUtils.subArray(bytes, 8, 12);
@ -46,52 +50,96 @@ public class ZipUtils {
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 outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile)) {
throw new IllegalArgumentException("Input and output files are the same");
}
ImmutableMap<String, ZipEntrySource> entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath);
byte[] originManifest = null;
ConcurrentHashMap<String, Boolean> originEntryName = new ConcurrentHashMap<>();
try (ZipInput input = new ZipInput(inputZipFilename)) {
int size = input.entries.values().size();
int index = 0;
AtomicLong count = new AtomicLong();
int reportInterval = size / 100;
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()) {
if (removePredict != null && removePredict.apply(inEntry.getName())) {
continue;
}
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()) {
Streams.pipeAll(inputStream, zioEntry.getOutputStream());
if (inputStream != null) {
ByteStreams.copy(inputStream, zioEntry.getOutputStream());
}
zipOutput.write(zioEntry);
replacedFileSet.add(inEntry.getName());
} catch (IOException e) {
throw new RuntimeException(e);
}
replacedFileSet.put(inEntry.getName(), true);
return zioEntry;
} 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) {
taskBundle.submitTask(()-> {
ZipEntrySource source = entryMap.get(name);
ZioEntry zioEntry = new ZioEntry(name);
zioEntry.setCompression(source.getCompressionMethod());
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);
progressCallback.accept(95 + (int) (index * 5.0 / difference.size()));
return zioEntry;
});
}
taskBundle.join();
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 {
@ -134,11 +182,8 @@ public class ZipUtils {
}
private InputStream getDataStream() {
// Optimize: read only once
if (dataSupplier != null) {
InputStream bytes = dataSupplier.get();
dataSupplier = null;
return bytes;
return dataSupplier.get();
}
return null;
}

View File

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

View File

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

View File

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

View File

@ -15,7 +15,10 @@
*/
package net.fornwall.apksigner.zipio;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.LinkedList;
@ -79,6 +82,11 @@ public final class ZipOutput implements AutoCloseable {
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 {
out.write(value, offset, length);
filePointer += length;