diff --git a/Makefile b/Makefile index a1ff139..7b03e03 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,17 @@ .PHONY: build build-debug clean +CMAKE ?= cmake +WASI_SDK_DIR ?= /opt/wasi-sdk +WASI_TOOLCHAIN_FILE ?= $(WASI_SDK_DIR)/share/cmake/wasi-sdk.cmake + build: @echo "Configuring and building qjs..." cd qjswasm/quickjs && \ rm -rf build && \ - cmake -B build \ + $(CMAKE) -B build \ -DQJS_BUILD_LIBC=ON \ -DQJS_BUILD_CLI_WITH_MIMALLOC=OFF \ - -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake \ + -DCMAKE_TOOLCHAIN_FILE=$(WASI_TOOLCHAIN_FILE) \ -DCMAKE_PROJECT_INCLUDE=../qjswasm.cmake @echo "Building qjs target..." make -C qjswasm/quickjs/build qjswasm -j$(nproc) @@ -20,11 +24,11 @@ build-debug: @echo "Configuring and building qjs with runtime address debug..." cd qjswasm/quickjs && \ rm -rf build && \ - cmake -B build \ + $(CMAKE) -B build \ -DQJS_BUILD_LIBC=ON \ -DQJS_BUILD_CLI_WITH_MIMALLOC=OFF \ -DQJS_DEBUG_RUNTIME_ADDRESS=ON \ - -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake \ + -DCMAKE_TOOLCHAIN_FILE=$(WASI_TOOLCHAIN_FILE) \ -DCMAKE_PROJECT_INCLUDE=../qjswasm.cmake @echo "Building qjs target..." make -C qjswasm/quickjs/build qjswasm -j$(nproc) diff --git a/common.go b/common.go index 84c09ab..01b48f7 100644 --- a/common.go +++ b/common.go @@ -59,21 +59,6 @@ var ( reflect.Float32: {-math.MaxFloat32, math.MaxFloat32}, reflect.Float64: {-math.MaxFloat64, math.MaxFloat64}, } - - // TypedArray types for validation. - typedArrayTypes = []string{ - "Uint8Array", - "Int8Array", - "Uint16Array", - "Int16Array", - "Uint32Array", - "Int32Array", - "Float32Array", - "Float64Array", - "BigInt64Array", - "BigUint64Array", - "DataView", - } ) // ObjectOrMap interface for unified object/map handling. @@ -554,13 +539,7 @@ func NumericBoundsCheck(floatVal float64, targetKind reflect.Kind) error { // IsTypedArray returns true if the input is TypedArray or DataView. func IsTypedArray(input *Value) bool { - for _, typeName := range typedArrayTypes { - if input.IsGlobalInstanceOf(typeName) { - return true - } - } - - return false + return input != nil && input.IntrinsicKind().IsTypedArray() } // processTempValue validates if temp is a valid result for the given T type. diff --git a/jstogo.go b/jstogo.go index 814a93d..76a49cb 100644 --- a/jstogo.go +++ b/jstogo.go @@ -60,13 +60,13 @@ func toGoValue[T any]( return v, err } - if input.IsGlobalInstanceOf("Date") { + if input.IsDate() { temp, err = JsTimeToGo(input) return v, err } - if input.IsGlobalInstanceOf("RegExp") { + if input.IsRegExp() { temp = input.String() return v, err diff --git a/qjs.wasm b/qjs.wasm index b1f0d25..2aaffc1 100755 Binary files a/qjs.wasm and b/qjs.wasm differ diff --git a/qjswasm/helpers.c b/qjswasm/helpers.c index afd8682..d0cd492 100644 --- a/qjswasm/helpers.c +++ b/qjswasm/helpers.c @@ -517,7 +517,7 @@ uint64_t *QJS_AtomToCString(JSContext *ctx, JSAtom atom) } // returns a packed uint64_t value containing both the string's memory address (high 32 bits) and length (low 32 bits). -uint64_t *QJS_GetOwnPropertyNames(JSContext *ctx, JSValue v) +uint64_t *QJS_GetOwnPropertyNames(JSContext *ctx, JSValue v, uint32_t flags) { JSPropertyEnum *ptr; uint32_t size; @@ -527,7 +527,7 @@ uint64_t *QJS_GetOwnPropertyNames(JSContext *ctx, JSValue v) &ptr, &size, v, - JS_GPN_STRING_MASK | JS_GPN_SYMBOL_MASK | JS_GPN_PRIVATE_MASK); + flags); if (result < 0) { @@ -547,6 +547,127 @@ uint64_t *QJS_GetOwnPropertyNames(JSContext *ctx, JSValue v) return packed_result; } +uint64_t QJS_GetOwnPropertyFlags(JSContext *ctx, JSValue obj, JSAtom prop) +{ + JSPropertyDescriptor desc = { + .flags = 0, + .value = JS_UNDEFINED, + .getter = JS_UNDEFINED, + .setter = JS_UNDEFINED, + }; + int result = JS_GetOwnProperty(ctx, &desc, obj, prop); + uint32_t status; + uint32_t flags = 0; + + if (result < 0) + { + status = UINT32_MAX; + } + else if (result == 0) + { + status = 0; + } + else + { + status = 1; + flags = (uint32_t)(desc.flags & (JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_TMASK)); + flags |= JS_PROP_HAS_CONFIGURABLE | JS_PROP_HAS_ENUMERABLE; + + if ((desc.flags & JS_PROP_TMASK) == JS_PROP_GETSET) + { + if (!JS_IsUndefined(desc.getter)) + { + flags |= JS_PROP_HAS_GET; + } + if (!JS_IsUndefined(desc.setter)) + { + flags |= JS_PROP_HAS_SET; + } + } + else + { + flags |= JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE; + } + + JS_FreeValue(ctx, desc.value); + JS_FreeValue(ctx, desc.getter); + JS_FreeValue(ctx, desc.setter); + } + + return ((uint64_t)status << 32) | flags; +} + +uint32_t QJS_GetIntrinsicKind(JSContext *ctx, JSValue obj) +{ + if (!JS_IsObject(obj)) + { + return 0; + } + + if (JS_IsArray(obj)) + { + return 2; + } + if (JS_IsDate(obj)) + { + return 3; + } + if (JS_IsRegExp(obj)) + { + return 4; + } + if (JS_IsMap(obj)) + { + return 5; + } + if (JS_IsSet(obj)) + { + return 6; + } + if (JS_IsArrayBuffer(obj)) + { + return 7; + } + if (JS_IsDataView(obj)) + { + return 8; + } + if (QJS_IsError(ctx, obj)) + { + return 21; + } + + switch (JS_GetTypedArrayType(obj)) + { + case JS_TYPED_ARRAY_UINT8: + return 9; + case JS_TYPED_ARRAY_UINT8C: + return 10; + case JS_TYPED_ARRAY_INT8: + return 11; + case JS_TYPED_ARRAY_UINT16: + return 12; + case JS_TYPED_ARRAY_INT16: + return 13; + case JS_TYPED_ARRAY_UINT32: + return 14; + case JS_TYPED_ARRAY_INT32: + return 15; + case JS_TYPED_ARRAY_BIG_INT64: + return 16; + case JS_TYPED_ARRAY_BIG_UINT64: + return 17; + case JS_TYPED_ARRAY_FLOAT32: + return 18; + case JS_TYPED_ARRAY_FLOAT64: + return 19; + case JS_TYPED_ARRAY_FLOAT16: + return 20; + default: + return 1; + } +} + JSValue QJS_ParseJSON(JSContext *ctx, const char *buf) { size_t len = strlen(buf); diff --git a/qjswasm/qjs.h b/qjswasm/qjs.h index d30e8e4..64abc34 100644 --- a/qjswasm/qjs.h +++ b/qjswasm/qjs.h @@ -132,7 +132,9 @@ JSValue QJS_NewBigUint64(JSContext *ctx, uint64_t val); JSValue QJS_NewFloat64(JSContext *ctx, uint64_t bits); uint64_t *QJS_AtomToCString(JSContext *ctx, JSAtom atom); -uint64_t *QJS_GetOwnPropertyNames(JSContext *ctx, JSValue v); +uint64_t *QJS_GetOwnPropertyNames(JSContext *ctx, JSValue v, uint32_t flags); +uint64_t QJS_GetOwnPropertyFlags(JSContext *ctx, JSValue obj, JSAtom prop); +uint32_t QJS_GetIntrinsicKind(JSContext *ctx, JSValue obj); JSValue QJS_ParseJSON(JSContext *ctx, const char *buf); JSValue QJS_NewBool(JSContext *ctx, int val); uint64_t *QJS_GetArrayBuffer(JSContext *ctx, JSValue obj); diff --git a/qjswasm/qjswasm.cmake b/qjswasm/qjswasm.cmake index 8fd8607..0b76a25 100644 --- a/qjswasm/qjswasm.cmake +++ b/qjswasm/qjswasm.cmake @@ -132,6 +132,8 @@ target_link_options(qjswasm PRIVATE "LINKER:--export=QJS_IsConstructor" "LINKER:--export=QJS_IsInstanceOf" "LINKER:--export=QJS_GetOwnPropertyNames" + "LINKER:--export=QJS_GetOwnPropertyFlags" + "LINKER:--export=QJS_GetIntrinsicKind" "LINKER:--export=QJS_ParseJSON" "LINKER:--export=QJS_NewBool" "LINKER:--export=QJS_NewBigInt64" diff --git a/value.go b/value.go index ace6eec..13217b7 100644 --- a/value.go +++ b/value.go @@ -35,6 +35,20 @@ type JSPropertyEnum struct { const jsPropertyEnumSize = uint32(unsafe.Sizeof(JSPropertyEnum{})) +const ( + JSPropConfigurable uint32 = 1 << 0 + JSPropWritable uint32 = 1 << 1 + JSPropEnumerable uint32 = 1 << 2 + JSPropHasGet uint32 = 1 << 11 + JSPropHasSet uint32 = 1 << 12 + JSPropHasValue uint32 = 1 << 13 + + JSGPNStringMask uint32 = 1 << 0 + JSGPNSymbolMask uint32 = 1 << 1 + JSGPNPrivateMask uint32 = 1 << 2 + JSGPNSetEnum uint32 = 1 << 5 +) + // Atom represents a JavaScript atom: // Object property names and some strings are stored as Atoms (unique strings) to save memory and allow fast comparison. type Atom struct { @@ -48,6 +62,117 @@ type OwnProperty struct { atom Atom } +type PropertyDescriptor struct { + Exists bool + Enumerable bool + Configurable bool + Writable bool + HasValue bool + HasGetter bool + HasSetter bool +} + +func (d PropertyDescriptor) IsAccessor() bool { + return d.HasGetter || d.HasSetter +} + +type IntrinsicKind uint32 + +const ( + IntrinsicUnknown IntrinsicKind = iota + IntrinsicObject + IntrinsicArray + IntrinsicDate + IntrinsicRegExp + IntrinsicMap + IntrinsicSet + IntrinsicArrayBuffer + IntrinsicDataView + IntrinsicUint8Array + IntrinsicUint8ClampedArray + IntrinsicInt8Array + IntrinsicUint16Array + IntrinsicInt16Array + IntrinsicUint32Array + IntrinsicInt32Array + IntrinsicBigInt64Array + IntrinsicBigUint64Array + IntrinsicFloat32Array + IntrinsicFloat64Array + IntrinsicFloat16Array + IntrinsicError +) + +func (k IntrinsicKind) String() string { + switch k { + case IntrinsicObject: + return "Object" + case IntrinsicArray: + return "Array" + case IntrinsicDate: + return "Date" + case IntrinsicRegExp: + return "RegExp" + case IntrinsicMap: + return "Map" + case IntrinsicSet: + return "Set" + case IntrinsicArrayBuffer: + return "ArrayBuffer" + case IntrinsicDataView: + return "DataView" + case IntrinsicUint8Array: + return "Uint8Array" + case IntrinsicUint8ClampedArray: + return "Uint8ClampedArray" + case IntrinsicInt8Array: + return "Int8Array" + case IntrinsicUint16Array: + return "Uint16Array" + case IntrinsicInt16Array: + return "Int16Array" + case IntrinsicUint32Array: + return "Uint32Array" + case IntrinsicInt32Array: + return "Int32Array" + case IntrinsicBigInt64Array: + return "BigInt64Array" + case IntrinsicBigUint64Array: + return "BigUint64Array" + case IntrinsicFloat32Array: + return "Float32Array" + case IntrinsicFloat64Array: + return "Float64Array" + case IntrinsicFloat16Array: + return "Float16Array" + case IntrinsicError: + return "Error" + default: + return "Unknown" + } +} + +func (k IntrinsicKind) IsTypedArray() bool { + switch k { + case IntrinsicDataView, + IntrinsicUint8Array, + IntrinsicUint8ClampedArray, + IntrinsicInt8Array, + IntrinsicUint16Array, + IntrinsicInt16Array, + IntrinsicUint32Array, + IntrinsicInt32Array, + IntrinsicBigInt64Array, + IntrinsicBigUint64Array, + IntrinsicFloat32Array, + IntrinsicFloat64Array, + IntrinsicFloat16Array: + return true + default: + return false + } +} + func (p OwnProperty) String() string { return p.atom.String() } @@ -210,6 +335,10 @@ func (v *Value) Type() string { return "ArrayBuffer" } + if kind := v.IntrinsicKind(); kind.IsTypedArray() { + return kind.String() + } + return "unknown" } @@ -219,7 +348,7 @@ func (v *Value) NewUndefined() *Value { // GetOwnPropertyNames returns the names of the properties of the value. func (v *Value) GetOwnPropertyNames() (_ []string, err error) { - pList := v.GetOwnProperties() + pList := v.getOwnProperties(JSGPNStringMask | JSGPNSymbolMask | JSGPNPrivateMask | JSGPNSetEnum) names := make([]string, len(pList)) @@ -231,10 +360,27 @@ func (v *Value) GetOwnPropertyNames() (_ []string, err error) { } func (v *Value) GetOwnProperties() []OwnProperty { + return v.getOwnProperties(JSGPNStringMask | JSGPNSymbolMask | JSGPNPrivateMask | JSGPNSetEnum) +} + +func (v *Value) EnumerableOwnPropertyNames() ([]string, error) { + props := v.getOwnProperties(JSGPNStringMask | JSGPNSetEnum) + names := make([]string, 0, len(props)) + for _, prop := range props { + if !prop.isEnumerable { + continue + } + names = append(names, prop.String()) + } + return names, nil +} + +func (v *Value) getOwnProperties(flags uint32) []OwnProperty { ptr, entriesCount := v.context.CallUnPack( "QJS_GetOwnPropertyNames", v.Ctx(), v.Raw(), + uint64(flags), ) if entriesCount == 0 { return []OwnProperty{} @@ -270,6 +416,42 @@ func (v *Value) GetOwnProperties() []OwnProperty { return property } +func (v *Value) OwnPropertyDescriptor(name string) (PropertyDescriptor, error) { + atom := v.context.NewAtom(name) + defer atom.Free() + + raw := v.Call("QJS_GetOwnPropertyFlags", v.Ctx(), v.Raw(), atom.Raw()).Handle().Uint64() + status := uint32(raw >> 32) + flags := uint32(raw) + + switch status { + case 0: + return PropertyDescriptor{}, nil + case 1: + return PropertyDescriptor{ + Exists: true, + Enumerable: flags&JSPropEnumerable != 0, + Configurable: flags&JSPropConfigurable != 0, + Writable: flags&JSPropWritable != 0, + HasValue: flags&JSPropHasValue != 0, + HasGetter: flags&JSPropHasGet != 0, + HasSetter: flags&JSPropHasSet != 0, + }, nil + default: + if v.context.HasException() { + return PropertyDescriptor{}, v.context.Exception() + } + return PropertyDescriptor{}, errors.New("failed to inspect own property descriptor") + } +} + +func (v *Value) IntrinsicKind() IntrinsicKind { + if v == nil || !v.IsObject() { + return IntrinsicUnknown + } + return IntrinsicKind(v.Call("QJS_GetIntrinsicKind", v.Ctx(), v.Raw()).Uint32()) +} + func (v *Value) GetProperty(name *Value) *Value { atom := v.Call("JS_ValueToAtom", v.Ctx(), name.Raw()) @@ -525,13 +707,11 @@ func (v *Value) Await() (*Value, error) { } func (v *Value) IsMap() bool { - return v.IsObject() && v.IsGlobalInstanceOf("Map") || - v.String() == "[object Map]" + return v.IntrinsicKind() == IntrinsicMap } func (v *Value) IsSet() bool { - return v.IsObject() && v.IsGlobalInstanceOf("Set") || - v.String() == "[object Set]" + return v.IntrinsicKind() == IntrinsicSet } // IsGlobalInstanceOf checks if the value is an instance of the given global constructor. @@ -550,8 +730,11 @@ func (v *Value) IsGlobalInstanceOf(name string) bool { // IsByteArray return true if the value is array buffer. func (v *Value) IsByteArray() bool { - return v.IsObject() && v.IsGlobalInstanceOf("ArrayBuffer") || - v.String() == "[object ArrayBuffer]" + return v.IntrinsicKind() == IntrinsicArrayBuffer +} + +func (v *Value) IsRegExp() bool { + return v.IntrinsicKind() == IntrinsicRegExp } // Object returns the object value of the value. diff --git a/value_test.go b/value_test.go index cbcd1f0..5011c99 100644 --- a/value_test.go +++ b/value_test.go @@ -267,6 +267,110 @@ func TestValueTypeChecks(t *testing.T) { }) } +func TestValueIntrinsicKind(t *testing.T) { + rt, ctx := setupTestContext(t) + defer rt.Close() + + cases := []struct { + name string + code string + want qjs.IntrinsicKind + }{ + {name: "plain_object", code: `({ a: 1 })`, want: qjs.IntrinsicObject}, + {name: "array", code: `[1, 2, 3]`, want: qjs.IntrinsicArray}, + {name: "date", code: `new Date("2024-01-02T03:04:05Z")`, want: qjs.IntrinsicDate}, + {name: "regexp", code: `/abc/gi`, want: qjs.IntrinsicRegExp}, + {name: "map", code: `new Map([["a", 1]])`, want: qjs.IntrinsicMap}, + {name: "set", code: `new Set([1, 2])`, want: qjs.IntrinsicSet}, + {name: "array_buffer", code: `new ArrayBuffer(8)`, want: qjs.IntrinsicArrayBuffer}, + {name: "data_view", code: `new DataView(new ArrayBuffer(8))`, want: qjs.IntrinsicDataView}, + {name: "uint8_array", code: `new Uint8Array(new ArrayBuffer(8))`, want: qjs.IntrinsicUint8Array}, + {name: "big_uint64_array", code: `new BigUint64Array(new ArrayBuffer(16))`, want: qjs.IntrinsicBigUint64Array}, + {name: "spoofed_to_string_tag", code: `({ [Symbol.toStringTag]: "Map" })`, want: qjs.IntrinsicObject}, + {name: "spoofed_constructor_name", code: `({ constructor: { name: "Set" } })`, want: qjs.IntrinsicObject}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + val := must(ctx.Eval("test.js", qjs.Code(tc.code))) + defer val.Free() + assert.Equal(t, tc.want, val.IntrinsicKind()) + }) + } +} + +func TestValueOwnPropertyDescriptor(t *testing.T) { + rt, ctx := setupTestContext(t) + defer rt.Close() + + t.Run("data_property", func(t *testing.T) { + val := must(ctx.Eval("test.js", qjs.Code(`({ a: 1 })`))) + defer val.Free() + + desc, err := val.OwnPropertyDescriptor("a") + require.NoError(t, err) + assert.True(t, desc.Exists) + assert.True(t, desc.Enumerable) + assert.True(t, desc.Configurable) + assert.True(t, desc.Writable) + assert.True(t, desc.HasValue) + assert.False(t, desc.IsAccessor()) + }) + + t.Run("enumerable_accessor", func(t *testing.T) { + val := must(ctx.Eval("test.js", qjs.Code(`(() => { + const obj = {}; + Object.defineProperty(obj, "a", { enumerable: true, get() { return 1; } }); + return obj; + })()`))) + defer val.Free() + + desc, err := val.OwnPropertyDescriptor("a") + require.NoError(t, err) + assert.True(t, desc.Exists) + assert.True(t, desc.Enumerable) + assert.True(t, desc.HasGetter) + assert.False(t, desc.HasValue) + assert.True(t, desc.IsAccessor()) + }) + + t.Run("non_enumerable_accessor", func(t *testing.T) { + val := must(ctx.Eval("test.js", qjs.Code(`(() => { + const obj = {}; + Object.defineProperty(obj, "a", { enumerable: false, get() { return 1; } }); + return obj; + })()`))) + defer val.Free() + + desc, err := val.OwnPropertyDescriptor("a") + require.NoError(t, err) + assert.True(t, desc.Exists) + assert.False(t, desc.Enumerable) + assert.True(t, desc.HasGetter) + assert.True(t, desc.IsAccessor()) + }) + + t.Run("null_prototype_object", func(t *testing.T) { + val := must(ctx.Eval("test.js", qjs.Code(`(() => { + const obj = Object.create(null); + Object.defineProperty(obj, "a", { value: 1, enumerable: true, writable: true, configurable: true }); + return obj; + })()`))) + defer val.Free() + + desc, err := val.OwnPropertyDescriptor("a") + require.NoError(t, err) + assert.True(t, desc.Exists) + assert.True(t, desc.Enumerable) + assert.True(t, desc.Writable) + assert.True(t, desc.Configurable) + + names, err := val.EnumerableOwnPropertyNames() + require.NoError(t, err) + assert.Equal(t, []string{"a"}, names) + }) +} + func TestValueConversions(t *testing.T) { rt, ctx := setupTestContext(t) defer rt.Close()