Skip to content

Commit

Permalink
refactor(Webpack): more reliable patching (#2237)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuckyz committed May 2, 2024
1 parent 0a598ae commit a055b1d
Show file tree
Hide file tree
Showing 8 changed files with 432 additions and 291 deletions.
264 changes: 155 additions & 109 deletions scripts/generateReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ page.on("pageerror", e => console.error("[Page Error]", e));

await page.setBypassCSP(true);

function runTime(token: string) {
async function runtime(token: string) {
console.log("[PUP_DEBUG]", "Starting test...");

try {
Expand All @@ -282,9 +282,13 @@ function runTime(token: string) {

// Monkey patch Logger to not log with custom css
// @ts-ignore
const originalLog = Vencord.Util.Logger.prototype._log;
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
if (level === "warn" || level === "error")
console[level]("[Vencord]", this.name + ":", ...args);
return console[level]("[Vencord]", this.name + ":", ...args);

return originalLog.call(this, level, levelColor, args);
};

// Force enable all plugins and patches
Expand All @@ -310,45 +314,30 @@ function runTime(token: string) {
});
});

Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);

// Force load all chunks
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
console.log("[PUP_DEBUG]", "Webpack is ready!");
let wreq: typeof Vencord.Webpack.wreq;

const { wreq } = Vencord.Webpack;

console.log("[PUP_DEBUG]", "Loading all chunks...");
const { canonicalizeMatch, Logger } = Vencord.Util;

let chunks = null as Record<number, string[]> | null;
const sym = Symbol("Vencord.chunksExtract");
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();

Object.defineProperty(Object.prototype, sym, {
get() {
chunks = this;
},
set() { },
configurable: true,
});
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);

await (wreq as any).el(sym);
delete Object.prototype[sym];
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g);

const validChunksEntryPoints = new Set<string>();
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();

if (!chunks) throw new Error("Failed to get chunks");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
if (chunkIds.length === 0) return;

for (const entryPoint in chunks) {
const chunkIds = chunks[entryPoint];
let invalidEntryPoint = false;
let invalidChunkGroup = false;

for (const id of chunkIds) {
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
Expand All @@ -359,111 +348,168 @@ function runTime(token: string) {

if (isWasm) {
invalidChunks.add(id);
invalidEntryPoint = true;
invalidChunkGroup = true;
continue;
}

validChunks.add(id);
}

if (!invalidEntryPoint)
validChunksEntryPoints.add(entryPoint);
}

for (const entryPoint of validChunksEntryPoints) {
if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, entryPoint]);
}
}));

// Loads all found valid chunk groups
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
)
);

// Requires the entry points for all valid chunk groups
for (const [, entryPoint] of validChunkGroups) {
try {
// Loads all chunks required for an entry point
await (wreq as any).el(entryPoint);
} catch (err) { }
if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) {
console.error(err);
}
}

// Matches "id" or id:
const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g;
const wreqU = wreq.u.toString();
// setImmediate to only check if all chunks were loaded after this function resolves
// We check if all chunks were loaded every time a factory is loaded
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
setTimeout(() => {
let allResolved = true;

const allChunks = [] as string[];
let currentMatch: RegExpExecArray | null;
for (let i = 0; i < chunksSearchPromises.length; i++) {
const isResolved = chunksSearchPromises[i]();

while ((currentMatch = chunkIdRegex.exec(wreqU)) != null) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
if (isResolved) {
// Remove finished promises to avoid having to iterate through a huge array everytime
chunksSearchPromises.splice(i--, 1);
} else {
allResolved = false;
}
}

allChunks.push(id);
if (allResolved) chunksSearchingResolve();
}, 0);
}

Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);

if (allChunks.length === 0) throw new Error("Failed to get all chunks");
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
console.log("[PUP_DEBUG]", "Loading all chunks...");

for (const id of chunksLeft) {
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
wreq = webpackRequire;

// Loads a chunk
if (!isWasm) await wreq.e(id as any);
}
Vencord.Webpack.factoryListeners.add(factory => {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);

// Make sure every chunk has finished loading
await new Promise(r => setTimeout(r, 1000));
chunksSearchPromises.push(() => isResolved);
});

for (const entryPoint of validChunksEntryPoints) {
try {
if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) {
console.error(err);
// setImmediate to only search the initial factories after Discord initialized the app
// our beforeInitListeners are called before Discord initializes the app
setTimeout(() => {
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);

chunksSearchPromises.push(() => isResolved);
}
}
}, 0);
});

console.log("[PUP_DEBUG]", "Finished loading all chunks!");
await chunksSearchingDone;

for (const patch of Vencord.Plugins.patches) {
if (!patch.all) {
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as string[];

for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
let method = searchType;
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;

if (searchType === "findComponent") method = "find";
if (searchType === "findExportedComponent") method = "findByProps";
if (searchType === "waitFor" || searchType === "waitForComponent") {
if (typeof args[0] === "string") method = "findByProps";
else method = "find";
}
if (searchType === "waitForStore") method = "findStore";
allChunks.push(id);
}

try {
let result: any;
if (allChunks.length === 0) throw new Error("Failed to get all chunks");

if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args;
result = factory();
} else if (method === "extractAndLoadChunks") {
const [code, matcher] = args;
// Chunks that are not loaded (not used) by Discord code anymore
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});

const module = Vencord.Webpack.findModuleFactory(...code);
if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher));
} else {
// @ts-ignore
result = Vencord.Webpack[method](...args);
}
await Promise.all(chunksLeft.map(async id => {
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));

if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
// Loads and requires a chunk
if (!isWasm) {
await wreq.e(id as any);
if (wreq.m[id]) wreq(id as any);
}
}));

console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
console.log("[PUP_DEBUG]", "Finished loading all chunks!");

for (const patch of Vencord.Plugins.patches) {
if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}

for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
let method = searchType;

if (searchType === "findComponent") method = "find";
if (searchType === "findExportedComponent") method = "findByProps";
if (searchType === "waitFor" || searchType === "waitForComponent") {
if (typeof args[0] === "string") method = "findByProps";
else method = "find";
}
if (searchType === "waitForStore") method = "findStore";

try {
let result: any;

if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args;
result = factory();
} else if (method === "extractAndLoadChunks") {
const [code, matcher] = args;

const module = Vencord.Webpack.findModuleFactory(...code);
if (module) result = module.toString().match(canonicalizeMatch(matcher));
} else {
// @ts-ignore
result = Vencord.Webpack[method](...args);
}

if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;

console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
}
}

setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
}, 1000));
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
} catch (e) {
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
process.exit(1);
Expand All @@ -473,7 +519,7 @@ function runTime(token: string) {
await page.evaluateOnNewDocument(`
${readFileSync("./dist/browser.js", "utf-8")}
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
`);

await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
Loading

0 comments on commit a055b1d

Please sign in to comment.