Skip to content

Commit 604bcac

Browse files
authored
Fixed Plugin installation issues(in some cases) (Acode-Foundation#1546)
1 parent 4a7bca9 commit 604bcac

File tree

2 files changed

+172
-22
lines changed

2 files changed

+172
-22
lines changed

src/lib/installPlugin.js

Lines changed: 137 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -173,21 +173,49 @@ export default async function installPlugin(
173173
await fsOperation(PLUGIN_DIR).createDirectory(id);
174174
}
175175

176+
// Track unsafe absolute entries to skip
177+
const ignoredUnsafeEntries = new Set();
178+
176179
const promises = Object.keys(zip.files).map(async (file) => {
177180
try {
178181
let correctFile = file;
179182
if (/\\/.test(correctFile)) {
180183
correctFile = correctFile.replace(/\\/g, "/");
181184
}
182185

186+
// Determine if the zip entry is a directory from JSZip metadata
187+
const isDirEntry = !!zip.files[file].dir || /\/$/.test(correctFile);
188+
189+
// If the original path is absolute or otherwise unsafe, skip it and warn later
190+
console.log(
191+
`Skipping unsafe path: ${file} : ${isUnsafeAbsolutePath(file)}`,
192+
);
193+
if (isUnsafeAbsolutePath(file)) {
194+
ignoredUnsafeEntries.add(file);
195+
return;
196+
}
197+
198+
// Sanitize path so it cannot escape pluginDir or start with '/'
199+
correctFile = sanitizeZipPath(correctFile, isDirEntry);
200+
if (!correctFile) return; // nothing to do
183201
const fileUrl = Url.join(pluginDir, correctFile);
184202

185-
if (!state.exists(correctFile)) {
186-
await createFileRecursive(pluginDir, correctFile);
203+
// Always ensure directories exist for dir entries
204+
if (isDirEntry) {
205+
await createFileRecursive(pluginDir, correctFile, true);
206+
return;
207+
}
208+
209+
// For files, ensure parent directory exists even if state claims it exists
210+
const lastSlash = correctFile.lastIndexOf("/");
211+
if (lastSlash >= 0) {
212+
const parentRel = correctFile.slice(0, lastSlash + 1);
213+
await createFileRecursive(pluginDir, parentRel, true);
187214
}
188215

189-
// Skip directories
190-
if (correctFile.endsWith("/")) return;
216+
if (!state.exists(correctFile)) {
217+
await createFileRecursive(pluginDir, correctFile, false);
218+
}
191219

192220
let data = await zip.files[file].async("ArrayBuffer");
193221

@@ -206,6 +234,20 @@ export default async function installPlugin(
206234
// Wait for all files to be processed
207235
await Promise.allSettled(promises);
208236

237+
// Emit a non-blocking warning if any unsafe entries were skipped
238+
if (!isDependency && ignoredUnsafeEntries.size) {
239+
const sample = Array.from(ignoredUnsafeEntries).slice(0, 3).join(", ");
240+
loaderDialog.setMessage(
241+
`Skipped ${ignoredUnsafeEntries.size} unsafe archive entr${
242+
ignoredUnsafeEntries.size === 1 ? "y" : "ies"
243+
} (e.g., ${sample})`,
244+
);
245+
console.warn(
246+
"Plugin installer: skipped unsafe absolute paths in archive:",
247+
Array.from(ignoredUnsafeEntries),
248+
);
249+
}
250+
209251
if (isDependency) {
210252
depsLoaders.push(async () => {
211253
await loadPlugin(id, true);
@@ -245,28 +287,105 @@ export default async function installPlugin(
245287
* @param {string} parent
246288
* @param {Array<string> | string} dir
247289
*/
248-
async function createFileRecursive(parent, dir) {
249-
let isDir = false;
290+
async function createFileRecursive(parent, dir, shouldBeDirAtEnd) {
291+
let wantDirEnd = !!shouldBeDirAtEnd;
292+
/** @type {string[]} */
293+
let parts;
250294
if (typeof dir === "string") {
251-
if (dir.endsWith("/")) {
252-
isDir = true;
253-
dir = dir.slice(0, -1);
254-
}
255-
dir = dir.split("/");
295+
if (dir.endsWith("/")) wantDirEnd = true;
296+
dir = dir.replace(/\\/g, "/");
297+
parts = dir.split("/");
298+
} else {
299+
parts = dir;
256300
}
257-
dir = dir.filter((d) => d);
258-
const cd = dir.shift();
301+
parts = parts.filter((d) => d);
302+
const cd = parts.shift();
303+
if (!cd) return;
259304
const newParent = Url.join(parent, cd);
305+
306+
const isLast = parts.length === 0;
307+
const needDir = !isLast || wantDirEnd;
260308
if (!(await fsOperation(newParent).exists())) {
261-
if (dir.length || isDir) {
262-
await fsOperation(parent).createDirectory(cd);
309+
if (needDir) {
310+
try {
311+
await fsOperation(parent).createDirectory(cd);
312+
} catch (e) {
313+
// If another concurrent task created it, consider it fine
314+
if (!(await fsOperation(newParent).exists())) throw e;
315+
}
263316
} else {
264-
await fsOperation(parent).createFile(cd);
317+
try {
318+
await fsOperation(parent).createFile(cd);
319+
} catch (e) {
320+
if (!(await fsOperation(newParent).exists())) throw e;
321+
}
322+
}
323+
}
324+
if (parts.length) {
325+
await createFileRecursive(newParent, parts, wantDirEnd);
326+
}
327+
}
328+
329+
/**
330+
* Sanitize zip entry path to ensure it's relative and safe under pluginDir
331+
* - Normalizes separators to '/'
332+
* - Strips leading slashes and Windows drive prefixes (e.g., C:/)
333+
* - Resolves '.' and '..' segments
334+
* - Preserves trailing slash for directory entries
335+
* @param {string} p
336+
* @param {boolean} isDir
337+
* @returns {string} sanitized relative path
338+
*/
339+
function sanitizeZipPath(p, isDir) {
340+
if (!p) return "";
341+
let path = String(p);
342+
// Normalize separators
343+
path = path.replace(/\\/g, "/");
344+
// Remove URL-like scheme if present accidentally
345+
path = path.replace(/^[a-zA-Z]+:\/\//, "");
346+
// Strip leading slashes
347+
path = path.replace(/^\/+/, "");
348+
// Strip Windows drive letter, e.g., C:/
349+
path = path.replace(/^[A-Za-z]:\//, "");
350+
351+
const parts = path.split("/");
352+
const stack = [];
353+
for (const part of parts) {
354+
if (!part || part === ".") continue;
355+
if (part === "..") {
356+
if (stack.length) stack.pop();
357+
continue;
265358
}
359+
stack.push(part);
266360
}
267-
if (dir.length) {
268-
await createFileRecursive(newParent, dir);
361+
let safe = stack.join("/");
362+
if (isDir && safe && !safe.endsWith("/")) safe += "/";
363+
return safe;
364+
}
365+
366+
/**
367+
* Detects unsafe absolute paths in zip entries that should be ignored.
368+
* Treats leading '/' as absolute, Windows drive roots like 'C:/' as absolute,
369+
* and common Android/Linux device roots like '/data', '/root', '/system'.
370+
* @param {string} p
371+
*/
372+
function isUnsafeAbsolutePath(p) {
373+
if (!p) return false;
374+
const s = String(p);
375+
if (/^[A-Za-z]:[\\\/]/.test(s)) return true; // Windows drive root
376+
if (s.startsWith("//")) return true; // network path
377+
if (s.startsWith("/")) {
378+
return (
379+
s.startsWith("/data") ||
380+
s.startsWith("/system") ||
381+
s.startsWith("/vendor") ||
382+
s.startsWith("/storage") ||
383+
s.startsWith("/sdcard") ||
384+
s.startsWith("/root") ||
385+
true // any leading slash is unsafe
386+
);
269387
}
388+
return false;
270389
}
271390

272391
/**

src/lib/installState.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,28 @@ export default class InstallState {
2626

2727
state.storeUrl = Url.join(INSTALL_STATE_STORAGE, state.id);
2828
if (await fsOperation(state.storeUrl).exists()) {
29-
state.store = JSON.parse(
30-
await fsOperation(state.storeUrl).readFile("utf-8"),
31-
);
29+
let raw = "{}";
30+
try {
31+
raw = await fsOperation(state.storeUrl).readFile("utf-8");
32+
state.store = JSON.parse(raw);
33+
} catch (err) {
34+
console.error(
35+
"InstallState: Failed to parse state file, deleting:",
36+
err,
37+
);
38+
// Delete corrupted state file to avoid parse errors such as 'Unexpected end of JSON'
39+
state.store = {};
40+
try {
41+
await fsOperation(state.storeUrl).delete();
42+
// Recreate a fresh empty file to keep invariant
43+
await fsOperation(INSTALL_STATE_STORAGE).createFile(state.id);
44+
} catch (writeErr) {
45+
console.error(
46+
"InstallState: Failed to recreate state file:",
47+
writeErr,
48+
);
49+
}
50+
}
3251

3352
const patchedStore = {};
3453
for (const [key, value] of Object.entries(state.store)) {
@@ -101,7 +120,19 @@ export default class InstallState {
101120
try {
102121
this.store = {};
103122
this.updatedStore = {};
104-
await fsOperation(this.storeUrl).writeFile("{}");
123+
// Delete the state file entirely to avoid corrupted/partial JSON issues
124+
if (await fsOperation(this.storeUrl).exists()) {
125+
try {
126+
await fsOperation(this.storeUrl).delete();
127+
} catch (delErr) {
128+
console.error(
129+
"InstallState: Failed to delete state file during clear:",
130+
delErr,
131+
);
132+
// As a fallback, overwrite with a valid empty JSON
133+
await fsOperation(this.storeUrl).writeFile("{}");
134+
}
135+
}
105136
} catch (error) {
106137
console.error("Failed to clear install state:", error);
107138
}

0 commit comments

Comments
 (0)