-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: implement native unzip for plugin installation performance impr… #2367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,9 @@ | |
| import java.nio.file.Paths; | ||
| import java.nio.file.StandardOpenOption; | ||
| import java.io.IOException; | ||
| import java.util.zip.ZipInputStream; | ||
| import java.util.zip.ZipEntry; | ||
| import java.io.BufferedOutputStream; | ||
| import android.app.Activity; | ||
| import android.app.PendingIntent; | ||
| import android.content.ClipData; | ||
|
|
@@ -214,6 +217,7 @@ public boolean execute( | |
| case "compare-file-text": | ||
| case "compare-texts": | ||
| case "extractAsset": | ||
| case "unzip": | ||
| case "pin-file-shortcut": | ||
| break; | ||
| case "get-configuration": | ||
|
|
@@ -442,6 +446,16 @@ public void run() { | |
|
|
||
| } | ||
| return; | ||
|
|
||
| case "unzip": | ||
| try { | ||
| String zipPath = args.getString(0); | ||
| String destPath = args.getString(1); | ||
| unzip(zipPath, destPath, callbackContext); | ||
| } catch (Exception e) { | ||
| callbackContext.error("Failed to unzip: " + e.getMessage()); | ||
| } | ||
| return; | ||
|
|
||
| case "getInstaller": | ||
| try { | ||
|
|
@@ -2184,4 +2198,90 @@ private void extractAsset(String assetName, String destinationPath, CallbackCont | |
| callback.error(sw.toString()); | ||
| } | ||
| } | ||
|
|
||
| private void unzip(String zipPath, String destPath, CallbackContext callback) { | ||
| try { | ||
| Uri zipUri = Uri.parse(zipPath); | ||
| File destDir = null; | ||
| Uri destUri = Uri.parse(destPath); | ||
| if ("file".equalsIgnoreCase(destUri.getScheme())) { | ||
| destDir = new File(destUri.getPath()); | ||
| } else { | ||
| destDir = new File(destPath); | ||
| } | ||
|
|
||
| if (!destDir.exists()) { | ||
| destDir.mkdirs(); | ||
| } | ||
|
|
||
| String canonicalDestDirPath = destDir.getCanonicalPath(); | ||
|
|
||
| InputStream is = null; | ||
| if ("file".equalsIgnoreCase(zipUri.getScheme())) { | ||
| is = new FileInputStream(new File(zipUri.getPath())); | ||
| } else if (zipUri.getScheme() != null) { | ||
| is = context.getContentResolver().openInputStream(zipUri); | ||
| } else { | ||
| is = new FileInputStream(new File(zipPath)); | ||
| } | ||
|
|
||
| if (is == null) { | ||
| callback.error("Could not open input stream for zip file"); | ||
| return; | ||
| } | ||
|
|
||
| try (ZipInputStream zis = new ZipInputStream(is)) { | ||
| ZipEntry entry; | ||
| byte[] buffer = new byte[8192]; | ||
| while ((entry = zis.getNextEntry()) != null) { | ||
| String name = entry.getName(); | ||
|
|
||
| // Normalize separator | ||
| name = name.replace('\\', '/'); | ||
|
|
||
| // Skip unsafe absolute paths | ||
| if (name.startsWith("/") || name.contains("://") || name.startsWith("\\\\") || (name.length() > 1 && name.charAt(1) == ':')) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| // Unsafe, skip it | ||
| zis.closeEntry(); | ||
| continue; | ||
| } | ||
|
|
||
| // Resolve relative paths | ||
| File file = new File(destDir, name); | ||
| String canonicalFilePath = file.getCanonicalPath(); | ||
|
|
||
| // Prevent Zip Slip | ||
| if (!canonicalFilePath.startsWith(canonicalDestDirPath + File.separator) && !canonicalFilePath.equals(canonicalDestDirPath)) { | ||
| zis.closeEntry(); | ||
| continue; | ||
| } | ||
|
|
||
| if (entry.isDirectory()) { | ||
| file.mkdirs(); | ||
| } else { | ||
| // Ensure parent directory exists | ||
| File parent = file.getParentFile(); | ||
| if (parent != null && !parent.exists()) { | ||
| parent.mkdirs(); | ||
| } | ||
|
|
||
| try (FileOutputStream fos = new FileOutputStream(file); | ||
| BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length)) { | ||
| int len; | ||
| while ((len = zis.read(buffer)) != -1) { | ||
| bos.write(buffer, 0, len); | ||
| } | ||
| bos.flush(); | ||
| } | ||
| } | ||
| zis.closeEntry(); | ||
| } | ||
| } | ||
| callback.success(); | ||
| } catch (Exception e) { | ||
| StringWriter sw = new StringWriter(); | ||
| e.printStackTrace(new PrintWriter(sw)); | ||
| callback.error(sw.toString()); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"1"The old code stored a SHA-256 content hash in
updatedStoreviastate.isUpdated(). The new loop writes"1"for every non-directory entry. Afterstate.save()this persists to disk, so any future code path that reads the stored value expecting a real hash (e.g. a delta-update check in a future install) will instead see"1"and treat everything as changed. The existingstate.exists()path works fine since it only tests key presence, but the semantics of the state file have silently shifted from "hash of last-installed content" to "was ever installed".