Make patcher work with getters

This commit is contained in:
Strencher 2022-10-08 10:40:45 +02:00
parent 46c57567d0
commit dd97539da3
1 changed files with 276 additions and 232 deletions

View File

@ -1,232 +1,276 @@
/** /**
* Patcher that can patch other functions allowing you to run code before, after or * Patcher that can patch other functions allowing you to run code before, after or
* instead of the original function. Can also alter arguments and return values. * instead of the original function. Can also alter arguments and return values.
* *
* This is from Zerebos' library {@link https://github.com/rauenzi/BDPluginLibrary} * This is from Zerebos' library {@link https://github.com/rauenzi/BDPluginLibrary}
* *
* @module Patcher * @module Patcher
* @version 0.0.2 * @version 0.0.2
*/ */
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
patch.module[patch.functionName] = patch.originalFunction; if (patch.getter) {
patch.proxyFunction = null; Object.defineProperty(patch.module, functionName, {
patch.children = []; get: () => patch.originalFunction,
}, configurable: true,
counter: 0, enumerable: true
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, {
* get: () => patch.proxyFunction,
* @callback module:Patcher~patchCallback set: value => (patch.originalFunction = value),
* @param {object} thisObject - `this` in the context of the original function. configurable: true,
* @param {arguments} arguments - The original arguments of the original function. enumerable: true
* @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. });
* @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. } else {
*/ patch.getter = false;
module[functionName] = patch.proxyFunction;
/** }
* 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. const descriptors = Object.assign({}, Object.getOwnPropertyDescriptors(patch.originalFunction), {
* __originalFunction: {
* @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. get: () => patch.originalFunction,
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype. configurable: true,
* @param {string} functionName - Name of the method to be patched enumerable: true,
* @param {module:Patcher~patchCallback} callback - Function to run before the original method writeable: true
* @param {object} options - Object used to pass additional options. },
* @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. toString: {
* @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place). value: () => patch.originalFunction.toString(),
* @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. configurable: true,
*/ enumerable: true,
static before(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "before"}));} writeable: true
}
/** });
* This method patches onto another function, allowing your code to run after.
* Using this, you are also able to modify the return value, using the return of your code instead. Object.defineProperties(patch.proxyFunction, descriptors);
*
* @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. this.patches.push(patch);
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype. return patch;
* @param {string} functionName - Name of the method to be patched }
* @param {module:Patcher~patchCallback} callback - Function to run instead of the original method
* @param {object} options - Object used to pass additional options. /**
* @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. * Function with no arguments and no return value that may be called to revert changes made by {@link module:Patcher}, restoring (unpatching) original method.
* @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place). * @callback module:Patcher~unpatch
* @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 after(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "after"}));} /**
* 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.
/** *
* This method patches onto another function, allowing your code to run instead. * The third argument for the callback will be `undefined` for `before` patches. `originalFunction` for `instead` patches and `returnValue` for `after` patches.
* Using this, you are also able to modify the return value, using the return of your code instead. *
* * @callback module:Patcher~patchCallback
* @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} thisObject - `this` in the context of the original function.
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype. * @param {arguments} arguments - The original arguments of the original function.
* @param {string} functionName - Name of the method to be patched * @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 {module:Patcher~patchCallback} callback - Function to run after the original method * @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.
* @param {object} options - Object used to pass additional options. */
* @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. * 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.
static instead(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "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.
* This method patches onto another function, allowing your code to run before, instead or after the original function. * @param {string} functionName - Name of the method to be patched
* Using this you are able to modify the incoming arguments before the original function is run as well as the return * @param {module:Patcher~patchCallback} callback - Function to run before the original method
* value before the original function actually returns. * @param {object} options - Object used to pass additional options.
* * @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 {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 {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
* @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype. * @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} functionName - Name of the method to be patched */
* @param {module:Patcher~patchCallback} callback - Function to run after the original method static before(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "before"}));}
* @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. * This method patches onto another function, allowing your code to run after.
* @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place). * Using this, you are also able to modify the return value, using the return of your code instead.
* @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} 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.
static pushChildPatch(caller, moduleToPatch, functionName, callback, options = {}) { * @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype.
const {type = "after", forcePatch = true} = options; * @param {string} functionName - Name of the method to be patched
const module = this.resolveModule(moduleToPatch); * @param {module:Patcher~patchCallback} callback - Function to run instead of the original method
if (!module) return null; * @param {object} options - Object used to pass additional options.
if (!module[functionName] && forcePatch) module[functionName] = 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.
if (!(module[functionName] instanceof Function)) return null; * @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.
if (typeof moduleToPatch === "string") options.displayName = moduleToPatch; */
const displayName = options.displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name; static after(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "after"}));}
const patchId = `${displayName}.${functionName}`; /**
const patch = this.patches.find(p => p.module == module && p.functionName == functionName) || this.makePatch(module, functionName, patchId); * This method patches onto another function, allowing your code to run instead.
if (!patch.proxyFunction) this.rePatch(patch); * Using this, you are also able to modify the return value, using the return of your code instead.
const child = { *
caller, * @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.
type, * @param {object} moduleToPatch - Object with the function to be patched. Can also patch an object's prototype.
id: patch.counter, * @param {string} functionName - Name of the method to be patched
callback, * @param {module:Patcher~patchCallback} callback - Function to run after the original method
unpatch: () => { * @param {object} options - Object used to pass additional options.
patch.children.splice(patch.children.findIndex(cpatch => cpatch.id === child.id && cpatch.type === type), 1); * @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.
if (patch.children.length <= 0) { * @param {boolean} [options.forcePatch=true] Set to `true` to patch even if the function doesnt exist. (Adds noop function in place).
const patchNum = this.patches.findIndex(p => p.module == module && p.functionName == functionName); * @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.
if (patchNum < 0) return; */
this.patches[patchNum].revert(); static instead(caller, moduleToPatch, functionName, callback, options = {}) {return this.pushChildPatch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "instead"}));}
this.patches.splice(patchNum, 1);
} /**
} * 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
patch.children.push(child); * value before the original function actually returns.
patch.counter++; *
return child.unpatch; * @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;
if (!Object.getOwnPropertyDescriptor(module, functionName)?.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;
}
}