Using Node's makeSafe on Array

In Node.js's primordials.js file (the one I based this question's code off of is found here) there is a function makeSafe which is passed a builtin constructor and produces a class derived from it which is protected against monkeypatching. SafeSet is based on Set, SafeMap is based on Map, and so forth.

makeSafe is insufficient when the class in question has methods which return iterators whose prototypes are anonymous. For this, the createSafeIterator is used to create a frozen class that produces iterator objects that use the original prototypes' next functions.

The original primordials.js doesn't have a SafeArray, so I chose to make one with it. The trouble is that SafeArray's returned iterators throw an error claiming the inner next is not a function even though typeof reports that it is.

Here is the code:

// Derived from https://raw.githubusercontent.com/nodejs/node/54e022315d7e037c429afbda73cf8bb8ff96d667/lib/internal/per_context/primordials.js

"use strict";

const { apply, bind, call } = Function.prototype;
const uncurryThis = bind.bind(call);

const applyBind = bind.bind(apply);

const createSafeIterator = (factory, next) => {
  class SafeIterator {
    constructor(iterable) {
      this._iterator = factory(iterable);
    }
    next() {
      console.log("SafeIterator#next: Inner next's type is " + typeof next);
      return next(this._iterator);
    }
    [Symbol.iterator]() {
      return this;
    }
  }
  Object.setPrototypeOf(SafeIterator.prototype, null);
  Object.freeze(SafeIterator.prototype);
  Object.freeze(SafeIterator);
  return SafeIterator;
};

const copyProps = (src, dest) => {
  Reflect.ownKeys(src).forEach((key) => {
    if (!Reflect.getOwnPropertyDescriptor(dest, key)) {
      Reflect.defineProperty(
        dest,
        key,
        Reflect.getOwnPropertyDescriptor(src, key));
    }
  });
};

const makeSafe = (unsafe, safe) => {
  if (Symbol.iterator in unsafe.prototype) {
    const dummy = new unsafe();
    let next;

    Reflect.ownKeys(unsafe.prototype).forEach((key) => {
      if (!Reflect.getOwnPropertyDescriptor(safe.prototype, key)) {
        const desc = Reflect.getOwnPropertyDescriptor(unsafe.prototype, key);
        if (
          typeof desc.value === 'function' &&
          desc.value.length === 0 &&
          // Original: Symbol.iterator in (desc.value.call(dummy) ?? {})
          Symbol.iterator in (typeof desc.value.call(dummy) === "object" ? desc.value.call(dummy) : {}) // Rewritten to handle non-object results such as from Array#join
        ) {
          const createIterator = uncurryThis(desc.value);
          next ??= uncurryThis(createIterator(dummy).next);
          const SafeIterator = createSafeIterator(createIterator, next);
          desc.value = function() {
            return new SafeIterator(this);
          };
        }
        Reflect.defineProperty(safe.prototype, key, desc);
      }
    });
  } else {
    copyProps(unsafe.prototype, safe.prototype);
  }
  copyProps(unsafe, safe);

  Object.setPrototypeOf(safe.prototype, null);
  Object.freeze(safe.prototype);
  Object.freeze(safe);
  return safe;
};

var SafeArray = makeSafe(Array, class SafeArray extends Array {
  constructor (i) {
    super(i);
  }
});

var SafeSet = makeSafe(Set, class SafeSet extends Set {
  constructor (i) {
    super(i);
  }
});

var sa = new SafeArray(1, 2, 3);
var ss = new SafeSet([1, 2, 3]);

console.log([...ss]); // Prints the typeof report three times and then "[1, 2, 3]" (without quotes and separated with newlines)
console.log([...sa]); // Prints typeof report and then throws "TypeError: next is not a function"

Why does it say that the inner next function is not function in the case of SafeArray?

1 answer

  • answered 2021-11-29 16:26 Randy Casburn

    There are two problems

    1. The first problem occurs when reflecting each method in the Array prototype in order to test their returned structure.

    The Array prototype methods have many more variations of return types than you have accounted for in your complex if() statement. For instance, .forEach() returns undefined while .find() may return any number of datatypes that are valid as an Array element.

    2. The second problem relates to using the typeof operator in an attempt to determine if the return type from the called method of the Array prototype. This test indicates that you want to operate on only those Array prototype methods that return a data type that has a Symbol.iterator.

    The problem: typeof returns "object" for Array objects and is a naive test in this code.

    To demonstrate, the following code snippet reduces your code to a Minimal Reproducible example. I have extracted the complex test into a separate function to demonstrate the issues above. The errors are caught and logged for your reference.

    It is important to recognize that not a single Array prototype method was processed using your tests.

    let unsafe = Array;
    let safe = {};
    safe.prototype = {};
    if (Symbol.iterator in unsafe.prototype) {
      const dummy = new unsafe();
      let next;
    
      Reflect.ownKeys(unsafe.prototype).forEach((key) => {
        if (!Reflect.getOwnPropertyDescriptor(safe.prototype, key)) {
          const desc = Reflect.getOwnPropertyDescriptor(unsafe.prototype, key);
          console.log(`Testing ${desc.value.name}`)
          if (complexTest(desc.value, dummy))
          {
            console.log(`Processing ${desc.value.name}`);
            console.log(' ');
          } else {
            console.log(`Not processed`);
            console.log(' ');
          }
        }
      });
    }
    function complexTest(toTest, dummy){
      if(!typeof toTest === 'function') return false;
      if(toTest.length !== 0) return false;
      let type;
      try {
        type = typeof toTest.call(dummy);
        Symbol.iterator in typeof toTest.call(dummy);
      } catch (err) {
        console.log(`Error with ${type} ||`, err.message);
        return false;
      }
      return true;
    }
    console.log('done');

    Solution

    The simple fix, as discussed in the comments to your question is to change replace the use of typeof with the use of the constructor property. This simple change will ensure that you only check for Symbol.iterator on the return from those methods that actually return an Array object.

    Here is the same code as above with that single change. Please note the remarkable difference in the results:

    let unsafe = Array;
    let safe = {};
    safe.prototype = {};
    if (Symbol.iterator in unsafe.prototype) {
      const dummy = new unsafe();
      let next;
    
      Reflect.ownKeys(unsafe.prototype).forEach((key) => {
        if (!Reflect.getOwnPropertyDescriptor(safe.prototype, key)) {
          const desc = Reflect.getOwnPropertyDescriptor(unsafe.prototype, key);
          console.log(`Testing ${desc.value.name}`)
          if (complexTest(desc.value, dummy))
          {
            console.log(`Processing ${desc.value.name}`);
            console.log(' ');
          } else {
            console.log(`Not processed`);
            console.log(' ');
          }
        }
      });
    }
    function complexTest(toTest, dummy){
      if(!typeof toTest === 'function') return false;
      if(toTest.length !== 0) return false;
      let type;
      try {
       type = typeof toTest.call(dummy);
        Symbol.iterator in toTest.call(dummy).constructor;
      } catch (err) {
        console.log(`Error with ${type} ||`, err.message);
        return false;
      }
      return true;
    }
    console.log('done');

    You may have to adjust your tests to cover more "unsafe" methods. I'll leave that to you.

How many English words
do you know?
Test your English vocabulary size, and measure
how many words do you know
Online Test
Powered by Examplum