Skip to content

fix(plugin-install): Move plugin zip extraction to native (Java) code, remove hardcoded 2-file batch limit - #2817

Open
codewithchai0605 wants to merge 5 commits into
Acode-Foundation:mainfrom
codewithchai0605:fix/plugin-install-native-extraction
Open

fix(plugin-install): Move plugin zip extraction to native (Java) code, remove hardcoded 2-file batch limit#2817
codewithchai0605 wants to merge 5 commits into
Acode-Foundation:mainfrom
codewithchai0605:fix/plugin-install-native-extraction

Conversation

@codewithchai0605

Copy link
Copy Markdown

Problem

installPlugin.js extracted plugin zip archives on the JS thread in
hardcoded batches of 2 files, yielding to the UI thread with
await new Promise(r => setTimeout(r, 0)) between batches. This was
presumably to stop the WebView from freezing during install, but it
made installing plugins with many files unnecessarily slow, and the
batch size had no principled basis.

Fix

Move the actual unzip + disk I/O to native Java code, running on
Cordova's background thread pool instead of the JS thread. Since
native code never touches the UI thread, there's nothing to protect
by batching — the whole archive is extracted in a single native call.

New plugin: src/plugins/pluginInstaller

  • PluginInstaller.java — opens the (already-downloaded, on-disk) zip
    with ZipFile, walks entries, sanitizes each path (mirrors the
    existing sanitizeZipPath / isUnsafeAbsolutePath logic from
    installPlugin.js so zip-slip protection isn't lost), writes files
    with plain java.io.File I/O, and computes SHA-256 checksums in the
    same format installState.js already uses, so existing install
    state remains valid.
  • PluginInstaller.js (www bridge) — exposes extractZip() /
    cancelExtract() over cordova.exec.
  • plugin.xml / package.json — standard Cordova plugin scaffolding,
    following the same pattern as the existing pluginContext plugin.

installPlugin.js changes

  • The old batching loop, and the createFileRecursive /
    sanitizeZipPath / isUnsafeAbsolutePath helpers it used, are
    replaced by a single call to PluginInstaller.extractZip().
  • The downloaded archive is staged as a temp file under
    CACHE_STORAGE and native code streams from that path, rather than
    base64-encoding the whole thing through the JS bridge (which would
    otherwise inflate the payload ~33% and hold several full copies of
    it in memory across JS/bridge/Java).
  • Extraction progress is reported back and shown in the loader
    dialog.
  • The loader's cancel button is now actually wired up: cancelling
    aborts the in-flight download (where the underlying transport
    supports it) or the in-flight extraction, instead of just closing
    the dialog while the install continues in the background.
  • A single bad/oversized archive entry is skipped and logged instead
    of aborting the entire install.

Hardening included along the way

  • Zip-slip protection is preserved, plus a cheap, purely lexical
    containment check as a second line of defense (no per-file
    filesystem/symlink-resolution syscalls).
  • Zip-bomb guards: a cap on any single entry's decompressed size and
    on the total decompressed size of the archive, since this code path
    extracts arbitrary user-supplied/downloaded archives.

installPlugin.js unzipped archives on the JS thread in hardcoded
batches of 2 files, with a setTimeout(0) between batches purely to
keep the WebView UI thread from freezing. This made installing
plugins with many files slow for no real reason.

Add a new Cordova plugin (src/plugins/pluginInstaller) that does the
unzip + disk I/O in Java on Cordova's background thread pool, so
there's no UI thread to protect and therefore no batch-size limit.

- Stream extraction from a temp file on disk (CACHE_STORAGE) instead
  of base64-encoding the archive through the JS bridge, avoiding
  several redundant in-memory copies of the payload.
- Preserve existing zip-slip protections (sanitizeZipPath /
  isUnsafeAbsolutePath), reimplemented natively, plus a cheap lexical
  containment check as defense in depth.
- Preserve the per-file SHA-256 checksum/update-skip behavior from
  installState.js so existing install state stays compatible.
- Add zip-bomb guards (per-entry and total decompressed size caps).
- Report extraction progress back to JS and surface it in the loader
  dialog.
- Add real cancellation support: the loader's cancel button now
  actually aborts the in-flight download/extraction instead of just
  hiding the dialog while the install keeps running in the
  background.
- One bad archive entry no longer aborts the whole install; failures
  are collected and logged, matching (and improving on) the previous
  per-file try/catch behavior.
@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR moves plugin archive extraction into a native Android Cordova plugin and adds staged directory swaps, cancellation, progress reporting, and extraction limits.

  • Adds the Java extractor and JavaScript Cordova bridge.
  • Stages plugin replacements and retains update backups through runtime loading.
  • Cleans abandoned staging and backup directories during startup.

Confidence Score: 3/5

The PR is not yet safe to merge because update backups can be discarded before the update is fully committed, and deferred dependency validation can strand unvalidated replacements without recoverable prior versions.

Backup lifetime does not cover install-state persistence, and dependency backups are isolated in deferred closures that the parent failure path cannot restore before startup cleanup removes them.

Files Needing Attention: src/lib/installPlugin.js and src/lib/loadPlugins.js

Important Files Changed

Filename Overview
src/lib/installPlugin.js Introduces staged updates and rollback, but releases backups before state persistence and defers dependency rollback beyond the dependency install's lifetime.
src/lib/loadPlugins.js Removes abandoned dot-prefixed installer directories at startup, which can also erase backups retained by deferred dependency validation.
src/plugins/pluginInstaller/src/android/PluginInstaller.java Implements native bounded extraction, path sanitization, checksums, progress events, and cooperative cancellation.
src/plugins/pluginInstaller/www/PluginInstaller.js Defines the Cordova extraction contract and maps progress, completion, errors, and cancellation into a cancellable promise.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Download plugin archive] --> B[Extract into staging directory]
  B --> C{Extraction complete?}
  C -->|No| D[Delete staging]
  C -->|Yes| E[Park existing plugin as backup]
  E --> F[Swap staging into plugin directory]
  F --> G[Defer or run plugin validation]
  G --> H{Load and state persistence succeed?}
  H -->|Yes| I[Delete backup]
  H -->|No| J[Restore backup]
Loading

Reviews (4): Last reviewed commit: "fix: implement backup and restore mechan..." | Re-trigger Greptile

Comment thread src/lib/installPlugin.js Outdated
@codewithchai0605

Copy link
Copy Markdown
Author

@greptile-apps review

Comment thread src/lib/installPlugin.js
@codewithchai0605

Copy link
Copy Markdown
Author

@greptile-apps review

Comment thread src/lib/installPlugin.js Outdated
@codewithchai0605

Copy link
Copy Markdown
Author

@greptile-apps review

Comment thread src/lib/installPlugin.js
Comment on lines +424 to +429
if (backupDir) {
const finishedBackupDir = backupDir;
backupDir = null;
fsOperation(finishedBackupDir)
.delete()
.catch(() => {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Backup discarded before commit

When an updated plugin loads successfully but persisting its new checksum store fails, commitOrRollback has already cleared backupDir and started deleting the previous version. The outer recovery then cannot restore that version, leaving the replacement installed with stale install state and no recoverable backup.

Knowledge Base Used: Plugin lifecycle and extension management

Comment thread src/lib/installPlugin.js
Comment on lines 437 to +440
if (isDependency) {
depsLoaders.push(async () => {
await loadPluginWithTimeout(id, true);
});
depsLoaders.push(() =>
commitOrRollback(() => loadPluginWithTimeout(id, true)),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dependency backups become orphaned

When the parent installation fails or is cancelled after updating dependencies but before all deferred loaders run, those dependencies have already swapped in their unvalidated replacements while their backups remain inside inaccessible per-call closures. The parent cannot restore them, and startup cleanup later deletes the parked backups, permanently removing the previous working versions.

Knowledge Base Used: Plugin lifecycle and extension management

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants