diff --git a/src/fake_node_modules/powercord/injector/index.js b/src/fake_node_modules/powercord/injector/index.js index d633d7f80..8c156d330 100644 --- a/src/fake_node_modules/powercord/injector/index.js +++ b/src/fake_node_modules/powercord/injector/index.js @@ -1,70 +1,67 @@ +"use strict"; + +const { performance } = require('perf_hooks'); + window.__$$injectorRuns = []; window.__$$injectorRecord = false; +const injectionsById = new Map(); + const injector = { - injections: [], + get injections() { return [...injectionsById.values()]; }, /** * Injects into a function * @param {String} injectionId ID of the injection, used for uninjecting - * @param {object} mod Module we should inject into + * @param {object} module Module we should inject into * @param {String} funcName Name of the function we're aiming at * @param {function} patch Function to inject * @param {Boolean} pre Whether the injection should run before original code or not */ - inject: (injectionId, mod, funcName, patch, pre = false) => { - if (!mod) { + inject: (injectionId, module, funcName, patch, pre = false) => { + if (!module) { return injector._error(`Tried to patch undefined (Injection ID "${injectionId}")`); } // todo: maybe get rid of this thanks to more deserialize magic? - if (mod.__$$remotePrototype) { - mod = mod.__$$remotePrototype; + if (module.__$$remotePrototype) { + module = module.__$$remotePrototype; } - if (injector.injections.find(i => i.id === injectionId)) { + if (injectionsById.has(injectionId)) { return injector._error(`Injection ID "${injectionId}" is already used!`); } - if (!mod.__powercordInjectionId || !mod.__powercordInjectionId[funcName]) { + let preInjections; + let postInjections; + if (module.__powercordInjections?.[funcName] === undefined) { // 1st injection - const id = Math.random().toString(16).slice(2); - mod.__powercordInjectionId = Object.assign((mod.__powercordInjectionId || {}), { [funcName]: id }); - mod[`__powercordOriginal_${funcName}`] = mod[funcName]; // To allow easier debugging - mod[funcName] = (_oldMethod => function (...args) { - const start = process.hrtime.bigint(); - const finalArgs = injector._runPreInjections(id, args, this); - if (finalArgs !== false && Array.isArray(finalArgs)) { - const call1 = process.hrtime.bigint(); - const returned = _oldMethod ? _oldMethod.call(this, ...finalArgs) : void 0; - const call2 = process.hrtime.bigint(); - // [Cynthia] I think (?) some of the crashes are due to functions injected in the runtime (such as onClick) - // which may throw an error and cause the thing to fail catastrophically. One fix would be to build a deep - // proxy of `returned`, and in the `set` handler wrap potentially sensitive stuff in a try/catch block. - - const res = injector._runInjections(id, finalArgs, returned, this); - const end = process.hrtime.bigint(); - if (window.__$$injectorRecord) { - window.__$$injectorRuns.push({ - ids: injector.injections.filter(i => i.module === id).map((i) => i.id), - duration: Number(end - start) - Number(call2 - call1) - }); - } + preInjections = []; + postInjections = []; + const originalMethod = module[funcName]; - return res; - } - })(mod[funcName]); + module.__powercordInjections ??= {}; + module.__powercordInjections[funcName] = { preInjections, postInjections } - injector.injections[id] = []; + module[`__powercordOriginal_${funcName}`] = originalMethod; // To allow easier debugging + + module[funcName] = injector._createHook(originalMethod, preInjections, postInjections); + } + else { + ({ preInjections, postInjections } = module.__powercordInjections[funcName]); } - injector.injections.push({ - module: mod.__powercordInjectionId[funcName], + const injection = { + module, + funcName, id: injectionId, method: patch, pre - }); + }; + + injectionsById.set(injectionId, injection); + (pre ? preInjections : postInjections).push(injection); }, /** @@ -72,58 +69,117 @@ const injector = { * @param {String} injectionId The injection specified during injection */ uninject: (injectionId) => { - injector.injections = injector.injections.filter(i => i.id !== injectionId); + const injection = injectionsById.get(injectionId); + if(injection !== undefined) { + const functionInjections = injection.module.__powercordInjections[injection.funcName]; + injector._arrayRemoveSingle(injection.pre ? functionInjections.preInjections : functionInjections.postInjections, injection); + } }, /** * Check if a function is injected * @param {String} injectionId The injection to check */ - isInjected: (injectionId) => injector.injections.some(i => i.id === injectionId), + isInjected: (injectionId) => injectionsById.has(injectionId), - _runPreInjections: (modId, originArgs, _this) => { - const injections = injector.injections.filter(i => i.module === modId && i.pre); - if (injections.length === 0) { - return originArgs; + _arrayRemoveSingle: (array, elementToRemove) => { + const index = array.indexOf(elementToRemove); + if(index !== -1) { + array.splice(index, 1); } - return injector._runPreInjectionsRecursive(injections, originArgs, _this); }, - _runPreInjectionsRecursive: (injections, originalArgs, _this) => { - const injection = injections.pop(); - let args; - try { - args = injection.method.call(_this, originalArgs); - } catch (e) { - injector._error(`Failed to run pre-injection "${injection.id}"`, e); - return injector._runPreInjectionsRecursive(injections, originalArgs, _this); - } + _createHook: (originalMethod, preInjections, postInjections) => { + return function () { + if(window.__$$injectorRecord) { + const startTime = performance.now(); + const finalArgs = injector._runPreInjections(preInjections, arguments, this); + const stopped = typeof finalArgs === 'number'; + let result; + let callDuration = 0; + if (!stopped) { + const beforeCallTime = performance.now(); + const originalResult = originalMethod?.apply(this, finalArgs); + const afterCallTime = performance.now(); + callDuration = afterCallTime - beforeCallTime; + + // [Cynthia] I think (?) some of the crashes are due to functions injected in the runtime (such as onClick) + // which may throw an error and cause the thing to fail catastrophically. One fix would be to build a deep + // proxy of `originalResult`, and in the `set` handler wrap potentially sensitive stuff in a try/catch block. + + result = injector._runInjections(postInjections, finalArgs, originalResult, this); + } - if (args === false) { - return false; - } + const endTime = performance.now(); + if (window.__$$injectorRecord) { + let ranPreInjections = preInjections; + let ranPostInjections = postInjections; + if(stopped) { + const stoppedAt = finalArgs; + ranPreInjections = preInjections.slice(0, stoppedAt + 1); + ranPostInjections = []; + } + + window.__$$injectorRuns.push({ + preIds: ranPreInjections.map(x => x.id), + postIds: ranPostInjections.map(x => x.id), + duration: endTime - startTime - callDuration + }); + } + + return result; + } + else { + const finalArgs = injector._runPreInjections(preInjections, arguments, this, originalMethod); + + if (typeof finalArgs !== 'number') { + const originalResult = originalMethod?.apply(this, finalArgs); + + return injector._runInjections(postInjections, finalArgs, originalResult, this); + } + } + }; + }, - if (!Array.isArray(args)) { - injector._error(`Pre-injection ${injection.id} returned something invalid. Injection will be ignored.`); - args = originalArgs; + _runPreInjections: (injections, originArgs, _this) => { + if (injections.length === 0) { + return originArgs; } - if (injections.length > 0) { - return injector._runPreInjectionsRecursive(injections, args, _this); + let args = originArgs; + for (let injectionIndex = 0; injectionIndex < injections.length; injectionIndex++) { + const injection = injections[injectionIndex]; + let newArgs; + try { + newArgs = injection.method.call(_this, args); + } catch (e) { + injector._error(`Failed to run pre-injection "${injection.id}"`, e); + continue; + } + + if (newArgs === false) { + return injectionIndex; + } + + if (Array.isArray(newArgs)) { + args = newArgs; + } + else { + injector._error(`Pre-injection ${injection.id} returned something invalid. Return value will be ignored.`); + } } return args; }, - _runInjections: (modId, originArgs, originReturn, _this) => { + _runInjections: (injections, originArgs, originReturn, _this) => { let finalReturn = originReturn; - const injections = injector.injections.filter(i => i.module === modId && !i.pre); - injections.forEach(i => { + for (const injection of injections) { try { - finalReturn = i.method.call(_this, originArgs, finalReturn); + finalReturn = injection.method.call(_this, originArgs, finalReturn); } catch (e) { - injector._error(`Failed to run injection "${i.id}"`, e); + injector._error(`Failed to run injection "${injection.id}"`, e); } - }); + } return finalReturn; },