diff --git a/package-lock.json b/package-lock.json
index 051ef47b6..2550e534e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -123,6 +123,7 @@
"cordova-plugin-system": "file:src/plugins/system",
"cordova-plugin-websocket": "file:src/plugins/websocket",
"css-loader": "^7.1.4",
+ "JsZip-Java": "file:src/plugins/jszip-java",
"mini-css-extract-plugin": "^2.10.2",
"path-browserify": "^1.0.1",
"postcss-loader": "^8.2.1",
@@ -9154,6 +9155,10 @@
"setimmediate": "^1.0.5"
}
},
+ "node_modules/JsZip-Java": {
+ "resolved": "src/plugins/jszip-java",
+ "link": true
+ },
"node_modules/just-diff": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz",
@@ -12102,6 +12107,12 @@
"dev": true,
"license": "ISC"
},
+ "src/plugins/jszip-java": {
+ "name": "JsZip-Java",
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
"src/plugins/pluginContext": {
"name": "com.foxdebug.acode.rk.plugin.plugincontext",
"version": "1.0.0",
diff --git a/package.json b/package.json
index 81feeea3a..1990e06b8 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,8 @@
"com.foxdebug.acode.rk.exec.proot": {},
"cordova-plugin-iap": {},
"com.foxdebug.acode.rk.customtabs": {},
- "cordova-plugin-system": {}
+ "cordova-plugin-system": {},
+ "com.foxdebug.acode.rk.zip": {}
},
"platforms": [
"android"
@@ -98,6 +99,7 @@
"cordova-plugin-system": "file:src/plugins/system",
"cordova-plugin-websocket": "file:src/plugins/websocket",
"css-loader": "^7.1.4",
+ "JsZip-Java": "file:src/plugins/jszip-java",
"mini-css-extract-plugin": "^2.10.2",
"path-browserify": "^1.0.1",
"postcss-loader": "^8.2.1",
diff --git a/src/lib/installPlugin.js b/src/lib/installPlugin.js
index b7de3508d..9d99a4c4a 100644
--- a/src/lib/installPlugin.js
+++ b/src/lib/installPlugin.js
@@ -3,7 +3,6 @@ import alert from "dialogs/alert";
import confirm from "dialogs/confirm";
import loader from "dialogs/loader";
import purchaseListener from "handlers/purchase";
-import JSZip from "jszip";
import helpers from "utils/helpers";
import Url from "utils/Url";
import config from "./config";
@@ -101,7 +100,7 @@ export default async function installPlugin(
}
if (plugin) {
- const zip = new JSZip();
+ const zip = new window.JSZip();
await zip.loadAsync(plugin);
if (!zip.files["plugin.json"]) {
diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js
index 34ee260cb..599f6c8a8 100644
--- a/src/pages/fileBrowser/fileBrowser.js
+++ b/src/pages/fileBrowser/fileBrowser.js
@@ -12,7 +12,6 @@ import confirm from "dialogs/confirm";
import loader from "dialogs/loader";
import prompt from "dialogs/prompt";
import select from "dialogs/select";
-import JSZip from "jszip";
import actionStack from "lib/actionStack";
import checkFiles from "lib/checkFiles";
import config from "lib/config";
@@ -302,7 +301,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
try {
const zipContent = await fsOperation(zipFile).readFile();
- const zip = await JSZip.loadAsync(zipContent);
+ const zip = await window.JSZip.loadAsync(zipContent);
const targetDir = currentDir.url;
const targetFs = fsOperation(targetDir);
@@ -381,7 +380,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
break;
}
- const zip = new JSZip();
+ const zip = new window.JSZip();
let loadingLoader = loader.create(
strings["loading"],
"Compressing files",
diff --git a/src/plugins/jszip-java/package.json b/src/plugins/jszip-java/package.json
new file mode 100644
index 000000000..ab1c6f092
--- /dev/null
+++ b/src/plugins/jszip-java/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "JsZip-Java",
+ "version": "1.0.0",
+ "description": "A complete drop-in replacement for jszip with java backend",
+ "cordova": {
+ "id": "com.foxdebug.acode.rk.zip",
+ "platforms": [
+ "android"
+ ]
+ },
+ "keywords": [
+ "ecosystem:cordova",
+ "cordova-android"
+ ],
+ "author": "@RohitKushvaha01",
+ "license": "MIT"
+}
diff --git a/src/plugins/jszip-java/plugin.xml b/src/plugins/jszip-java/plugin.xml
new file mode 100644
index 000000000..cbb6ff3e7
--- /dev/null
+++ b/src/plugins/jszip-java/plugin.xml
@@ -0,0 +1,32 @@
+
+
+ JsZip-Java
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plugins/jszip-java/src/android/JsZip.java b/src/plugins/jszip-java/src/android/JsZip.java
new file mode 100644
index 000000000..6af727344
--- /dev/null
+++ b/src/plugins/jszip-java/src/android/JsZip.java
@@ -0,0 +1,904 @@
+package com.foxdebug.acode.rk.zip;
+
+import android.util.Base64;
+import android.util.Log;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+import org.apache.cordova.*;
+import org.json.*;
+
+public class JsZip extends CordovaPlugin {
+
+ private final ConcurrentHashMap instances =
+ new ConcurrentHashMap<>();
+
+ private static class FileEntry {
+
+ String name;
+ boolean isDir;
+ long date;
+ String comment;
+ Integer unixPermissions;
+ Integer dosPermissions;
+ File tempFile;
+ }
+
+ private static class ZipInstance {
+
+ String id;
+ File baseDir;
+ ConcurrentHashMap entries = new ConcurrentHashMap<>();
+
+ ZipInstance(String id, File cacheDir) {
+ this.id = id;
+ this.baseDir = new File(cacheDir, "jszip_" + id);
+ if (!baseDir.exists()) {
+ baseDir.mkdirs();
+ }
+ }
+
+ void destroy() {
+ deleteDir(baseDir);
+ }
+ }
+
+ private static void deleteDir(File file) {
+ if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteDir(child);
+ }
+ }
+ }
+ file.delete();
+ }
+
+ private static Method setExternalAttributesMethod = null;
+ private static Method getExternalAttributesMethod = null;
+
+ static {
+ try {
+ setExternalAttributesMethod = ZipEntry.class.getMethod(
+ "setExternalAttributes",
+ long.class
+ );
+ } catch (Exception e) {
+ // Not available
+ }
+ try {
+ getExternalAttributesMethod = ZipEntry.class.getMethod(
+ "getExternalAttributes"
+ );
+ } catch (Exception e) {
+ // Not available
+ }
+ }
+
+ private static void setUnixPermissions(ZipEntry ze, FileEntry entry) {
+ if (setExternalAttributesMethod != null) {
+ try {
+ long attrs = 0;
+ if (entry.unixPermissions != null) {
+ attrs = ((long) entry.unixPermissions) << 16;
+ } else if (entry.dosPermissions != null) {
+ attrs = entry.dosPermissions & 0xFF;
+ } else {
+ attrs = (entry.isDir ? 0755 : 0644) << 16;
+ }
+ if (entry.isDir) {
+ attrs |= 0x10;
+ }
+ setExternalAttributesMethod.invoke(ze, attrs);
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ }
+
+ private static Integer getUnixPermissions(ZipEntry ze) {
+ if (getExternalAttributesMethod != null) {
+ try {
+ long attrs = (Long) getExternalAttributesMethod.invoke(ze);
+ if (attrs != 0) {
+ return (int) ((attrs >> 16) & 0xFFFF);
+ }
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void pluginInitialize() {
+ super.pluginInitialize();
+ cordova.getThreadPool().execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ File cacheDir = cordova.getActivity().getCacheDir();
+ File[] children = cacheDir.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ if (child.getName().startsWith("jszip_")) {
+ deleteDir(child);
+ }
+ }
+ }
+ }
+ }
+ );
+ }
+
+ @Override
+ public void onDestroy() {
+ cordova.getThreadPool().execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ for (String id : instances.keySet()) {
+ destroyInstance(id, null);
+ }
+ }
+ }
+ );
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean execute(
+ String action,
+ CordovaArgs args,
+ final CallbackContext callbackContext
+ ) throws JSONException {
+ cordova.getThreadPool().execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if ("create".equals(action)) {
+ String id = args.getString(0);
+ createInstance(id);
+ callbackContext.success();
+ } else if ("addFile".equals(action)) {
+ String id = args.getString(0);
+ String path = args.getString(1);
+ JSONObject options = args.optJSONObject(3);
+ if (options == null) {
+ options = new JSONObject();
+ }
+ addFile(id, path, args, options, callbackContext);
+ } else if ("removeFile".equals(action)) {
+ String id = args.getString(0);
+ String path = args.getString(1);
+ removeFile(id, path, callbackContext);
+ } else if ("load".equals(action)) {
+ String id = args.getString(0);
+ JSONObject options = args.optJSONObject(2);
+ if (options == null) {
+ options = new JSONObject();
+ }
+ loadZip(id, args, options, callbackContext);
+ } else if ("getFileContent".equals(action)) {
+ String id = args.getString(0);
+ String path = args.getString(1);
+ String type = args.getString(2);
+ getFileContent(id, path, type, callbackContext);
+ } else if ("generate".equals(action)) {
+ String id = args.getString(0);
+ String prefix = args.getString(1);
+ JSONObject options = args.optJSONObject(2);
+ if (options == null) {
+ options = new JSONObject();
+ }
+ generateZip(id, prefix, options, callbackContext);
+ } else if ("destroy".equals(action)) {
+ String id = args.getString(0);
+ destroyInstance(id, callbackContext);
+ } else if ("extractToDir".equals(action)) {
+ String id = args.getString(0);
+ String prefix = args.getString(1);
+ String targetDir = args.getString(2);
+ extractToDir(id, prefix, targetDir, callbackContext);
+ } else if ("extractZipFileToDir".equals(action)) {
+ String zipFilePath = args.getString(0);
+ String targetDir = args.getString(1);
+ extractZipFileToDir(zipFilePath, targetDir, callbackContext);
+ } else {
+ callbackContext.error("Invalid action: " + action);
+ }
+ } catch (Exception e) {
+ callbackContext.error(
+ "Error in action " + action + ": " + e.getMessage()
+ );
+ }
+ }
+ }
+ );
+ return true;
+ }
+
+ private void createInstance(String id) {
+ File cacheDir = cordova.getActivity().getCacheDir();
+ ZipInstance instance = new ZipInstance(id, cacheDir);
+ instances.put(id, instance);
+ }
+
+ private void addFile(
+ String id,
+ String name,
+ CordovaArgs args,
+ JSONObject options,
+ CallbackContext callbackContext
+ ) {
+ try {
+ ZipInstance instance = instances.get(id);
+ if (instance == null) {
+ callbackContext.error("Instance not found");
+ return;
+ }
+
+ boolean isDir = options.optBoolean("dir", false);
+ FileEntry entry = instance.entries.get(name);
+ if (entry == null) {
+ entry = new FileEntry();
+ entry.name = name;
+ instance.entries.put(name, entry);
+ }
+
+ entry.isDir = isDir;
+ entry.comment = options.optString("comment", "");
+ entry.date = options.optLong("date", System.currentTimeMillis());
+ if (!options.isNull("unixPermissions")) {
+ entry.unixPermissions = options.optInt("unixPermissions");
+ }
+ if (!options.isNull("dosPermissions")) {
+ entry.dosPermissions = options.optInt("dosPermissions");
+ }
+
+ if (!isDir && !args.isNull(2)) {
+ if (entry.tempFile == null) {
+ entry.tempFile = new File(
+ instance.baseDir,
+ UUID.randomUUID().toString() + ".tmp"
+ );
+ }
+
+ String dataType = options.optString("dataType", "string");
+ byte[] bytes;
+ if ("arraybuffer".equals(dataType)) {
+ bytes = args.getArrayBuffer(2);
+ } else if ("base64".equals(dataType)) {
+ String data = args.getString(2);
+ bytes = Base64.decode(data, Base64.DEFAULT);
+ } else {
+ String data = args.getString(2);
+ boolean binary = options.optBoolean("binary", false);
+ if (binary) {
+ bytes = data.getBytes("ISO-8859-1");
+ } else {
+ bytes = data.getBytes("UTF-8");
+ }
+ }
+
+ try (FileOutputStream fos = new FileOutputStream(entry.tempFile)) {
+ fos.write(bytes);
+ }
+ }
+
+ callbackContext.success();
+ } catch (Exception e) {
+ callbackContext.error("Failed to add file: " + e.getMessage());
+ }
+ }
+
+ private void removeFile(
+ String id,
+ String name,
+ CallbackContext callbackContext
+ ) {
+ try {
+ ZipInstance instance = instances.get(id);
+ if (instance == null) {
+ callbackContext.error("Instance not found");
+ return;
+ }
+
+ String normalizedDir = name.endsWith("/") ? name : name + "/";
+ for (String key : instance.entries.keySet()) {
+ if (
+ key.equals(name) ||
+ key.equals(normalizedDir) ||
+ key.startsWith(normalizedDir)
+ ) {
+ FileEntry entry = instance.entries.remove(key);
+ if (
+ entry != null && entry.tempFile != null && entry.tempFile.exists()
+ ) {
+ entry.tempFile.delete();
+ }
+ }
+ }
+ callbackContext.success();
+ } catch (Exception e) {
+ callbackContext.error("Failed to remove file: " + e.getMessage());
+ }
+ }
+
+ private void loadZip(
+ String id,
+ CordovaArgs args,
+ JSONObject options,
+ CallbackContext callbackContext
+ ) {
+ File tempZipFile = null;
+ try {
+ ZipInstance instance = instances.get(id);
+ if (instance == null) {
+ callbackContext.error("Instance not found");
+ return;
+ }
+
+ // Clean current entries if any
+ for (FileEntry entry : instance.entries.values()) {
+ if (entry.tempFile != null && entry.tempFile.exists()) {
+ entry.tempFile.delete();
+ }
+ }
+ instance.entries.clear();
+
+ String dataType = options.optString("dataType", "base64");
+ byte[] zipBytes;
+ if ("arraybuffer".equals(dataType)) {
+ zipBytes = args.getArrayBuffer(1);
+ } else if ("base64".equals(dataType)) {
+ String data = args.getString(1);
+ zipBytes = Base64.decode(data, Base64.DEFAULT);
+ } else {
+ String data = args.getString(1);
+ zipBytes = data.getBytes("ISO-8859-1");
+ }
+
+ //Log.d("JsZip", "loadZip: dataType=" + dataType + ", zipBytesLength=" + zipBytes.length);
+ //System.out.println("[JsZip-Java Debug Native] loadZip: dataType=" + dataType + ", zipBytesLength=" + zipBytes.length);
+
+ // Write to a temporary ZIP file in cache
+ tempZipFile = new File(
+ instance.baseDir,
+ "temp_load_" + UUID.randomUUID().toString() + ".zip"
+ );
+ try (FileOutputStream fos = new FileOutputStream(tempZipFile)) {
+ fos.write(zipBytes);
+ }
+
+ boolean createFolders = options.optBoolean("createFolders", false);
+ JSONArray metaList = new JSONArray();
+
+ // Open using ZipFile (robust against streaming Data Descriptors)
+ try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(tempZipFile)) {
+ java.util.Enumeration extends ZipEntry> entries = zf.entries();
+
+ while (entries.hasMoreElements()) {
+ ZipEntry ze = entries.nextElement();
+ String name = ze.getName();
+ boolean isDir = ze.isDirectory();
+
+ /*System.out.println(
+ "[JsZip-Java Debug Native] Found entry: " +
+ name +
+ ", isDir: " +
+ isDir +
+ ", compressedSize: " +
+ ze.getCompressedSize()
+ );*/
+
+ FileEntry entry = new FileEntry();
+ entry.name = name;
+ entry.isDir = isDir;
+ entry.date = ze.getTime();
+ entry.comment = ze.getComment();
+ entry.unixPermissions = getUnixPermissions(ze);
+ if (entry.unixPermissions == null) {
+ entry.unixPermissions = isDir ? 0755 : 0644;
+ }
+
+ if (!isDir) {
+ entry.tempFile = new File(
+ instance.baseDir,
+ UUID.randomUUID().toString() + ".tmp"
+ );
+ try (
+ java.io.InputStream is = zf.getInputStream(ze);
+ FileOutputStream fos = new FileOutputStream(entry.tempFile)
+ ) {
+ byte[] buffer = new byte[8192];
+ int count;
+ while ((count = is.read(buffer)) != -1) {
+ fos.write(buffer, 0, count);
+ }
+ }
+ }
+
+ instance.entries.put(name, entry);
+
+ JSONObject meta = new JSONObject();
+ meta.put("name", name);
+ meta.put("dir", isDir);
+ meta.put("date", entry.date);
+ meta.put("comment", entry.comment != null ? entry.comment : "");
+ meta.put("unixPermissions", entry.unixPermissions);
+ meta.put(
+ "dosPermissions",
+ entry.dosPermissions != null
+ ? entry.dosPermissions
+ : JSONObject.NULL
+ );
+ metaList.put(meta);
+ }
+ }
+
+ if (createFolders) {
+ java.util.HashSet impliedDirs = new java.util.HashSet<>();
+ for (String name : instance.entries.keySet()) {
+ String[] parts = name.split("/");
+ String current = "";
+ for (int i = 0; i < parts.length - 1; i++) {
+ current += parts[i] + "/";
+ if (!instance.entries.containsKey(current)) {
+ impliedDirs.add(current);
+ }
+ }
+ }
+ for (String dirPath : impliedDirs) {
+ FileEntry folderEntry = new FileEntry();
+ folderEntry.name = dirPath;
+ folderEntry.isDir = true;
+ folderEntry.date = System.currentTimeMillis();
+ folderEntry.unixPermissions = 0755;
+ instance.entries.put(dirPath, folderEntry);
+
+ JSONObject meta = new JSONObject();
+ meta.put("name", dirPath);
+ meta.put("dir", true);
+ meta.put("date", folderEntry.date);
+ meta.put("comment", "");
+ meta.put("unixPermissions", 0755);
+ meta.put("dosPermissions", JSONObject.NULL);
+ metaList.put(meta);
+ }
+ }
+
+ JSONObject resultObj = new JSONObject();
+ resultObj.put("files", metaList);
+ resultObj.put("base64Length", zipBytes.length); // Maintain compatibility/logs
+ resultObj.put("zipBytesLength", zipBytes.length);
+ callbackContext.success(resultObj);
+ } catch (Exception e) {
+ callbackContext.error("Failed to load ZIP: " + e.getMessage());
+ } finally {
+ if (tempZipFile != null && tempZipFile.exists()) {
+ tempZipFile.delete();
+ }
+ }
+ }
+
+ private void streamFile(File file, CallbackContext callbackContext) {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ byte[] buffer = new byte[262144]; // 256 KB chunk size
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ byte[] chunk = new byte[count];
+ System.arraycopy(buffer, 0, chunk, 0, count);
+ PluginResult chunkResult = new PluginResult(
+ PluginResult.Status.OK,
+ chunk
+ );
+ chunkResult.setKeepCallback(true);
+ callbackContext.sendPluginResult(chunkResult);
+ }
+
+ // Send final "done" signal
+ JSONObject doneObj = new JSONObject();
+ doneObj.put("done", true);
+ PluginResult finalResult = new PluginResult(
+ PluginResult.Status.OK,
+ doneObj
+ );
+ finalResult.setKeepCallback(false);
+ callbackContext.sendPluginResult(finalResult);
+ } catch (Exception e) {
+ callbackContext.error("Failed to stream file: " + e.getMessage());
+ }
+ }
+
+ private void getFileContent(
+ String id,
+ String name,
+ String type,
+ CallbackContext callbackContext
+ ) {
+ try {
+ ZipInstance instance = instances.get(id);
+ if (instance == null) {
+ callbackContext.error("Instance not found");
+ return;
+ }
+
+ FileEntry entry = instance.entries.get(name);
+ if (entry == null) {
+ callbackContext.error("File not found: " + name);
+ return;
+ }
+
+ if (entry.isDir) {
+ callbackContext.error("Cannot get content of a directory");
+ return;
+ }
+
+ if (entry.tempFile == null || !entry.tempFile.exists()) {
+ callbackContext.error("Temporary file not found for entry: " + name);
+ return;
+ }
+
+ streamFile(entry.tempFile, callbackContext);
+ } catch (Exception e) {
+ callbackContext.error("Failed to get file content: " + e.getMessage());
+ }
+ }
+
+ private void generateZip(
+ String id,
+ String prefix,
+ JSONObject options,
+ CallbackContext callbackContext
+ ) {
+ File zipFile = null;
+ try {
+ ZipInstance instance = instances.get(id);
+ if (instance == null) {
+ callbackContext.error("Instance not found");
+ return;
+ }
+
+ String compression = options.optString("compression", "STORE");
+ int compressionLevel = options.optInt("compressionLevel", 6);
+ String comment = options.optString("comment", null);
+
+ zipFile = new File(
+ instance.baseDir,
+ "output_" + UUID.randomUUID().toString() + ".zip"
+ );
+
+ try (
+ FileOutputStream fos = new FileOutputStream(zipFile);
+ ZipOutputStream zos = new ZipOutputStream(fos)
+ ) {
+ if ("DEFLATE".equalsIgnoreCase(compression)) {
+ zos.setMethod(ZipOutputStream.DEFLATED);
+ zos.setLevel(compressionLevel);
+ } else {
+ zos.setMethod(ZipOutputStream.STORED);
+ }
+
+ if (comment != null && !comment.isEmpty()) {
+ zos.setComment(comment);
+ }
+
+ List sortedKeys = new ArrayList<>(instance.entries.keySet());
+ Collections.sort(sortedKeys);
+
+ int totalEntries = 0;
+ for (String key : sortedKeys) {
+ if (key.startsWith(prefix)) {
+ totalEntries++;
+ }
+ }
+
+ int currentEntryIndex = 0;
+ for (String key : sortedKeys) {
+ if (!key.startsWith(prefix)) {
+ continue;
+ }
+
+ FileEntry entry = instance.entries.get(key);
+ if (entry == null) {
+ continue;
+ }
+
+ String zipPath = key.substring(prefix.length());
+ if (zipPath.isEmpty()) {
+ continue;
+ }
+
+ currentEntryIndex++;
+
+ // Send progress update
+ JSONObject progress = new JSONObject();
+ progress.put("progress", true);
+ progress.put("percent", (currentEntryIndex * 100) / totalEntries);
+ progress.put("currentFile", zipPath);
+ PluginResult progressResult = new PluginResult(
+ PluginResult.Status.OK,
+ progress
+ );
+ progressResult.setKeepCallback(true);
+ callbackContext.sendPluginResult(progressResult);
+
+ ZipEntry ze = new ZipEntry(zipPath);
+ ze.setTime(entry.date);
+ if (entry.comment != null && !entry.comment.isEmpty()) {
+ ze.setComment(entry.comment);
+ }
+ setUnixPermissions(ze, entry);
+
+ if (entry.isDir) {
+ if (!zipPath.endsWith("/")) {
+ ze = new ZipEntry(zipPath + "/");
+ setUnixPermissions(ze, entry);
+ }
+ if (!"DEFLATE".equalsIgnoreCase(compression)) {
+ ze.setSize(0);
+ ze.setCompressedSize(0);
+ ze.setCrc(0);
+ }
+ zos.putNextEntry(ze);
+ zos.closeEntry();
+ } else {
+ if (entry.tempFile != null && entry.tempFile.exists()) {
+ long size = entry.tempFile.length();
+ if (!"DEFLATE".equalsIgnoreCase(compression)) {
+ ze.setSize(size);
+ ze.setCompressedSize(size);
+ long crc = calculateCRC32(entry.tempFile);
+ ze.setCrc(crc);
+ }
+
+ zos.putNextEntry(ze);
+
+ try (FileInputStream fis = new FileInputStream(entry.tempFile)) {
+ byte[] buffer = new byte[8192];
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ zos.write(buffer, 0, count);
+ }
+ }
+ zos.closeEntry();
+ }
+ }
+ }
+
+ zos.finish();
+ }
+
+ streamFile(zipFile, callbackContext);
+ } catch (Exception e) {
+ callbackContext.error("Failed to generate ZIP: " + e.getMessage());
+ } finally {
+ if (zipFile != null && zipFile.exists()) {
+ zipFile.delete();
+ }
+ }
+ }
+
+ private static long calculateCRC32(File file) throws IOException {
+ CRC32 crc = new CRC32();
+ try (FileInputStream fis = new FileInputStream(file)) {
+ byte[] buffer = new byte[8192];
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ crc.update(buffer, 0, count);
+ }
+ }
+ return crc.getValue();
+ }
+
+ private static boolean isSafePath(File destDir, File destFile)
+ throws IOException {
+ String destDirCanonical = destDir.getCanonicalPath();
+ String destFileCanonical = destFile.getCanonicalPath();
+ return (
+ destFileCanonical.startsWith(destDirCanonical + File.separator) ||
+ destFileCanonical.equals(destDirCanonical)
+ );
+ }
+
+ private void extractToDir(
+ String id,
+ String prefix,
+ String targetDir,
+ CallbackContext callbackContext
+ ) {
+ try {
+ ZipInstance instance = instances.get(id);
+ if (instance == null) {
+ callbackContext.error("Instance not found");
+ return;
+ }
+
+ File destDir = new File(targetDir);
+ if (!destDir.exists()) {
+ destDir.mkdirs();
+ }
+
+ List sortedKeys = new ArrayList<>(instance.entries.keySet());
+ Collections.sort(sortedKeys);
+
+ int totalEntries = 0;
+ for (String key : sortedKeys) {
+ if (key.startsWith(prefix)) {
+ totalEntries++;
+ }
+ }
+
+ int currentEntryIndex = 0;
+ for (String key : sortedKeys) {
+ if (!key.startsWith(prefix)) {
+ continue;
+ }
+
+ FileEntry entry = instance.entries.get(key);
+ if (entry == null) {
+ continue;
+ }
+
+ String relativePath = key.substring(prefix.length());
+ if (relativePath.isEmpty()) {
+ continue;
+ }
+
+ currentEntryIndex++;
+
+ // Progress report
+ JSONObject progress = new JSONObject();
+ progress.put("progress", true);
+ progress.put("percent", (currentEntryIndex * 100) / totalEntries);
+ progress.put("currentFile", relativePath);
+ PluginResult progressResult = new PluginResult(
+ PluginResult.Status.OK,
+ progress
+ );
+ progressResult.setKeepCallback(true);
+ callbackContext.sendPluginResult(progressResult);
+
+ File destFile = new File(destDir, relativePath);
+ if (!isSafePath(destDir, destFile)) {
+ throw new SecurityException(
+ "Path traversal attempt detected in entry: " + relativePath
+ );
+ }
+
+ if (entry.isDir) {
+ destFile.mkdirs();
+ } else {
+ File parent = destFile.getParentFile();
+ if (parent != null && !parent.exists()) {
+ parent.mkdirs();
+ }
+
+ if (entry.tempFile != null && entry.tempFile.exists()) {
+ try (
+ FileInputStream fis = new FileInputStream(entry.tempFile);
+ FileOutputStream fos = new FileOutputStream(destFile)
+ ) {
+ byte[] buffer = new byte[8192];
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ fos.write(buffer, 0, count);
+ }
+ }
+ }
+ }
+ }
+
+ callbackContext.success();
+ } catch (Exception e) {
+ callbackContext.error("Failed to extract: " + e.getMessage());
+ }
+ }
+
+ private void extractZipFileToDir(
+ String zipFilePath,
+ String targetDir,
+ CallbackContext callbackContext
+ ) {
+ try {
+ File zipFile = new File(zipFilePath);
+ if (!zipFile.exists()) {
+ callbackContext.error("Source zip file not found: " + zipFilePath);
+ return;
+ }
+
+ File destDir = new File(targetDir);
+ if (!destDir.exists()) {
+ destDir.mkdirs();
+ }
+
+ try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile)) {
+ int totalEntries = zf.size();
+ int currentEntryIndex = 0;
+ java.util.Enumeration extends ZipEntry> entries = zf.entries();
+
+ while (entries.hasMoreElements()) {
+ ZipEntry ze = entries.nextElement();
+ String name = ze.getName();
+ boolean isDir = ze.isDirectory();
+
+ currentEntryIndex++;
+
+ // Progress report
+ JSONObject progress = new JSONObject();
+ progress.put("progress", true);
+ progress.put("percent", (currentEntryIndex * 100) / totalEntries);
+ progress.put("currentFile", name);
+ PluginResult progressResult = new PluginResult(
+ PluginResult.Status.OK,
+ progress
+ );
+ progressResult.setKeepCallback(true);
+ callbackContext.sendPluginResult(progressResult);
+
+ File destFile = new File(destDir, name);
+ if (!isSafePath(destDir, destFile)) {
+ throw new SecurityException(
+ "Path traversal attempt detected in entry: " + name
+ );
+ }
+
+ if (isDir) {
+ destFile.mkdirs();
+ } else {
+ File parent = destFile.getParentFile();
+ if (parent != null && !parent.exists()) {
+ parent.mkdirs();
+ }
+
+ try (
+ java.io.InputStream is = zf.getInputStream(ze);
+ FileOutputStream fos = new FileOutputStream(destFile)
+ ) {
+ byte[] buffer = new byte[8192];
+ int count;
+ while ((count = is.read(buffer)) != -1) {
+ fos.write(buffer, 0, count);
+ }
+ }
+ }
+ }
+ }
+
+ callbackContext.success();
+ } catch (Exception e) {
+ callbackContext.error("Failed to extract ZIP: " + e.getMessage());
+ }
+ }
+
+ private void destroyInstance(String id, CallbackContext callbackContext) {
+ try {
+ ZipInstance instance = instances.remove(id);
+ if (instance != null) {
+ instance.destroy();
+ }
+ if (callbackContext != null) {
+ callbackContext.success();
+ }
+ } catch (Exception e) {
+ if (callbackContext != null) {
+ callbackContext.error("Failed to destroy: " + e.getMessage());
+ }
+ }
+ }
+}
diff --git a/src/plugins/jszip-java/www/JsZip.js b/src/plugins/jszip-java/www/JsZip.js
new file mode 100644
index 000000000..6c4c091e0
--- /dev/null
+++ b/src/plugins/jszip-java/www/JsZip.js
@@ -0,0 +1,747 @@
+/*
+ * JsZip-Java - A complete drop-in replacement for JSZip with a Java backend for Cordova.
+ */
+
+var exec = require('cordova/exec');
+
+function genUUID() {
+ return 'zip_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
+}
+
+var registry = typeof FinalizationRegistry !== 'undefined' ? new FinalizationRegistry(function(id) {
+ exec(null, null, "JsZip", "destroy", [id]);
+}) : null;
+
+function arrayBufferToBase64(bufferOrView) {
+ var bytes;
+ if (bufferOrView instanceof ArrayBuffer) {
+ bytes = new Uint8Array(bufferOrView);
+ } else if (ArrayBuffer.isView(bufferOrView)) {
+ bytes = new Uint8Array(bufferOrView.buffer, bufferOrView.byteOffset, bufferOrView.byteLength);
+ } else {
+ throw new Error("Invalid buffer type");
+ }
+ var binary = '';
+ var len = bytes.byteLength;
+ var chunk = 8192;
+ for (var i = 0; i < len; i += chunk) {
+ var slice = bytes.subarray(i, i + chunk);
+ binary += String.fromCharCode.apply(null, slice);
+ }
+ return window.btoa(binary);
+}
+
+function JSZip() {
+ if (!(this instanceof JSZip)) {
+ return new JSZip();
+ }
+ this.id = genUUID();
+ this.root = this;
+ this.prefix = "";
+ this.files = {};
+ this._pending = [];
+
+ // Notify native of instance creation
+ exec(null, null, "JsZip", "create", [this.id]);
+
+ if (registry) {
+ registry.register(this, this.id);
+ }
+}
+
+JSZip.support = {
+ arraybuffer: true,
+ uint8array: true,
+ blob: true,
+ nodebuffer: false,
+ base64: true
+};
+
+JSZip.external = {
+ Promise: window.Promise
+};
+
+JSZip.version = "3.10.1";
+
+JSZip.defaults = {
+ compression: "STORE",
+ compressionOptions: {
+ level: null
+ }
+};
+
+JSZip.prototype.folder = function(name) {
+ var self = this;
+ if (name instanceof RegExp) {
+ var results = [];
+ var regex = name;
+ for (var filename in self.files) {
+ var file = self.files[filename];
+ if (file.dir && filename.indexOf(self.prefix) === 0) {
+ var relative = filename.slice(self.prefix.length);
+ if (regex.test(relative)) {
+ results.push(file);
+ }
+ }
+ }
+ return results;
+ }
+
+ if (!name.endsWith('/')) {
+ name += '/';
+ }
+ var fullPath = self.prefix + name;
+
+ if (!self.root.files[fullPath]) {
+ var folderObj = new JSZipObject(fullPath, true, self.root);
+ self.root.files[fullPath] = folderObj;
+
+ var promise = new Promise(function(resolve, reject) {
+ exec(resolve, reject, "JsZip", "addFile", [self.root.id, fullPath, null, { dir: true }]);
+ });
+ self.root._pending.push(promise);
+ }
+
+ var child = Object.create(JSZip.prototype);
+ child.id = self.root.id;
+ child.root = self.root;
+ child.prefix = fullPath;
+ child.files = self.root.files;
+ child._pending = self.root._pending;
+ return child;
+};
+
+JSZip.prototype.file = function(name, data, options) {
+ if (name instanceof RegExp) {
+ var results = [];
+ var regex = name;
+ for (var filename in this.files) {
+ var file = this.files[filename];
+ if (!file.dir && filename.indexOf(this.prefix) === 0) {
+ var relative = filename.slice(this.prefix.length);
+ if (regex.test(relative)) {
+ results.push(file);
+ }
+ }
+ }
+ return results;
+ }
+
+ if (data === undefined || data === null) {
+ var fullPath = this.prefix + name;
+ if (options && options.dir) {
+ return this.folder(name);
+ }
+ return this.root.files[fullPath] || null;
+ }
+
+ var fullPath = this.prefix + name;
+ options = options || {};
+
+ var fileObj = new JSZipObject(fullPath, false, this.root, options);
+ fileObj._data = data;
+ this.root.files[fullPath] = fileObj;
+
+ if (typeof data.then === 'function') {
+ var self = this;
+ var promise = data.then(function(resolvedData) {
+ fileObj._data = resolvedData;
+ return self._addFileToNativePromise(fullPath, resolvedData, options);
+ });
+ this.root._pending.push(promise);
+ } else {
+ this._addFileToNative(fullPath, data, options);
+ }
+
+ return this;
+};
+
+JSZip.prototype._ensureParentFolders = function(fullPath) {
+ var self = this;
+ var parts = fullPath.split('/');
+ var current = "";
+ var promise = Promise.resolve();
+
+ for (var i = 0; i < parts.length - 1; i++) {
+ current += parts[i] + "/";
+ if (!this.root.files[current]) {
+ (function(folderPath) {
+ var folderObj = new JSZipObject(folderPath, true, self.root);
+ self.root.files[folderPath] = folderObj;
+ promise = promise.then(function() {
+ return new Promise(function(resolve, reject) {
+ exec(
+ resolve,
+ reject,
+ "JsZip",
+ "addFile",
+ [self.root.id, folderPath, null, { dir: true }]
+ );
+ });
+ });
+ })(current);
+ }
+ }
+ return promise;
+};
+
+JSZip.prototype._addFileToNative = function(fullPath, data, options) {
+ var promise = this._addFileToNativePromise(fullPath, data, options);
+ this.root._pending.push(promise);
+ return promise;
+};
+
+JSZip.prototype._addFileToNativePromise = function(fullPath, data, options) {
+ var self = this;
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ resolve(e.target.result);
+ };
+ reader.onerror = function(err) {
+ reject(err);
+ };
+ reader.readAsArrayBuffer(data);
+ }).then(function(buffer) {
+ return self._addFileToNativePromise(fullPath, buffer, options);
+ });
+ }
+
+ if (data && typeof data.async === 'function') {
+ var fileObj = self.root.files[fullPath];
+ return data.async("arraybuffer").then(function(buffer) {
+ if (fileObj) {
+ fileObj._data = buffer;
+ }
+ return self._addFileToNativePromise(fullPath, buffer, options);
+ });
+ }
+
+ var createFolders = options.createFolders !== false;
+ var parentPromise = Promise.resolve();
+ if (createFolders) {
+ parentPromise = self._ensureParentFolders(fullPath);
+ }
+
+ var dataType = "string";
+ var payload = data;
+ if (data instanceof ArrayBuffer) {
+ dataType = "arraybuffer";
+ payload = data;
+ } else if (ArrayBuffer.isView(data)) {
+ dataType = "arraybuffer";
+ payload = data;
+ } else if (options.base64) {
+ dataType = "base64";
+ }
+
+ return parentPromise.then(function() {
+ return new Promise(function(resolve, reject) {
+ exec(
+ function() {
+ var fileObj = self.root.files[fullPath];
+ if (fileObj) {
+ delete fileObj._data;
+ }
+ resolve();
+ },
+ function(err) {
+ reject(err);
+ },
+ "JsZip",
+ "addFile",
+ [
+ self.root.id,
+ fullPath,
+ payload,
+ {
+ dataType: dataType,
+ dir: options.dir || false,
+ date: options.date ? (options.date instanceof Date ? options.date.getTime() : options.date) : null,
+ comment: options.comment || "",
+ unixPermissions: options.unixPermissions || null,
+ dosPermissions: options.dosPermissions || null,
+ binary: options.binary || false
+ }
+ ]
+ );
+ });
+ });
+};
+
+JSZip.prototype.remove = function(name) {
+ var self = this;
+ var fullPath = self.prefix + name;
+ var normalizedPath = fullPath.endsWith('/') ? fullPath : fullPath + '/';
+
+ for (var filename in self.root.files) {
+ if (filename === fullPath || filename === normalizedPath || filename.indexOf(normalizedPath) === 0) {
+ delete self.root.files[filename];
+ }
+ }
+
+ var promise = new Promise(function(resolve, reject) {
+ exec(resolve, reject, "JsZip", "removeFile", [self.root.id, fullPath]);
+ });
+ self.root._pending.push(promise);
+ return self;
+};
+
+JSZip.prototype.forEach = function(callback) {
+ for (var filename in this.files) {
+ if (filename.indexOf(this.prefix) === 0 && filename !== this.prefix) {
+ var relativePath = filename.slice(this.prefix.length);
+ callback(relativePath, this.files[filename]);
+ }
+ }
+};
+
+JSZip.prototype.filter = function(callback) {
+ var results = [];
+ this.forEach(function(relativePath, file) {
+ if (callback(relativePath, file)) {
+ results.push(file);
+ }
+ });
+ return results;
+};
+
+JSZip.prototype._convertOutput = function(arrayBuffer, type, mimeType) {
+ type = (type || "").toLowerCase();
+
+ if (type === "arraybuffer") {
+ return arrayBuffer;
+ }
+
+ var bytes = new Uint8Array(arrayBuffer);
+ var len = bytes.byteLength;
+
+ if (type === "uint8array") {
+ return bytes;
+ }
+
+ if (type === "blob") {
+ return new Blob([arrayBuffer], { type: mimeType });
+ }
+
+ if (type === "array") {
+ var arr = new Array(len);
+ for (var i = 0; i < len; i++) {
+ arr[i] = bytes[i];
+ }
+ return arr;
+ }
+
+ // For string formats, we convert to binary string first
+ var binaryString = '';
+ var chunk = 8192;
+ for (var i = 0; i < len; i += chunk) {
+ var slice = bytes.subarray(i, i + chunk);
+ binaryString += String.fromCharCode.apply(null, slice);
+ }
+
+ if (type === "string" || type === "binarystring") {
+ return binaryString;
+ }
+
+ if (type === "base64") {
+ return window.btoa(binaryString);
+ }
+
+ if (type === "text") {
+ if (typeof TextDecoder !== 'undefined') {
+ return new TextDecoder().decode(arrayBuffer);
+ } else {
+ return decodeURIComponent(escape(binaryString));
+ }
+ }
+
+ throw new Error("Type '" + type + "' is not supported or not implemented.");
+};
+
+JSZip.prototype.generateAsync = function(options, onUpdate) {
+ options = options || {};
+ var type = options.type || "base64";
+ var compression = options.compression || "STORE";
+ var compressionOptions = options.compressionOptions || {};
+ var comment = options.comment || null;
+ var mimeType = options.mimeType || "application/zip";
+ var self = this;
+
+ return Promise.all(this.root._pending).then(function() {
+ self.root._pending = [];
+
+ return new Promise(function(resolve, reject) {
+ var chunks = [];
+ exec(
+ function(result) {
+ if (result && result.progress) {
+ if (typeof onUpdate === 'function') {
+ onUpdate({
+ percent: result.percent,
+ currentFile: result.currentFile
+ });
+ }
+ return;
+ }
+ if (result && result.done) {
+ var totalLength = 0;
+ for (var i = 0; i < chunks.length; i++) {
+ totalLength += chunks[i].byteLength;
+ }
+ var merged = new Uint8Array(totalLength);
+ var offset = 0;
+ for (var i = 0; i < chunks.length; i++) {
+ merged.set(new Uint8Array(chunks[i]), offset);
+ offset += chunks[i].byteLength;
+ }
+ try {
+ resolve(self._convertOutput(merged.buffer, type, mimeType));
+ } catch (e) {
+ reject(e);
+ }
+ return;
+ }
+ if (result instanceof ArrayBuffer) {
+ chunks.push(result);
+ } else if (ArrayBuffer.isView(result)) {
+ chunks.push(result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength));
+ }
+ },
+ function(err) {
+ reject(err);
+ },
+ "JsZip",
+ "generate",
+ [
+ self.root.id,
+ self.prefix,
+ {
+ compression: compression,
+ compressionLevel: compressionOptions.level || 6,
+ comment: comment
+ }
+ ]
+ );
+ });
+ });
+};
+
+JSZip.prototype.loadAsync = function(data, options) {
+ options = options || {};
+ var self = this;
+
+ return Promise.resolve()
+ .then(function() {
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ resolve({
+ payload: e.target.result,
+ dataType: "arraybuffer"
+ });
+ };
+ reader.onerror = function(err) {
+ reject(err);
+ };
+ reader.readAsArrayBuffer(data);
+ });
+ } else if (data instanceof ArrayBuffer) {
+ return {
+ payload: data,
+ dataType: "arraybuffer"
+ };
+ } else if (ArrayBuffer.isView(data)) {
+ return {
+ payload: data,
+ dataType: "arraybuffer"
+ };
+ } else if (typeof data === 'string') {
+ if (options.base64) {
+ return {
+ payload: data,
+ dataType: "base64"
+ };
+ } else {
+ return {
+ payload: data,
+ dataType: "string"
+ };
+ }
+ } else {
+ throw new Error("Unsupported data type for loadAsync");
+ }
+ })
+ .then(function(normalized) {
+ return new Promise(function(resolve, reject) {
+ exec(
+ function(result) {
+ var filesList = (result && result.files) ? result.files : result;
+ filesList.forEach(function(meta) {
+ var fileObj = new JSZipObject(
+ meta.name,
+ meta.dir,
+ self.root,
+ {
+ date: meta.date ? new Date(meta.date) : null,
+ comment: meta.comment || "",
+ unixPermissions: meta.unixPermissions,
+ dosPermissions: meta.dosPermissions
+ }
+ );
+ self.root.files[meta.name] = fileObj;
+ });
+ resolve(self);
+ },
+ function(err) {
+ reject(err);
+ },
+ "JsZip",
+ "load",
+ [
+ self.root.id,
+ normalized.payload,
+ {
+ dataType: normalized.dataType,
+ checkCRC32: options.checkCRC32 || false,
+ createFolders: options.createFolders || false
+ }
+ ]
+ );
+ });
+ });
+};
+
+JSZip.loadAsync = function(data, options) {
+ return new JSZip().loadAsync(data, options);
+};
+
+JSZip.prototype.extractToDir = function(targetDir, onUpdate) {
+ var self = this;
+ return Promise.all(this.root._pending).then(function() {
+ self.root._pending = [];
+
+ return new Promise(function(resolve, reject) {
+ exec(
+ function(result) {
+ if (result && result.progress) {
+ if (typeof onUpdate === 'function') {
+ onUpdate({
+ percent: result.percent,
+ currentFile: result.currentFile
+ });
+ }
+ return;
+ }
+ resolve();
+ },
+ function(err) {
+ reject(err);
+ },
+ "JsZip",
+ "extractToDir",
+ [
+ self.root.id,
+ self.prefix,
+ targetDir
+ ]
+ );
+ });
+ });
+};
+
+JSZip.extractToDir = function(zipFilePath, targetDir, onUpdate) {
+ return new Promise(function(resolve, reject) {
+ exec(
+ function(result) {
+ if (result && result.progress) {
+ if (typeof onUpdate === 'function') {
+ onUpdate({
+ percent: result.percent,
+ currentFile: result.currentFile
+ });
+ }
+ return;
+ }
+ resolve();
+ },
+ function(err) {
+ reject(err);
+ },
+ "JsZip",
+ "extractZipFileToDir",
+ [
+ zipFilePath,
+ targetDir
+ ]
+ );
+ });
+};
+
+JSZip.prototype.destroy = function() {
+ exec(null, null, "JsZip", "destroy", [this.root.id]);
+};
+
+JSZip.prototype.load = function() {
+ throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.");
+};
+
+JSZip.prototype.generate = function() {
+ throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.");
+};
+
+JSZip.prototype.generateNodeStream = function() {
+ throw new Error("generateNodeStream is not supported by this platform");
+};
+
+JSZip.prototype.generateInternalStream = function() {
+ throw new Error("generateInternalStream is not supported by this platform");
+};
+
+
+function JSZipObject(name, dir, root, options) {
+ this.name = name;
+ this.dir = dir || false;
+ this.root = root;
+
+ options = options || {};
+ this.date = options.date || new Date();
+ this.comment = options.comment || "";
+ this.unixPermissions = options.unixPermissions || null;
+ this.dosPermissions = options.dosPermissions || null;
+ this.options = options;
+}
+
+JSZipObject.prototype.async = function(type, onUpdate) {
+ var self = this;
+
+ function resolveLocal(data) {
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
+ if (type.toLowerCase() === "blob") {
+ return Promise.resolve(data);
+ }
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ try {
+ resolve(self.root._convertOutput(e.target.result, type, data.type));
+ } catch (e) {
+ reject(e);
+ }
+ };
+ reader.onerror = function(err) {
+ reject(err);
+ };
+ reader.readAsArrayBuffer(data);
+ });
+ }
+
+ var buffer;
+ if (data instanceof ArrayBuffer) {
+ buffer = data;
+ } else if (ArrayBuffer.isView(data)) {
+ buffer = data.buffer;
+ } else if (typeof data === 'string') {
+ var bytes = new Uint8Array(data.length);
+ for (var i = 0; i < data.length; i++) {
+ bytes[i] = data.charCodeAt(i);
+ }
+ buffer = bytes.buffer;
+ }
+
+ try {
+ return Promise.resolve(self.root._convertOutput(buffer || data, type, "application/octet-stream"));
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+
+ if (this._data !== undefined && this._data !== null) {
+ if (typeof this._data.then === 'function') {
+ return this._data.then(resolveLocal);
+ }
+ return resolveLocal(this._data);
+ }
+
+ var chunks = [];
+ return new Promise(function(resolve, reject) {
+ exec(
+ function(result) {
+ if (result && result.progress) {
+ if (typeof onUpdate === 'function') {
+ onUpdate({
+ percent: result.percent,
+ currentFile: self.name
+ });
+ }
+ return;
+ }
+ if (result && result.done) {
+ var totalLength = 0;
+ for (var i = 0; i < chunks.length; i++) {
+ totalLength += chunks[i].byteLength;
+ }
+ var merged = new Uint8Array(totalLength);
+ var offset = 0;
+ for (var i = 0; i < chunks.length; i++) {
+ merged.set(new Uint8Array(chunks[i]), offset);
+ offset += chunks[i].byteLength;
+ }
+ try {
+ resolve(self.root._convertOutput(merged.buffer, type, "application/octet-stream"));
+ } catch (e) {
+ reject(e);
+ }
+ return;
+ }
+ if (result instanceof ArrayBuffer) {
+ chunks.push(result);
+ } else if (ArrayBuffer.isView(result)) {
+ chunks.push(result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength));
+ }
+ },
+ function(err) {
+ reject(err);
+ },
+ "JsZip",
+ "getFileContent",
+ [self.root.id, self.name, type]
+ );
+ });
+};
+
+JSZipObject.prototype.nodeStream = function() {
+ throw new Error("nodeStream is not supported by this platform");
+};
+
+JSZipObject.prototype.internalStream = function() {
+ throw new Error("internalStream is not supported by this platform");
+};
+
+JSZipObject.prototype.asText = function() {
+ throw new Error("this method has been removed in JSZip 3.0, please check the upgrade guide.");
+};
+
+JSZipObject.prototype.asBinary = function() {
+ throw new Error("this method has been removed in JSZip 3.0, please check the upgrade guide.");
+};
+
+JSZipObject.prototype.asNodeBuffer = function() {
+ throw new Error("this method has been removed in JSZip 3.0, please check the upgrade guide.");
+};
+
+JSZipObject.prototype.asUint8Array = function() {
+ throw new Error("this method has been removed in JSZip 3.0, please check the upgrade guide.");
+};
+
+JSZipObject.prototype.asArrayBuffer = function() {
+ throw new Error("this method has been removed in JSZip 3.0, please check the upgrade guide.");
+};
+
+if (typeof window !== 'undefined') {
+ window.JSZip = JSZip;
+}
+
+module.exports = JSZip;
diff --git a/src/test/jszip.tests.js b/src/test/jszip.tests.js
new file mode 100644
index 000000000..734394ac7
--- /dev/null
+++ b/src/test/jszip.tests.js
@@ -0,0 +1,330 @@
+import JSZip from "jszip";
+import { TestRunner } from "./tester";
+
+export async function runJsZipTests(writeOutput) {
+ const runner = new TestRunner("JSZip-Java Compatibility Tests");
+
+ const helperCompareBuffers = (buf1, buf2) => {
+ const arr1 = new Uint8Array(buf1);
+ const arr2 = new Uint8Array(buf2);
+ if (arr1.length !== arr2.length) return false;
+ for (let i = 0; i < arr1.length; i++) {
+ if (arr1[i] !== arr2[i]) return false;
+ }
+ return true;
+ };
+
+ // Test 1: Constructor and instance check
+ runner.test("Instance Creation", (test) => {
+ const stdZip = new JSZip();
+ const jvZip = new window.JSZip();
+
+ test.assert(stdZip instanceof JSZip, "stdZip should be instance of JSZip");
+ test.assert(
+ jvZip instanceof window.JSZip,
+ "jvZip should be instance of window.JSZip",
+ );
+ test.assertEqual(
+ typeof jvZip.file,
+ "function",
+ "jvZip.file should be a function",
+ );
+ test.assertEqual(
+ typeof jvZip.folder,
+ "function",
+ "jvZip.folder should be a function",
+ );
+ });
+
+ // Test 2: API Function Presence & Equivalence
+ runner.test("API Function Presence", (test) => {
+ const stdZip = new JSZip();
+ const jvZip = new window.JSZip();
+
+ // Static methods on JSZip constructor
+ const stdStaticKeys = Object.keys(JSZip);
+ for (const key of stdStaticKeys) {
+ if (typeof JSZip[key] === "function") {
+ test.assertEqual(
+ typeof window.JSZip[key],
+ "function",
+ `Static function '${key}' must be present on window.JSZip`,
+ );
+ }
+ }
+
+ // Instance methods / prototype methods
+ const stdProto = Object.getPrototypeOf(stdZip);
+ const stdProtoKeys = Object.getOwnPropertyNames(stdProto);
+
+ for (const key of stdProtoKeys) {
+ if (key === "constructor") continue;
+ // Filter internal/private methods (methods starting with _)
+ if (key.startsWith("_")) continue;
+ if (typeof stdProto[key] === "function") {
+ test.assertEqual(
+ typeof Object.getPrototypeOf(jvZip)[key],
+ "function",
+ `Instance prototype function '${key}' must be present on window.JSZip`,
+ );
+ }
+ }
+
+ // JSZipObject prototype methods
+ const stdFile = stdZip.file("dummy.txt", "content").file("dummy.txt");
+ const jvFile = jvZip.file("dummy.txt", "content").file("dummy.txt");
+
+ const stdFileProto = Object.getPrototypeOf(stdFile);
+ const stdFileProtoKeys = Object.getOwnPropertyNames(stdFileProto);
+
+ for (const key of stdFileProtoKeys) {
+ if (key === "constructor") continue;
+ if (key.startsWith("_")) continue;
+ if (typeof stdFileProto[key] === "function") {
+ test.assertEqual(
+ typeof Object.getPrototypeOf(jvFile)[key],
+ "function",
+ `JSZipObject prototype function '${key}' must be present on JSZipObject`,
+ );
+ }
+ }
+ });
+
+ // Test 3: Adding and reading text file content
+ runner.test("File Add & Async Read (Text)", async (test) => {
+ const stdZip = new JSZip();
+ const jvZip = new window.JSZip();
+
+ const content = "Hello JSZip World!";
+ stdZip.file("test.txt", content);
+ jvZip.file("test.txt", content);
+
+ const stdRes = await stdZip.file("test.txt").async("text");
+ const jvRes = await jvZip.file("test.txt").async("text");
+
+ test.assertEqual(
+ jvRes,
+ stdRes,
+ "Returned text content must match standard JSZip",
+ );
+ test.assertEqual(
+ jvRes,
+ content,
+ "Returned content must match original content",
+ );
+ });
+
+ // Test 4: Adding and reading binary/arraybuffer file content
+ runner.test("File Add & Async Read (Binary ArrayBuffer)", async (test) => {
+ const stdZip = new JSZip();
+ const jvZip = new window.JSZip();
+
+ const bytes = new Uint8Array([72, 101, 108, 108, 111, 0, 1, 2, 3]);
+ const buffer = bytes.buffer;
+
+ stdZip.file("bin.dat", buffer);
+ jvZip.file("bin.dat", buffer);
+
+ const stdBuf = await stdZip.file("bin.dat").async("arraybuffer");
+ const jvBuf = await jvZip.file("bin.dat").async("arraybuffer");
+
+ test.assert(
+ helperCompareBuffers(jvBuf, stdBuf),
+ "Returned ArrayBuffer should match standard JSZip",
+ );
+
+ const stdArr = await stdZip.file("bin.dat").async("uint8array");
+ const jvArr = await jvZip.file("bin.dat").async("uint8array");
+
+ test.assertEqual(
+ jvArr.length,
+ stdArr.length,
+ "Uint8Array lengths must match",
+ );
+ test.assertEqual(jvArr[5], 0, "Binary zero must be preserved");
+ test.assertEqual(jvArr[8], 3, "Binary values must match");
+ });
+
+ // Test 5: folder prefixing and nested hierarchy
+ runner.test("Folder Navigation & Structure", async (test) => {
+ const stdZip = new JSZip();
+ const jvZip = new window.JSZip();
+
+ const stdSub = stdZip.folder("nested").folder("deep");
+ const jvSub = jvZip.folder("nested").folder("deep");
+
+ stdSub.file("hello.txt", "inside nested");
+ jvSub.file("hello.txt", "inside nested");
+
+ // Access via root
+ const stdRes = await stdZip.file("nested/deep/hello.txt").async("text");
+ const jvRes = await jvZip.file("nested/deep/hello.txt").async("text");
+
+ test.assertEqual(
+ jvRes,
+ stdRes,
+ "Nested file content should match via root access",
+ );
+
+ // Access via child instance
+ const stdResChild = await stdSub.file("hello.txt").async("text");
+ const jvResChild = await jvSub.file("hello.txt").async("text");
+
+ test.assertEqual(
+ jvResChild,
+ stdResChild,
+ "Nested file content should match via child access",
+ );
+ });
+
+ // Test 6: Files directory listing and iteration comparison
+ runner.test("Directory listing & forEach comparison", (test) => {
+ const stdZip = new JSZip();
+ const jvZip = new window.JSZip();
+
+ const setup = (zip) => {
+ zip.file("a.txt", "1");
+ zip.file("b/c.txt", "2");
+ zip.folder("b/d");
+ };
+
+ setup(stdZip);
+ setup(jvZip);
+
+ const stdList = [];
+ stdZip.forEach((rel, obj) => {
+ stdList.push({ path: rel, isDir: obj.dir });
+ });
+
+ const jvList = [];
+ jvZip.forEach((rel, obj) => {
+ jvList.push({ path: rel, isDir: obj.dir });
+ });
+
+ test.assertEqual(
+ jvList.length,
+ stdList.length,
+ "forEach count must match standard JSZip",
+ );
+
+ stdList.sort((a, b) => a.path.localeCompare(b.path));
+ jvList.sort((a, b) => a.path.localeCompare(b.path));
+
+ for (let i = 0; i < stdList.length; i++) {
+ test.assertEqual(
+ jvList[i].path,
+ stdList[i].path,
+ `Entry ${i} path must match`,
+ );
+ test.assertEqual(
+ jvList[i].isDir,
+ stdList[i].isDir,
+ `Entry ${i} isDir must match`,
+ );
+ }
+ });
+
+ // Test 7: Removing files and directories
+ runner.test("File & Folder Removal", async (test) => {
+ const stdZip = new JSZip();
+ const jvZip = new window.JSZip();
+
+ stdZip.file("test1.txt", "data");
+ stdZip.file("folder/test2.txt", "data");
+ stdZip.remove("test1.txt");
+ stdZip.remove("folder");
+
+ jvZip.file("test1.txt", "data");
+ jvZip.file("folder/test2.txt", "data");
+ jvZip.remove("test1.txt");
+ jvZip.remove("folder");
+
+ test.assertEqual(
+ jvZip.file("test1.txt"),
+ null,
+ "test1.txt must be null after removal",
+ );
+ test.assertEqual(
+ jvZip.file("folder/test2.txt"),
+ null,
+ "folder/test2.txt must be null after folder removal",
+ );
+
+ const stdKeys = Object.keys(stdZip.files);
+ const jvKeys = Object.keys(jvZip.files);
+ test.assertEqual(
+ jvKeys.length,
+ stdKeys.length,
+ "Remaining file keys count must match standard JSZip",
+ );
+ });
+
+ // Test 8: Output Type conversion compatibility (Base64, BinaryString)
+ runner.test("Type Conversions (base64, binarystring)", async (test) => {
+ const jvZip = new window.JSZip();
+ const content = "Testing type conversions \x00\x01\x02";
+ jvZip.file("data.txt", content, { binary: true });
+
+ const b64 = await jvZip.file("data.txt").async("base64");
+ const binStr = await jvZip.file("data.txt").async("binarystring");
+ const text = await jvZip.file("data.txt").async("text");
+
+ test.assertEqual(
+ b64,
+ window.btoa(content),
+ "Base64 conversion should match window.btoa",
+ );
+ test.assertEqual(
+ binStr,
+ content,
+ "BinaryString must match original raw string",
+ );
+ test.assert(
+ text.startsWith("Testing type conversions"),
+ "Text must resolve correctly",
+ );
+ });
+
+ // Test 9: ZIP Compilation and Cross-Load compatibility
+ runner.test("ZIP Generation & Cross-Loading", async (test) => {
+ const stdZip = new JSZip();
+ stdZip.file("info.json", JSON.stringify({ version: "1.0.0" }));
+ stdZip.file("images/logo.png", new Uint8Array([1, 2, 3, 4]).buffer);
+
+ // 1. Generate using standard, load in jszip-java
+ const stdZipBuffer = await stdZip.generateAsync({ type: "arraybuffer" });
+
+ const jvZip = new window.JSZip();
+ await jvZip.loadAsync(stdZipBuffer);
+
+ const jvJson = await jvZip.file("info.json").async("text");
+ const jvPng = await jvZip.file("images/logo.png").async("uint8array");
+
+ test.assertEqual(
+ JSON.parse(jvJson).version,
+ "1.0.0",
+ "Loaded JSON config should match",
+ );
+ test.assertEqual(jvPng.length, 4, "Loaded nested binary length must match");
+ test.assertEqual(jvPng[3], 4, "Loaded binary byte must match");
+
+ // 2. Generate using jszip-java, load in standard
+ const jvZipBuffer = await jvZip.generateAsync({
+ type: "arraybuffer",
+ compression: "DEFLATE",
+ });
+
+ const crossStdZip = new JSZip();
+ await crossStdZip.loadAsync(jvZipBuffer);
+
+ const crossJson = await crossStdZip.file("info.json").async("text");
+ test.assertEqual(
+ JSON.parse(crossJson).version,
+ "1.0.0",
+ "Cross-loaded standard JSZip must read jszip-java output successfully",
+ );
+ });
+
+ // Run tests
+ return await runner.run(writeOutput);
+}
diff --git a/src/test/test-definitions.js b/src/test/test-definitions.js
index f29c7cead..dea571513 100644
--- a/src/test/test-definitions.js
+++ b/src/test/test-definitions.js
@@ -2,6 +2,7 @@ import { runAceCompatibilityTests } from "./ace.test";
import { runCodeMirrorTests } from "./editor.tests";
import { runExecutorTests } from "./exec.tests";
import { runFsTests } from "./fs.tests";
+import { runJsZipTests } from "./jszip.tests";
import { runSanityTests } from "./sanity.tests";
import { runUrlTests } from "./url.tests";
@@ -14,6 +15,7 @@ export const testDefinitions = [
runExecutorTests,
runUrlTests,
runFsTests,
+ runJsZipTests,
runCodeMirrorTests,
runAceCompatibilityTests,
];