mirror of
https://gitcode.com/gh_mirrors/re/react-native-pushy.git
synced 2025-11-03 06:43:10 +08:00
502 lines
16 KiB
Java
502 lines
16 KiB
Java
package cn.reactnative.modules.update;
|
|
|
|
import android.content.Context;
|
|
import android.os.AsyncTask;
|
|
import android.util.Log;
|
|
import com.facebook.react.bridge.Arguments;
|
|
import com.facebook.react.bridge.WritableMap;
|
|
|
|
import okhttp3.OkHttpClient;
|
|
import okhttp3.Request;
|
|
import okhttp3.Response;
|
|
import okhttp3.ResponseBody;
|
|
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
import org.json.JSONTokener;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.Enumeration;
|
|
import java.util.Iterator;
|
|
import java.util.zip.ZipEntry;
|
|
import java.util.HashMap;
|
|
|
|
import okio.BufferedSink;
|
|
import okio.BufferedSource;
|
|
import okio.Okio;
|
|
import static cn.reactnative.modules.update.UpdateModule.sendEvent;
|
|
|
|
|
|
class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
|
|
final int DOWNLOAD_CHUNK_SIZE = 4096;
|
|
|
|
Context context;
|
|
String hash;
|
|
|
|
DownloadTask(Context context) {
|
|
this.context = context;
|
|
}
|
|
|
|
static {
|
|
System.loadLibrary("rnupdate");
|
|
}
|
|
|
|
private void removeDirectory(File file) throws IOException {
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Removing " + file);
|
|
}
|
|
if (file.isDirectory()) {
|
|
File[] files = file.listFiles();
|
|
for (File f : files) {
|
|
String name = f.getName();
|
|
if (name.equals(".") || name.equals("..")) {
|
|
continue;
|
|
}
|
|
removeDirectory(f);
|
|
}
|
|
}
|
|
if (file.exists() && !file.delete()) {
|
|
throw new IOException("Failed to delete directory");
|
|
}
|
|
}
|
|
|
|
private void downloadFile(DownloadTaskParams param) throws IOException {
|
|
String url = param.url;
|
|
File writePath = param.targetFile;
|
|
this.hash = param.hash;
|
|
OkHttpClient client = new OkHttpClient();
|
|
Request request = new Request.Builder().url(url)
|
|
.build();
|
|
Response response = client.newCall(request).execute();
|
|
if (response.code() > 299) {
|
|
throw new Error("Server return code " + response.code());
|
|
}
|
|
ResponseBody body = response.body();
|
|
long contentLength = body.contentLength();
|
|
BufferedSource source = body.source();
|
|
|
|
if (writePath.exists()) {
|
|
writePath.delete();
|
|
}
|
|
|
|
BufferedSink sink = Okio.buffer(Okio.sink(writePath));
|
|
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Downloading " + url);
|
|
}
|
|
|
|
long bytesRead = 0;
|
|
long received = 0;
|
|
int currentPercentage = 0;
|
|
while ((bytesRead = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {
|
|
received += bytesRead;
|
|
sink.emit();
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Progress " + received + "/" + contentLength);
|
|
}
|
|
|
|
int percentage = (int)(received * 100.0 / contentLength + 0.5);
|
|
if (percentage > currentPercentage) {
|
|
currentPercentage = percentage;
|
|
publishProgress(new long[]{received, contentLength});
|
|
}
|
|
}
|
|
if (received != contentLength) {
|
|
throw new Error("Unexpected eof while reading downloaded update");
|
|
}
|
|
publishProgress(new long[]{received, contentLength});
|
|
sink.writeAll(source);
|
|
sink.close();
|
|
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Download finished");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onProgressUpdate(long[]... values) {
|
|
super.onProgressUpdate(values);
|
|
WritableMap params = Arguments.createMap();
|
|
params.putDouble("received", (values[0][0]));
|
|
params.putDouble("total", (values[0][1]));
|
|
params.putString("hash", this.hash);
|
|
sendEvent("RCTPushyDownloadProgress", params);
|
|
|
|
}
|
|
|
|
byte[] buffer = new byte[1024*4];
|
|
|
|
private static native byte[] hdiffPatch(byte[] origin, byte[] patch);
|
|
|
|
|
|
private void copyFile(File from, File fmd) throws IOException {
|
|
int count;
|
|
|
|
InputStream in = new FileInputStream(from);
|
|
FileOutputStream fout = new FileOutputStream(fmd);
|
|
|
|
while ((count = in.read(buffer)) != -1)
|
|
{
|
|
fout.write(buffer, 0, count);
|
|
}
|
|
|
|
fout.close();
|
|
in.close();
|
|
}
|
|
|
|
private byte[] readBytes(InputStream zis) throws IOException {
|
|
int count;
|
|
|
|
ByteArrayOutputStream fout = new ByteArrayOutputStream();
|
|
while ((count = zis.read(buffer)) != -1)
|
|
{
|
|
fout.write(buffer, 0, count);
|
|
}
|
|
|
|
fout.close();
|
|
zis.close();
|
|
return fout.toByteArray();
|
|
}
|
|
|
|
private byte[] readOriginBundle() throws IOException {
|
|
InputStream in;
|
|
try {
|
|
in = context.getAssets().open("index.android.bundle");
|
|
} catch (Exception e) {
|
|
return new byte[0];
|
|
}
|
|
int count;
|
|
|
|
ByteArrayOutputStream fout = new ByteArrayOutputStream();
|
|
while ((count = in.read(buffer)) != -1)
|
|
{
|
|
fout.write(buffer, 0, count);
|
|
}
|
|
|
|
fout.close();
|
|
in.close();
|
|
return fout.toByteArray();
|
|
}
|
|
|
|
private byte[] readFile(File file) throws IOException {
|
|
InputStream in = new FileInputStream(file);
|
|
int count;
|
|
|
|
ByteArrayOutputStream fout = new ByteArrayOutputStream();
|
|
while ((count = in.read(buffer)) != -1)
|
|
{
|
|
fout.write(buffer, 0, count);
|
|
}
|
|
|
|
fout.close();
|
|
in.close();
|
|
return fout.toByteArray();
|
|
}
|
|
|
|
private void copyFilesWithBlacklist(String current, File from, File to, JSONObject blackList) throws IOException {
|
|
File[] files = from.listFiles();
|
|
for (File file : files) {
|
|
if (file.isDirectory()) {
|
|
String subName = current + file.getName() + '/';
|
|
if (blackList.has(subName)) {
|
|
continue;
|
|
}
|
|
File toFile = new File(to, file.getName());
|
|
if (!toFile.exists()) {
|
|
toFile.mkdir();
|
|
}
|
|
copyFilesWithBlacklist(subName, file, toFile, blackList);
|
|
} else if (!blackList.has(current + file.getName())) {
|
|
// Copy file.
|
|
File toFile = new File(to, file.getName());
|
|
if (!toFile.exists()) {
|
|
copyFile(file, toFile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void copyFilesWithBlacklist(File from, File to, JSONObject blackList) throws IOException {
|
|
copyFilesWithBlacklist("", from, to, blackList);
|
|
}
|
|
|
|
private void doFullPatch(DownloadTaskParams param) throws IOException {
|
|
downloadFile(param);
|
|
|
|
removeDirectory(param.unzipDirectory);
|
|
param.unzipDirectory.mkdirs();
|
|
|
|
SafeZipFile zipFile = new SafeZipFile(param.targetFile);
|
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
while (entries.hasMoreElements()) {
|
|
ZipEntry ze = entries.nextElement();
|
|
|
|
zipFile.unzipToPath(ze, param.unzipDirectory);
|
|
}
|
|
|
|
zipFile.close();
|
|
|
|
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Unzip finished");
|
|
}
|
|
}
|
|
|
|
private void copyFromResource(HashMap<String, ArrayList<File> > resToCopy) throws IOException {
|
|
SafeZipFile zipFile = new SafeZipFile(new File(context.getPackageResourcePath()));
|
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
while (entries.hasMoreElements()) {
|
|
ZipEntry ze = entries.nextElement();
|
|
|
|
String fn = ze.getName();
|
|
ArrayList<File> targets = resToCopy.get(fn);
|
|
if (targets != null) {
|
|
File lastTarget = null;
|
|
for (File target: targets) {
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Copying from resource " + fn + " to " + target);
|
|
}
|
|
if (lastTarget != null) {
|
|
copyFile(lastTarget, target);
|
|
} else {
|
|
zipFile.unzipToFile(ze, target);
|
|
lastTarget = target;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
zipFile.close();
|
|
}
|
|
|
|
private void doPatchFromApk(DownloadTaskParams param) throws IOException, JSONException {
|
|
downloadFile(param);
|
|
|
|
removeDirectory(param.unzipDirectory);
|
|
param.unzipDirectory.mkdirs();
|
|
HashMap<String, ArrayList<File>> copyList = new HashMap<String, ArrayList<File>>();
|
|
|
|
boolean foundDiff = false;
|
|
boolean foundBundlePatch = false;
|
|
|
|
SafeZipFile zipFile = new SafeZipFile(param.targetFile);
|
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
while (entries.hasMoreElements()) {
|
|
ZipEntry ze = entries.nextElement();
|
|
String fn = ze.getName();
|
|
|
|
if (fn.equals("__diff.json")) {
|
|
foundDiff = true;
|
|
// copy files from assets
|
|
byte[] bytes = readBytes(zipFile.getInputStream(ze));
|
|
String json = new String(bytes, "UTF-8");
|
|
JSONObject obj = (JSONObject)new JSONTokener(json).nextValue();
|
|
|
|
JSONObject copies = obj.getJSONObject("copies");
|
|
Iterator<?> keys = copies.keys();
|
|
while( keys.hasNext() ) {
|
|
String to = (String)keys.next();
|
|
String from = copies.getString(to);
|
|
if (from.isEmpty()) {
|
|
from = to;
|
|
}
|
|
ArrayList<File> target = null;
|
|
if (!copyList.containsKey(from)) {
|
|
target = new ArrayList<File>();
|
|
copyList.put(from, target);
|
|
} else {
|
|
target = copyList.get((from));
|
|
}
|
|
File toFile = new File(param.unzipDirectory, to);
|
|
|
|
// Fixing a Zip Path Traversal Vulnerability
|
|
// https://support.google.com/faqs/answer/9294009
|
|
String canonicalPath = toFile.getCanonicalPath();
|
|
if (!canonicalPath.startsWith(param.unzipDirectory.getCanonicalPath() + File.separator)) {
|
|
throw new SecurityException("Illegal name: " + to);
|
|
}
|
|
target.add(toFile);
|
|
}
|
|
continue;
|
|
}
|
|
if (fn.equals("index.bundlejs.patch")) {
|
|
foundBundlePatch = true;
|
|
|
|
byte[] patched = hdiffPatch(readOriginBundle(), readBytes(zipFile.getInputStream(ze)));
|
|
|
|
FileOutputStream fout = new FileOutputStream(new File(param.unzipDirectory, "index.bundlejs"));
|
|
fout.write(patched);
|
|
fout.close();
|
|
continue;
|
|
}
|
|
|
|
|
|
zipFile.unzipToPath(ze, param.unzipDirectory);
|
|
}
|
|
|
|
zipFile.close();
|
|
|
|
|
|
if (!foundDiff) {
|
|
throw new Error("diff.json not found");
|
|
}
|
|
if (!foundBundlePatch) {
|
|
throw new Error("bundle patch not found");
|
|
}
|
|
|
|
copyFromResource(copyList);
|
|
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Unzip finished");
|
|
}
|
|
|
|
}
|
|
|
|
private void doPatchFromPpk(DownloadTaskParams param) throws IOException, JSONException {
|
|
downloadFile(param);
|
|
|
|
removeDirectory(param.unzipDirectory);
|
|
param.unzipDirectory.mkdirs();
|
|
|
|
int count;
|
|
String filename;
|
|
boolean foundDiff = false;
|
|
boolean foundBundlePatch = false;
|
|
|
|
|
|
SafeZipFile zipFile = new SafeZipFile(param.targetFile);
|
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
while (entries.hasMoreElements()) {
|
|
ZipEntry ze = entries.nextElement();
|
|
String fn = ze.getName();
|
|
|
|
if (fn.equals("__diff.json")) {
|
|
foundDiff = true;
|
|
// copy files from assets
|
|
byte[] bytes = readBytes(zipFile.getInputStream(ze));
|
|
String json = new String(bytes, "UTF-8");
|
|
JSONObject obj = (JSONObject)new JSONTokener(json).nextValue();
|
|
|
|
JSONObject copies = obj.getJSONObject("copies");
|
|
Iterator<?> keys = copies.keys();
|
|
while( keys.hasNext() ) {
|
|
String to = (String)keys.next();
|
|
String from = copies.getString(to);
|
|
if (from.isEmpty()) {
|
|
from = to;
|
|
}
|
|
copyFile(new File(param.originDirectory, from), new File(param.unzipDirectory, to));
|
|
}
|
|
JSONObject blackList = obj.getJSONObject("deletes");
|
|
copyFilesWithBlacklist(param.originDirectory, param.unzipDirectory, blackList);
|
|
continue;
|
|
}
|
|
if (fn.equals("index.bundlejs.patch")) {
|
|
foundBundlePatch = true;
|
|
byte[] patched = hdiffPatch(readFile(new File(param.originDirectory, "index.bundlejs")), readBytes(zipFile.getInputStream(ze)));
|
|
|
|
FileOutputStream fout = new FileOutputStream(new File(param.unzipDirectory, "index.bundlejs"));
|
|
fout.write(patched);
|
|
fout.close();
|
|
continue;
|
|
}
|
|
|
|
zipFile.unzipToPath(ze, param.unzipDirectory);
|
|
}
|
|
|
|
zipFile.close();
|
|
|
|
if (!foundDiff) {
|
|
throw new Error("diff.json not found");
|
|
}
|
|
if (!foundBundlePatch) {
|
|
throw new Error("bundle patch not found");
|
|
}
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Unzip finished");
|
|
}
|
|
}
|
|
private void doCleanUp(DownloadTaskParams param) throws IOException {
|
|
if (UpdateContext.DEBUG) {
|
|
Log.d("RNUpdate", "Start cleaning up");
|
|
}
|
|
File root = param.unzipDirectory;
|
|
for (File sub : root.listFiles()) {
|
|
if (sub.getName().charAt(0) == '.') {
|
|
continue;
|
|
}
|
|
if (sub.isFile()) {
|
|
sub.delete();
|
|
} else {
|
|
if (sub.getName().equals(param.hash) || sub.getName().equals(param.originHash)) {
|
|
continue;
|
|
}
|
|
removeDirectory(sub);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected Void doInBackground(DownloadTaskParams... params) {
|
|
int taskType = params[0].type;
|
|
try {
|
|
switch (taskType) {
|
|
case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
|
|
doFullPatch(params[0]);
|
|
break;
|
|
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK:
|
|
doPatchFromApk(params[0]);
|
|
break;
|
|
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
|
|
doPatchFromPpk(params[0]);
|
|
break;
|
|
case DownloadTaskParams.TASK_TYPE_CLEANUP:
|
|
doCleanUp(params[0]);
|
|
break;
|
|
case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
|
|
downloadFile(params[0]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (params[0].listener != null) {
|
|
params[0].listener.onDownloadCompleted(params[0]);
|
|
}
|
|
} catch (Throwable e) {
|
|
if (UpdateContext.DEBUG) {
|
|
e.printStackTrace();
|
|
}
|
|
switch (taskType) {
|
|
case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
|
|
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK:
|
|
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
|
|
try {
|
|
removeDirectory(params[0].unzipDirectory);
|
|
} catch (IOException ioException) {
|
|
ioException.printStackTrace();
|
|
}
|
|
break;
|
|
case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
|
|
// if (targetToClean.exists()) {
|
|
params[0].targetFile.delete();
|
|
// }
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
Log.e("pushy", "download task failed", e);
|
|
|
|
if (params[0].listener != null) {
|
|
params[0].listener.onDownloadFailed(e);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
}
|