From 03de5fd3ccb86b16264ea648f4dd4caefda892d5 Mon Sep 17 00:00:00 2001 From: Ben Noordhuis Date: Sat, 1 Mar 2025 21:59:19 +0100 Subject: [PATCH 1/2] Add JS_IsProxy, JS_GetProxyHandler and JS_GetProxyTarget This commit also turns JS_IsArray into a simple predicate function. It no longer punches automatically through proxies because that can raise exceptions and is inconsistent with the other predicate functions. Fixes: https://github.com/quickjs-ng/quickjs/issues/938 --- api-test.c | 34 ++++++++++++++++++++++++++++++ quickjs.c | 62 +++++++++++++++++++++++++++++++++++++++++++++--------- quickjs.h | 10 ++++++++- 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/api-test.c b/api-test.c index 0fb33beb8..8988e6673 100644 --- a/api-test.c +++ b/api-test.c @@ -147,11 +147,45 @@ static void raw_context_global_var(void) JS_FreeRuntime(rt); } +static void is_array(void) +{ + JSRuntime *rt = JS_NewRuntime(); + JSContext *ctx = JS_NewContext(rt); + { + static const char code[] = "[]"; + JSValue ret = JS_Eval(ctx, code, strlen(code), "*", JS_EVAL_TYPE_GLOBAL); + assert(!JS_IsException(ret)); + assert(JS_IsArray(ret)); + JS_FreeValue(ctx, ret); + } + { + static const char code[] = "new Proxy([], {})"; + JSValue ret = JS_Eval(ctx, code, strlen(code), "*", JS_EVAL_TYPE_GLOBAL); + assert(!JS_IsException(ret)); + assert(!JS_IsArray(ret)); + assert(JS_IsProxy(ret)); + JSValue handler = JS_GetProxyHandler(ctx, ret); + JSValue target = JS_GetProxyTarget(ctx, ret); + assert(!JS_IsException(handler)); + assert(!JS_IsException(target)); + assert(!JS_IsProxy(handler)); + assert(!JS_IsProxy(target)); + assert(JS_IsObject(handler)); + assert(JS_IsArray(target)); + JS_FreeValue(ctx, handler); + JS_FreeValue(ctx, target); + JS_FreeValue(ctx, ret); + } + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + int main(void) { sync_call(); async_call(); async_call_stack_overflow(); raw_context_global_var(); + is_array(); return 0; } diff --git a/quickjs.c b/quickjs.c index ba87aaaff..d76d0321d 100644 --- a/quickjs.c +++ b/quickjs.c @@ -12059,8 +12059,17 @@ static __maybe_unused void JS_DumpValue(JSRuntime *rt, JSValue val) } } +bool JS_IsArray(JSValue val) +{ + if (JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT) { + JSObject *p = JS_VALUE_GET_OBJ(val); + return p->class_id == JS_CLASS_ARRAY; + } + return false; +} + /* return -1 if exception (proxy case) or true/false */ -int JS_IsArray(JSContext *ctx, JSValue val) +static int js_is_array(JSContext *ctx, JSValue val) { JSObject *p; if (JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT) { @@ -36880,7 +36889,7 @@ static JSValue js_object_toString(JSContext *ctx, JSValue this_val, obj = JS_ToObject(ctx, this_val); if (JS_IsException(obj)) return obj; - is_array = JS_IsArray(ctx, obj); + is_array = js_is_array(ctx, obj); if (is_array < 0) { JS_FreeValue(ctx, obj); return JS_EXCEPTION; @@ -38197,7 +38206,7 @@ static JSValue js_array_isArray(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { int ret; - ret = JS_IsArray(ctx, argv[0]); + ret = js_is_array(ctx, argv[0]); if (ret < 0) return JS_EXCEPTION; else @@ -38217,7 +38226,7 @@ static JSValue JS_ArraySpeciesCreate(JSContext *ctx, JSValue obj, int res; JSContext *realm; - res = JS_IsArray(ctx, obj); + res = js_is_array(ctx, obj); if (res < 0) return JS_EXCEPTION; if (!res) @@ -38274,7 +38283,7 @@ static int JS_isConcatSpreadable(JSContext *ctx, JSValue obj) return -1; if (!JS_IsUndefined(val)) return JS_ToBoolFree(ctx, val); - return JS_IsArray(ctx, obj); + return js_is_array(ctx, obj); } static JSValue js_array_at(JSContext *ctx, JSValue this_val, @@ -39555,7 +39564,7 @@ static int64_t JS_FlattenIntoArray(JSContext *ctx, JSValue target, return -1; } if (depth > 0) { - is_array = JS_IsArray(ctx, element); + is_array = js_is_array(ctx, element); if (is_array < 0) goto fail; if (is_array) { @@ -45088,7 +45097,7 @@ static JSValue internalize_json_property(JSContext *ctx, JSValue holder, if (JS_IsException(val)) return val; if (JS_IsObject(val)) { - is_array = JS_IsArray(ctx, val); + is_array = js_is_array(ctx, val); if (is_array < 0) goto fail; if (is_array) { @@ -45299,7 +45308,7 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, v = js_array_push(ctx, jsc->stack, 1, &val, 0); if (check_exception_free(ctx, v)) goto exception; - ret = JS_IsArray(ctx, val); + ret = js_is_array(ctx, val); if (ret < 0) goto exception; if (ret) { @@ -45447,7 +45456,7 @@ JSValue JS_JSONStringify(JSContext *ctx, JSValue obj, if (JS_IsFunction(ctx, replacer)) { jsc->replacer_func = replacer; } else { - res = JS_IsArray(ctx, replacer); + res = js_is_array(ctx, replacer); if (res < 0) goto exception; if (res) { @@ -46591,7 +46600,7 @@ static int js_proxy_isArray(JSContext *ctx, JSValue obj) JS_ThrowTypeErrorRevokedProxy(ctx); return -1; } - return JS_IsArray(ctx, s->target); + return js_is_array(ctx, s->target); } static const JSClassExoticMethods js_proxy_exotic_methods = { @@ -46708,6 +46717,39 @@ void JS_AddIntrinsicProxy(JSContext *ctx) obj1, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); } +bool JS_IsProxy(JSValue val) +{ + if (JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT) { + JSObject *p = JS_VALUE_GET_OBJ(val); + return p->class_id == JS_CLASS_PROXY; + } + return false; +} + +static JSValue js_get_proxy_field(JSContext *ctx, JSValue proxy, int offset) +{ + if (JS_VALUE_GET_TAG(proxy) == JS_TAG_OBJECT) { + JSObject *p = JS_VALUE_GET_OBJ(proxy); + if (p->class_id == JS_CLASS_PROXY) { + JSProxyData *s = JS_GetOpaque(proxy, JS_CLASS_PROXY); + if (s->is_revoked) + return JS_ThrowTypeErrorRevokedProxy(ctx); + return *(JSValue *)((char *)s + offset); + } + } + return JS_ThrowTypeError(ctx, "not a proxy"); +} + +JSValue JS_GetProxyTarget(JSContext *ctx, JSValue proxy) +{ + return js_get_proxy_field(ctx, proxy, offsetof(JSProxyData, target)); +} + +JSValue JS_GetProxyHandler(JSContext *ctx, JSValue proxy) +{ + return js_get_proxy_field(ctx, proxy, offsetof(JSProxyData, handler)); +} + /* Symbol */ static JSValue js_symbol_constructor(JSContext *ctx, JSValue new_target, diff --git a/quickjs.h b/quickjs.h index bcdd87165..46841fee6 100644 --- a/quickjs.h +++ b/quickjs.h @@ -748,7 +748,15 @@ JS_EXTERN JSValue JS_NewArray(JSContext *ctx); // takes ownership of the values JS_EXTERN JSValue JS_NewArrayFrom(JSContext *ctx, int count, const JSValue *values); -JS_EXTERN int JS_IsArray(JSContext *ctx, JSValue val); +// reader beware: JS_IsArray used to "punch" through proxies and check +// if the target object is an array but it no longer does; use JS_IsProxy +// and JS_GetProxyTarget instead, and remember that the target itself can +// also be a proxy, ad infinitum +JS_EXTERN bool JS_IsArray(JSValue val); + +JS_EXTERN bool JS_IsProxy(JSValue val); +JS_EXTERN JSValue JS_GetProxyTarget(JSContext *ctx, JSValue proxy); +JS_EXTERN JSValue JS_GetProxyHandler(JSContext *ctx, JSValue proxy); JS_EXTERN JSValue JS_NewDate(JSContext *ctx, double epoch_ms); JS_EXTERN bool JS_IsDate(JSValue v); From 852a38caa98a21a96e3558ba2ca631d16df52688 Mon Sep 17 00:00:00 2001 From: Ben Noordhuis Date: Sat, 1 Mar 2025 22:23:04 +0100 Subject: [PATCH 2/2] squash! js_dup --- quickjs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index d76d0321d..a1b376656 100644 --- a/quickjs.c +++ b/quickjs.c @@ -46734,7 +46734,7 @@ static JSValue js_get_proxy_field(JSContext *ctx, JSValue proxy, int offset) JSProxyData *s = JS_GetOpaque(proxy, JS_CLASS_PROXY); if (s->is_revoked) return JS_ThrowTypeErrorRevokedProxy(ctx); - return *(JSValue *)((char *)s + offset); + return js_dup(*(JSValue *)((char *)s + offset)); } } return JS_ThrowTypeError(ctx, "not a proxy");