Skip to content

Commit

Permalink
add safe classes
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Jul 6, 2023
1 parent 4f742ff commit 4fdc61a
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 0 deletions.
116 changes: 116 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,16 @@ export {
WeakSetPrototypeHas,
WeakSetPrototypeSymbolToStringTag,

//
makeSafe,
SafeMap,
SafeWeakMap,
SafeSet,
SafeWeakSet,
SafeFinalizationRegistry,
SafeWeakRef,
SafePromise,

//////
// bonus: node core doesn't need to harden these, since it has internal
// references to them, but it's very handy when dealing with scenarios
Expand Down Expand Up @@ -2335,6 +2345,103 @@ const processHasUncaughtExceptionCaptureCallback = staticCall(
const processEmitWarning = bind.bind(ogProcess.emitWarning, ogProcess)
const processDebugPort = Number(ogProcess.debugPort)


const createSafeIterator = <T, TReturn, TNext>(factory: (self: T) => IterableIterator<T>, next: (...args: [] | [TNext]) => IteratorResult<T, TReturn>) => {
class SafeIterator implements IterableIterator<T> {
_iterator: any;
constructor(iterable: T) {
this._iterator = factory(iterable);
}
next() {
return next(this._iterator);
}
[Symbol.iterator]() {
return this;
}
[SymbolIterator]() {
return this;
}
}
ObjectSetPrototypeOf(SafeIterator.prototype, null);
ObjectFreeze(SafeIterator.prototype);
ObjectFreeze(SafeIterator);
return SafeIterator;
};

const copyProps = (src: object, dest: object) => {
ArrayPrototypeForEach(ReflectOwnKeys(src), (key) => {
if (!ReflectGetOwnPropertyDescriptor(dest, key)) {
ReflectDefineProperty(
dest,
key,
{ __proto__: null, ...ReflectGetOwnPropertyDescriptor(src, key) } as PropertyDescriptor);
}
});
};

interface Constractable<T> extends NewableFunction { new(...args: any[]): T; };
const makeSafe = <T, C extends Constractable<any>>(unsafe: C, safe: C) => {
if (SymbolIterator in unsafe.prototype) {
const dummy = new unsafe();
let next; // We can reuse the same `next` method.

ArrayPrototypeForEach(ReflectOwnKeys(unsafe.prototype), (key) => {
if (!ReflectGetOwnPropertyDescriptor(safe.prototype, key)) {
const desc = ReflectGetOwnPropertyDescriptor(unsafe.prototype, key);
if (desc &&
typeof desc.value === 'function' &&
desc.value.length === 0 &&
SymbolIterator in (FunctionPrototypeCall(desc.value, dummy) ?? {})
) {
const x: string[] = [];
const y = x[Symbol.iterator];

const createIterator = uncurryThis(desc.value) as unknown as (val: T) => IterableIterator<any>;
next ??= uncurryThis(createIterator(dummy).next);
const SafeIterator = createSafeIterator(createIterator, next);
desc.value = function() {
return new SafeIterator(this as unknown as T);
};
}
ReflectDefineProperty(safe.prototype, key, { __proto__: null, ...desc } as PropertyDescriptor);
}
});
} else {
copyProps(unsafe.prototype, safe.prototype);
}
copyProps(unsafe, safe);

ObjectSetPrototypeOf(safe.prototype, null);
ObjectFreeze(safe.prototype);
ObjectFreeze(safe);
return safe;
};
const SafeMap = makeSafe(Map, class SafeMap<K, V> extends Map<K, V> {
constructor(entries?: readonly (readonly [K, V])[] | null) { super(entries); }
});
const SafeWeakMap = makeSafe(WeakMap, class SafeWeakMap<K extends object, V> extends WeakMap<K, V> {
constructor(entries?: readonly ([K, V])[] | null) { super(entries); }
});
const SafeSet = makeSafe(Set, class SafeSet<T = any> extends Set<T> {
constructor(values?: readonly T[] | null) { super(values); }
});
const SafeWeakSet = makeSafe(WeakSet, class SafeWeakSet<T extends object> extends WeakSet<T> {
constructor(values?: readonly T[] | null) { super(values); }
});

const SafeFinalizationRegistry = makeSafe(FinalizationRegistry,
class SafeFinalizationRegistry<T> extends FinalizationRegistry<T> {
constructor(cleanupCallback: (heldValue: T) => void) { super(cleanupCallback); }
});
const SafeWeakRef = makeSafe(WeakRef, class SafeWeakRef<T extends object> extends WeakRef<T> {
constructor(target: T) { super(target); }
});

const SafePromise = makeSafe(Promise, class SafePromise<T> extends Promise<T> {
constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) { super(executor); }
});


const PRIMORDIALS = OBJECT.defineProperties(
OBJECT.assign(OBJECT.create(null) as {}, {
// utilities
Expand Down Expand Up @@ -3108,6 +3215,15 @@ const PRIMORDIALS = OBJECT.defineProperties(
WeakSetPrototypeHas,
WeakSetPrototypeSymbolToStringTag,

makeSafe,
SafeMap,
SafeWeakMap,
SafeSet,
SafeWeakSet,
SafeFinalizationRegistry,
SafeWeakRef,
SafePromise,

//////
// bonus: node core doesn't need to harden these, since it has internal
// references to them, but it's very handy when dealing with scenarios
Expand Down
8 changes: 8 additions & 0 deletions tap-snapshots/test/index.ts.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ Object {
"JSONParse": Function (...args),
"JSONStringify": Function (...args),
"JSONSymbolToStringTag": "JSON",
"makeSafe": Function makeSafe(unsafe, safe),
"Map": Function Map(),
"MapGetSymbolSpecies": Function bound call(),
"MapLength": 0,
Expand Down Expand Up @@ -559,6 +560,13 @@ Object {
"RegExpSetLastParen": Function bound call(),
"RegExpSetLeftContext": Function bound call(),
"RegExpSetRightContext": Function bound call(),
"SafeFinalizationRegistry": Function SafeFinalizationRegistry(classSafeFinalizationRegistryextendsFinalizationRegistry),
"SafeMap": Function SafeMap(classSafeMapextendsMap),
"SafePromise": Function SafePromise(classSafePromiseextendsPromise),
"SafeSet": Function SafeSet(classSafeSetextendsSet),
"SafeWeakMap": Function SafeWeakMap(classSafeWeakMapextendsWeakMap),
"SafeWeakRef": Function SafeWeakRef(classSafeWeakRefextendsWeakRef),
"SafeWeakSet": Function SafeWeakSet(classSafeWeakSetextendsWeakSet),
"Set": Function Set(),
"SetGetSymbolSpecies": Function bound call(),
"setImmediate": Function (...args),
Expand Down
22 changes: 22 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,25 @@ t.test('maybeStaticCall', t => {
t.equal(fn, undefined)
t.end()
})

t.test('makeSafe', t => {
const originals = [[Object, undefined], [Map, Map.prototype.has], [Set, Set.prototype.has]] as const;
const tamperRunRestore = <T extends ( ...args: any[]) => any>(fn: T) => {
originals.forEach(([obj]) => (obj.prototype as any).has = () => { throw new Error('should not be called') });
const value = fn();
originals.forEach(([obj, original]) => (obj.prototype as any).has = original);
return value;
};
const map = new primordials.SafeMap();
const set = new primordials.SafeSet();

t.equal(tamperRunRestore(() => map.has('foo')), false)
t.equal(tamperRunRestore(() => set.has('foo')), false)
map.set('foo', 'bar')
set.add('foo')
t.equal(tamperRunRestore(() => map.has('foo')), true)
t.equal(tamperRunRestore(() => set.has('foo')), true)


t.end();
});

0 comments on commit 4fdc61a

Please sign in to comment.