-
Notifications
You must be signed in to change notification settings - Fork 167
Expand file tree
/
Copy pathRNZipArchiveModule.java
More file actions
521 lines (446 loc) · 19.1 KB
/
RNZipArchiveModule.java
File metadata and controls
521 lines (446 loc) · 19.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
package com.rnziparchive;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.modules.core.DeviceEventManagerModule;
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.io.PrintWriter;
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;
//import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.CompressionMethod;
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;
public class RNZipArchiveModule extends ReactContextBaseJavaModule {
private static final String TAG = RNZipArchiveModule.class.getSimpleName();
private static final String PROGRESS_EVENT_NAME = "zipArchiveProgressEvent";
private static final String EVENT_KEY_FILENAME = "filePath";
private static final String EVENT_KEY_PROGRESS = "progress";
public RNZipArchiveModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "RNZipArchive";
}
@ReactMethod
public void isPasswordProtected(final String zipFilePath, final Promise promise) {
try {
net.lingala.zip4j.ZipFile zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
promise.resolve(zipFile.isEncrypted());
} catch (ZipException ex) {
promise.reject(null, String.format("Unable to check for encryption due to: %s", getStackTrace(ex)));
}
}
@ReactMethod
public void unzipWithPassword(final String zipFilePath, final String destDirectory,
final String password, final Promise promise) {
new Thread(new Runnable() {
@Override
public void run() {
try {
net.lingala.zip4j.ZipFile zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
if (zipFile.isEncrypted()) {
zipFile.setPassword(password.toCharArray());
} else {
promise.reject(null, String.format("Zip file: %s is not password protected", zipFilePath));
}
List fileHeaderList = zipFile.getFileHeaders();
List extractedFileNames = new ArrayList<>();
int totalFiles = fileHeaderList.size();
updateProgress(0, 1, zipFilePath); // force 0%
for (int i = 0; i < totalFiles; i++) {
FileHeader fileHeader = (FileHeader) fileHeaderList.get(i);
File fout = new File(destDirectory, fileHeader.getFileName());
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));
}
if (!fileHeader.isDirectory()) {
zipFile.extractFile(fileHeader, destDirectory);
extractedFileNames.add(fileHeader.getFileName());
}
updateProgress(i + 1, totalFiles, zipFilePath);
}
promise.resolve(Arguments.fromList(extractedFileNames));
} catch (Exception ex) {
updateProgress(0, 1, zipFilePath); // force 0%
promise.reject(null, String.format("Failed to unzip file, due to: %s", getStackTrace(ex)));
}
}
}).start();
}
@ReactMethod
public void unzip(final String zipFilePath, final String destDirectory, final String charset, final Promise promise) {
new Thread(new Runnable() {
@Override
public void run() {
// Check the file exists
try {
new File(zipFilePath);
} catch (NullPointerException e) {
promise.reject(null, "Couldn't open file " + zipFilePath + ". ");
return;
}
try {
// Find the total uncompressed size of every file in the zip, so we can
// get an accurate progress measurement
final long totalUncompressedBytes = getUncompressedSize(zipFilePath, charset);
File destDir = new File(destDirectory);
if (!destDir.exists()) {
//noinspection ResultOfMethodCallIgnored
destDir.mkdirs();
}
updateProgress(0, 1, zipFilePath); // force 0%
// We use arrays here so we can update values
// from inside the callback
final long[] extractedBytes = {0};
final int[] lastPercentage = {0};
net.lingala.zip4j.ZipFile zipFile = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
zipFile.setCharset(Charset.forName(charset));
} else {
zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
}
ProgressMonitor progressMonitor = zipFile.getProgressMonitor();
zipFile.setRunInThread(true);
zipFile.extractAll(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());
}
}
}).start();
}
/**
* Extract a zip held in the assets directory.
* <p>
* Note that the progress value isn't as accurate as when unzipping
* from a file. When reading a zip from a stream, we can't
* get accurate uncompressed sizes for files (ZipEntry#getCompressedSize() returns -1).
* <p>
* Instead, we compare the number of bytes extracted to the size of the compressed zip file.
* In most cases this means the progress 'stays on' 100% for a little bit (compressedSize < uncompressed size)
*/
@ReactMethod
public void unzipAssets(final String assetsPath, final String destDirectory, final Promise promise) {
new Thread(new Runnable() {
@Override
public void run() {
InputStream assetsInputStream;
final long compressedSize;
try {
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;
}
try {
try {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
//noinspection ResultOfMethodCallIgnored
destDir.mkdirs();
}
ZipInputStream zipIn = new ZipInputStream(assetsInputStream);
BufferedInputStream bin = new BufferedInputStream(zipIn);
ZipEntry entry;
long extractedBytes = 0;
updateProgress(extractedBytes, compressedSize, 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));
}
if (!fout.exists()) {
//noinspection ResultOfMethodCallIgnored
(new File(fout.getParent())).mkdirs();
}
FileOutputStream out = new FileOutputStream(fout);
BufferedOutputStream Bout = new BufferedOutputStream(out);
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(compressedSize, compressedSize, assetsPath); // force 100%
bin.close();
zipIn.close();
} catch (Exception ex) {
ex.printStackTrace();
updateProgress(0, 1, assetsPath); // force 0%
throw new Exception(String.format("Couldn't extract %s", assetsPath));
}
} catch (Exception ex) {
promise.reject(null, ex.getMessage());
return;
}
promise.resolve(destDirectory);
}
}).start();
}
@ReactMethod
public void zipFiles(final ReadableArray files, final String destDirectory, final double compressionLevel, final Promise promise) {
zip(files.toArrayList(), destDirectory, compressionLevel, promise);
}
@ReactMethod
public void zipFolder(final String folder, final String destFile, final double compressionLevel, final Promise promise) {
ArrayList<Object> folderAsArrayList = new ArrayList<>();
folderAsArrayList.add(folder);
zip(folderAsArrayList, destFile, compressionLevel, promise);
}
@ReactMethod
public void zipFilesWithPassword(final ReadableArray files, final String destFile, final String password,
String encryptionMethod, final double compressionLevel, Promise promise) {
zipWithPassword(files.toArrayList(), destFile, password, encryptionMethod, compressionLevel, promise);
}
@ReactMethod
public void zipFolderWithPassword(final String folder, final String destFile, final String password,
String encryptionMethod, final double compressionLevel, Promise promise) {
ArrayList<Object> folderAsArrayList = new ArrayList<>();
folderAsArrayList.add(folder);
zipWithPassword(folderAsArrayList, destFile, password, encryptionMethod, compressionLevel, promise);
}
private void zipWithPassword(final ArrayList<Object> filesOrDirectory, final String destFile, final String password,
String encryptionMethod, final double compressionLevel, Promise promise) {
try{
ZipParameters parameters = new ZipParameters();
parameters.setCompressionMethod(CompressionMethod.DEFLATE);
parameters.setCompressionLevel(getCompressionLevel(compressionLevel));
String encParts[] = encryptionMethod.split("-");
if (password != null && !password.isEmpty()) {
parameters.setEncryptFiles(true);
if (encParts[0].equals("AES")) {
parameters.setEncryptionMethod(EncryptionMethod.AES);
if (encParts[1].equals("128")) {
parameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_128);
} else if (encParts[1].equals("256")) {
parameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
} else {
parameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_128);
}
} else if (encryptionMethod.equals("STANDARD")) {
parameters.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD_VARIANT_STRONG);
Log.d(TAG, "Standard Encryption");
} else {
parameters.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD);
Log.d(TAG, "Encryption type not supported default to Standard Encryption");
}
} else {
promise.reject(null, "Password is empty");
}
processZip(filesOrDirectory, destFile, parameters, promise, password.toCharArray());
} catch (Exception ex) {
promise.reject(null, ex.getMessage());
return;
}
}
private void zip(final ArrayList<Object> filesOrDirectory, final String destFile, final double compressionLevel, final Promise promise) {
try{
ZipParameters parameters = new ZipParameters();
parameters.setCompressionMethod(CompressionMethod.DEFLATE);
parameters.setCompressionLevel(getCompressionLevel(compressionLevel));
processZip(filesOrDirectory, destFile, parameters, promise, null);
} catch (Exception ex) {
promise.reject(null, ex.getMessage());
return;
}
}
private void processZip(final ArrayList<Object> entries, final String destFile, final ZipParameters parameters, final Promise promise, final char[] password) {
new Thread(new Runnable() {
@Override
public void run() {
try {
net.lingala.zip4j.ZipFile zipFile;
if (password != null) {
zipFile = new net.lingala.zip4j.ZipFile(destFile, password);
} else {
zipFile = new net.lingala.zip4j.ZipFile(destFile);
}
updateProgress(0, 100, destFile);
int totalFiles = 0;
int fileCounter = 0;
for (int i = 0; i < entries.size(); i++) {
File f = new File(entries.get(i).toString());
if (f.exists()) {
if (f.isDirectory()) {
List<File> files = Arrays.asList(f.listFiles());
totalFiles += files.size();
for (int j = 0; j < files.size(); j++) {
if (files.get(j).isDirectory()) {
zipFile.addFolder(files.get(j), parameters);
}
else {
zipFile.addFile(files.get(j), parameters);
}
fileCounter += 1;
updateProgress(fileCounter, totalFiles, destFile);
}
} else {
totalFiles += 1;
zipFile.addFile(f, parameters);
fileCounter += 1;
updateProgress(fileCounter, totalFiles, destFile);
}
}
else {
promise.reject(null, "File or folder does not exist");
}
updateProgress(1, 1, destFile); // force 100%
}
promise.resolve(destFile);
} catch (Exception ex) {
promise.reject(null, ex.getMessage());
return;
}
}
}).start();
}
protected void updateProgress(long extractedBytes, long totalSize, String zipFilePath) {
// Ensure progress can't overflow 1
double progress = Math.min((double) extractedBytes / (double) totalSize, 1);
Log.d(TAG, String.format("updateProgress: %.0f%%", progress * 100));
WritableMap map = Arguments.createMap();
map.putString(EVENT_KEY_FILENAME, zipFilePath);
map.putDouble(EVENT_KEY_PROGRESS, progress);
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(PROGRESS_EVENT_NAME, map);
}
@ReactMethod
public void getUncompressedSize(String zipFilePath, String charset, final Promise promise) {
long totalSize = getUncompressedSize(zipFilePath, charset);
promise.resolve((double) totalSize);
}
/**
* Return the uncompressed size of the ZipFile (only works for files on disk, not in assets)
*
* @return -1 on failure
*/
private long getUncompressedSize(String zipFilePath, String charset) {
long totalSize = 0;
try {
net.lingala.zip4j.ZipFile zipFile = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
zipFile.setCharset(Charset.forName(charset));
} else {
zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
}
final List <FileHeader> files = zipFile.getFileHeaders();
for(FileHeader it : files) {
long size = it.getUncompressedSize();
if (size != -1) {
totalSize += size;
}
}
zipFile.close();
} catch (IOException ignored) {
return -1;
}
return totalSize;
}
private static CompressionLevel getCompressionLevel(double compressionLevel) {
switch (compressionLevel) {
case -1:
return CompressionLevel.NORMAL;
case 0:
return CompressionLevel.NO_COMPRESSION;
case 1:
return CompressionLevel.FASTEST;
case 2:
return CompressionLevel.FASTER;
case 3:
return CompressionLevel.FAST;
case 4:
return CompressionLevel.MEDIUM_FAST;
case 5:
return CompressionLevel.NORMAL;
case 6:
return CompressionLevel.HIGHER;
case 7:
return CompressionLevel.MAXIMUM;
case 8:
return CompressionLevel.PRE_ULTRA;
case 9:
return CompressionLevel.ULTRA;
default:
Log.w(TAG, "Unsupported compression level: " + level + ", defaulting to NORMAL (5)");
return CompressionLevel.NORMAL;
}
}
/**
* Returns the exception stack trace as a string
*/
private String getStackTrace(Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
return sw.toString();
}
@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
@ReactMethod
public void removeListeners(Integer count) {
// Keep: Required for RN built in Event Emitter Calls.
}
}