fixed overly strict regression in parameter binding

This commit is contained in:
Joshua Wise
2026-07-21 01:48:16 -05:00
parent 0aacfb2972
commit 67da3d312e
2 changed files with 50 additions and 6 deletions
+10 -6
View File
@@ -41,16 +41,20 @@ private:
return Napi::Value(env, proto);
}
// An object is "plain" if its prototype is null or Object.prototype. Unlike
// the original V8 implementation, we cannot look up Object.prototype within
// the object's creation context (Node-API has no such concept), so plain
// objects from other contexts (e.g., the "vm" module) are not recognized.
// An object is "plain" if its prototype is null or a top-level prototype
// (i.e., some realm's Object.prototype). Rather than comparing against the
// current realm's Object.prototype -- which would reject plain objects from
// other contexts, such as the "vm" module -- we walk to the top of the
// prototype chain. A plain object's chain is exactly "obj -> proto -> null",
// so its immediate prototype must itself have a null prototype. This mirrors
// the cross-realm heuristic used by lodash's isPlainObject.
static bool IsPlainObject(Napi::Env env, Napi::Object obj) {
Napi::Value proto = GetPrototype(env, obj);
if (proto.IsEmpty()) return false;
if (proto.IsNull()) return true;
Napi::Value baseProto = GetPrototype(env, Napi::Object::New(env));
return !baseProto.IsEmpty() && proto.StrictEquals(baseProto);
Napi::Value grandproto = GetPrototype(env, proto.As<Napi::Object>());
if (grandproto.IsEmpty()) return false;
return grandproto.IsNull();
}
void Fail(Napi::Value (*Throw)(Napi::Env, const char*), Napi::Env env, const char* message) {
+40
View File
@@ -81,6 +81,46 @@ describe('Statement#bind()', function () {
stmt = this.db.prepare('INSERT INTO entries VALUES (@a, @a, ?)');
stmt.bind({ a: '123', b: null }, null);
});
it('should not treat class instances as plain objects', function () {
const stmt = this.db.prepare('INSERT INTO entries VALUES (@a, @a, ?)');
expect(() => stmt.bind(new (class { get a() { return "123"; } })(), null)).to.throw(TypeError);
expect(() => stmt.bind(Object.assign(new Date(), { a: 123 }), null)).to.throw(TypeError);
expect(() => stmt.bind(Object.assign(new Number(123), { a: 123 }), null)).to.throw(TypeError);
expect(() => stmt.bind(Object.assign(() => {}, { a: 123 }), null)).to.throw(TypeError);
});
it('should not treat class instances from other realms as plain objects', function () {
const vm = require('vm');
const context = vm.createContext({});
// A class instance from another realm is not a plain object, so passing it
// where named parameters are expected must throw.
const foreignInstance = vm.runInContext('(new (class { get a() { return "123"; } })())', context);
const stmt = this.db.prepare('INSERT INTO entries VALUES (@a, @a, ?)');
expect(() => stmt.bind(foreignInstance, null)).to.throw(TypeError);
});
it('should recognize plain objects created in other realms', function () {
const vm = require('vm');
const context = vm.createContext({});
// A plain object created in another realm has a different Object.prototype
// identity, but should still be recognized as a plain object.
const foreignPlainObject = vm.runInContext('({ a: "123", b: null })', context);
expect(Object.getPrototypeOf(foreignPlainObject)).to.not.equal(Object.prototype);
const stmt = this.db.prepare('INSERT INTO entries VALUES (@a, @a, ?)');
stmt.bind(foreignPlainObject, null);
stmt.run();
const row = this.db.prepare('SELECT * FROM entries WHERE rowid=1').get();
expect(row.a).to.equal('123');
// An object created via Object.create(null) in another realm is plain too.
const foreignNullProtoObject = vm.runInContext('Object.assign(Object.create(null), { a: "456", b: null })', context);
const stmt2 = this.db.prepare('INSERT INTO entries VALUES (@a, @a, ?)');
stmt2.bind(foreignNullProtoObject, null);
stmt2.run();
const row2 = this.db.prepare('SELECT * FROM entries WHERE rowid=2').get();
expect(row2.a).to.equal('456');
});
it('should propagate exceptions thrown while accessing array/object members', function () {
const arr = [22];
const obj = {};