/*
* Tencent is pleased to support the open source community by making Tinker available.
*
* Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.tinker.lib.patch;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.SystemClock;
import com.tencent.tinker.commons.dexpatcher.DexPatchApplier;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.util.TinkerLog;
import com.tencent.tinker.loader.TinkerParallelDexOptimizer;
import com.tencent.tinker.loader.TinkerRuntimeException;
import com.tencent.tinker.loader.shareutil.ShareConstants;
import com.tencent.tinker.loader.shareutil.ShareDexDiffPatchInfo;
import com.tencent.tinker.loader.shareutil.ShareElfFile;
import com.tencent.tinker.loader.shareutil.SharePatchFileUtil;
import com.tencent.tinker.loader.shareutil.ShareSecurityCheck;
import com.tencent.tinker.loader.shareutil.ShareTinkerInternals;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import dalvik.system.DexFile;
/**
* Created by zhangshaowen on 16/4/12.
*/
public class DexDiffPatchInternal extends BasePatchInternal {
protected static final String TAG = "Tinker.DexDiffPatchInternal";
protected static final int WAIT_ASYN_OAT_TIME = 10 * 1000;
protected static final int MAX_WAIT_COUNT = 30;
private static ArrayList<File> optFiles = new ArrayList<>();
protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
String patchVersionDirectory, File patchFile) {
if (!manager.isEnabledForDex()) {
TinkerLog.w(TAG, "patch recover, dex is not enabled");
return true;
}
String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);
if (dexMeta == null) {
TinkerLog.w(TAG, "patch recover, dex is not contained");
return true;
}
long begin = SystemClock.elapsedRealtime();
boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
long cost = SystemClock.elapsedRealtime() - begin;
TinkerLog.i(TAG, "recover dex result:%b, cost:%d", result, cost);
return result;
}
protected static boolean waitAndCheckDexOptFile(File patchFile, Tinker manager) {
if (optFiles.isEmpty()) {
return true;
}
int size = optFiles.size() * 6;
if (size > MAX_WAIT_COUNT) {
size = MAX_WAIT_COUNT;
}
TinkerLog.i(TAG, "dex count: %d, final wait time: %d", optFiles.size(), size);
for (int i = 0; i < size; i++) {
if (!checkAllDexOptFile(optFiles, i + 1)) {
try {
Thread.sleep(WAIT_ASYN_OAT_TIME);
} catch (InterruptedException e) {
TinkerLog.e(TAG, "thread sleep InterruptedException e:" + e);
}
}
}
List<File> failDexFiles = new ArrayList<>();
// check again, if still can be found, just return
for (File file : optFiles) {
TinkerLog.i(TAG, "check dex optimizer file exist: %s, size %d", file.getName(), file.length());
if (!SharePatchFileUtil.isLegalFile(file)) {
TinkerLog.e(TAG, "final parallel dex optimizer file %s is not exist, return false", file.getName());
failDexFiles.add(file);
}
}
if (!failDexFiles.isEmpty()) {
manager.getPatchReporter().onPatchDexOptFail(patchFile, failDexFiles,
new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_EXIST_FAIL));
return false;
}
if (Build.VERSION.SDK_INT >= 21) {
Throwable lastThrowable = null;
for (File file : optFiles) {
TinkerLog.i(TAG, "check dex optimizer file format: %s, size %d", file.getName(), file.length());
int returnType;
try {
returnType = ShareElfFile.getFileTypeByMagic(file);
} catch (IOException e) {
// read error just continue
continue;
}
if (returnType == ShareElfFile.FILE_TYPE_ELF) {
ShareElfFile elfFile = null;
try {
elfFile = new ShareElfFile(file);
} catch (Throwable e) {
TinkerLog.e(TAG, "final parallel dex optimizer file %s is not elf format, return false", file.getName());
failDexFiles.add(file);
lastThrowable = e;
} finally {
if (elfFile != null) {
try {
elfFile.close();
} catch (IOException ignore) {
}
}
}
}
}
if (!failDexFiles.isEmpty()) {
Throwable returnThrowable = lastThrowable == null
? new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL)
: new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL, lastThrowable);
manager.getPatchReporter().onPatchDexOptFail(patchFile, failDexFiles,
returnThrowable);
return false;
}
}
return true;
}
private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
return false;
}
final Tinker manager = Tinker.with(context);
File dexFiles = new File(dir);
File[] files = dexFiles.listFiles();
optFiles.clear();
if (files != null) {
final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
if (!optimizeDexDirectoryFile.exists() && !optimizeDexDirectoryFile.mkdirs()) {
TinkerLog.w(TAG, "patch recover, make optimizeDexDirectoryFile fail");
return false;
}
// add opt files
for (File file : files) {
String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
optFiles.add(new File(outputPathName));
}
TinkerLog.i(TAG, "patch recover, try to optimize dex file count:%d, optimizeDexDirectory:%s", files.length, optimizeDexDirectory);
// only use parallel dex optimizer for art
if (ShareTinkerInternals.isVmArt()) {
final List<File> failOptDexFile = new Vector<>();
final Throwable[] throwable = new Throwable[1];
// try parallel dex optimizer
TinkerParallelDexOptimizer.optimizeAll(
Arrays.asList(files), optimizeDexDirectoryFile,
new TinkerParallelDexOptimizer.ResultCallback() {
long startTime;
@Override
public void onStart(File dexFile, File optimizedDir) {
startTime = System.currentTimeMillis();
TinkerLog.i(TAG, "start to parallel optimize dex %s, size: %d", dexFile.getPath(), dexFile.length());
}
@Override
public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
// Do nothing.
TinkerLog.i(TAG, "success to parallel optimize dex %s, opt file size: %d, use time %d",
dexFile.getPath(), optimizedFile.length(), (System.currentTimeMillis() - startTime));
}
@Override
public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
TinkerLog.i(TAG, "fail to parallel optimize dex %s use time %d",
dexFile.getPath(), (System.currentTimeMillis() - startTime));
failOptDexFile.add(dexFile);
throwable[0] = thr;
}
}
);
if (!failOptDexFile.isEmpty()) {
manager.getPatchReporter().onPatchDexOptFail(patchFile, failOptDexFile, throwable[0]);
return false;
}
// for dalvik, machine hardware performance is much worse than art machine
} else {
for (File file : files) {
try {
String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
long start = System.currentTimeMillis();
DexFile.loadDex(file.getAbsolutePath(), outputPathName, 0);
TinkerLog.i(TAG, "success single dex optimize file, path: %s, opt file size: %d, use time: %d", file.getPath(), new File(outputPathName).length(),
(System.currentTimeMillis() - start));
} catch (Throwable e) {
TinkerLog.e(TAG, "single dex optimize or load failed, path:" + file.getPath());
List<File> failedList = new ArrayList<>();
failedList.add(file);
manager.getPatchReporter().onPatchDexOptFail(patchFile, failedList, e);
return false;
}
}
}
}
return true;
}
/**
* for ViVo or some other rom, they would make dex2oat asynchronous
* so we need to check whether oat file is actually generated.
* @param files
* @param count
* @return
*/
private static boolean checkAllDexOptFile(ArrayList<File> files, int count) {
for (File file : files) {
if (!SharePatchFileUtil.isLegalFile(file)) {
TinkerLog.e(TAG, "parallel dex optimizer file %s is not exist, just wait %d times", file.getName(), count);
return false;
}
}
return true;
}
private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
//parse
ArrayList<ShareDexDiffPatchInfo> patchList = new ArrayList<>();
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
if (patchList.isEmpty()) {
TinkerLog.w(TAG, "extract patch list is empty! type:%s:", ShareTinkerInternals.getTypeString(type));
return true;
}
File directory = new File(dir);
if (!directory.exists()) {
directory.mkdirs();
}
//I think it is better to extract the raw files from apk
Tinker manager = Tinker.with(context);
ZipFile apk = null;
ZipFile patch = null;
try {
ApplicationInfo applicationInfo = context.getApplicationInfo();
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
TinkerLog.w(TAG, "applicationInfo == null!!!!");
return false;
}
String apkPath = applicationInfo.sourceDir;
apk = new ZipFile(apkPath);
patch = new ZipFile(patchFile);
for (ShareDexDiffPatchInfo info : patchList) {
long start = System.currentTimeMillis();
final String infoPath = info.path;
String patchRealPath;
if (infoPath.equals("")) {
patchRealPath = info.rawName;
} else {
patchRealPath = info.path + "/" + info.rawName;
}
String dexDiffMd5 = info.dexDiffMd5;
String oldDexCrc = info.oldDexCrC;
if (!ShareTinkerInternals.isVmArt() && info.destMd5InDvm.equals("0")) {
TinkerLog.w(TAG, "patch dex %s is only for art, just continue", patchRealPath);
continue;
}
String extractedFileMd5 = ShareTinkerInternals.isVmArt() ? info.destMd5InArt : info.destMd5InDvm;
if (!SharePatchFileUtil.checkIfMd5Valid(extractedFileMd5)) {
TinkerLog.w(TAG, "meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, extractedFileMd5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
File extractedFile = new File(dir + info.realName);
//check file whether already exist
if (extractedFile.exists()) {
if (SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
//it is ok, just continue
TinkerLog.w(TAG, "dex file %s is already exist, and md5 match, just continue", extractedFile.getPath());
continue;
} else {
TinkerLog.w(TAG, "have a mismatch corrupted dex " + extractedFile.getPath());
extractedFile.delete();
}
} else {
extractedFile.getParentFile().mkdirs();
}
ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
if (oldDexCrc.equals("0")) {
if (patchFileEntry == null) {
TinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
//it is a new file, but maybe we need to repack the dex file
if (!extractDexFile(patch, patchFileEntry, extractedFile, info)) {
TinkerLog.w(TAG, "Failed to extract raw patch file " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
} else if (dexDiffMd5.equals("0")) {
// skip process old dex for real dalvik vm
if (!ShareTinkerInternals.isVmArt()) {
continue;
}
if (rawApkFileEntry == null) {
TinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
//check source crc instead of md5 for faster
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if (!rawEntryCrc.equals(oldDexCrc)) {
TinkerLog.e(TAG, "apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// Small patched dex generating strategy was disabled, we copy full original dex directly now.
//patchDexFile(apk, patch, rawApkFileEntry, null, info, smallPatchInfoFile, extractedFile);
extractDexFile(apk, rawApkFileEntry, extractedFile, info);
if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
TinkerLog.w(TAG, "Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
SharePatchFileUtil.safeDeleteFile(extractedFile);
return false;
}
} else {
if (patchFileEntry == null) {
TinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
if (!SharePatchFileUtil.checkIfMd5Valid(dexDiffMd5)) {
TinkerLog.w(TAG, "meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, dexDiffMd5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
if (rawApkFileEntry == null) {
TinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
//check source crc instead of md5 for faster
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if (!rawEntryCrc.equals(oldDexCrc)) {
TinkerLog.e(TAG, "apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
TinkerLog.w(TAG, "Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
SharePatchFileUtil.safeDeleteFile(extractedFile);
return false;
}
TinkerLog.w(TAG, "success recover dex file: %s, size: %d, use time: %d",
extractedFile.getPath(), extractedFile.length(), (System.currentTimeMillis() - start));
}
}
} catch (Throwable e) {
throw new TinkerRuntimeException("patch " + ShareTinkerInternals.getTypeString(type) + " extract failed (" + e.getMessage() + ").", e);
} finally {
SharePatchFileUtil.closeZip(apk);
SharePatchFileUtil.closeZip(patch);
}
return true;
}
/**
* repack dex to jar
*
* @param zipFile
* @param entryFile
* @param extractTo
* @param targetMd5
* @return boolean
* @throws IOException
*/
private static boolean extractDexToJar(ZipFile zipFile, ZipEntry entryFile, File extractTo, String targetMd5) throws IOException {
int numAttempts = 0;
boolean isExtractionSuccessful = false;
while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
numAttempts++;
FileOutputStream fos = new FileOutputStream(extractTo);
InputStream in = zipFile.getInputStream(entryFile);
ZipOutputStream zos = null;
BufferedInputStream bis = null;
TinkerLog.i(TAG, "try Extracting " + extractTo.getPath());
try {
zos = new ZipOutputStream(new
BufferedOutputStream(fos));
bis = new BufferedInputStream(in);
byte[] buffer = new byte[ShareConstants.BUFFER_SIZE];
ZipEntry entry = new ZipEntry(ShareConstants.DEX_IN_JAR);
zos.putNextEntry(entry);
int length = bis.read(buffer);
while (length != -1) {
zos.write(buffer, 0, length);
length = bis.read(buffer);
}
zos.closeEntry();
} finally {
SharePatchFileUtil.closeQuietly(bis);
SharePatchFileUtil.closeQuietly(zos);
}
isExtractionSuccessful = SharePatchFileUtil.verifyDexFileMd5(extractTo, targetMd5);
TinkerLog.i(TAG, "isExtractionSuccessful: %b", isExtractionSuccessful);
if (!isExtractionSuccessful) {
extractTo.delete();
if (extractTo.exists()) {
TinkerLog.e(TAG, "Failed to delete corrupted dex " + extractTo.getPath());
}
}
}
return isExtractionSuccessful;
}
// /**
// * reject dalvik vm, but sdk version is larger than 21
// */
// private static void checkVmArtProperty() {
// boolean art = ShareTinkerInternals.isVmArt();
// if (!art && Build.VERSION.SDK_INT >= 21) {
// throw new TinkerRuntimeException(ShareConstants.CHECK_VM_PROPERTY_FAIL + ", it is dalvik vm, but sdk version " + Build.VERSION.SDK_INT + " is larger than 21!");
// }
// }
private static boolean extractDexFile(ZipFile zipFile, ZipEntry entryFile, File extractTo, ShareDexDiffPatchInfo dexInfo) throws IOException {
final String fileMd5 = ShareTinkerInternals.isVmArt() ? dexInfo.destMd5InArt : dexInfo.destMd5InDvm;
final String rawName = dexInfo.rawName;
final boolean isJarMode = dexInfo.isJarMode;
//it is raw dex and we use jar mode, so we need to zip it!
if (SharePatchFileUtil.isRawDexFile(rawName) && isJarMode) {
return extractDexToJar(zipFile, entryFile, extractTo, fileMd5);
}
return extract(zipFile, entryFile, extractTo, fileMd5, true);
}
/**
* Generate patched dex file (May wrapped it by a jar if needed.)
* @param baseApk
* OldApk.
* @param patchPkg
* Patch package, it is also a zip file.
* @param oldDexEntry
* ZipEntry of old dex.
* @param patchFileEntry
* ZipEntry of patch file. (also ends with .dex) This could be null.
* @param patchInfo
* Parsed patch info from package-meta.txt
* @param patchedDexFile
* Patched dex file, may be a jar.
*
* <b>Notice: patchFileEntry and smallPatchInfoFile cannot both be null.</b>
*
* @throws IOException
*/
private static void patchDexFile(
ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
InputStream oldDexStream = null;
InputStream patchFileStream = null;
try {
oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);
final boolean isRawDexFile = SharePatchFileUtil.isRawDexFile(patchInfo.rawName);
if (!isRawDexFile || patchInfo.isJarMode) {
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(patchedDexFile)));
zos.putNextEntry(new ZipEntry(ShareConstants.DEX_IN_JAR));
// Old dex is not a raw dex file.
if (!isRawDexFile) {
ZipInputStream zis = null;
try {
zis = new ZipInputStream(oldDexStream);
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (ShareConstants.DEX_IN_JAR.equals(entry.getName())) break;
}
if (entry == null) {
throw new TinkerRuntimeException("can't recognize zip dex format file:" + patchedDexFile.getAbsolutePath());
}
new DexPatchApplier(zis, patchFileStream).executeAndSaveTo(zos);
} finally {
SharePatchFileUtil.closeQuietly(zis);
}
} else {
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(zos);
}
zos.closeEntry();
} finally {
SharePatchFileUtil.closeQuietly(zos);
}
} else {
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
}
} finally {
SharePatchFileUtil.closeQuietly(oldDexStream);
SharePatchFileUtil.closeQuietly(patchFileStream);
}
}
}