UNPKG

@fast-check/poisoning

Version:

Set of utilities to ease detection and revert of poisoning

311 lines (310 loc) 12.6 kB
//#region src/internals/PoisoningFreeArray.ts const safeArrayFrom = Array.from; const safeArrayMap = Array.prototype.map; const safeArrayPush = Array.prototype.push; const safeArrayShift = Array.prototype.shift; const safeArraySort = Array.prototype.sort; const safeObjectDefineProperty$3 = Object.defineProperty; /** Alias for Array.prototype.map */ const MapSymbol = Symbol("safe.map"); /** Alias for Array.prototype.push */ const PushSymbol = Symbol("safe.push"); /** Alias for Array.prototype.shift */ const ShiftSymbol = Symbol("safe.shift"); /** Alias for Array.prototype.sort */ const SortSymbol = Symbol("safe.sort"); /** Alter an instance of Array to include non-poisonable methods */ function toPoisoningFreeArray(instance) { safeObjectDefineProperty$3(instance, MapSymbol, { value: safeArrayMap, configurable: false, enumerable: false, writable: false }); safeObjectDefineProperty$3(instance, PushSymbol, { value: safeArrayPush, configurable: false, enumerable: false, writable: false }); safeObjectDefineProperty$3(instance, ShiftSymbol, { value: safeArrayShift, configurable: false, enumerable: false, writable: false }); safeObjectDefineProperty$3(instance, SortSymbol, { value: safeArraySort, configurable: false, enumerable: false, writable: false }); return instance; } /** Factory responsible to build instances of PoisoningFreeArray */ const PoisoningFreeArray = { from(arrayLike) { return toPoisoningFreeArray(safeArrayFrom(arrayLike)); } }; //#endregion //#region src/internals/PoisoningFreeMap.ts const SMap = Map; const safeMapGet = Map.prototype.get; const safeMapHas = Map.prototype.has; const safeMapEntries = Map.prototype.entries; const safeMapSet = Map.prototype.set; const safeObjectDefineProperty$2 = Object.defineProperty; /** Alias for Map.prototype.get */ const GetSymbol = Symbol("safe.get"); /** Alias for Map.prototype.has */ const HasSymbol$1 = Symbol("safe.has"); /** Alias for Map.prototype.entries */ const EntriesSymbol = Symbol("safe.entries"); /** Alias for Map.prototype.set */ const SetSymbol = Symbol("safe.set"); /** Alter an instance of Map to include non-poisonable methods */ function toPoisoningFreeMap(instance) { safeObjectDefineProperty$2(instance, GetSymbol, { value: safeMapGet, configurable: false, enumerable: false, writable: false }); safeObjectDefineProperty$2(instance, HasSymbol$1, { value: safeMapHas, configurable: false, enumerable: false, writable: false }); safeObjectDefineProperty$2(instance, EntriesSymbol, { value: safeMapEntries, configurable: false, enumerable: false, writable: false }); safeObjectDefineProperty$2(instance, SetSymbol, { value: safeMapSet, configurable: false, enumerable: false, writable: false }); return instance; } /** Factory responsible to build instances of PoisoningFreeMap */ const PoisoningFreeMap = { from(ins) { return toPoisoningFreeMap(new SMap(ins)); } }; //#endregion //#region src/internals/PoisoningFreeSet.ts const SSet = Set; const safeSetAdd = Set.prototype.add; const safeSetHas = Set.prototype.has; const safeObjectDefineProperty$1 = Object.defineProperty; /** Alias for Set.prototype.add */ const AddSymbol = Symbol("safe.add"); /** Alias for Set.prototype.has */ const HasSymbol = Symbol("safe.has"); /** Alter an instance of Set to include non-poisonable methods */ function toPoisoningFreeSet(instance) { safeObjectDefineProperty$1(instance, AddSymbol, { value: safeSetAdd, configurable: false, enumerable: false, writable: false }); safeObjectDefineProperty$1(instance, HasSymbol, { value: safeSetHas, configurable: false, enumerable: false, writable: false }); return instance; } /** Factory responsible to build instances of PoisoningFreeMap */ const PoisoningFreeSet = { from(ins) { return toPoisoningFreeSet(new SSet(ins)); } }; //#endregion //#region src/internals/CaptureAllGlobals.ts const SString$1 = String; const safeObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors; const safeObjectGetOwnPropertyNames$1 = Object.getOwnPropertyNames; const safeObjectGetOwnPropertySymbols$1 = Object.getOwnPropertySymbols; function compareKeys(keyA, keyB) { const sA = SString$1(keyA[0]); const sB = SString$1(keyB[0]); return sA < sB ? -1 : sA > sB ? 1 : 0; } function extractAllDescriptorsDetails(instance) { const descriptors = safeObjectGetOwnPropertyDescriptors(instance); const allDescriptors = PoisoningFreeArray.from([...safeObjectGetOwnPropertyNames$1(descriptors), ...safeObjectGetOwnPropertySymbols$1(descriptors)]); return PoisoningFreeArray.from(allDescriptors[MapSymbol]((name) => [name, descriptors[name]]))[SortSymbol](compareKeys); } /** Flag the roots on already existing globals */ function flagRootRecursively(knownGlobals, instance, currentRoot) { const storedGlobal = knownGlobals[GetSymbol](instance); if (storedGlobal === void 0) return; if (storedGlobal.rootAncestors[HasSymbol](currentRoot)) return; storedGlobal.rootAncestors[AddSymbol](currentRoot); if (storedGlobal.depth <= 1) return; for (const [, descriptor] of storedGlobal.properties) flagRootRecursively(knownGlobals, descriptor.value, currentRoot); } /** Capture all globals accessible from globalThis */ function captureAllGlobals() { const knownGlobals = PoisoningFreeMap.from(); const nextCaptures = PoisoningFreeArray.from([{ instance: globalThis, name: "globalThis", currentDepth: 0, lastRootInPath: "globalThis" }]); while (nextCaptures.length !== 0) { const { instance, name, currentDepth, lastRootInPath } = nextCaptures[ShiftSymbol](); if (typeof instance !== "function" && typeof instance !== "object") continue; if (instance === null || instance === void 0) continue; if (knownGlobals[HasSymbol$1](instance)) { flagRootRecursively(knownGlobals, instance, lastRootInPath); continue; } const allDescriptorsDetails = extractAllDescriptorsDetails(instance); const localGlobal = { name, depth: currentDepth, properties: PoisoningFreeMap.from(), rootAncestors: PoisoningFreeSet.from([lastRootInPath]) }; knownGlobals[SetSymbol](instance, localGlobal); for (let index = 0; index !== allDescriptorsDetails.length; ++index) { const descriptorName = allDescriptorsDetails[index][0]; const descriptor = allDescriptorsDetails[index][1]; localGlobal.properties[SetSymbol](descriptorName, descriptor); if (typeof descriptorName === "symbol") continue; const subGlobalName = currentDepth !== 0 ? name + "." + SString$1(descriptorName) : SString$1(descriptorName); const newLastRootInPath = currentDepth <= 1 ? name : lastRootInPath; nextCaptures[PushSymbol]({ instance: descriptor.value, name: subGlobalName, currentDepth: currentDepth + 1, lastRootInPath: newLastRootInPath }); } } return knownGlobals; } //#endregion //#region src/internals/FilterNonEligibleDiffs.ts /** Check whether or not a global has to be ignored for diff tracking */ function shouldIgnoreGlobal(globalDetails, ignoredRootRegex) { switch (globalDetails.depth) { case 0: return false; case 1: return ignoredRootRegex.test(globalDetails.name); default: { let allRootsIgnored = true; const allRoots = [...globalDetails.rootAncestors]; for (let rootIndex = 0; rootIndex !== allRoots.length; ++rootIndex) allRootsIgnored = allRootsIgnored && ignoredRootRegex.test(allRoots[rootIndex]); return allRootsIgnored; } } } /** Check whether or not a property from a global has to be ignored for diff tracking */ function shouldIgnoreProperty(globalDetails, propertyName, ignoredRootRegex) { return globalDetails.depth === 0 && ignoredRootRegex.test(propertyName); } //#endregion //#region src/internals/TrackDiffsOnGlobal.ts const SString = String; const safeObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const safeObjectGetOwnPropertyNames = Object.getOwnPropertyNames; const safeObjectGetOwnPropertySymbols = Object.getOwnPropertySymbols; const safeObjectIs = Object.is; const safeObjectDefineProperty = Object.defineProperty; /** Compute the diff between two versions of globals */ function trackDiffsOnGlobals(initialGlobals, isEligibleGlobal, isEligibleProperty) { const allInitialGlobals = [...initialGlobals[EntriesSymbol]()]; const observedDiffs = PoisoningFreeArray.from([]); for (let index = 0; index !== allInitialGlobals.length; ++index) { const instance = allInitialGlobals[index][0]; const globalDetails = allInitialGlobals[index][1]; if (!isEligibleGlobal(globalDetails)) continue; const name = globalDetails.name; const initialProperties = globalDetails.properties; const initialPropertiesList = [...initialProperties[EntriesSymbol]()]; for (let propertyIndex = 0; propertyIndex !== initialPropertiesList.length; ++propertyIndex) { const propertyName = initialPropertiesList[propertyIndex][0]; const initialPropertyDescriptor = initialPropertiesList[propertyIndex][1]; if (!isEligibleProperty(globalDetails, SString(propertyName))) continue; const currentDescriptor = safeObjectGetOwnPropertyDescriptor(instance, propertyName); if (currentDescriptor === void 0) observedDiffs[PushSymbol]({ keyName: SString(propertyName), fullyQualifiedKeyName: name + "." + SString(propertyName), type: "removed", patch: () => { safeObjectDefineProperty(instance, propertyName, initialPropertyDescriptor); }, globalDetails }); else if (!safeObjectIs(initialPropertyDescriptor.value, currentDescriptor.value) || !safeObjectIs(initialPropertyDescriptor.get, currentDescriptor.get) || !safeObjectIs(initialPropertyDescriptor.set, currentDescriptor.set)) observedDiffs[PushSymbol]({ keyName: SString(propertyName), fullyQualifiedKeyName: name + "." + SString(propertyName), type: "changed", patch: () => { safeObjectDefineProperty(instance, propertyName, initialPropertyDescriptor); }, globalDetails }); } const currentDescriptorsList = [...safeObjectGetOwnPropertyNames(instance), ...safeObjectGetOwnPropertySymbols(instance)]; for (let descriptorIndex = 0; descriptorIndex !== currentDescriptorsList.length; ++descriptorIndex) { const propertyName = currentDescriptorsList[descriptorIndex]; if (!isEligibleProperty(globalDetails, SString(propertyName))) continue; if (!initialProperties[HasSymbol$1](propertyName)) observedDiffs[PushSymbol]({ keyName: SString(propertyName), fullyQualifiedKeyName: name + "." + SString(propertyName), type: "added", patch: () => { delete instance[propertyName]; }, globalDetails }); } } return [...observedDiffs]; } //#endregion //#region src/main.ts const initialGlobals = captureAllGlobals(); /** Internal helper to share the extraction logic */ function trackDiffsOnGlobalsBasedOnOptions(options) { const ignoredRootRegex = options !== void 0 && options.ignoredRootRegex !== void 0 ? options.ignoredRootRegex : void 0; return ignoredRootRegex !== void 0 ? trackDiffsOnGlobals(initialGlobals, (globalDetails) => !shouldIgnoreGlobal(globalDetails, ignoredRootRegex), (globalDetails, propertyName) => !shouldIgnoreProperty(globalDetails, propertyName, ignoredRootRegex)) : trackDiffsOnGlobals(initialGlobals, () => true, () => true); } /** * Restore all globals as they were when first importing this package. * * Remark: At least, it attempts to do so */ function restoreGlobals(options) { const diffs = trackDiffsOnGlobalsBasedOnOptions(options); for (let index = 0; index !== diffs.length; ++index) diffs[index].patch(); } /** * Check whether or not some globlas have been poisoned by some code. * * Poisoned being one of the following changes: * - a new entity is accessible directly or indirectly from `globalThis` * - an entity referenced directly or indirectly on `globalThis` has been altered * - an entity referenced directly or indirectly on `globalThis` has been dropped * * Here are some examples of such changes: * - someone added a new global on `window` (browser case) or `global` (node case) or modern `globalThis` (everywhere) * - someone changed `Array.prototype.map` into another function */ function assertNoPoisoning(options) { const diffs = trackDiffsOnGlobalsBasedOnOptions(options); if (diffs.length !== 0) { let impactedElements = diffs[0].fullyQualifiedKeyName; for (let index = 1; index !== diffs.length; ++index) impactedElements += ", " + diffs[index].fullyQualifiedKeyName; throw new Error("Poisoning detected on " + impactedElements); } } //#endregion export { assertNoPoisoning, restoreGlobals };