Socket
Socket
Sign inDemoInstall

@contrast/patcher

Package Overview
Dependencies
Maintainers
0
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contrast/patcher - npm Package Compare versions

Comparing version 1.9.0 to 1.10.0

242

lib/index.js

@@ -19,2 +19,3 @@ /*

const { threadId } = require('worker_threads');
const { hrtime } = require('process');

@@ -32,2 +33,3 @@ /**

function isFunction(fn) {
// why do both need to be checked?
return fn instanceof Function || typeof fn === 'function';

@@ -56,2 +58,4 @@ }

const promisifyCustom = Symbol.for('nodejs.util.promisify.custom');
const perf = new core.Perf('patcher');
const perfIsEnabled = core.Perf.isEnabled;

@@ -85,45 +89,2 @@ /**

/**
* Run the given hooks with the supplied arguments and `this` target
*
* @param {any} data arguments for the hooks execution
* @param {Object} thisTarget this target for the `apply` call of the hooks
* @param {Array} fnHooksArr array of hooks to execute
*/
function runHooks(data, thisTarget, fnHooksArr) {
fnHooksArr.forEach((hook) => {
hook.apply(thisTarget, [data]);
});
}
/**
* Get the result of a function call
*
* @this {Function} the hooked function
* @param {Function} fn the original function
* @param {any} target the constructor being called (if called with new)
* @param {Object} fnHooks an object containing maps of the hooks set for this function
* @param {Object} data
*/
function getResult(fn, data, target, fnHooks) {
const self = this;
const arounds = Array.from(fnHooks.around.values());
if (arounds.length) {
const pipe = arounds.reverse().reduce(
(innerNext, around) =>
function next() {
return around(innerNext, data);
},
function next() {
return runOriginalFunction.call(self, fn, data, target);
}
);
return pipe();
}
return runOriginalFunction.call(this, fn, data, target);
}
/**
* Invoke the original function in the current context with the

@@ -166,33 +127,6 @@ * passed-in arguments and store its result

function hooked(...args) {
const target = new.target;
// get the hooked function. it will be either the hooked function with perf
// or without.
const hooked = makeHookedFunction(fn, options);
const data = {
hooked,
orig: fn,
funcKey: options.funcKey,
obj: new.target || this,
name: options.name,
args,
result: undefined,
};
const fnHooks = hooks.get(hooked);
// Run pre hooks if there are some
if (fnHooks && fnHooks.pre.size) {
runHooks(data, this, Array.from(fnHooks.pre.values()));
}
// Run original function
data.result = getResult.call(this, fn, data, target, fnHooks);
// Run post hooks if there are some
if (fnHooks && fnHooks.post.size) {
runHooks(data, this, Array.from(fnHooks.post.values()));
}
return data.result;
}
/**

@@ -305,2 +239,161 @@ * copy over all properties if there are properties on the original function.

/**
* factory to make a hooked function either with or without perf data. the
* arguments are exactly the same as `hookFunction`. it exists to create a
* closure around the `fn` and `options` arguments and to isolate the
* logic differences in the hooked function to a single place.
*
* @param {Function} fn original function being hooked
* @param {Object} options
* @param {string} options.name - name of the function (usually signature)
* @param {boolean} options.usePerf - collect perf data for this hooked function
* @returns {Function} the hooked function
*
*/
function makeHookedFunction(fn, options) {
//
// the hooked function that does not capture perf data. this is very close
// to the original hooked function. the primary difference is that it does
// the pre/post hooks in a loop rather than calling a function.
//
function hooked(...args) {
const target = new.target;
const data = {
hooked,
orig: fn,
funcKey: options.funcKey,
obj: new.target || this,
name: options.name,
args,
result: undefined,
};
const fnHooks = hooks.get(hooked);
if (!fnHooks) {
return (data.result = runOriginalFunction.call(this, fn, data, target));
}
// Run pre hooks
if (fnHooks.pre.size) {
for (const pre of fnHooks.pre.values()) {
pre.call(this, data);
}
}
if (fnHooks.around.size === 0) {
data.result = runOriginalFunction.call(this, fn, data, target);
} else {
// chain around hooks, from last to first to original, via recursion
const self = this;
const arounds = Array.from(fnHooks.around.values()).reverse();
const pipe = arounds.reduce(
(innerNext, around) =>
function next() {
return around(innerNext, data);
},
function next() {
return runOriginalFunction.call(self, fn, data, target);
}
);
data.result = pipe();
}
// Run post hooks
if (fnHooks.post.size) {
for (const post of fnHooks.post.values()) {
post.call(this, data);
}
}
return data.result;
}
//
// a functional duplicate of hooked, but has perf measurement of the hooked
// and original functions.
//
function perfHooked(...args) {
// do perf work before getting the start time
const outerTag = `patcher${options.name}:wrapper`;
const innerTag = `${options.name}:native`; // name is patcher:original-function-name
const start = hrtime.bigint();
const target = new.target;
const data = {
hooked: perfHooked,
orig: fn,
funcKey: options.funcKey,
obj: new.target || this,
name: options.name, //String.prototype.substring
args,
result: undefined,
};
// create functions to run the original function. this replaces the original
// `runOriginalFunction()` with one that doesn't require passing arguments. my
// thinking is that minimizes wrapping overhead.
let runOriginalFunction;
if (target) {
runOriginalFunction = () => Reflect.construct(fn, args, options.name !== 'Object' ? target : unwrap(target));
} else {
runOriginalFunction = () => fn.apply(this, args);
}
runOriginalFunction = perf.wrapSync(runOriginalFunction, innerTag);
// if the function isn't hooked, just run the original function and records the
// time with a modified tag indicating that it wasn't hooked. i don't think this
// should happen (why are we in the hooked function if there are no hooks?), but
// it's here just in case.
const fnHooks = hooks.get(perfHooked);
if (!fnHooks) {
const result = (data.result = runOriginalFunction());
perf.record(`${outerTag}:unhooked`, start);
return result;
}
// Run any pre hooks
if (fnHooks.pre.size) {
for (const pre of fnHooks.pre.values()) {
pre.call(this, data);
}
}
// if there are no around hooks, then no need to do the work to chain them.
if (fnHooks.around.size === 0) {
data.result = runOriginalFunction();
} else {
// chain around hooks, from last to first to original, via recursion
const arounds = Array.from(fnHooks.around.values()).reverse();
const pipe = arounds.reduce(
(innerNext, around) =>
function next() {
return around(innerNext, data);
},
function next() {
return runOriginalFunction();
}
);
// execute the chained hooks then the original function
data.result = pipe();
}
// Run any post hooks
if (fnHooks.post.size) {
for (const post of fnHooks.post.values()) {
post.call(this, data);
}
}
perf.record(outerTag, start);
return data.result;
}
return (perfIsEnabled && options.usePerf) ? perfHooked : hooked;
}
/**
* Patch a given function or object's property with 'pre', 'post',

@@ -349,2 +442,5 @@ * and/or 'around' hooks

// hmmm. if we wrap the fn then hooks.get(fn) won't work if it's patched again.
// should we hook the wrapped function?
// if no property, hook a function directly.

@@ -351,0 +447,0 @@ const fn = prop ? hook(obj, prop, options) : hookFunction(obj, options);

@@ -7,2 +7,3 @@ 'use strict';

const mocks = require('@contrast/test/mocks');
const Perf = require('@contrast/perf');

@@ -32,2 +33,3 @@ const sym = Symbol('foo');

core.logger = mocks.logger();
core.Perf = Perf;
patcher = require('.')(core);

@@ -34,0 +36,0 @@ });

{
"name": "@contrast/patcher",
"version": "1.9.0",
"version": "1.10.0",
"description": "Advanced monkey patching--registers hooks to run in and around functions",

@@ -20,4 +20,4 @@ "license": "SEE LICENSE IN LICENSE",

"dependencies": {
"@contrast/logger": "1.10.0"
"@contrast/logger": "1.11.0"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc