From 9eb115f950dd184c7a4bc8965a3e00235e4614a0 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 16:25:38 +0530 Subject: [PATCH 1/8] fix: plugin install --- package-lock.json | 11 + package.json | 4 +- src/lib/installPlugin.js | 3 +- src/pages/fileBrowser/fileBrowser.js | 5 +- src/plugins/jszip-java/package.json | 17 + src/plugins/jszip-java/plugin.xml | 32 + src/plugins/jszip-java/src/android/JsZip.java | 781 ++++++++++++++++++ src/plugins/jszip-java/www/JsZip.js | 560 +++++++++++++ 8 files changed, 1407 insertions(+), 6 deletions(-) create mode 100644 src/plugins/jszip-java/package.json create mode 100644 src/plugins/jszip-java/plugin.xml create mode 100644 src/plugins/jszip-java/src/android/JsZip.java create mode 100644 src/plugins/jszip-java/www/JsZip.js 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 29a0930fb..a54b5c274 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..43a9c44da --- /dev/null +++ b/src/plugins/jszip-java/src/android/JsZip.java @@ -0,0 +1,781 @@ +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, + JSONArray 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); + String data = args.isNull(2) ? null : args.getString(2); + JSONObject options = args.optJSONObject(3); + if (options == null) { + options = new JSONObject(); + } + addFile(id, path, data, 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); + String base64Data = args.getString(1); + JSONObject options = args.optJSONObject(2); + if (options == null) { + options = new JSONObject(); + } + loadZip(id, base64Data, 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, String data, 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 && data != null) { + if (entry.tempFile == null) { + entry.tempFile = new File(instance.baseDir, UUID.randomUUID().toString() + ".tmp"); + } + + String dataType = options.optString("dataType", "string"); + byte[] bytes; + if ("base64".equals(dataType)) { + bytes = Base64.decode(data, Base64.DEFAULT); + } else { + 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, String base64Data, 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(); + + byte[] zipBytes = Base64.decode(base64Data, Base64.DEFAULT); + Log.d("JsZip", "loadZip: base64Length=" + base64Data.length() + ", zipBytesLength=" + zipBytes.length); + System.out.println("[JsZip-Java Debug Native] loadZip: base64Length=" + base64Data.length() + ", 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 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", base64Data.length()); + 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 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; + } + + byte[] bytes = new byte[(int) entry.tempFile.length()]; + try (FileInputStream fis = new FileInputStream(entry.tempFile)) { + int offset = 0; + int numRead = 0; + while (offset < bytes.length && (numRead = fis.read(bytes, offset, bytes.length - offset)) >= 0) { + offset += numRead; + } + } + + String base64Data = Base64.encodeToString(bytes, Base64.NO_WRAP); + JSONObject result = new JSONObject(); + result.put("data", base64Data); + callbackContext.success(result); + } catch (Exception e) { + callbackContext.error("Failed to get file content: " + e.getMessage()); + } + } + + private void generateZip(String id, String prefix, JSONObject options, CallbackContext callbackContext) { + 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); + + File 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(); + } + + byte[] zipBytes = new byte[(int) zipFile.length()]; + try (FileInputStream fis = new FileInputStream(zipFile)) { + int offset = 0; + int numRead = 0; + while (offset < zipBytes.length && (numRead = fis.read(zipBytes, offset, zipBytes.length - offset)) >= 0) { + offset += numRead; + } + } + + zipFile.delete(); + + String base64Data = Base64.encodeToString(zipBytes, Base64.NO_WRAP); + JSONObject result = new JSONObject(); + result.put("data", base64Data); + callbackContext.success(result); + + } catch (Exception e) { + callbackContext.error("Failed to generate ZIP: " + e.getMessage()); + } + } + + 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 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..7be93b257 --- /dev/null +++ b/src/plugins/jszip-java/www/JsZip.js @@ -0,0 +1,560 @@ +/* + * 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); +} + +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]); +} + +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) { + 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 (!name.endsWith('/')) { + name += '/'; + } + var fullPath = this.prefix + name; + + if (!this.root.files[fullPath]) { + var folderObj = new JSZipObject(fullPath, true, this.root); + this.root.files[fullPath] = folderObj; + + exec(null, null, "JsZip", "addFile", [this.root.id, fullPath, null, { dir: true }]); + } + + var child = Object.create(JSZip.prototype); + child.id = this.root.id; + child.root = this.root; + child.prefix = fullPath; + child.files = this.root.files; + child._pending = this.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 || {}; + + if (typeof data.then === 'function') { + var self = this; + var promise = data.then(function(resolvedData) { + return self._addFileToNative(fullPath, resolvedData, options); + }); + this.root._pending.push(promise); + + var fileObj = new JSZipObject(fullPath, false, this.root, options); + this.root.files[fullPath] = fileObj; + } else { + this._addFileToNative(fullPath, data, options); + var fileObj = new JSZipObject(fullPath, false, this.root, options); + this.root.files[fullPath] = fileObj; + } + + return this; +}; + +JSZip.prototype._ensureParentFolders = function(fullPath) { + var parts = fullPath.split('/'); + var current = ""; + for (var i = 0; i < parts.length - 1; i++) { + current += parts[i] + "/"; + if (!this.root.files[current]) { + var folderObj = new JSZipObject(current, true, this.root); + this.root.files[current] = folderObj; + exec(null, null, "JsZip", "addFile", [this.root.id, current, null, { dir: true }]); + } + } +}; + +JSZip.prototype._addFileToNative = function(fullPath, data, options) { + var self = this; + if (typeof Blob !== 'undefined' && data instanceof Blob) { + var promise = 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._addFileToNative(fullPath, buffer, options); + }); + this.root._pending.push(promise); + return; + } + + var createFolders = options.createFolders !== false; + if (createFolders) { + this._ensureParentFolders(fullPath); + } + + var dataType = "string"; + var payload = data; + if (data instanceof ArrayBuffer) { + dataType = "base64"; + payload = arrayBufferToBase64(data); + } else if (ArrayBuffer.isView(data)) { + dataType = "base64"; + payload = arrayBufferToBase64(data); + } else if (options.base64) { + dataType = "base64"; + } + + if (data && typeof data.async === 'function') { + var promise = data.async("arraybuffer").then(function(buffer) { + return self._addFileToNative(fullPath, buffer, options); + }); + this.root._pending.push(promise); + return; + } + + exec(null, null, "JsZip", "addFile", [ + this.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 fullPath = this.prefix + name; + var normalizedPath = fullPath.endsWith('/') ? fullPath : fullPath + '/'; + + for (var filename in this.root.files) { + if (filename === fullPath || filename === normalizedPath || filename.indexOf(normalizedPath) === 0) { + delete this.root.files[filename]; + } + } + + exec(null, null, "JsZip", "removeFile", [this.root.id, fullPath]); + return this; +}; + +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(base64Data, type, mimeType) { + type = (type || "").toLowerCase(); + if (type === "base64") { + return base64Data; + } + + var binaryString = window.atob(base64Data); + var len = binaryString.length; + + if (type === "string" || type === "binarystring") { + return binaryString; + } + + if (type === "text") { + return decodeURIComponent(escape(binaryString)); + } + + var bytes = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + if (type === "array") { + var arr = new Array(len); + for (var i = 0; i < len; i++) { + arr[i] = bytes[i]; + } + return arr; + } + + if (type === "uint8array") { + return bytes; + } + + if (type === "arraybuffer") { + return bytes.buffer; + } + + if (type === "blob") { + return new Blob([bytes], { type: mimeType }); + } + + 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) { + exec( + function(result) { + if (result && result.progress) { + if (typeof onUpdate === 'function') { + onUpdate({ + percent: result.percent, + currentFile: result.currentFile + }); + } + return; + } + var base64Data = result.data; + try { + resolve(self._convertOutput(base64Data, type, mimeType)); + } catch (e) { + reject(e); + } + }, + 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: arrayBufferToBase64(e.target.result), + dataType: "base64" + }); + }; + reader.onerror = function(err) { + reject(err); + }; + reader.readAsArrayBuffer(data); + }); + } else if (data instanceof ArrayBuffer) { + return { + payload: arrayBufferToBase64(data), + dataType: "base64" + }; + } else if (ArrayBuffer.isView(data)) { + return { + payload: arrayBufferToBase64(data), + dataType: "base64" + }; + } else if (typeof data === 'string') { + if (options.base64) { + return { + payload: data, + dataType: "base64" + }; + } else { + var base64 = window.btoa(data); + return { + payload: base64, + dataType: "base64" + }; + } + } 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, + { + 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]); +}; + +var registry = typeof FinalizationRegistry !== 'undefined' ? new FinalizationRegistry(function(id) { + exec(null, null, "JsZip", "destroy", [id]); +}) : null; + +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; + 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; + } + var base64Data = result.data; + try { + resolve(self.root._convertOutput(base64Data, type, "application/octet-stream")); + } catch (e) { + reject(e); + } + }, + 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"); +}; + +if (typeof window !== 'undefined') { + window.JSZip = JSZip; +} + +module.exports = JSZip; From 6ee46a31d3e39b002da0b7549778de67548de183 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 16:36:29 +0530 Subject: [PATCH 2/8] improvements --- src/plugins/jszip-java/src/android/JsZip.java | 54 +++--- src/plugins/jszip-java/www/JsZip.js | 179 ++++++++++++------ 2 files changed, 157 insertions(+), 76 deletions(-) diff --git a/src/plugins/jszip-java/src/android/JsZip.java b/src/plugins/jszip-java/src/android/JsZip.java index 43a9c44da..a08e8d7e0 100644 --- a/src/plugins/jszip-java/src/android/JsZip.java +++ b/src/plugins/jszip-java/src/android/JsZip.java @@ -149,7 +149,7 @@ public void run() { @Override public boolean execute( String action, - JSONArray args, + CordovaArgs args, final CallbackContext callbackContext ) throws JSONException { cordova.getThreadPool().execute( @@ -164,24 +164,22 @@ public void run() { } else if ("addFile".equals(action)) { String id = args.getString(0); String path = args.getString(1); - String data = args.isNull(2) ? null : args.getString(2); JSONObject options = args.optJSONObject(3); if (options == null) { options = new JSONObject(); } - addFile(id, path, data, options, callbackContext); + 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); - String base64Data = args.getString(1); JSONObject options = args.optJSONObject(2); if (options == null) { options = new JSONObject(); } - loadZip(id, base64Data, options, callbackContext); + loadZip(id, args, options, callbackContext); } else if ("getFileContent".equals(action)) { String id = args.getString(0); String path = args.getString(1); @@ -225,7 +223,7 @@ private void createInstance(String id) { instances.put(id, instance); } - private void addFile(String id, String name, String data, JSONObject options, CallbackContext callbackContext) { + private void addFile(String id, String name, CordovaArgs args, JSONObject options, CallbackContext callbackContext) { try { ZipInstance instance = instances.get(id); if (instance == null) { @@ -251,16 +249,20 @@ private void addFile(String id, String name, String data, JSONObject options, Ca entry.dosPermissions = options.optInt("dosPermissions"); } - if (!isDir && data != null) { + 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 ("base64".equals(dataType)) { + 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"); @@ -303,7 +305,7 @@ private void removeFile(String id, String name, CallbackContext callbackContext) } } - private void loadZip(String id, String base64Data, JSONObject options, CallbackContext callbackContext) { + private void loadZip(String id, CordovaArgs args, JSONObject options, CallbackContext callbackContext) { File tempZipFile = null; try { ZipInstance instance = instances.get(id); @@ -320,9 +322,20 @@ private void loadZip(String id, String base64Data, JSONObject options, CallbackC } instance.entries.clear(); - byte[] zipBytes = Base64.decode(base64Data, Base64.DEFAULT); - Log.d("JsZip", "loadZip: base64Length=" + base64Data.length() + ", zipBytesLength=" + zipBytes.length); - System.out.println("[JsZip-Java Debug Native] loadZip: base64Length=" + base64Data.length() + ", zipBytesLength=" + zipBytes.length); + 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"); @@ -412,7 +425,7 @@ private void loadZip(String id, String base64Data, JSONObject options, CallbackC JSONObject resultObj = new JSONObject(); resultObj.put("files", metaList); - resultObj.put("base64Length", base64Data.length()); + resultObj.put("base64Length", zipBytes.length); // Maintain compatibility/logs resultObj.put("zipBytesLength", zipBytes.length); callbackContext.success(resultObj); } catch (Exception e) { @@ -457,10 +470,9 @@ private void getFileContent(String id, String name, String type, CallbackContext } } - String base64Data = Base64.encodeToString(bytes, Base64.NO_WRAP); - JSONObject result = new JSONObject(); - result.put("data", base64Data); - callbackContext.success(result); + // Return byte[] directly as raw binary + PluginResult result = new PluginResult(PluginResult.Status.OK, bytes); + callbackContext.sendPluginResult(result); } catch (Exception e) { callbackContext.error("Failed to get file content: " + e.getMessage()); } @@ -588,11 +600,9 @@ private void generateZip(String id, String prefix, JSONObject options, CallbackC zipFile.delete(); - String base64Data = Base64.encodeToString(zipBytes, Base64.NO_WRAP); - JSONObject result = new JSONObject(); - result.put("data", base64Data); - callbackContext.success(result); - + // Return byte[] directly as raw binary + PluginResult result = new PluginResult(PluginResult.Status.OK, zipBytes); + callbackContext.sendPluginResult(result); } catch (Exception e) { callbackContext.error("Failed to generate ZIP: " + e.getMessage()); } diff --git a/src/plugins/jszip-java/www/JsZip.js b/src/plugins/jszip-java/www/JsZip.js index 7be93b257..de57a4552 100644 --- a/src/plugins/jszip-java/www/JsZip.js +++ b/src/plugins/jszip-java/www/JsZip.js @@ -126,19 +126,19 @@ JSZip.prototype.file = function(name, data, options) { 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._addFileToNative(fullPath, resolvedData, options); }); this.root._pending.push(promise); - - var fileObj = new JSZipObject(fullPath, false, this.root, options); - this.root.files[fullPath] = fileObj; } else { this._addFileToNative(fullPath, data, options); - var fileObj = new JSZipObject(fullPath, false, this.root, options); - this.root.files[fullPath] = fileObj; } return this; @@ -184,37 +184,52 @@ JSZip.prototype._addFileToNative = function(fullPath, data, options) { var dataType = "string"; var payload = data; if (data instanceof ArrayBuffer) { - dataType = "base64"; - payload = arrayBufferToBase64(data); + dataType = "arraybuffer"; + payload = data; } else if (ArrayBuffer.isView(data)) { - dataType = "base64"; - payload = arrayBufferToBase64(data); + dataType = "arraybuffer"; + payload = data; } else if (options.base64) { dataType = "base64"; } if (data && typeof data.async === 'function') { + var fileObj = self.root.files[fullPath]; var promise = data.async("arraybuffer").then(function(buffer) { + if (fileObj) { + fileObj._data = buffer; + } return self._addFileToNative(fullPath, buffer, options); }); this.root._pending.push(promise); return; } - exec(null, null, "JsZip", "addFile", [ - this.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 - } - ]); + exec( + function() { + var fileObj = self.root.files[fullPath]; + if (fileObj) { + delete fileObj._data; + } + }, + null, + "JsZip", + "addFile", + [ + this.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) { @@ -250,26 +265,22 @@ JSZip.prototype.filter = function(callback) { return results; }; -JSZip.prototype._convertOutput = function(base64Data, type, mimeType) { +JSZip.prototype._convertOutput = function(arrayBuffer, type, mimeType) { type = (type || "").toLowerCase(); - if (type === "base64") { - return base64Data; - } - var binaryString = window.atob(base64Data); - var len = binaryString.length; - - if (type === "string" || type === "binarystring") { - return binaryString; + if (type === "arraybuffer") { + return arrayBuffer; } + + var bytes = new Uint8Array(arrayBuffer); + var len = bytes.byteLength; - if (type === "text") { - return decodeURIComponent(escape(binaryString)); + if (type === "uint8array") { + return bytes; } - var bytes = new Uint8Array(len); - for (var i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); + if (type === "blob") { + return new Blob([arrayBuffer], { type: mimeType }); } if (type === "array") { @@ -280,16 +291,28 @@ JSZip.prototype._convertOutput = function(base64Data, type, mimeType) { return arr; } - if (type === "uint8array") { - return bytes; + // 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 === "arraybuffer") { - return bytes.buffer; + if (type === "string" || type === "binarystring") { + return binaryString; } - if (type === "blob") { - return new Blob([bytes], { type: mimeType }); + 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."); @@ -356,8 +379,8 @@ JSZip.prototype.loadAsync = function(data, options) { var reader = new FileReader(); reader.onload = function(e) { resolve({ - payload: arrayBufferToBase64(e.target.result), - dataType: "base64" + payload: e.target.result, + dataType: "arraybuffer" }); }; reader.onerror = function(err) { @@ -367,13 +390,13 @@ JSZip.prototype.loadAsync = function(data, options) { }); } else if (data instanceof ArrayBuffer) { return { - payload: arrayBufferToBase64(data), - dataType: "base64" + payload: data, + dataType: "arraybuffer" }; } else if (ArrayBuffer.isView(data)) { return { - payload: arrayBufferToBase64(data), - dataType: "base64" + payload: data, + dataType: "arraybuffer" }; } else if (typeof data === 'string') { if (options.base64) { @@ -382,10 +405,9 @@ JSZip.prototype.loadAsync = function(data, options) { dataType: "base64" }; } else { - var base64 = window.btoa(data); return { - payload: base64, - dataType: "base64" + payload: data, + dataType: "string" }; } } else { @@ -422,6 +444,7 @@ JSZip.prototype.loadAsync = function(data, options) { self.root.id, normalized.payload, { + dataType: normalized.dataType, checkCRC32: options.checkCRC32 || false, createFolders: options.createFolders || false } @@ -520,6 +543,55 @@ function JSZipObject(name, dir, root, 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); + } + return new Promise(function(resolve, reject) { exec( function(result) { @@ -532,9 +604,8 @@ JSZipObject.prototype.async = function(type, onUpdate) { } return; } - var base64Data = result.data; try { - resolve(self.root._convertOutput(base64Data, type, "application/octet-stream")); + resolve(self.root._convertOutput(result, type, "application/octet-stream")); } catch (e) { reject(e); } From 1034d35b140aa778e02a02ff61d92c991c5833a4 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 16:50:23 +0530 Subject: [PATCH 3/8] fix: issues --- src/plugins/jszip-java/src/android/JsZip.java | 1553 +++++++++-------- src/plugins/jszip-java/www/JsZip.js | 55 +- 2 files changed, 879 insertions(+), 729 deletions(-) diff --git a/src/plugins/jszip-java/src/android/JsZip.java b/src/plugins/jszip-java/src/android/JsZip.java index a08e8d7e0..6af727344 100644 --- a/src/plugins/jszip-java/src/android/JsZip.java +++ b/src/plugins/jszip-java/src/android/JsZip.java @@ -21,771 +21,884 @@ 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<>(); + private final ConcurrentHashMap instances = + new ConcurrentHashMap<>(); - ZipInstance(String id, File cacheDir) { - this.id = id; - this.baseDir = new File(cacheDir, "jszip_" + id); - if (!baseDir.exists()) { - baseDir.mkdirs(); - } - } + private static class FileEntry { - void destroy() { - deleteDir(baseDir); - } - } + String name; + boolean isDir; + long date; + String comment; + Integer unixPermissions; + Integer dosPermissions; + File tempFile; + } - 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 class ZipInstance { - 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 - } - } + String id; + File baseDir; + ConcurrentHashMap entries = new ConcurrentHashMap<>(); - 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 - } - } + ZipInstance(String id, File cacheDir) { + this.id = id; + this.baseDir = new File(cacheDir, "jszip_" + id); + if (!baseDir.exists()) { + baseDir.mkdirs(); + } } - 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; + void destroy() { + deleteDir(baseDir); } - - @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(); + } + + private static void deleteDir(File file) { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteDir(child); + } + } } - - @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; + 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 } - - private void createInstance(String id) { - File cacheDir = cordova.getActivity().getCacheDir(); - ZipInstance instance = new ZipInstance(id, cacheDir); - instances.put(id, instance); + try { + getExternalAttributesMethod = ZipEntry.class.getMethod( + "getExternalAttributes" + ); + } catch (Exception e) { + // Not available } - - 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 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 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 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 + } } - - 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); + 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 { - 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 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(); + callbackContext.error("Invalid action: " + action); } + } catch (Exception e) { + callbackContext.error( + "Error in action " + action + ": " + 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; - } - - byte[] bytes = new byte[(int) entry.tempFile.length()]; - try (FileInputStream fis = new FileInputStream(entry.tempFile)) { - int offset = 0; - int numRead = 0; - while (offset < bytes.length && (numRead = fis.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - } - - // Return byte[] directly as raw binary - PluginResult result = new PluginResult(PluginResult.Status.OK, bytes); - callbackContext.sendPluginResult(result); - } catch (Exception e) { - callbackContext.error("Failed to get file content: " + 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" + ); } - } - - private void generateZip(String id, String prefix, JSONObject options, CallbackContext callbackContext) { - 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); - - File 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(); - } - - byte[] zipBytes = new byte[(int) zipFile.length()]; - try (FileInputStream fis = new FileInputStream(zipFile)) { - int offset = 0; - int numRead = 0; - while (offset < zipBytes.length && (numRead = fis.read(zipBytes, offset, zipBytes.length - offset)) >= 0) { - offset += numRead; - } - } + 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"); + } + } - zipFile.delete(); + try (FileOutputStream fos = new FileOutputStream(entry.tempFile)) { + fos.write(bytes); + } + } - // Return byte[] directly as raw binary - PluginResult result = new PluginResult(PluginResult.Status.OK, zipBytes); - callbackContext.sendPluginResult(result); - } catch (Exception e) { - callbackContext.error("Failed to generate ZIP: " + e.getMessage()); + 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 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); - } + } + + 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 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); } - return crc.getValue(); + } + + 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 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 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); + } - 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; - } + if (comment != null && !comment.isEmpty()) { + zos.setComment(comment); + } - File destDir = new File(targetDir); - if (!destDir.exists()) { - destDir.mkdirs(); - } + List sortedKeys = new ArrayList<>(instance.entries.keySet()); + Collections.sort(sortedKeys); - List sortedKeys = new ArrayList<>(instance.entries.keySet()); - Collections.sort(sortedKeys); + int totalEntries = 0; + for (String key : sortedKeys) { + if (key.startsWith(prefix)) { + totalEntries++; + } + } - 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(); } + } + } - 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); + zos.finish(); + } - File destFile = new File(destDir, relativePath); - if (!isSafePath(destDir, destFile)) { - throw new SecurityException("Path traversal attempt detected in entry: " + relativePath); - } + 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++; + } + } - 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); - } - } - } - } - } + int currentEntryIndex = 0; + for (String key : sortedKeys) { + if (!key.startsWith(prefix)) { + continue; + } - callbackContext.success(); - } catch (Exception e) { - callbackContext.error("Failed to extract: " + e.getMessage()); + FileEntry entry = instance.entries.get(key); + if (entry == null) { + continue; } - } - 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; - } + String relativePath = key.substring(prefix.length()); + if (relativePath.isEmpty()) { + continue; + } - File destDir = new File(targetDir); - if (!destDir.exists()) { - destDir.mkdirs(); - } + currentEntryIndex++; - try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile)) { - int totalEntries = zf.size(); - int currentEntryIndex = 0; - java.util.Enumeration 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); - } - } - } - } - } + // 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 + ); + } - callbackContext.success(); - } catch (Exception e) { - callbackContext.error("Failed to extract ZIP: " + e.getMessage()); + 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); + } + } + } } - } + } - 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()); - } + 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 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 index de57a4552..ff0f4892e 100644 --- a/src/plugins/jszip-java/www/JsZip.js +++ b/src/plugins/jszip-java/www/JsZip.js @@ -331,6 +331,7 @@ JSZip.prototype.generateAsync = function(options, onUpdate) { self.root._pending = []; return new Promise(function(resolve, reject) { + var chunks = []; exec( function(result) { if (result && result.progress) { @@ -342,11 +343,28 @@ JSZip.prototype.generateAsync = function(options, onUpdate) { } return; } - var base64Data = result.data; - try { - resolve(self._convertOutput(base64Data, type, mimeType)); - } catch (e) { - reject(e); + 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) { @@ -592,6 +610,7 @@ JSZipObject.prototype.async = function(type, onUpdate) { return resolveLocal(this._data); } + var chunks = []; return new Promise(function(resolve, reject) { exec( function(result) { @@ -604,10 +623,28 @@ JSZipObject.prototype.async = function(type, onUpdate) { } return; } - try { - resolve(self.root._convertOutput(result, type, "application/octet-stream")); - } catch (e) { - reject(e); + 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) { From 14f08004b6473fa6f4f56139201e4d7e8068b479 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 17:03:08 +0530 Subject: [PATCH 4/8] fix: issues --- src/plugins/jszip-java/www/JsZip.js | 137 ++++++++++++++++++---------- 1 file changed, 87 insertions(+), 50 deletions(-) diff --git a/src/plugins/jszip-java/www/JsZip.js b/src/plugins/jszip-java/www/JsZip.js index ff0f4892e..3121e1f7e 100644 --- a/src/plugins/jszip-java/www/JsZip.js +++ b/src/plugins/jszip-java/www/JsZip.js @@ -8,6 +8,10 @@ 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) { @@ -39,6 +43,10 @@ function JSZip() { // Notify native of instance creation exec(null, null, "JsZip", "create", [this.id]); + + if (registry) { + registry.register(this, this.id); + } } JSZip.support = { @@ -87,7 +95,10 @@ JSZip.prototype.folder = function(name) { var folderObj = new JSZipObject(fullPath, true, this.root); this.root.files[fullPath] = folderObj; - exec(null, null, "JsZip", "addFile", [this.root.id, fullPath, null, { dir: true }]); + var promise = new Promise(function(resolve, reject) { + exec(resolve, reject, "JsZip", "addFile", [this.root.id, fullPath, null, { dir: true }]); + }); + this.root._pending.push(promise); } var child = Object.create(JSZip.prototype); @@ -134,7 +145,7 @@ JSZip.prototype.file = function(name, data, options) { var self = this; var promise = data.then(function(resolvedData) { fileObj._data = resolvedData; - return self._addFileToNative(fullPath, resolvedData, options); + return self._addFileToNativePromise(fullPath, resolvedData, options); }); this.root._pending.push(promise); } else { @@ -145,22 +156,44 @@ JSZip.prototype.file = function(name, data, options) { }; 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]) { - var folderObj = new JSZipObject(current, true, this.root); - this.root.files[current] = folderObj; - exec(null, null, "JsZip", "addFile", [this.root.id, current, null, { dir: true }]); + (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) { - var promise = new Promise(function(resolve, reject) { + return new Promise(function(resolve, reject) { var reader = new FileReader(); reader.onload = function(e) { resolve(e.target.result); @@ -170,15 +203,24 @@ JSZip.prototype._addFileToNative = function(fullPath, data, options) { }; reader.readAsArrayBuffer(data); }).then(function(buffer) { - return self._addFileToNative(fullPath, buffer, options); + 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); }); - this.root._pending.push(promise); - return; } var createFolders = options.createFolders !== false; + var parentPromise = Promise.resolve(); if (createFolders) { - this._ensureParentFolders(fullPath); + parentPromise = this._ensureParentFolders(fullPath); } var dataType = "string"; @@ -193,43 +235,38 @@ JSZip.prototype._addFileToNative = function(fullPath, data, options) { dataType = "base64"; } - if (data && typeof data.async === 'function') { - var fileObj = self.root.files[fullPath]; - var promise = data.async("arraybuffer").then(function(buffer) { - if (fileObj) { - fileObj._data = buffer; - } - return self._addFileToNative(fullPath, buffer, options); + 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 + } + ] + ); }); - this.root._pending.push(promise); - return; - } - - exec( - function() { - var fileObj = self.root.files[fullPath]; - if (fileObj) { - delete fileObj._data; - } - }, - null, - "JsZip", - "addFile", - [ - this.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) { @@ -242,7 +279,10 @@ JSZip.prototype.remove = function(name) { } } - exec(null, null, "JsZip", "removeFile", [this.root.id, fullPath]); + var promise = new Promise(function(resolve, reject) { + exec(resolve, reject, "JsZip", "removeFile", [this.root.id, fullPath]); + }); + this.root._pending.push(promise); return this; }; @@ -542,9 +582,6 @@ JSZip.prototype.destroy = function() { exec(null, null, "JsZip", "destroy", [this.root.id]); }; -var registry = typeof FinalizationRegistry !== 'undefined' ? new FinalizationRegistry(function(id) { - exec(null, null, "JsZip", "destroy", [id]); -}) : null; function JSZipObject(name, dir, root, options) { this.name = name; From 674950eaee53126d5947ca058c7b9812e0085cc1 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 17:11:24 +0530 Subject: [PATCH 5/8] fix: issues --- src/plugins/jszip-java/www/JsZip.js | 44 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/plugins/jszip-java/www/JsZip.js b/src/plugins/jszip-java/www/JsZip.js index 3121e1f7e..272be022f 100644 --- a/src/plugins/jszip-java/www/JsZip.js +++ b/src/plugins/jszip-java/www/JsZip.js @@ -71,13 +71,14 @@ JSZip.defaults = { }; JSZip.prototype.folder = function(name) { + var self = this; 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); + 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); } @@ -89,24 +90,24 @@ JSZip.prototype.folder = function(name) { if (!name.endsWith('/')) { name += '/'; } - var fullPath = this.prefix + name; + var fullPath = self.prefix + name; - if (!this.root.files[fullPath]) { - var folderObj = new JSZipObject(fullPath, true, this.root); - this.root.files[fullPath] = folderObj; + 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", [this.root.id, fullPath, null, { dir: true }]); + exec(resolve, reject, "JsZip", "addFile", [self.root.id, fullPath, null, { dir: true }]); }); - this.root._pending.push(promise); + self.root._pending.push(promise); } var child = Object.create(JSZip.prototype); - child.id = this.root.id; - child.root = this.root; + child.id = self.root.id; + child.root = self.root; child.prefix = fullPath; - child.files = this.root.files; - child._pending = this.root._pending; + child.files = self.root.files; + child._pending = self.root._pending; return child; }; @@ -220,7 +221,7 @@ JSZip.prototype._addFileToNativePromise = function(fullPath, data, options) { var createFolders = options.createFolders !== false; var parentPromise = Promise.resolve(); if (createFolders) { - parentPromise = this._ensureParentFolders(fullPath); + parentPromise = self._ensureParentFolders(fullPath); } var dataType = "string"; @@ -270,20 +271,21 @@ JSZip.prototype._addFileToNativePromise = function(fullPath, data, options) { }; JSZip.prototype.remove = function(name) { - var fullPath = this.prefix + name; + var self = this; + var fullPath = self.prefix + name; var normalizedPath = fullPath.endsWith('/') ? fullPath : fullPath + '/'; - for (var filename in this.root.files) { + for (var filename in self.root.files) { if (filename === fullPath || filename === normalizedPath || filename.indexOf(normalizedPath) === 0) { - delete this.root.files[filename]; + delete self.root.files[filename]; } } var promise = new Promise(function(resolve, reject) { - exec(resolve, reject, "JsZip", "removeFile", [this.root.id, fullPath]); + exec(resolve, reject, "JsZip", "removeFile", [self.root.id, fullPath]); }); - this.root._pending.push(promise); - return this; + self.root._pending.push(promise); + return self; }; JSZip.prototype.forEach = function(callback) { From 963085fce6ca89d5630a7c1020c96e70e28c8c04 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 17:28:42 +0530 Subject: [PATCH 6/8] feat: improvements + added tests --- src/plugins/jszip-java/www/JsZip.js | 40 +++++ src/test/jszip.tests.js | 238 ++++++++++++++++++++++++++++ src/test/tester.js | 2 + 3 files changed, 280 insertions(+) create mode 100644 src/test/jszip.tests.js diff --git a/src/plugins/jszip-java/www/JsZip.js b/src/plugins/jszip-java/www/JsZip.js index 272be022f..6c4c091e0 100644 --- a/src/plugins/jszip-java/www/JsZip.js +++ b/src/plugins/jszip-java/www/JsZip.js @@ -584,6 +584,22 @@ 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; @@ -700,6 +716,30 @@ 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; } diff --git a/src/test/jszip.tests.js b/src/test/jszip.tests.js new file mode 100644 index 000000000..cd537bc0e --- /dev/null +++ b/src/test/jszip.tests.js @@ -0,0 +1,238 @@ +import { TestRunner } from "./tester"; +import JSZip from "jszip"; + +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/tester.js b/src/test/tester.js index be3ba8ac6..a8110ee5d 100644 --- a/src/test/tester.js +++ b/src/test/tester.js @@ -3,6 +3,7 @@ import { runCodeMirrorTests } from "./editor.tests"; import { runExecutorTests } from "./exec.tests"; import { runSanityTests } from "./sanity.tests"; import { runUrlTests } from "./url.tests"; +import { runJsZipTests } from "./jszip.tests"; export async function runAllTests() { const terminal = acode.require("terminal"); @@ -22,6 +23,7 @@ export async function runAllTests() { await runAceCompatibilityTests(write); await runExecutorTests(write); await runUrlTests(write); + await runJsZipTests(write); write("\x1b[36m\x1b[1mTests completed!\x1b[0m\n"); } catch (error) { From 4a80d3223d9c55ff9c694f6a62a038706ae3906e Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 17:29:49 +0530 Subject: [PATCH 7/8] format --- src/test/jszip.tests.js | 158 +++++++++++++++++++++++++++++++--------- src/test/tester.js | 2 +- 2 files changed, 126 insertions(+), 34 deletions(-) diff --git a/src/test/jszip.tests.js b/src/test/jszip.tests.js index cd537bc0e..734394ac7 100644 --- a/src/test/jszip.tests.js +++ b/src/test/jszip.tests.js @@ -1,5 +1,5 @@ -import { TestRunner } from "./tester"; import JSZip from "jszip"; +import { TestRunner } from "./tester"; export async function runJsZipTests(writeOutput) { const runner = new TestRunner("JSZip-Java Compatibility Tests"); @@ -20,9 +20,20 @@ export async function runJsZipTests(writeOutput) { 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.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 @@ -34,35 +45,47 @@ export async function runJsZipTests(writeOutput) { 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`); + 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`); + 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.assertEqual( + typeof Object.getPrototypeOf(jvFile)[key], + "function", + `JSZipObject prototype function '${key}' must be present on JSZipObject`, + ); } } }); @@ -79,8 +102,16 @@ export async function runJsZipTests(writeOutput) { 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.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 @@ -97,12 +128,19 @@ export async function runJsZipTests(writeOutput) { 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"); - + 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.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"); }); @@ -122,13 +160,21 @@ export async function runJsZipTests(writeOutput) { 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"); - + 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.assertEqual( + jvResChild, + stdResChild, + "Nested file content should match via child access", + ); }); // Test 6: Files directory listing and iteration comparison @@ -155,14 +201,26 @@ export async function runJsZipTests(writeOutput) { jvList.push({ path: rel, isDir: obj.dir }); }); - test.assertEqual(jvList.length, stdList.length, "forEach count must match standard JSZip"); - + 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.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`, + ); } }); @@ -181,12 +239,24 @@ export async function runJsZipTests(writeOutput) { 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"); + 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.assertEqual( + jvKeys.length, + stdKeys.length, + "Remaining file keys count must match standard JSZip", + ); }); // Test 8: Output Type conversion compatibility (Base64, BinaryString) @@ -199,9 +269,20 @@ export async function runJsZipTests(writeOutput) { 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.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 @@ -212,25 +293,36 @@ export async function runJsZipTests(writeOutput) { // 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( + 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 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"); + test.assertEqual( + JSON.parse(crossJson).version, + "1.0.0", + "Cross-loaded standard JSZip must read jszip-java output successfully", + ); }); // Run tests diff --git a/src/test/tester.js b/src/test/tester.js index a8110ee5d..d9f368f31 100644 --- a/src/test/tester.js +++ b/src/test/tester.js @@ -1,9 +1,9 @@ import { runAceCompatibilityTests } from "./ace.test"; import { runCodeMirrorTests } from "./editor.tests"; import { runExecutorTests } from "./exec.tests"; +import { runJsZipTests } from "./jszip.tests"; import { runSanityTests } from "./sanity.tests"; import { runUrlTests } from "./url.tests"; -import { runJsZipTests } from "./jszip.tests"; export async function runAllTests() { const terminal = acode.require("terminal"); From 00e34ff6964872d4df6cfdb7252f3f899472c76e Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 18:40:42 +0530 Subject: [PATCH 8/8] format --- src/test/test-definitions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/test-definitions.js b/src/test/test-definitions.js index 99d73aa2a..dea571513 100644 --- a/src/test/test-definitions.js +++ b/src/test/test-definitions.js @@ -14,8 +14,8 @@ export const testDefinitions = [ runSanityTests, runExecutorTests, runUrlTests, - runFsTests, - runJsZipTests, + runFsTests, + runJsZipTests, runCodeMirrorTests, runAceCompatibilityTests, ];