diff --git a/PORTING.md b/PORTING.md index 4607b87..ed4f21d 100644 --- a/PORTING.md +++ b/PORTING.md @@ -63,7 +63,7 @@ Tests covering the engine-specific part of Node-API, defined in `js_native_api.h | `test_instance_data` | Not ported | Medium | | `test_new_target` | Ported ✅ | Easy | | `test_number` | Ported ✅ | Easy | -| `test_object` | Not ported | Hard | +| `test_object` | Partial | Hard | | `test_promise` | Ported ✅ | Easy | | `test_properties` | Ported ✅ | Easy | | `test_reference` | Ported ✅ | Medium | diff --git a/eslint.config.js b/eslint.config.js index ef33613..02f283a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -98,6 +98,7 @@ export default defineConfig([ assert: 'readonly', loadAddon: 'readonly', mustCall: 'readonly', + mustCallAtLeast: 'readonly', mustNotCall: 'readonly', gc: 'readonly', gcUntil: 'readonly', diff --git a/implementors/node/features.js b/implementors/node/features.js index fec2137..d915eb0 100644 --- a/implementors/node/features.js +++ b/implementors/node/features.js @@ -31,4 +31,10 @@ globalThis.runtimeFeatures = { major > 25 || (major === 25 && minor >= 4) || (major === 24 && (minor > 13 || (minor === 13 && patch >= 1))), + + // Object APIs report a throwing proxy handler as napi_pending_exception only + // since Node.js v22.0.0 (nodejs/node@52fcf14258b). It was not backported to + // v20.x, where the exception is left pending but the call reports another + // status, so the throw escapes the addon instead. + proxyHandlerExceptions: major >= 22, }; diff --git a/implementors/node/must-call.js b/implementors/node/must-call.js index 2792ad3..09b6d78 100644 --- a/implementors/node/must-call.js +++ b/implementors/node/must-call.js @@ -1,17 +1,10 @@ const pendingCalls = []; -/** - * Wraps a function and asserts it is called exactly `exact` times before the - * process exits. If `fn` is omitted, a no-op function is used. - * - * Usage: - * promise.then(mustCall((result) => { - * assert.strictEqual(result, 42); - * })); - */ -const mustCall = (fn, exact = 1) => { +// `expected` is a lower bound when `atLeast` is set, an exact count otherwise. +const track = (fn, expected, atLeast) => { const entry = { - exact, + expected, + atLeast, actual: 0, name: fn?.name || '', error: new Error(), // capture call-site stack @@ -23,6 +16,25 @@ const mustCall = (fn, exact = 1) => { }; }; +/** + * Wraps a function and asserts it is called exactly `exact` times before the + * process exits. If `fn` is omitted, a no-op function is used. + * + * Usage: + * promise.then(mustCall((result) => { + * assert.strictEqual(result, 42); + * })); + */ +const mustCall = (fn, exact = 1) => track(fn, exact, false); + +/** + * Like `mustCall`, but asserts only a lower bound: the wrapper must be called + * at least `minimum` times, and any number of further calls is fine. Use it + * when the runtime decides how often a callback fires (e.g. a proxy trap the + * engine may consult more than once). + */ +const mustCallAtLeast = (fn, minimum = 1) => track(fn, minimum, true); + /** * Returns a function that throws immediately if called. */ @@ -34,13 +46,17 @@ const mustNotCall = (msg) => { process.on('exit', () => { for (const entry of pendingCalls) { - if (entry.actual !== entry.exact) { + const satisfied = entry.atLeast ? + entry.actual >= entry.expected : + entry.actual === entry.expected; + if (!satisfied) { entry.error.message = - `mustCall "${entry.name}" expected ${entry.exact} call(s) ` + + `mustCall${entry.atLeast ? 'AtLeast' : ''} "${entry.name}" expected ` + + `${entry.atLeast ? 'at least ' : ''}${entry.expected} call(s) ` + `but got ${entry.actual}`; throw entry.error; } } }); -Object.assign(globalThis, { mustCall, mustNotCall }); +Object.assign(globalThis, { mustCall, mustCallAtLeast, mustNotCall }); diff --git a/tests/harness/must-call-at-least-child.mjs b/tests/harness/must-call-at-least-child.mjs new file mode 100644 index 0000000..672a656 --- /dev/null +++ b/tests/harness/must-call-at-least-child.mjs @@ -0,0 +1,4 @@ +// Spawned by must-call.js. Calls a wrapper that demands at least two calls +// only once, so the parent can assert that the shortfall is reported at exit. +const wrapper = mustCallAtLeast(function underCalled() {}, 2); +wrapper(); diff --git a/tests/harness/must-call.js b/tests/harness/must-call.js index 1445bed..6f7925c 100644 --- a/tests/harness/must-call.js +++ b/tests/harness/must-call.js @@ -26,6 +26,35 @@ if (typeof mustCall !== 'function') { assert.strictEqual(result, undefined); } +// mustCallAtLeast is a function +if (typeof mustCallAtLeast !== 'function') { + throw new Error('Expected a global mustCallAtLeast function'); +} + +// mustCallAtLeast forwards arguments and return value, and tolerates more +// calls than the minimum +{ + const wrapper = mustCallAtLeast((a, b) => a + b, 2); + assert.strictEqual(wrapper(2, 3), 5); + assert.strictEqual(wrapper(4, 5), 9); + assert.strictEqual(wrapper(6, 7), 13); +} + +// mustCallAtLeast defaults its minimum to one call +{ + const wrapper = mustCallAtLeast(); + const result = wrapper('ignored'); + assert.strictEqual(result, undefined); +} + +// Falling short of the minimum fails. The count is only checked at process +// exit, so observing the failure needs a child process. +if (runtimeFeatures.spawn) { + const result = await spawnTest('must-call-at-least-child.mjs'); + assert.notStrictEqual(result.status, 0, 'an under-called mustCallAtLeast should fail the child'); + assert.match(result.stderr, /underCalled.*at least 2 call\(s\) but got 1/); +} + // mustNotCall is a function if (typeof mustNotCall !== 'function') { throw new Error('Expected a global mustNotCall function'); diff --git a/tests/js-native-api/test_object/CMakeLists.txt b/tests/js-native-api/test_object/CMakeLists.txt new file mode 100644 index 0000000..84913ca --- /dev/null +++ b/tests/js-native-api/test_object/CMakeLists.txt @@ -0,0 +1,2 @@ +add_node_api_cts_addon(test_object test_object.c test_null.c) +add_node_api_cts_addon(test_exceptions test_exceptions.c) diff --git a/tests/js-native-api/test_object/test.js b/tests/js-native-api/test_object/test.js new file mode 100644 index 0000000..5ae3627 --- /dev/null +++ b/tests/js-native-api/test_object/test.js @@ -0,0 +1,396 @@ +const test_object = loadAddon('test_object'); + +const object = { + hello: 'world', + array: [1, 94, 'str', 12.321, { test: 'obj in arr' }], + newObject: { + test: 'obj in obj', + }, +}; + +assert.strictEqual(test_object.Get(object, 'hello'), 'world'); +assert.strictEqual(test_object.GetNamed(object, 'hello'), 'world'); +assert.deepStrictEqual(test_object.Get(object, 'array'), [ + 1, 94, 'str', 12.321, { test: 'obj in arr' }, +]); +assert.deepStrictEqual(test_object.Get(object, 'newObject'), { + test: 'obj in obj', +}); + +assert.ok(test_object.Has(object, 'hello')); +assert.ok(test_object.HasNamed(object, 'hello')); +assert.ok(test_object.Has(object, 'array')); +assert.ok(test_object.Has(object, 'newObject')); + +const newObject = test_object.New(); +assert.ok(test_object.Has(newObject, 'test_number')); +assert.strictEqual(newObject.test_number, 987654321); +assert.strictEqual(newObject.test_string, 'test string'); + +{ + // napi_get_property walks the prototype chain. + function MyObject() { + this.foo = 42; + this.bar = 43; + } + + MyObject.prototype.bar = 44; + MyObject.prototype.baz = 45; + + const obj = new MyObject(); + + assert.strictEqual(test_object.Get(obj, 'foo'), 42); + assert.strictEqual(test_object.Get(obj, 'bar'), 43); + assert.strictEqual(test_object.Get(obj, 'baz'), 45); + assert.strictEqual( + test_object.Get(obj, 'toString'), + Object.prototype.toString, + ); +} + +{ + // napi_has_own_property fails if the key is not a name. + [true, false, null, undefined, {}, [], 0, 1, () => {}].forEach((value) => { + assert.throws( + () => test_object.HasOwn({}, value), + /^Error: A string or symbol was expected$/, + ); + }); +} + +{ + // napi_has_own_property does not walk the prototype chain. + const symbol1 = Symbol(); + const symbol2 = Symbol(); + + function MyObject() { + this.foo = 42; + this.bar = 43; + this[symbol1] = 44; + } + + MyObject.prototype.bar = 45; + MyObject.prototype.baz = 46; + MyObject.prototype[symbol2] = 47; + + const obj = new MyObject(); + + assert.strictEqual(test_object.HasOwn(obj, 'foo'), true); + assert.strictEqual(test_object.HasOwn(obj, 'bar'), true); + assert.strictEqual(test_object.HasOwn(obj, symbol1), true); + assert.strictEqual(test_object.HasOwn(obj, 'baz'), false); + assert.strictEqual(test_object.HasOwn(obj, 'toString'), false); + assert.strictEqual(test_object.HasOwn(obj, symbol2), false); +} + +{ + // Inflate increases every own enumerable property by 1. + const cube = { + x: 10, + y: 10, + z: 10, + }; + + assert.deepStrictEqual(test_object.Inflate(cube), { x: 11, y: 11, z: 11 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 12, y: 12, z: 12 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 13, y: 13, z: 13 }); + cube.t = 13; + assert.deepStrictEqual(test_object.Inflate(cube), { + x: 14, y: 14, z: 14, t: 14, + }); + + const sym1 = Symbol('1'); + const sym2 = Symbol('2'); + const sym3 = Symbol('3'); + const sym4 = Symbol('4'); + const object2 = { + [sym1]: '@@iterator', + [sym2]: sym3, + }; + + assert.ok(test_object.Has(object2, sym1)); + assert.ok(test_object.Has(object2, sym2)); + assert.strictEqual(test_object.Get(object2, sym1), '@@iterator'); + assert.strictEqual(test_object.Get(object2, sym2), sym3); + assert.ok(test_object.Set(object2, 'string', 'value')); + assert.ok(test_object.SetNamed(object2, 'named_string', 'value')); + assert.ok(test_object.Set(object2, sym4, 123)); + assert.ok(test_object.Has(object2, 'string')); + assert.ok(test_object.HasNamed(object2, 'named_string')); + assert.ok(test_object.Has(object2, sym4)); + assert.strictEqual(test_object.Get(object2, 'string'), 'value'); + assert.strictEqual(test_object.Get(object2, sym4), 123); +} + +{ + // Wrap a pointer in a JS object, then verify the pointer can be unwrapped. + const wrapper = {}; + test_object.Wrap(wrapper); + + assert.ok(test_object.Unwrap(wrapper)); +} + +{ + // Wrapping does not break an object's prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + assert.ok(test_object.Unwrap(wrapper)); + assert.strictEqual(wrapper.protoA, true); +} + +{ + // The pointer can still be unwrapped after inserting a link in the + // prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + const protoB = { protoB: true }; + Object.setPrototypeOf(protoB, Object.getPrototypeOf(wrapper)); + Object.setPrototypeOf(wrapper, protoB); + + assert.ok(test_object.Unwrap(wrapper)); + assert.strictEqual(wrapper.protoA, true); + assert.strictEqual(wrapper.protoB, true); +} + +{ + // Objects can be type-tagged and type-tag-checked. + const obj1 = test_object.TypeTaggedInstance(0); + const obj2 = test_object.TypeTaggedInstance(1); + const obj3 = test_object.TypeTaggedInstance(2); + const obj4 = test_object.TypeTaggedInstance(3); + const external = test_object.TypeTaggedExternal(2); + const plainExternal = test_object.PlainExternal(); + + // Type tag indices greater than the largest available index are rejected. + assert.throws(() => test_object.TypeTaggedInstance(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + assert.throws(() => test_object.TypeTaggedExternal(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + + // Type tags are correctly accepted. + assert.strictEqual(test_object.CheckTypeTag(0, obj1), true); + assert.strictEqual(test_object.CheckTypeTag(1, obj2), true); + assert.strictEqual(test_object.CheckTypeTag(2, obj3), true); + assert.strictEqual(test_object.CheckTypeTag(3, obj4), true); + assert.strictEqual(test_object.CheckTypeTag(2, external), true); + + // Wrongly tagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, obj2), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj1), false); + assert.strictEqual(test_object.CheckTypeTag(0, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj4), false); + assert.strictEqual(test_object.CheckTypeTag(2, obj4), false); + assert.strictEqual(test_object.CheckTypeTag(3, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(4, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(0, external), false); + assert.strictEqual(test_object.CheckTypeTag(1, external), false); + assert.strictEqual(test_object.CheckTypeTag(3, external), false); + assert.strictEqual(test_object.CheckTypeTag(4, external), false); + + // Untagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, {}), false); + assert.strictEqual(test_object.CheckTypeTag(1, {}), false); + assert.strictEqual(test_object.CheckTypeTag(0, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(1, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(2, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(3, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(4, plainExternal), false); +} + +{ + // Normal and nonexistent properties can be deleted. + const sym = Symbol(); + const obj = { foo: 'bar', [sym]: 'baz' }; + + assert.strictEqual('foo' in obj, true); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, sym), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, false); + assert.strictEqual('does_not_exist' in obj, false); +} + +{ + // Non-configurable properties are not deleted. + const obj = {}; + + Object.defineProperty(obj, 'foo', { configurable: false }); + assert.strictEqual(test_object.Delete(obj, 'foo'), false); + assert.strictEqual('foo' in obj, true); +} + +{ + // Prototype properties are not deleted. + function Foo() { + this.foo = 'bar'; + } + + Foo.prototype.foo = 'baz'; + + const obj = new Foo(); + + assert.strictEqual(obj.foo, 'bar'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); +} + +{ + // napi_get_property_names gets the right set of property names: includes + // prototypes, only enumerable properties, skips symbols, and includes + // indices converted to strings. + const object = { + __proto__: { + inherited: 1, + }, + }; + + const fooSymbol = Symbol('foo'); + + object.normal = 2; + object[fooSymbol] = 3; + Object.defineProperty(object, 'unenumerable', { + value: 4, + enumerable: false, + writable: true, + configurable: true, + }); + Object.defineProperty(object, 'writable', { + value: 4, + enumerable: true, + writable: true, + configurable: false, + }); + Object.defineProperty(object, 'configurable', { + value: 4, + enumerable: true, + writable: false, + configurable: true, + }); + object[5] = 5; + + assert.deepStrictEqual(test_object.GetPropertyNames(object), [ + '5', + 'normal', + 'writable', + 'configurable', + 'inherited', + ]); + + assert.deepStrictEqual(test_object.GetSymbolNames(object), [fooSymbol]); + + assert.deepStrictEqual(test_object.GetEnumerableWritableNames(object), [ + '5', + 'normal', + 'writable', + fooSymbol, + 'inherited', + ]); + + assert.deepStrictEqual(test_object.GetOwnWritableNames(object), [ + '5', + 'normal', + 'unenumerable', + 'writable', + fooSymbol, + ]); + + assert.deepStrictEqual(test_object.GetEnumerableConfigurableNames(object), [ + '5', + 'normal', + 'configurable', + fooSymbol, + 'inherited', + ]); + + assert.deepStrictEqual(test_object.GetOwnConfigurableNames(object), [ + '5', + 'normal', + 'unenumerable', + 'configurable', + fooSymbol, + ]); +} + +// Passing NULL to napi_set_property reports the right error. +assert.deepStrictEqual(test_object.TestSetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}); + +// Passing NULL to napi_has_property reports the right error. +assert.deepStrictEqual(test_object.TestHasProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +// Passing NULL to napi_get_property reports the right error. +assert.deepStrictEqual(test_object.TestGetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +{ + const obj = { x: 'a', y: 'b', z: 'c' }; + + test_object.TestSeal(obj); + + assert.strictEqual(Object.isSealed(obj), true); + + // Test files are modules and therefore strict, so these violations throw + // rather than fail silently. The message text is engine-specific, so only + // the error type is asserted. + assert.throws(() => { + obj.w = 'd'; + }, TypeError); + + assert.throws(() => { + delete obj.x; + }, TypeError); + + // Sealing still allows updating existing properties. + obj.x = 'd'; + assert.strictEqual(obj.x, 'd'); +} + +{ + const obj = { x: 10, y: 10, z: 10 }; + + test_object.TestFreeze(obj); + + assert.strictEqual(Object.isFrozen(obj), true); + + assert.throws(() => { + obj.x = 10; + }, TypeError); + + assert.throws(() => { + obj.w = 15; + }, TypeError); + + assert.throws(() => { + delete obj.x; + }, TypeError); +} diff --git a/tests/js-native-api/test_object/test_exceptions.c b/tests/js-native-api/test_object/test_exceptions.c new file mode 100644 index 0000000..32ce771 --- /dev/null +++ b/tests/js-native-api/test_object/test_exceptions.c @@ -0,0 +1,83 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestExceptions(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value target = args[0]; + napi_value exception, key, value; + napi_status status; + bool is_exception_pending; + bool bool_result; + + NODE_API_CALL(env, + napi_create_string_utf8(env, "key", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL( + env, napi_create_string_utf8(env, "value", NAPI_AUTO_LENGTH, &value)); + +// Each call must report the trap's throw as a pending exception. The +// exception value itself is discarded; clearing it readies the next call. +#define PROCEDURE(call) \ + { \ + status = (call); \ + NODE_API_ASSERT( \ + env, status == napi_pending_exception, "expect exception pending"); \ + NODE_API_CALL(env, napi_is_exception_pending(env, &is_exception_pending)); \ + NODE_API_ASSERT(env, is_exception_pending, "expect exception pending"); \ + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); \ + } + + // Properties. + PROCEDURE(napi_set_property(env, target, key, value)); + PROCEDURE(napi_set_named_property(env, target, "key", value)); + PROCEDURE(napi_has_property(env, target, key, &bool_result)); + PROCEDURE(napi_has_own_property(env, target, key, &bool_result)); + PROCEDURE(napi_has_named_property(env, target, "key", &bool_result)); + PROCEDURE(napi_get_property(env, target, key, &value)); + PROCEDURE(napi_get_named_property(env, target, "key", &value)); + PROCEDURE(napi_delete_property(env, target, key, &bool_result)); + + // Elements. + PROCEDURE(napi_set_element(env, target, 0, value)); + PROCEDURE(napi_has_element(env, target, 0, &bool_result)); + PROCEDURE(napi_get_element(env, target, 0, &value)); + PROCEDURE(napi_delete_element(env, target, 0, &bool_result)); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY_VALUE("key", value), + }; + PROCEDURE(napi_define_properties( + env, target, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + PROCEDURE(napi_get_all_property_names(env, + target, + napi_key_own_only, + napi_key_enumerable, + napi_key_keep_numbers, + &value)); + PROCEDURE(napi_get_property_names(env, target, &value)); + +#undef PROCEDURE + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("testExceptions", TestExceptions), + }; + + NODE_API_CALL(env, + napi_define_properties(env, + exports, + sizeof(descriptors) / + sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/tests/js-native-api/test_object/test_exceptions.js b/tests/js-native-api/test_object/test_exceptions.js new file mode 100644 index 0000000..f0a9a4b --- /dev/null +++ b/tests/js-native-api/test_object/test_exceptions.js @@ -0,0 +1,33 @@ +// A runtime whose object APIs don't surface a throwing proxy handler as a +// pending exception leaves the throw to escape the addon entirely, so there is +// nothing meaningful to assert there. +if (!runtimeFeatures.proxyHandlerExceptions) { + skipTest(); +} + +const { testExceptions } = loadAddon('test_exceptions'); + +function throws() { + throw new Error('foobar'); +} + +// Every object API the addon calls must report the trap's throw as a pending +// exception rather than swallowing it or returning napi_ok. The native side +// asserts that for each call and clears the exception before the next one. +// +// mustCallAtLeast, not mustCall: how many times an engine consults a given +// trap is unspecified, so only the lower bound is portable. +testExceptions( + new Proxy( + {}, + { + get: mustCallAtLeast(throws), + getOwnPropertyDescriptor: mustCallAtLeast(throws), + defineProperty: mustCallAtLeast(throws), + deleteProperty: mustCallAtLeast(throws), + has: mustCallAtLeast(throws), + set: mustCallAtLeast(throws), + ownKeys: mustCallAtLeast(throws), + }, + ), +); diff --git a/tests/js-native-api/test_object/test_null.c b/tests/js-native-api/test_object/test_null.c new file mode 100644 index 0000000..1e154f9 --- /dev/null +++ b/tests/js-native-api/test_object/test_null.c @@ -0,0 +1,419 @@ +#include + +#include "../common.h" +#include "test_null.h" + +static napi_value SetProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, key; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL( + env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_property(NULL, object, key, object)); + + napi_set_property(env, NULL, key, object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_property(env, object, NULL, object); + add_last_status(env, "keyIsNull", return_value); + + napi_set_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, key, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL( + env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_property(NULL, object, key, &prop)); + + napi_get_property(env, NULL, key, &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property(env, object, NULL, &prop); + add_last_status(env, "keyIsNull", return_value); + + napi_get_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value TestBoolValuedPropApi( + napi_env env, + napi_status (*api)(napi_env, napi_value, napi_value, bool*)) { + napi_value return_value, object, key; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL( + env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + api(NULL, object, key, &result)); + + api(env, NULL, key, &result); + add_last_status(env, "objectIsNull", return_value); + + api(env, object, NULL, &result); + add_last_status(env, "keyIsNull", return_value); + + api(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_has_property); +} + +static napi_value HasOwnProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_has_own_property); +} + +static napi_value DeleteProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_delete_property); +} + +static napi_value SetNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_named_property(NULL, object, "key", object)); + + napi_set_named_property(env, NULL, "key", object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_named_property(env, object, NULL, object); + add_last_status(env, "keyIsNull", return_value); + + napi_set_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_named_property(NULL, object, "key", &prop)); + + napi_get_named_property(env, NULL, "key", &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_named_property(env, object, NULL, &prop); + add_last_status(env, "keyIsNull", return_value); + + napi_get_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_has_named_property(NULL, object, "key", &result)); + + napi_has_named_property(env, NULL, "key", &result); + add_last_status(env, "objectIsNull", return_value); + + napi_has_named_property(env, object, NULL, &result); + add_last_status(env, "keyIsNull", return_value); + + napi_has_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value SetElement(napi_env env, napi_callback_info info) { + napi_value return_value, object; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_element(NULL, object, 0, object)); + + napi_set_element(env, NULL, 0, object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_element(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetElement(napi_env env, napi_callback_info info) { + napi_value return_value, object, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_element(NULL, object, 0, &prop)); + + napi_get_element(env, NULL, 0, &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_element(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value TestBoolValuedElementApi( + napi_env env, napi_status (*api)(napi_env, napi_value, uint32_t, bool*)) { + napi_value return_value, object; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + api(NULL, object, 0, &result)); + + api(env, NULL, 0, &result); + add_last_status(env, "objectIsNull", return_value); + + api(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasElement(napi_env env, napi_callback_info info) { + return TestBoolValuedElementApi(env, napi_has_element); +} + +static napi_value DeleteElement(napi_env env, napi_callback_info info) { + return TestBoolValuedElementApi(env, napi_delete_element); +} + +static napi_value DefineProperties(napi_env env, napi_callback_info info) { + napi_value object, return_value; + + napi_property_descriptor desc = {"prop", + NULL, + DefineProperties, + NULL, + NULL, + NULL, + napi_enumerable, + NULL}; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_define_properties(NULL, object, 1, &desc)); + + napi_define_properties(env, NULL, 1, &desc); + add_last_status(env, "objectIsNull", return_value); + + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "descriptorListIsNull", return_value); + + // Upstream quirk carried over deliberately: the two cases below mutate + // `desc` but still pass a NULL descriptor list, so they re-test + // descriptorListIsNull rather than the field they are named after. Passing + // `&desc` instead segfaults -- napi_define_properties dereferences a + // descriptor that names no property and carries no value, getter, setter or + // method. Reported upstream; the names are kept so the expectations match + // nodejs/node. + desc.utf8name = NULL; + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "utf8nameIsNull", return_value); + desc.utf8name = "prop"; + + desc.method = NULL; + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "methodIsNull", return_value); + desc.method = DefineProperties; + + return return_value; +} + +static napi_value GetPropertyNames(napi_env env, napi_callback_info info) { + napi_value return_value, props; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_property_names(NULL, return_value, &props)); + + napi_get_property_names(env, NULL, &props); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property_names(env, return_value, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetAllPropertyNames(napi_env env, napi_callback_info info) { + napi_value return_value, props; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_all_property_names(NULL, + return_value, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + &props)); + + napi_get_all_property_names(env, + NULL, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + &props); + add_last_status(env, "objectIsNull", return_value); + + napi_get_all_property_names(env, + return_value, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetPrototype(napi_env env, napi_callback_info info) { + napi_value return_value, proto; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_prototype(NULL, return_value, &proto)); + + napi_get_prototype(env, NULL, &proto); + add_last_status(env, "objectIsNull", return_value); + + napi_get_prototype(env, return_value, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("setProperty", SetProperty), + DECLARE_NODE_API_PROPERTY("getProperty", GetProperty), + DECLARE_NODE_API_PROPERTY("hasProperty", HasProperty), + DECLARE_NODE_API_PROPERTY("hasOwnProperty", HasOwnProperty), + DECLARE_NODE_API_PROPERTY("deleteProperty", DeleteProperty), + DECLARE_NODE_API_PROPERTY("setNamedProperty", SetNamedProperty), + DECLARE_NODE_API_PROPERTY("getNamedProperty", GetNamedProperty), + DECLARE_NODE_API_PROPERTY("hasNamedProperty", HasNamedProperty), + DECLARE_NODE_API_PROPERTY("setElement", SetElement), + DECLARE_NODE_API_PROPERTY("getElement", GetElement), + DECLARE_NODE_API_PROPERTY("hasElement", HasElement), + DECLARE_NODE_API_PROPERTY("deleteElement", DeleteElement), + DECLARE_NODE_API_PROPERTY("defineProperties", DefineProperties), + DECLARE_NODE_API_PROPERTY("getPropertyNames", GetPropertyNames), + DECLARE_NODE_API_PROPERTY("getAllPropertyNames", GetAllPropertyNames), + DECLARE_NODE_API_PROPERTY("getPrototype", GetPrototype), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID( + env, + napi_define_properties(env, + test_null, + sizeof(test_null_props) / + sizeof(*test_null_props), + test_null_props)); + + const napi_property_descriptor test_null_set = { + "testNull", NULL, NULL, NULL, NULL, test_null, napi_enumerable, NULL}; + + NODE_API_CALL_RETURN_VOID( + env, napi_define_properties(env, exports, 1, &test_null_set)); +} diff --git a/tests/js-native-api/test_object/test_null.h b/tests/js-native-api/test_object/test_null.h new file mode 100644 index 0000000..e10b708 --- /dev/null +++ b/tests/js-native-api/test_object/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ diff --git a/tests/js-native-api/test_object/test_null.js b/tests/js-native-api/test_object/test_null.js new file mode 100644 index 0000000..e0044ee --- /dev/null +++ b/tests/js-native-api/test_object/test_null.js @@ -0,0 +1,52 @@ +// Test passing NULL to object-related Node-APIs. +const { testNull } = loadAddon('test_object'); + +const expectedForProperty = { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; + +assert.deepStrictEqual(testNull.setProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.getProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.hasProperty(), expectedForProperty); +// This is the addon's own hasOwnProperty export, not Object.prototype's. +// eslint-disable-next-line no-prototype-builtins +assert.deepStrictEqual(testNull.hasOwnProperty(), expectedForProperty); +// Not wanting the result of a deletion is allowed. +assert.deepStrictEqual(testNull.deleteProperty(), { + ...expectedForProperty, + valueIsNull: 'napi_ok', +}); +assert.deepStrictEqual(testNull.setNamedProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.getNamedProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.hasNamedProperty(), expectedForProperty); + +const expectedForElement = { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; + +assert.deepStrictEqual(testNull.setElement(), expectedForElement); +assert.deepStrictEqual(testNull.getElement(), expectedForElement); +assert.deepStrictEqual(testNull.hasElement(), expectedForElement); +// Not wanting the result of a deletion is allowed. +assert.deepStrictEqual(testNull.deleteElement(), { + ...expectedForElement, + valueIsNull: 'napi_ok', +}); + +assert.deepStrictEqual(testNull.defineProperties(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + descriptorListIsNull: 'Invalid argument', + utf8nameIsNull: 'Invalid argument', + methodIsNull: 'Invalid argument', +}); + +// expectedForElement also describes the APIs below. +assert.deepStrictEqual(testNull.getPropertyNames(), expectedForElement); +assert.deepStrictEqual(testNull.getAllPropertyNames(), expectedForElement); +assert.deepStrictEqual(testNull.getPrototype(), expectedForElement); diff --git a/tests/js-native-api/test_object/test_object.c b/tests/js-native-api/test_object/test_object.c new file mode 100644 index 0000000..5134f7a --- /dev/null +++ b/tests/js-native-api/test_object/test_object.c @@ -0,0 +1,668 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static int test_value = 3; + +static napi_value Get(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + napi_value output; + NODE_API_CALL(env, napi_get_property(env, args[0], args[1], &output)); + + return output; +} + +static napi_value GetNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, + value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL( + env, napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT( + env, key_length <= 255, "Cannot accommodate keys longer than 255 bytes"); + napi_value output; + NODE_API_CALL(env, napi_get_named_property(env, args[0], key, &output)); + + return output; +} + +static napi_value GetPropertyNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_value output; + NODE_API_CALL(env, napi_get_property_names(env, args[0], &output)); + + return output; +} + +// Returns the property names selected by the given filter/conversion, so the +// test can compare each napi_get_all_property_names mode against JS. +static napi_value GetAllNames(napi_env env, + napi_callback_info info, + napi_key_collection_mode mode, + napi_key_filter filter) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names(env, + args[0], + mode, + filter, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetSymbolNames(napi_env env, napi_callback_info info) { + return GetAllNames( + env, info, napi_key_include_prototypes, napi_key_skip_strings); +} + +static napi_value GetEnumerableWritableNames(napi_env env, + napi_callback_info info) { + return GetAllNames(env, + info, + napi_key_include_prototypes, + napi_key_enumerable | napi_key_writable); +} + +static napi_value GetOwnWritableNames(napi_env env, napi_callback_info info) { + return GetAllNames(env, info, napi_key_own_only, napi_key_writable); +} + +static napi_value GetEnumerableConfigurableNames(napi_env env, + napi_callback_info info) { + return GetAllNames(env, + info, + napi_key_include_prototypes, + napi_key_enumerable | napi_key_configurable); +} + +static napi_value GetOwnConfigurableNames(napi_env env, + napi_callback_info info) { + return GetAllNames(env, info, napi_key_own_only, napi_key_configurable); +} + +static napi_value Set(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + NODE_API_CALL(env, napi_set_property(env, args[0], args[1], args[2])); + + napi_value valuetrue; + NODE_API_CALL(env, napi_get_boolean(env, true, &valuetrue)); + + return valuetrue; +} + +static napi_value SetNamed(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, + value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL( + env, napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT( + env, key_length <= 255, "Cannot accommodate keys longer than 255 bytes"); + + NODE_API_CALL(env, napi_set_named_property(env, args[0], key, args[2])); + + napi_value value_true; + NODE_API_CALL(env, napi_get_boolean(env, true, &value_true)); + + return value_true; +} + +static napi_value Has(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + bool has_property; + NODE_API_CALL(env, napi_has_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, + value_type1 == napi_string || value_type1 == napi_symbol, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL( + env, napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT( + env, key_length <= 255, "Cannot accommodate keys longer than 255 bytes"); + + bool has_property; + NODE_API_CALL(env, napi_has_named_property(env, args[0], key, &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasOwn(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + // The key is deliberately not type-checked here: the test asserts that + // napi_has_own_property itself rejects a key that is not a name. + bool has_property; + NODE_API_CALL(env, + napi_has_own_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value Delete(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + bool result; + napi_value ret; + NODE_API_CALL(env, napi_delete_property(env, args[0], args[1], &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &ret)); + + return ret; +} + +static napi_value New(napi_env env, napi_callback_info info) { + napi_value ret; + NODE_API_CALL(env, napi_create_object(env, &ret)); + + napi_value num; + NODE_API_CALL(env, napi_create_int32(env, 987654321, &num)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_number", num)); + + napi_value str; + const char* str_val = "test string"; + size_t str_len = strlen(str_val); + NODE_API_CALL(env, napi_create_string_utf8(env, str_val, str_len, &str)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_string", str)); + + return ret; +} + +static napi_value Inflate(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_value obj = args[0]; + napi_value propertynames; + NODE_API_CALL(env, napi_get_property_names(env, obj, &propertynames)); + + uint32_t i, length; + NODE_API_CALL(env, napi_get_array_length(env, propertynames, &length)); + + for (i = 0; i < length; i++) { + napi_value property_str; + NODE_API_CALL(env, napi_get_element(env, propertynames, i, &property_str)); + + napi_value value; + NODE_API_CALL(env, napi_get_property(env, obj, property_str, &value)); + + double double_val; + NODE_API_CALL(env, napi_get_value_double(env, value, &double_val)); + NODE_API_CALL(env, napi_create_double(env, double_val + 1, &value)); + NODE_API_CALL(env, napi_set_property(env, obj, property_str, value)); + } + + return obj; +} + +static napi_value Wrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_CALL(env, napi_wrap(env, arg, &test_value, NULL, NULL, NULL)); + return NULL; +} + +static napi_value Unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + void* data; + NODE_API_CALL(env, napi_unwrap(env, arg, &data)); + + bool is_expected = (data != NULL && *(int*)data == 3); + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, is_expected, &result)); + return result; +} + +static napi_value TestSetProperty(napi_env env, napi_callback_info info) { + napi_status status; + napi_value object, key, value; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(env, napi_create_object(env, &value)); + + status = napi_set_property(NULL, object, key, value); + + add_returned_status( + env, "envIsNull", object, "Invalid argument", napi_invalid_arg, status); + + napi_set_property(env, NULL, key, value); + add_last_status(env, "objectIsNull", object); + + napi_set_property(env, object, NULL, value); + add_last_status(env, "keyIsNull", object); + + napi_set_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", object); + + return object; +} + +static napi_value TestHasProperty(napi_env env, napi_callback_info info) { + napi_status status; + napi_value object, key; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + status = napi_has_property(NULL, object, key, &result); + + add_returned_status( + env, "envIsNull", object, "Invalid argument", napi_invalid_arg, status); + + napi_has_property(env, NULL, key, &result); + add_last_status(env, "objectIsNull", object); + + napi_has_property(env, object, NULL, &result); + add_last_status(env, "keyIsNull", object); + + napi_has_property(env, object, key, NULL); + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestGetProperty(napi_env env, napi_callback_info info) { + napi_status status; + napi_value object, key, result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(env, napi_create_object(env, &result)); + + status = napi_get_property(NULL, object, key, &result); + + add_returned_status( + env, "envIsNull", object, "Invalid argument", napi_invalid_arg, status); + + napi_get_property(env, NULL, key, &result); + add_last_status(env, "objectIsNull", object); + + napi_get_property(env, object, NULL, &result); + add_last_status(env, "keyIsNull", object); + + napi_get_property(env, object, key, NULL); + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestFreeze(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_CALL(env, napi_object_freeze(env, args[0])); + + return args[0]; +} + +static napi_value TestSeal(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_CALL(env, napi_object_seal(env, args[0])); + + return args[0]; +} + +// Type tags are basically 128-bit UUIDs. The zero tag and the half-zero tags +// are included so an implementation cannot pass by treating "empty" as a +// wildcard. +#define TYPE_TAG_COUNT 5 +static const napi_type_tag type_tags[TYPE_TAG_COUNT] = { + {0xdaf987b3cc62481a, 0xb745b0497f299531}, + {0xbb7936c374084d9b, 0xa9548d0762eeedb9}, + {0xa5ed9ce2e4c00c38, 0}, + {0, 0}, + {0xa5ed9ce2e4c00c38, 0xdaf987b3cc62481a}, +}; + +#define VALIDATE_TYPE_INDEX(env, type_index) \ + do { \ + if ((type_index) >= TYPE_TAG_COUNT) { \ + NODE_API_CALL((env), \ + napi_throw_range_error((env), \ + "NODE_API_TEST_INVALID_TYPE_INDEX", \ + "Invalid type index")); \ + } \ + } while (0) + +// V8 will not allow us to construct an external with a NULL data value. +#define IN_LIEU_OF_NULL ((void*)0x1) + +// Tags `instance`, then wipes the local copy of the tag. A tagging +// implementation that stored a pointer rather than the 128-bit value would be +// left pointing at cleared stack memory, so CheckTypeTag would fail. +static napi_status tag_and_clear(napi_env env, + napi_value instance, + uint32_t type_index) { + napi_type_tag tag = type_tags[type_index]; + NODE_API_CHECK_STATUS(napi_type_tag_object(env, instance, &tag)); + memset(&tag, 0, sizeof(tag)); + return napi_ok; +} + +static napi_value TypeTaggedInstance(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL(env, napi_create_object(env, &instance)); + NODE_API_CALL(env, tag_and_clear(env, instance, type_index)); + + return instance; +} + +static napi_value PlainExternal(napi_env env, napi_callback_info info) { + napi_value instance; + + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + + return instance; +} + +static napi_value TypeTaggedExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + NODE_API_CALL(env, tag_and_clear(env, instance, type_index)); + + return instance; +} + +static napi_value CheckTypeTag(napi_env env, napi_callback_info info) { + size_t argc = 2; + bool result; + napi_value argv[2], js_result; + uint32_t type_index; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, argv[0], &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL( + env, + napi_check_object_type_tag( + env, argv[1], &type_tags[type_index], &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &js_result)); + + return js_result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("Get", Get), + DECLARE_NODE_API_PROPERTY("GetNamed", GetNamed), + DECLARE_NODE_API_PROPERTY("GetPropertyNames", GetPropertyNames), + DECLARE_NODE_API_PROPERTY("GetSymbolNames", GetSymbolNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableWritableNames", + GetEnumerableWritableNames), + DECLARE_NODE_API_PROPERTY("GetOwnWritableNames", GetOwnWritableNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableConfigurableNames", + GetEnumerableConfigurableNames), + DECLARE_NODE_API_PROPERTY("GetOwnConfigurableNames", + GetOwnConfigurableNames), + DECLARE_NODE_API_PROPERTY("Set", Set), + DECLARE_NODE_API_PROPERTY("SetNamed", SetNamed), + DECLARE_NODE_API_PROPERTY("Has", Has), + DECLARE_NODE_API_PROPERTY("HasNamed", HasNamed), + DECLARE_NODE_API_PROPERTY("HasOwn", HasOwn), + DECLARE_NODE_API_PROPERTY("Delete", Delete), + DECLARE_NODE_API_PROPERTY("New", New), + DECLARE_NODE_API_PROPERTY("Inflate", Inflate), + DECLARE_NODE_API_PROPERTY("Wrap", Wrap), + DECLARE_NODE_API_PROPERTY("Unwrap", Unwrap), + DECLARE_NODE_API_PROPERTY("TestSetProperty", TestSetProperty), + DECLARE_NODE_API_PROPERTY("TestHasProperty", TestHasProperty), + DECLARE_NODE_API_PROPERTY("TestGetProperty", TestGetProperty), + DECLARE_NODE_API_PROPERTY("TypeTaggedInstance", TypeTaggedInstance), + DECLARE_NODE_API_PROPERTY("TypeTaggedExternal", TypeTaggedExternal), + DECLARE_NODE_API_PROPERTY("PlainExternal", PlainExternal), + DECLARE_NODE_API_PROPERTY("CheckTypeTag", CheckTypeTag), + DECLARE_NODE_API_PROPERTY("TestFreeze", TestFreeze), + DECLARE_NODE_API_PROPERTY("TestSeal", TestSeal), + }; + + init_test_null(env, exports); + + NODE_API_CALL(env, + napi_define_properties(env, + exports, + sizeof(descriptors) / + sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END