Change how patching modules works (#1474)

This commit is contained in:
Zerebos 2022-11-06 14:33:47 -05:00 committed by GitHub
parent 690bfb79cb
commit 03efbfd447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 225 additions and 273 deletions

View File

@ -41,7 +41,8 @@ export default function () {
if (!Reflect.has(exports, key) || target[key]) continue; if (!Reflect.has(exports, key) || target[key]) continue;
Object.defineProperty(target, key, { Object.defineProperty(target, key, {
get: exports[key], get: () => exports[key](),
set: v => {exports[key] = () => v;},
enumerable: true, enumerable: true,
configurable: true configurable: true
}); });

View File

@ -3,275 +3,226 @@
* instead of the original function. Can also alter arguments and return values. * instead of the original function. Can also alter arguments and return values.
*/ */
import Logger from "common/logger"; import Logger from "common/logger";
import DiscordModules from "./discordmodules"; import DiscordModules from "./discordmodules";
import WebpackModules from "./webpackmodules"; import WebpackModules from "./webpackmodules";
export default class Patcher { export default class Patcher {
static get patches() {return this._patches || (this._patches = []);} static get patches() {return this._patches || (this._patches = []);}
/** /**
* Returns all the patches done by a specific caller * Returns all the patches done by a specific caller
* @param {string} name - Name of the patch caller * @param {string} name - Name of the patch caller
* @method * @method
*/ */
static getPatchesByCaller(name) { static getPatchesByCaller(name) {
if (!name) return []; if (!name) return [];
const patches = []; const patches = [];
for (const patch of this.patches) { for (const patch of this.patches) {
for (const childPatch of patch.children) { for (const childPatch of patch.children) {
if (childPatch.caller === name) patches.push(childPatch); if (childPatch.caller === name) patches.push(childPatch);
} }
} }
return patches; return patches;
} }
/** /**
* Unpatches all patches passed, or when a string is passed unpatches all * Unpatches all patches passed, or when a string is passed unpatches all
* patches done by that specific caller. * patches done by that specific caller.
* @param {Array|string} patches - Either an array of patches to unpatch or a caller name * @param {Array|string} patches - Either an array of patches to unpatch or a caller name
*/ */
static unpatchAll(patches) { static unpatchAll(patches) {
if (typeof patches === "string") patches = this.getPatchesByCaller(patches); if (typeof patches === "string") patches = this.getPatchesByCaller(patches);
for (const patch of patches) { for (const patch of patches) {
patch.unpatch(); patch.unpatch();
} }
} }
static resolveModule(module) { static resolveModule(module) {
if (!module || typeof(module) === "function" || (typeof(module) === "object" && !Array.isArray(module))) return module; if (!module || typeof(module) === "function" || (typeof(module) === "object" && !Array.isArray(module))) return module;
if (typeof module === "string") return DiscordModules[module]; if (typeof module === "string") return DiscordModules[module];
if (Array.isArray(module)) return WebpackModules.findByUniqueProperties(module); if (Array.isArray(module)) return WebpackModules.findByUniqueProperties(module);
return null; return null;
} }
static makeOverride(patch) { static makeOverride(patch) {
return function () { return function () {
let returnValue; let returnValue;
if (!patch.children || !patch.children.length) return patch.originalFunction.apply(this, arguments); if (!patch.children || !patch.children.length) return patch.originalFunction.apply(this, arguments);
for (const superPatch of patch.children.filter(c => c.type === "before")) { for (const superPatch of patch.children.filter(c => c.type === "before")) {
try { try {
superPatch.callback(this, arguments); superPatch.callback(this, arguments);
} }
catch (err) { catch (err) {
Logger.err("Patcher", `Could not fire before callback of ${patch.functionName} for ${superPatch.caller}`, err); Logger.err("Patcher", `Could not fire before callback of ${patch.functionName} for ${superPatch.caller}`, err);
} }
} }
const insteads = patch.children.filter(c => c.type === "instead"); const insteads = patch.children.filter(c => c.type === "instead");
if (!insteads.length) {returnValue = patch.originalFunction.apply(this, arguments);} if (!insteads.length) {returnValue = patch.originalFunction.apply(this, arguments);}
else { else {
for (const insteadPatch of insteads) { for (const insteadPatch of insteads) {
try { try {
const tempReturn = insteadPatch.callback(this, arguments, patch.originalFunction.bind(this)); const tempReturn = insteadPatch.callback(this, arguments, patch.originalFunction.bind(this));
if (typeof(tempReturn) !== "undefined") returnValue = tempReturn; if (typeof(tempReturn) !== "undefined") returnValue = tempReturn;
} }
catch (err) { catch (err) {
Logger.err("Patcher", `Could not fire instead callback of ${patch.functionName} for ${insteadPatch.caller}`, err); Logger.err("Patcher", `Could not fire instead callback of ${patch.functionName} for ${insteadPatch.caller}`, err);
} }
} }
} }
for (const slavePatch of patch.children.filter(c => c.type === "after")) { for (const slavePatch of patch.children.filter(c => c.type === "after")) {
try { try {
const tempReturn = slavePatch.callback(this, arguments, returnValue); const tempReturn = slavePatch.callback(this, arguments, returnValue);
if (typeof(tempReturn) !== "undefined") returnValue = tempReturn; if (typeof(tempReturn) !== "undefined") returnValue = tempReturn;
} }
catch (err) { catch (err) {
Logger.err("Patcher", `Could not fire after callback of ${patch.functionName} for ${slavePatch.caller}`, err); Logger.err("Patcher", `Could not fire after callback of ${patch.functionName} for ${slavePatch.caller}`, err);
} }
} }
return returnValue; return returnValue;
}; };
} }
static rePatch(patch) { static rePatch(patch) {
patch.proxyFunction = patch.module[patch.functionName] = this.makeOverride(patch); patch.proxyFunction = patch.module[patch.functionName] = this.makeOverride(patch);
} }
static makePatch(module, functionName, name) { static makePatch(module, functionName, name) {
const patch = { const patch = {
name, name,
module, module,
functionName, functionName,
originalFunction: module[functionName], originalFunction: module[functionName],
proxyFunction: null, proxyFunction: null,
revert: () => { // Calling revert will destroy any patches added to the same module after this revert: () => { // Calling revert will destroy any patches added to the same module after this
if (patch.getter) { patch.module[patch.functionName] = patch.originalFunction;
Object.defineProperty(patch.module, functionName, { patch.proxyFunction = null;
...Object.getOwnPropertyDescriptor(patch.module, functionName), patch.children = [];
get: () => patch.originalFunction, },
set: undefined counter: 0,
}); children: []
} };
else { patch.proxyFunction = module[functionName] = this.makeOverride(patch);
patch.module[patch.functionName] = patch.originalFunction; Object.assign(module[functionName], patch.originalFunction);
} module[functionName].__originalFunction = patch.originalFunction;
module[functionName].toString = () => patch.originalFunction.toString();
patch.proxyFunction = null; this.patches.push(patch);
patch.children = []; return patch;
}, }
counter: 0,
children: [] /**
}; * Function with no arguments and no return value that may be called to revert changes made by {@link module:Patcher}, restoring (unpatching) original method.
* @callback module:Patcher~unpatch
patch.proxyFunction = this.makeOverride(patch); */
const descriptor = Object.getOwnPropertyDescriptor(module, functionName); /**
* A callback that modifies method logic. This callback is called on each call of the original method and is provided all data about original call. Any of the data can be modified if necessary, but do so wisely.
if (descriptor?.get) { *
patch.getter = true; * The third argument for the callback will be `undefined` for `before` patches. `originalFunction` for `instead` patches and `returnValue` for `after` patches.
Object.defineProperty(module, functionName, { *
configurable: true, * @callback module:Patcher~patchCallback
enumerable: true, * @param {object} thisObject - `this` in the context of the original function.
...descriptor, * @param {arguments} args - The original arguments of the original function.
get: () => patch.proxyFunction, * @param {(function|*)} extraValue - For `instead` patches, this is the original function from the module. For `after` patches, this is the return value of the function.
// eslint-disable-next-line no-setter-return * @return {*} Makes sense only when using an `instead` or `after` patch. If something other than `undefined` is returned, the returned value replaces the value of `returnValue`. If used for `before` the return value is ignored.
set: value => (patch.originalFunction = value) */
});
} /**
else { * This method patches onto another function, allowing your code to run beforehand.
patch.getter = false; * Using this, you are also able to modify the incoming arguments before the original method is run.
module[functionName] = patch.proxyFunction; *
} * @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care.
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype.
const descriptors = Object.assign({}, Object.getOwnPropertyDescriptors(patch.originalFunction), { * @param {string} functionName - Name of the method to be patched
__originalFunction: { * @param {module:Patcher~patchCallback} callback - Function to run before the original method
get: () => patch.originalFunction, * @param {object} options - Object used to pass additional options.
configurable: true, * @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
enumerable: true, * @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
writeable: true * @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
}, */
toString: { static before(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "before"}));}
value: () => patch.originalFunction.toString(),
configurable: true, /**
enumerable: true, * This method patches onto another function, allowing your code to run after.
writeable: true * Using this, you are also able to modify the return value, using the return of your code instead.
} *
}); * @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care.
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype.
Object.defineProperties(patch.proxyFunction, descriptors); * @param {string} functionName - Name of the method to be patched
* @param {module:Patcher~patchCallback} callback - Function to run instead of the original method
this.patches.push(patch); * @param {object} options - Object used to pass additional options.
return patch; * @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
} * @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
* @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
/** */
* Function with no arguments and no return value that may be called to revert changes made by {@link module:Patcher}, restoring (unpatching) original method. static after(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "after"}));}
* @callback module:Patcher~unpatch
*/ /**
* This method patches onto another function, allowing your code to run instead.
/** * Using this, you are also able to modify the return value, using the return of your code instead.
* A callback that modifies method logic. This callback is called on each call of the original method and is provided all data about original call. Any of the data can be modified if necessary, but do so wisely. *
* * @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care.
* The third argument for the callback will be `undefined` for `before` patches. `originalFunction` for `instead` patches and `returnValue` for `after` patches. * @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype.
* * @param {string} functionName - Name of the method to be patched
* @callback module:Patcher~patchCallback * @param {module:Patcher~patchCallback} callback - Function to run after the original method
* @param {object} thisObject - `this` in the context of the original function. * @param {object} options - Object used to pass additional options.
* @param {args} args - The original arguments of the original function. * @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
* @param {(function|*)} extraValue - For `instead` patches, this is the original function from the module. For `after` patches, this is the return value of the function. * @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
* @return {*} Makes sense only when using an `instead` or `after` patch. If something other than `undefined` is returned, the returned value replaces the value of `returnValue`. If used for `before` the return value is ignored. * @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
*/ */
static instead(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "instead"}));}
/**
* This method patches onto another function, allowing your code to run beforehand. /**
* Using this, you are also able to modify the incoming arguments before the original method is run. * This method patches onto another function, allowing your code to run before, instead or after the original function.
* * Using this you are able to modify the incoming arguments before the original function is run as well as the return
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care. * value before the original function actually returns.
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype. *
* @param {string} functionName - Name of the method to be patched * @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care.
* @param {module:Patcher~patchCallback} callback - Function to run before the original method * @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype.
* @param {object} options - Object used to pass additional options. * @param {string} functionName - Name of the method to be patched
* @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically. * @param {module:Patcher~patchCallback} callback - Function to run after the original method
* @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place). * @param {object} options - Object used to pass additional options.
* @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped. * @param {string} [options.type=after] - Determines whether to run the function `before`, `instead`, or `after` the original.
*/ * @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
static before(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "before"}));} * @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
* @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
/** */
* This method patches onto another function, allowing your code to run after. static pushChildPatch(caller, moduleToPatch, functionName, callback, options = {}) {
* Using this, you are also able to modify the return value, using the return of your code instead. const {type = "after", forcePatch = true} = options;
* const module = this.resolveModule(moduleToPatch);
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care. if (!module) return null;
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype. if (!module[functionName] && forcePatch) module[functionName] = function() {};
* @param {string} functionName - Name of the method to be patched if (!(module[functionName] instanceof Function)) return null;
* @param {module:Patcher~patchCallback} callback - Function to run instead of the original method
* @param {object} options - Object used to pass additional options. if (typeof moduleToPatch === "string") options.displayName = moduleToPatch;
* @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically. const displayName = options.displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
* @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
* @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped. const patchId = `${displayName}.${functionName}`;
*/ const patch = this.patches.find(p => p.module == module && p.functionName == functionName) || this.makePatch(module, functionName, patchId);
static after(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "after"}));} if (!patch.proxyFunction) this.rePatch(patch);
const child = {
/** caller,
* This method patches onto another function, allowing your code to run instead. type,
* Using this, you are also able to modify the return value, using the return of your code instead. id: patch.counter,
* callback,
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care. unpatch: () => {
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype. patch.children.splice(patch.children.findIndex(cpatch => cpatch.id === child.id && cpatch.type === type), 1);
* @param {string} functionName - Name of the method to be patched if (patch.children.length <= 0) {
* @param {module:Patcher~patchCallback} callback - Function to run after the original method const patchNum = this.patches.findIndex(p => p.module == module && p.functionName == functionName);
* @param {object} options - Object used to pass additional options. if (patchNum < 0) return;
* @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically. this.patches[patchNum].revert();
* @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place). this.patches.splice(patchNum, 1);
* @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped. }
*/ }
static instead(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "instead"}));} };
patch.children.push(child);
/** patch.counter++;
* This method patches onto another function, allowing your code to run before, instead or after the original function. return child.unpatch;
* Using this you are able to modify the incoming arguments before the original function is run as well as the return }
* value before the original function actually returns.
* }
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link module:Patcher.unpatchAll}. Use `""` if you don't care.
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype.
* @param {string} functionName - Name of the method to be patched
* @param {module:Patcher~patchCallback} callback - Function to run after the original method
* @param {object} options - Object used to pass additional options.
* @param {string} [options.type=after] - Determines whether to run the function `before`, `instead`, or `after` the original.
* @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
* @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
* @return {module:Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
*/
static pushChildPatch(caller, moduleToPatch, functionName, callback, options = {}) {
const {type = "after", forcePatch = true} = options;
const module = this.resolveModule(moduleToPatch);
if (!module) return null;
if (!module[functionName] && forcePatch) module[functionName] = function() {};
if (!(module[functionName] instanceof Function)) return null;
const descriptor = Object.getOwnPropertyDescriptor(module, functionName);
if (descriptor && !descriptor?.configurable) {
Logger.err("Patcher", `Cannot patch ${functionName} of Module, property is readonly.`);
return null;
}
if (typeof moduleToPatch === "string") options.displayName = moduleToPatch;
const displayName = options.displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
const patchId = `${displayName}.${functionName}`;
const patch = this.patches.find(p => p.module == module && p.functionName == functionName) || this.makePatch(module, functionName, patchId);
if (!patch.proxyFunction) this.rePatch(patch);
const child = {
caller,
type,
id: patch.counter,
callback,
unpatch: () => {
patch.children.splice(patch.children.findIndex(cpatch => cpatch.id === child.id && cpatch.type === type), 1);
if (patch.children.length <= 0) {
const patchNum = this.patches.findIndex(p => p.module == module && p.functionName == functionName);
if (patchNum < 0) return;
this.patches[patchNum].revert();
this.patches.splice(patchNum, 1);
}
}
};
patch.children.push(child);
patch.counter++;
return child.unpatch;
}
}