Equicord/src/main/applyHostPatch.ts
Etorix 8ddfb38264
Questify: Patch Fix. (#1088)
* Style Fix.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-16 09:30:45 -04:00

120 lines
4.1 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "original-fs";
import { basename, dirname, join } from "path";
const STUB_PACKAGE = JSON.stringify({ name: "discord", main: "index.js" });
const VERSION_PREFIX = "app-";
const makeStubIndex = (patcherPath: string) =>
`require(${JSON.stringify(patcherPath)});`;
/** `_app.asar` next to `app.asar` marks any patched install. */
export const isAlreadyPatched = (resources: string) =>
existsSync(join(resources, "_app.asar"));
/**
* apply the folder-shim patch to a discord `resources/` directory.
*
* renames vanilla `app.asar` to `_app.asar`, creates an `app/` folder
* whose `index.js` requires the given patcher script. electron prefers
* the folder over the asar of the same name and loads it first.
*
* idempotent. returns `false` if the directory is already patched or
* has no vanilla `app.asar`. throws on partial failure after rolling
* back any disk changes already made.
*/
export const patchResourcesDir = (resources: string, patcherJsPath: string): boolean => {
const app = join(resources, "app.asar");
const _app = join(resources, "_app.asar");
if (isAlreadyPatched(resources)) return false;
if (!existsSync(app)) return false;
try {
if (lstatSync(app).isDirectory()) return false;
} catch {
return false;
}
const undo: Array<() => void> = [];
try {
renameSync(app, _app);
undo.push(() => renameSync(_app, app));
mkdirSync(app);
undo.push(() => {
const indexPath = join(app, "index.js");
const pkgPath = join(app, "package.json");
if (existsSync(indexPath)) unlinkSync(indexPath);
if (existsSync(pkgPath)) unlinkSync(pkgPath);
rmdirSync(app);
});
writeFileSync(join(app, "package.json"), STUB_PACKAGE);
writeFileSync(join(app, "index.js"), makeStubIndex(patcherJsPath));
return true;
} catch (err) {
for (let i = undo.length - 1; i >= 0; i--) {
try {
undo[i]();
} catch (cleanupErr) {
console.error("[Equicord] Rollback step failed", cleanupErr);
}
}
throw err;
}
};
const parsePart = (s: string) => parseInt(s, 10) || 0;
const isNewer = ($new: string, old: string): boolean => {
const newParts = $new.slice(VERSION_PREFIX.length).split(".").map(parsePart);
const oldParts = old.slice(VERSION_PREFIX.length).split(".").map(parsePart);
const len = Math.max(newParts.length, oldParts.length);
for (let i = 0; i < len; i++) {
const n = newParts[i] ?? 0;
const o = oldParts[i] ?? 0;
if (n > o) return true;
if (n < o) return false;
}
return false;
};
/**
* find the newest sibling `app-VERSION` directory's `resources/` path.
*
* squirrel-only layout. returns `null` on non-win32 platforms, or when
* the running process is already in the newest sibling.
*/
export const findStaleSibling = (currentExeDir: string): string | null => {
if (process.platform !== "win32") return null;
const discordPath = dirname(currentExeDir);
const currentVersion = basename(currentExeDir);
let latest = currentVersion;
try {
for (const name of readdirSync(discordPath)) {
if (!name.startsWith(VERSION_PREFIX)) continue;
let isDir = false;
try {
isDir = statSync(join(discordPath, name)).isDirectory();
} catch (statErr) {
console.error("[Equicord] Skipping unreadable sibling", name, statErr);
continue;
}
if (!isDir) continue;
if (isNewer(name, latest)) latest = name;
}
} catch (err) {
console.error("[Equicord] Failed to scan for sibling versions", err);
return null;
}
if (latest === currentVersion) return null;
return join(discordPath, latest, "resources");
};