/*
* 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.SystemClock;
import com.tencent.tinker.bsdiff.BSPatch;
import com.tencent.tinker.commons.resutil.ResUtil;
import com.tencent.tinker.commons.ziputil.TinkerZipEntry;
import com.tencent.tinker.commons.ziputil.TinkerZipFile;
import com.tencent.tinker.commons.ziputil.TinkerZipOutputStream;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.util.TinkerLog;
import com.tencent.tinker.loader.TinkerRuntimeException;
import com.tencent.tinker.loader.shareutil.ShareConstants;
import com.tencent.tinker.loader.shareutil.SharePatchFileUtil;
import com.tencent.tinker.loader.shareutil.ShareResPatchInfo;
import com.tencent.tinker.loader.shareutil.ShareSecurityCheck;
import com.tencent.tinker.loader.shareutil.ShareTinkerInternals;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Created by zhangshaowen on 2016/8/8.
*/
public class ResDiffPatchInternal extends BasePatchInternal {
protected static final String TAG = "Tinker.ResDiffPatchInternal";
protected static boolean tryRecoverResourceFiles(Tinker manager, ShareSecurityCheck checker, Context context,
String patchVersionDirectory, File patchFile) {
if (!manager.isEnabledForResource()) {
TinkerLog.w(TAG, "patch recover, resource is not enabled");
return true;
}
String resourceMeta = checker.getMetaContentMap().get(RES_META_FILE);
if (resourceMeta == null || resourceMeta.length() == 0) {
TinkerLog.w(TAG, "patch recover, resource is not contained");
return true;
}
long begin = SystemClock.elapsedRealtime();
boolean result = patchResourceExtractViaResourceDiff(context, patchVersionDirectory, resourceMeta, patchFile);
long cost = SystemClock.elapsedRealtime() - begin;
TinkerLog.i(TAG, "recover resource result:%b, cost:%d", result, cost);
return result;
}
private static boolean patchResourceExtractViaResourceDiff(Context context, String patchVersionDirectory,
String meta, File patchFile) {
String dir = patchVersionDirectory + "/" + ShareConstants.RES_PATH + "/";
if (!extractResourceDiffInternals(context, dir, meta, patchFile, TYPE_RESOURCE)) {
TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
return false;
}
return true;
}
private static boolean extractResourceDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
ShareResPatchInfo resPatchInfo = new ShareResPatchInfo();
ShareResPatchInfo.parseAllResPatchInfo(meta, resPatchInfo);
TinkerLog.i(TAG, "res dir: %s, meta: %s", dir, resPatchInfo.toString());
Tinker manager = Tinker.with(context);
if (!SharePatchFileUtil.checkIfMd5Valid(resPatchInfo.resArscMd5)) {
TinkerLog.w(TAG, "resource meta file md5 mismatch, type:%s, md5: %s", ShareTinkerInternals.getTypeString(type), resPatchInfo.resArscMd5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
File directory = new File(dir);
File resOutput = new File(directory, ShareConstants.RES_NAME);
//check result file whether already exist
if (resOutput.exists()) {
if (SharePatchFileUtil.checkResourceArscMd5(resOutput, resPatchInfo.resArscMd5)) {
//it is ok, just continue
TinkerLog.w(TAG, "resource file %s is already exist, and md5 match, just return true", resOutput.getPath());
return true;
} else {
TinkerLog.w(TAG, "have a mismatch corrupted resource " + resOutput.getPath());
resOutput.delete();
}
} else {
resOutput.getParentFile().mkdirs();
}
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;
if (!checkAndExtractResourceLargeFile(context, apkPath, directory, patchFile, resPatchInfo, type)) {
return false;
}
TinkerZipOutputStream out = null;
TinkerZipFile oldApk = null;
TinkerZipFile newApk = null;
int totalEntryCount = 0;
try {
out = new TinkerZipOutputStream(new BufferedOutputStream(new FileOutputStream(resOutput)));
oldApk = new TinkerZipFile(apkPath);
newApk = new TinkerZipFile(patchFile);
final Enumeration<? extends TinkerZipEntry> entries = oldApk.entries();
while (entries.hasMoreElements()) {
TinkerZipEntry zipEntry = entries.nextElement();
if (zipEntry == null) {
throw new TinkerRuntimeException("zipEntry is null when get from oldApk");
}
String name = zipEntry.getName();
if (name.contains("../")) {
continue;
}
if (ShareResPatchInfo.checkFileInPattern(resPatchInfo.patterns, name)) {
//won't contain in add set.
if (!resPatchInfo.deleteRes.contains(name)
&& !resPatchInfo.modRes.contains(name)
&& !resPatchInfo.largeModRes.contains(name)
&& !name.equals(ShareConstants.RES_MANIFEST)) {
ResUtil.extractTinkerEntry(oldApk, zipEntry, out);
totalEntryCount++;
}
}
}
//process manifest
TinkerZipEntry manifestZipEntry = oldApk.getEntry(ShareConstants.RES_MANIFEST);
if (manifestZipEntry == null) {
TinkerLog.w(TAG, "manifest patch entry is null. path:" + ShareConstants.RES_MANIFEST);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, ShareConstants.RES_MANIFEST, type);
return false;
}
ResUtil.extractTinkerEntry(oldApk, manifestZipEntry, out);
totalEntryCount++;
for (String name : resPatchInfo.largeModRes) {
TinkerZipEntry largeZipEntry = oldApk.getEntry(name);
if (largeZipEntry == null) {
TinkerLog.w(TAG, "large patch entry is null. path:" + name);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type);
return false;
}
ShareResPatchInfo.LargeModeInfo largeModeInfo = resPatchInfo.largeModMap.get(name);
ResUtil.extractLargeModifyFile(largeZipEntry, largeModeInfo.file, largeModeInfo.crc, out);
totalEntryCount++;
}
for (String name : resPatchInfo.addRes) {
TinkerZipEntry addZipEntry = newApk.getEntry(name);
if (addZipEntry == null) {
TinkerLog.w(TAG, "add patch entry is null. path:" + name);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type);
return false;
}
ResUtil.extractTinkerEntry(newApk, addZipEntry, out);
totalEntryCount++;
}
for (String name : resPatchInfo.modRes) {
TinkerZipEntry modZipEntry = newApk.getEntry(name);
if (modZipEntry == null) {
TinkerLog.w(TAG, "mod patch entry is null. path:" + name);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type);
return false;
}
ResUtil.extractTinkerEntry(newApk, modZipEntry, out);
totalEntryCount++;
}
// set comment back
out.setComment(oldApk.getComment());
} finally {
if (out != null) {
out.close();
}
if (oldApk != null) {
oldApk.close();
}
if (newApk != null) {
newApk.close();
}
//delete temp files
for (ShareResPatchInfo.LargeModeInfo largeModeInfo : resPatchInfo.largeModMap.values()) {
SharePatchFileUtil.safeDeleteFile(largeModeInfo.file);
}
}
boolean result = SharePatchFileUtil.checkResourceArscMd5(resOutput, resPatchInfo.resArscMd5);
if (!result) {
TinkerLog.i(TAG, "check final new resource file fail path:%s, entry count:%d, size:%d", resOutput.getAbsolutePath(), totalEntryCount, resOutput.length());
SharePatchFileUtil.safeDeleteFile(resOutput);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, ShareConstants.RES_NAME, type);
return false;
}
TinkerLog.i(TAG, "final new resource file:%s, entry count:%d, size:%d", resOutput.getAbsolutePath(), totalEntryCount, resOutput.length());
} catch (Throwable e) {
// e.printStackTrace();
throw new TinkerRuntimeException("patch " + ShareTinkerInternals.getTypeString(type) + " extract failed (" + e.getMessage() + ").", e);
}
return true;
}
private static boolean checkAndExtractResourceLargeFile(Context context, String apkPath, File directory,
File patchFile, ShareResPatchInfo resPatchInfo, int type) {
long start = System.currentTimeMillis();
Tinker manager = Tinker.with(context);
ZipFile apkFile = null;
ZipFile patchZipFile = null;
try {
//recover resources.arsc first
apkFile = new ZipFile(apkPath);
ZipEntry arscEntry = apkFile.getEntry(ShareConstants.RES_ARSC);
File arscFile = new File(directory, ShareConstants.RES_ARSC);
if (arscEntry == null) {
TinkerLog.w(TAG, "resources apk entry is null. path:" + ShareConstants.RES_ARSC);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, arscFile, ShareConstants.RES_ARSC, type);
return false;
}
//use base resources.arsc crc to identify base.apk
String baseArscCrc = String.valueOf(arscEntry.getCrc());
if (!baseArscCrc.equals(resPatchInfo.arscBaseCrc)) {
TinkerLog.e(TAG, "resources.arsc's crc is not equal, expect crc: %s, got crc: %s", resPatchInfo.arscBaseCrc, baseArscCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, arscFile, ShareConstants.RES_ARSC, type);
return false;
}
//resource arsc is not changed, just return true
if (resPatchInfo.largeModRes.isEmpty()) {
TinkerLog.i(TAG, "no large modify resources, just return");
return true;
}
for (String name : resPatchInfo.largeModRes) {
long largeStart = System.currentTimeMillis();
ShareResPatchInfo.LargeModeInfo largeModeInfo = resPatchInfo.largeModMap.get(name);
if (largeModeInfo == null) {
TinkerLog.w(TAG, "resource not found largeModeInfo, type:%s, name: %s", ShareTinkerInternals.getTypeString(type), name);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
largeModeInfo.file = new File(directory, name);
SharePatchFileUtil.ensureFileDirectory(largeModeInfo.file);
//we do not check the intermediate files' md5 to save time, use check whether it is 32 length
if (!SharePatchFileUtil.checkIfMd5Valid(largeModeInfo.md5)) {
TinkerLog.w(TAG, "resource meta file md5 mismatch, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), name, largeModeInfo.md5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
patchZipFile = new ZipFile(patchFile);
ZipEntry patchEntry = patchZipFile.getEntry(name);
if (patchEntry == null) {
TinkerLog.w(TAG, "large mod patch entry is null. path:" + name);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, largeModeInfo.file, name, type);
return false;
}
ZipEntry baseEntry = apkFile.getEntry(name);
if (baseEntry == null) {
TinkerLog.w(TAG, "resources apk entry is null. path:" + name);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, largeModeInfo.file, name, type);
return false;
}
InputStream oldStream = null;
InputStream newStream = null;
try {
oldStream = apkFile.getInputStream(baseEntry);
newStream = patchZipFile.getInputStream(patchEntry);
BSPatch.patchFast(oldStream, newStream, largeModeInfo.file);
} finally {
SharePatchFileUtil.closeQuietly(oldStream);
SharePatchFileUtil.closeQuietly(newStream);
}
//go go go bsdiff get the
if (!SharePatchFileUtil.verifyFileMd5(largeModeInfo.file, largeModeInfo.md5)) {
TinkerLog.w(TAG, "Failed to recover large modify file:%s", largeModeInfo.file.getPath());
SharePatchFileUtil.safeDeleteFile(largeModeInfo.file);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, largeModeInfo.file, name, type);
return false;
}
TinkerLog.w(TAG, "success recover large modify file:%s, file size:%d, use time:%d", largeModeInfo.file.getPath(), largeModeInfo.file.length(), (System.currentTimeMillis() - largeStart));
}
TinkerLog.w(TAG, "success recover all large modify use time:%d", (System.currentTimeMillis() - start));
} catch (Throwable e) {
// e.printStackTrace();
throw new TinkerRuntimeException("patch " + ShareTinkerInternals.getTypeString(type) + " extract failed (" + e.getMessage() + ").", e);
} finally {
SharePatchFileUtil.closeZip(apkFile);
SharePatchFileUtil.closeZip(patchZipFile);
}
return true;
}
}