Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for "content://" uri and progress during zip extraction on Android #322

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 56 additions & 36 deletions android/src/main/java/com/rnziparchive/RNZipArchiveModule.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.rnziparchive;

import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Build;
import android.util.Log;

Expand All @@ -23,6 +24,7 @@
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
Expand All @@ -36,6 +38,7 @@
import net.lingala.zip4j.model.enums.CompressionLevel;
import net.lingala.zip4j.model.enums.EncryptionMethod;
import net.lingala.zip4j.model.enums.AesKeyStrength;
import net.lingala.zip4j.progress.ProgressMonitor;

import java.nio.charset.Charset;

Expand Down Expand Up @@ -67,7 +70,7 @@ public void isPasswordProtected(final String zipFilePath, final Promise promise)

@ReactMethod
public void unzipWithPassword(final String zipFilePath, final String destDirectory,
final String password, final Promise promise) {
final String password, final Promise promise) {
new Thread(new Runnable() {
@Override
public void run() {
Expand All @@ -92,11 +95,11 @@ public void run() {
String destDirCanonicalPath = (new File(destDirectory).getCanonicalPath()) + File.separator;

if (!canonicalPath.startsWith(destDirCanonicalPath)) {
throw new SecurityException(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
throw new SecurityException(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
}

if (!fileHeader.isDirectory()) {
zipFile.extractFile(fileHeader, destDirectory);
zipFile.extractFile(fileHeader, destDirectory);
extractedFileNames.add(fileHeader.getFileName());
}
updateProgress(i + 1, totalFiles, zipFilePath);
Expand Down Expand Up @@ -149,11 +152,26 @@ public void run() {
zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
}

ProgressMonitor progressMonitor = zipFile.getProgressMonitor();

zipFile.setRunInThread(true);
zipFile.extractAll(destDirectory);

zipFile.close();
updateProgress(1, 1, zipFilePath); // force 100%
promise.resolve(destDirectory);
while (!progressMonitor.getState().equals(ProgressMonitor.State.READY)) {
updateProgress(progressMonitor.getWorkCompleted(), progressMonitor.getTotalWork(), zipFilePath);

Thread.sleep(100);
}

if (progressMonitor.getResult().equals(ProgressMonitor.Result.SUCCESS)) {
zipFile.close();
updateProgress(1, 1, zipFilePath); // force 100%
promise.resolve(destDirectory);
} else if (progressMonitor.getResult().equals(ProgressMonitor.Result.ERROR)) {
throw new Exception("Error occurred. Error message: " + progressMonitor.getException().getMessage());
} else if (progressMonitor.getResult().equals(ProgressMonitor.Result.CANCELLED)) {
throw new Exception("Task cancelled");
}
} catch (Exception ex) {
updateProgress(0, 1, zipFilePath); // force 0%
promise.reject(null, "Failed to extract file " + ex.getLocalizedMessage());
Expand All @@ -178,12 +196,21 @@ public void unzipAssets(final String assetsPath, final String destDirectory, fin
@Override
public void run() {
InputStream assetsInputStream;
final long size;
final long compressedSize;

try {
assetsInputStream = getReactApplicationContext().getAssets().open(assetsPath);
AssetFileDescriptor fileDescriptor = getReactApplicationContext().getAssets().openFd(assetsPath);
size = fileDescriptor.getLength();
if(assetsPath.startsWith("content://")) {
var assetUri = Uri.parse(assetsPath);
var contentResolver = getReactApplicationContext().getContentResolver();

assetsInputStream = contentResolver.openInputStream(assetUri);
var fileDescriptor = contentResolver.openFileDescriptor(assetUri, "r");
compressedSize = fileDescriptor.getStatSize();
} else {
assetsInputStream = getReactApplicationContext().getAssets().open(assetsPath);
AssetFileDescriptor fileDescriptor = getReactApplicationContext().getAssets().openFd(assetsPath);
compressedSize = fileDescriptor.getLength();
}
} catch (IOException e) {
promise.reject(null, String.format("Asset file `%s` could not be opened", assetsPath));
return;
Expand All @@ -201,51 +228,44 @@ public void run() {

ZipEntry entry;

final long[] extractedBytes = {0};
final int[] lastPercentage = {0};
long extractedBytes = 0;
updateProgress(extractedBytes, compressedSize, assetsPath); // force 0%

updateProgress(0, 1, assetsPath); // force 0%
File fout;
while ((entry = zipIn.getNextEntry()) != null) {
if (entry.isDirectory()) continue;

Log.i("rnziparchive", "Extracting: " + entry.getName());

fout = new File(destDirectory, entry.getName());
String canonicalPath = fout.getCanonicalPath();
String destDirCanonicalPath = (new File(destDirectory).getCanonicalPath()) + File.separator;

if (!canonicalPath.startsWith(destDirCanonicalPath)) {
throw new SecurityException(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
throw new SecurityException(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
}

if (!fout.exists()) {
//noinspection ResultOfMethodCallIgnored
(new File(fout.getParent())).mkdirs();
}

final ZipEntry finalEntry = entry;
StreamUtil.ProgressCallback cb = new StreamUtil.ProgressCallback() {
@Override
public void onCopyProgress(long bytesRead) {
extractedBytes[0] += bytesRead;

int lastTime = lastPercentage[0];
int percentDone = (int) ((double) extractedBytes[0] * 100 / (double) size);

// update at most once per percent.
if (percentDone > lastTime) {
lastPercentage[0] = percentDone;
updateProgress(extractedBytes[0], size, finalEntry.getName());
}
}
};

FileOutputStream out = new FileOutputStream(fout);
BufferedOutputStream Bout = new BufferedOutputStream(out);
StreamUtil.copy(bin, Bout, cb);
StreamUtil.copy(bin, Bout, null);
Bout.close();
out.close();

extractedBytes += entry.getCompressedSize();

// do not let the percentage go over 99% because we want it to hit 100% only when we are sure it's finished
if(extractedBytes > compressedSize*0.99) extractedBytes = (long) (compressedSize*0.99);

updateProgress(extractedBytes, compressedSize, entry.getName());
}

updateProgress(1, 1, assetsPath); // force 100%
updateProgress(compressedSize, compressedSize, assetsPath); // force 100%

bin.close();
zipIn.close();
} catch (Exception ex) {
Expand Down Expand Up @@ -276,21 +296,21 @@ public void zipFolder(final String folder, final String destFile, final Promise

@ReactMethod
public void zipFilesWithPassword(final ReadableArray files, final String destFile, final String password,
String encryptionMethod, Promise promise) {
String encryptionMethod, Promise promise) {
zipWithPassword(files.toArrayList(), destFile, password, encryptionMethod, promise);
}


@ReactMethod
public void zipFolderWithPassword(final String folder, final String destFile, final String password,
String encryptionMethod, Promise promise) {
String encryptionMethod, Promise promise) {
ArrayList<Object> folderAsArrayList = new ArrayList<>();
folderAsArrayList.add(folder);
zipWithPassword(folderAsArrayList, destFile, password, encryptionMethod, promise);
}

private void zipWithPassword(final ArrayList<Object> filesOrDirectory, final String destFile, final String password,
String encryptionMethod, Promise promise) {
String encryptionMethod, Promise promise) {
try{

ZipParameters parameters = new ZipParameters();
Expand Down