diff --git a/Makefile b/Makefile index dcbbf7e5e..f6092cd22 100644 --- a/Makefile +++ b/Makefile @@ -243,7 +243,7 @@ endif all: $(OBJDIR) $(OBJDIR)/quickjs.check.o $(OBJDIR)/qjs.check.o $(PROGS) -QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/dtoa.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o +QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/quickjs-host.o $(OBJDIR)/quickjs-dv.o $(OBJDIR)/quickjs-sha256.o $(OBJDIR)/dtoa.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS) diff --git a/quickjs-dv.c b/quickjs-dv.c new file mode 100644 index 000000000..e82dad50d --- /dev/null +++ b/quickjs-dv.c @@ -0,0 +1,1087 @@ +#include "quickjs.h" +#include +#include +#include +#include +#include + +#ifndef JS_CLASS_OBJECT +/* Upstream class id for Object; used here to fetch the intrinsic prototype + without relying on the global Object binding. */ +#define JS_CLASS_OBJECT 1 +#endif + +/* Internal allocator hooks from quickjs.c */ +extern void *js_malloc(JSContext *ctx, size_t size); +extern void *js_realloc(JSContext *ctx, void *ptr, size_t size); +extern void js_free(JSContext *ctx, void *ptr); + +#define DV_CBOR_MAJOR_UINT 0 +#define DV_CBOR_MAJOR_NINT 1 +#define DV_CBOR_MAJOR_TEXT 3 +#define DV_CBOR_MAJOR_ARRAY 4 +#define DV_CBOR_MAJOR_MAP 5 +#define DV_CBOR_MAJOR_SIMPLE 7 + +static const int64_t dv_max_safe_int = 9007199254740991LL; /* 2^53 - 1 */ +static const int64_t dv_min_safe_int = -9007199254740991LL; /* -(2^53 - 1) */ + +static const JSDvLimits *dv_limits_or_default(const JSDvLimits *limits) { + return limits ? limits : &JS_DV_LIMIT_DEFAULTS; +} + +typedef struct { + JSContext *ctx; + uint8_t *data; + size_t size; + size_t capacity; + size_t max_size; +} JSDvBuilder; + +typedef struct { + const uint8_t *data; + size_t size; + size_t pos; + JSContext *ctx; +} JSDvReader; + +typedef struct { + JSAtom atom; + JSValue key_value; + uint8_t *encoded_key; + size_t encoded_key_len; +} JSDvKeyEntry; + +static int dv_throw(JSContext *ctx, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[256]; + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + JS_ThrowTypeError(ctx, "%s", buf); + return -1; +} + +static int dv_builder_reserve(JSDvBuilder *builder, size_t additional) { + if (additional > builder->max_size - builder->size) { + JS_ThrowTypeError( + builder->ctx, + "DV encode: encoded DV exceeds maxEncodedBytes (%zu > %zu)", + builder->size + additional, + builder->max_size); + return -1; + } + + size_t required = builder->size + additional; + if (required <= builder->capacity) { + return 0; + } + + size_t new_capacity = builder->capacity ? builder->capacity : 64; + while (new_capacity < required) { + size_t next = new_capacity * 2; + if (next <= new_capacity) { + new_capacity = builder->max_size; + break; + } + new_capacity = next; + } + + if (new_capacity > builder->max_size) { + new_capacity = builder->max_size; + } + + uint8_t *reallocated = js_realloc(builder->ctx, builder->data, new_capacity); + if (!reallocated) { + return -1; + } + + builder->data = reallocated; + builder->capacity = new_capacity; + return 0; +} + +static void dv_builder_free(JSDvBuilder *builder) { + if (builder->data) { + js_free(builder->ctx, builder->data); + builder->data = NULL; + } + builder->size = 0; + builder->capacity = 0; +} + +static int dv_builder_push_u8(JSDvBuilder *builder, uint8_t value) { + if (dv_builder_reserve(builder, 1) != 0) { + return -1; + } + builder->data[builder->size++] = value; + return 0; +} + +static int dv_builder_push_bytes(JSDvBuilder *builder, const uint8_t *data, size_t length) { + if (length == 0) { + return 0; + } + if (dv_builder_reserve(builder, length) != 0) { + return -1; + } + memcpy(builder->data + builder->size, data, length); + builder->size += length; + return 0; +} + +static int dv_builder_push_u64_be(JSDvBuilder *builder, uint64_t value, size_t width) { + uint8_t buf[8]; + for (size_t i = 0; i < width; i++) { + buf[width - 1 - i] = (uint8_t)(value & 0xff); + value >>= 8; + } + return dv_builder_push_bytes(builder, buf, width); +} + +static int dv_validate_utf8(JSContext *ctx, const uint8_t *data, size_t length) { + size_t i = 0; + while (i < length) { + uint8_t byte = data[i]; + uint32_t codepoint; + size_t needed; + + if (byte < 0x80) { + codepoint = byte; + needed = 0; + } else if ((byte & 0xe0) == 0xc0) { + codepoint = byte & 0x1f; + needed = 1; + if (codepoint == 0) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + } else if ((byte & 0xf0) == 0xe0) { + codepoint = byte & 0x0f; + needed = 2; + } else if ((byte & 0xf8) == 0xf0) { + codepoint = byte & 0x07; + needed = 3; + } else { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + + if (i + needed >= length) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + + for (size_t j = 0; j < needed; j++) { + uint8_t cont = data[i + 1 + j]; + if ((cont & 0xc0) != 0x80) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + codepoint = (codepoint << 6) | (cont & 0x3f); + } + + if ((needed == 1 && codepoint < 0x80) || + (needed == 2 && codepoint < 0x800) || + (needed == 3 && codepoint < 0x10000)) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + + if (codepoint > 0x10ffff) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + if (codepoint >= 0xd800 && codepoint <= 0xdfff) { + return dv_throw(ctx, "DV string contains lone surrogate code points"); + } + + i += needed + 1; + } + return 0; +} + +static int dv_encode_type_and_length(JSDvBuilder *builder, uint8_t major, uint64_t length) { + if (length <= 23) { + return dv_builder_push_u8(builder, (uint8_t)((major << 5) | length)); + } + if (length <= 0xff) { + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 24)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 1); + } + if (length <= 0xffff) { + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 25)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 2); + } + if (length <= 0xffffffff) { + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 26)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 4); + } + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 27)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 8); +} + +static int dv_encode_number(JSContext *ctx, double value, JSDvBuilder *builder) { + if (!isfinite(value)) { + return dv_throw(ctx, "DV numbers must be finite"); + } + + if (value == 0 && signbit(value)) { + value = 0; + } + + double int_part; + if (modf(value, &int_part) == 0.0) { + if (value > (double)dv_max_safe_int || value < (double)dv_min_safe_int) { + return dv_throw(ctx, + "integer is outside safe range (not in [%" PRId64 ", %" PRId64 "])", + dv_min_safe_int, + dv_max_safe_int); + } + + uint64_t unsigned_val; + if (value >= 0) { + unsigned_val = (uint64_t)value; + return dv_encode_type_and_length(builder, DV_CBOR_MAJOR_UINT, unsigned_val); + } + + unsigned_val = (uint64_t)(-1 - (int64_t)value); + return dv_encode_type_and_length(builder, DV_CBOR_MAJOR_NINT, unsigned_val); + } + + if (dv_builder_push_u8(builder, 0xfb) != 0) { + return -1; + } + union { + double d; + uint64_t u; + } u; + u.d = value; + return dv_builder_push_u64_be(builder, u.u, 8); +} + +static int dv_encode_string_bytes(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint8_t **out_bytes, + size_t *out_len) { + size_t byte_length = 0; + const char *raw = JS_ToCStringLen2(ctx, &byte_length, value, 0); + if (!raw) { + return -1; + } + + if (byte_length > limits->max_string_bytes) { + JS_FreeCString(ctx, raw); + return dv_throw(ctx, + "string exceeds maxStringBytes (%zu > %u)", + byte_length, + limits->max_string_bytes); + } + + if (dv_validate_utf8(ctx, (const uint8_t *)raw, byte_length) != 0) { + JS_FreeCString(ctx, raw); + return -1; + } + + uint8_t *copy = NULL; + if (byte_length > 0) { + copy = js_malloc(ctx, byte_length); + if (!copy) { + JS_FreeCString(ctx, raw); + return -1; + } + memcpy(copy, raw, byte_length); + } + JS_FreeCString(ctx, raw); + *out_bytes = copy; + *out_len = byte_length; + return 0; +} + +static int dv_encode_value(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder); + +static int dv_encode_array(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder) { + uint32_t next_depth = depth + 1; + if (next_depth > limits->max_depth) { + return dv_throw(ctx, "maxDepth %u exceeded", limits->max_depth); + } + + uint64_t length64 = 0; + JSValue length_val = JS_GetPropertyStr(ctx, value, "length"); + if (JS_IsException(length_val)) { + return -1; + } + int len_rc = JS_ToIndex(ctx, &length64, length_val); + JS_FreeValue(ctx, length_val); + if (len_rc < 0) { + return -1; + } + + if (length64 > limits->max_array_length) { + return dv_throw(ctx, + "array length exceeds maxArrayLength (%" PRIu64 " > %u)", + length64, + limits->max_array_length); + } + + uint32_t length = (uint32_t)length64; + if (dv_encode_type_and_length(builder, DV_CBOR_MAJOR_ARRAY, length) != 0) { + return -1; + } + + for (uint32_t i = 0; i < length; i++) { + JSValue element = JS_GetPropertyUint32(ctx, value, i); + if (JS_IsException(element)) { + return -1; + } + int rc = dv_encode_value(ctx, element, limits, next_depth, builder); + JS_FreeValue(ctx, element); + if (rc != 0) { + return -1; + } + } + + return 0; +} + +static int dv_compare_encoded_keys(const void *a, const void *b) { + const JSDvKeyEntry *ka = (const JSDvKeyEntry *)a; + const JSDvKeyEntry *kb = (const JSDvKeyEntry *)b; + + if (ka->encoded_key_len != kb->encoded_key_len) { + return ka->encoded_key_len < kb->encoded_key_len ? -1 : 1; + } + return memcmp(ka->encoded_key, kb->encoded_key, ka->encoded_key_len); +} + +static int dv_encode_object(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder) { + uint32_t next_depth = depth + 1; + if (next_depth > limits->max_depth) { + return dv_throw(ctx, "maxDepth %u exceeded", limits->max_depth); + } + + JSValue proto = JS_GetPrototype(ctx, value); + if (JS_IsException(proto)) { + return -1; + } + + int is_plain = 0; + if (JS_IsNull(proto)) { + is_plain = 1; + } else { + JSValue object_proto = JS_GetClassProto(ctx, JS_CLASS_OBJECT); + if (JS_IsException(object_proto)) { + JS_FreeValue(ctx, proto); + return -1; + } + + int same = JS_SameValue(ctx, proto, object_proto); + JS_FreeValue(ctx, object_proto); + if (same < 0) { + JS_FreeValue(ctx, proto); + return -1; + } + + is_plain = same; + } + + JS_FreeValue(ctx, proto); + + if (!is_plain) { + return dv_throw(ctx, "unsupported DV type: object"); + } + + JSPropertyEnum *props = NULL; + uint32_t prop_len = 0; + if (JS_GetOwnPropertyNames(ctx, + &props, + &prop_len, + value, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + return -1; + } + + if (prop_len > limits->max_map_length) { + JS_FreePropertyEnum(ctx, props, prop_len); + return dv_throw(ctx, + "map entries exceed maxMapLength (%u > %u)", + prop_len, + limits->max_map_length); + } + + JSDvKeyEntry *entries = NULL; + if (prop_len > 0) { + entries = js_malloc(ctx, prop_len * sizeof(*entries)); + if (!entries) { + JS_FreePropertyEnum(ctx, props, prop_len); + return -1; + } + } + + for (uint32_t i = 0; i < prop_len; i++) { + entries[i].atom = props[i].atom; + entries[i].encoded_key = NULL; + entries[i].encoded_key_len = 0; + entries[i].key_value = JS_UNDEFINED; + + JSValue key_val = JS_AtomToString(ctx, props[i].atom); + if (JS_IsException(key_val)) { + goto encode_object_error; + } + + uint8_t *key_bytes = NULL; + size_t key_len = 0; + if (dv_encode_string_bytes(ctx, key_val, limits, &key_bytes, &key_len) != 0) { + JS_FreeValue(ctx, key_val); + goto encode_object_error; + } + + JSDvBuilder key_builder = { + .ctx = ctx, + .data = NULL, + .size = 0, + .capacity = 0, + .max_size = limits->max_encoded_bytes, + }; + + if (dv_encode_type_and_length(&key_builder, DV_CBOR_MAJOR_TEXT, key_len) != 0 || + dv_builder_push_bytes(&key_builder, key_bytes, key_len) != 0) { + dv_builder_free(&key_builder); + js_free(ctx, key_bytes); + JS_FreeValue(ctx, key_val); + goto encode_object_error; + } + + if (key_bytes) { + js_free(ctx, key_bytes); + } + + entries[i].encoded_key = key_builder.data; + entries[i].encoded_key_len = key_builder.size; + entries[i].key_value = key_val; + } + + qsort(entries, prop_len, sizeof(entries[0]), dv_compare_encoded_keys); + + for (uint32_t i = 1; i < prop_len; i++) { + int cmp = dv_compare_encoded_keys(&entries[i - 1], &entries[i]); + if (cmp == 0) { + JSValue str_val = JS_JSONStringify(ctx, + entries[i].key_value, + JS_UNDEFINED, + JS_UNDEFINED); + if (!JS_IsException(str_val)) { + const char *dup = JS_ToCString(ctx, str_val); + const char *dup_key = dup ? dup : ""; + dv_throw(ctx, "map contains duplicate key %s", dup_key); + if (dup) { + JS_FreeCString(ctx, dup); + } + JS_FreeValue(ctx, str_val); + } else { + dv_throw(ctx, "map contains duplicate key"); + } + goto encode_object_error; + } + } + + if (dv_encode_type_and_length(builder, DV_CBOR_MAJOR_MAP, prop_len) != 0) { + goto encode_object_error; + } + + for (uint32_t i = 0; i < prop_len; i++) { + if (dv_builder_push_bytes(builder, entries[i].encoded_key, entries[i].encoded_key_len) != 0) { + goto encode_object_error; + } + JSValue prop_val = JS_GetProperty(ctx, value, entries[i].atom); + if (JS_IsException(prop_val)) { + goto encode_object_error; + } + int rc = dv_encode_value(ctx, prop_val, limits, next_depth, builder); + JS_FreeValue(ctx, prop_val); + if (rc != 0) { + goto encode_object_error; + } + } + + for (uint32_t i = 0; i < prop_len; i++) { + if (entries[i].encoded_key) { + js_free(ctx, entries[i].encoded_key); + } + JS_FreeValue(ctx, entries[i].key_value); + } + if (entries) { + js_free(ctx, entries); + } + if (props) { + JS_FreePropertyEnum(ctx, props, prop_len); + } + return 0; + +encode_object_error: + if (entries) { + for (uint32_t i = 0; i < prop_len; i++) { + if (entries[i].encoded_key) { + js_free(ctx, entries[i].encoded_key); + } + if (!JS_IsUndefined(entries[i].key_value)) { + JS_FreeValue(ctx, entries[i].key_value); + } + } + js_free(ctx, entries); + } + if (props) { + JS_FreePropertyEnum(ctx, props, prop_len); + } + return -1; +} + +static int dv_encode_value(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder) { + if (depth > limits->max_depth) { + return dv_throw(ctx, "maxDepth %u exceeded", limits->max_depth); + } + + int tag = JS_VALUE_GET_NORM_TAG(value); + switch (tag) { + case JS_TAG_NULL: + return dv_builder_push_u8(builder, 0xf6); + case JS_TAG_BOOL: { + return dv_builder_push_u8(builder, JS_VALUE_GET_BOOL(value) ? 0xf5 : 0xf4); + } + case JS_TAG_INT: + return dv_encode_number(ctx, JS_VALUE_GET_INT(value), builder); + case JS_TAG_FLOAT64: + return dv_encode_number(ctx, JS_VALUE_GET_FLOAT64(value), builder); + case JS_TAG_STRING: + case JS_TAG_STRING_ROPE: { + uint8_t *bytes = NULL; + size_t len = 0; + if (dv_encode_string_bytes(ctx, value, limits, &bytes, &len) != 0) { + return -1; + } + int rc = dv_encode_type_and_length(builder, DV_CBOR_MAJOR_TEXT, len); + if (rc == 0) { + rc = dv_builder_push_bytes(builder, bytes, len); + } + if (bytes) { + js_free(ctx, bytes); + } + return rc; + } + case JS_TAG_OBJECT: { + int is_array = JS_IsArray(ctx, value); + if (is_array < 0) { + return -1; + } + if (is_array) { + return dv_encode_array(ctx, value, limits, depth, builder); + } + return dv_encode_object(ctx, value, limits, depth, builder); + } + default: + return dv_throw(ctx, "unsupported DV type: %d", tag); + } +} + +int JS_EncodeDV(JSContext *ctx, + JSValueConst value, + const JSDvLimits *maybe_limits, + JSDvBuffer *out_buffer) { + const JSDvLimits *limits = dv_limits_or_default(maybe_limits); + if (out_buffer) { + out_buffer->data = NULL; + out_buffer->length = 0; + } + + JSDvBuilder builder = { + .ctx = ctx, + .data = NULL, + .size = 0, + .capacity = 0, + .max_size = limits->max_encoded_bytes, + }; + + if (dv_encode_value(ctx, value, limits, 0, &builder) != 0) { + dv_builder_free(&builder); + return -1; + } + + if (out_buffer) { + out_buffer->data = builder.data; + out_buffer->length = builder.size; + } else { + dv_builder_free(&builder); + } + return 0; +} + +static int dv_reader_need(JSDvReader *reader, size_t amount) { + if (reader->pos + amount > reader->size) { + JS_ThrowTypeError(reader->ctx, "unexpected end of buffer"); + return -1; + } + return 0; +} + +static int dv_reader_read_u8(JSDvReader *reader, uint8_t *out) { + if (dv_reader_need(reader, 1) != 0) { + return -1; + } + *out = reader->data[reader->pos++]; + return 0; +} + +static int dv_reader_read_be(JSDvReader *reader, size_t width, uint64_t *out) { + if (dv_reader_need(reader, width) != 0) { + return -1; + } + uint64_t value = 0; + for (size_t i = 0; i < width; i++) { + value = (value << 8) | reader->data[reader->pos + i]; + } + reader->pos += width; + *out = value; + return 0; +} + +static int dv_read_length(JSDvReader *reader, uint8_t additional, uint64_t *out) { + if (additional <= 23) { + *out = additional; + return 0; + } + + uint64_t value = 0; + switch (additional) { + case 24: + if (dv_reader_read_be(reader, 1, &value) != 0) { + return -1; + } + if (value < 24) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 25: + if (dv_reader_read_be(reader, 2, &value) != 0) { + return -1; + } + if (value <= 0xff) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 26: + if (dv_reader_read_be(reader, 4, &value) != 0) { + return -1; + } + if (value <= 0xffff) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 27: + if (dv_reader_read_be(reader, 8, &value) != 0) { + return -1; + } + if (value <= 0xffffffff) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 31: + return dv_throw(reader->ctx, "indefinite lengths are not allowed"); + default: + return dv_throw(reader->ctx, "unsupported additional info %u", additional); + } + + *out = value; + return 0; +} + +static JSValue dv_decode_value(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth); + +static JSValue dv_decode_text(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint8_t additional) { + uint64_t length64 = 0; + if (dv_read_length(reader, additional, &length64) != 0) { + return JS_EXCEPTION; + } + + if (length64 > limits->max_string_bytes) { + JS_ThrowTypeError(ctx, + "string exceeds maxStringBytes (%" PRIu64 " > %u)", + length64, + limits->max_string_bytes); + return JS_EXCEPTION; + } + + size_t length = (size_t)length64; + if (dv_reader_need(reader, length) != 0) { + return JS_EXCEPTION; + } + + const uint8_t *bytes = reader->data + reader->pos; + if (dv_validate_utf8(ctx, bytes, length) != 0) { + return JS_EXCEPTION; + } + + JSValue str = JS_NewStringLen(ctx, (const char *)bytes, length); + reader->pos += length; + return str; +} + +static JSValue dv_decode_array(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth, + uint8_t additional) { + uint64_t length64 = 0; + if (dv_read_length(reader, additional, &length64) != 0) { + return JS_EXCEPTION; + } + + if (length64 > limits->max_array_length) { + JS_ThrowTypeError(ctx, + "array length exceeds maxArrayLength (%" PRIu64 " > %u)", + length64, + limits->max_array_length); + return JS_EXCEPTION; + } + + if (depth + 1 > limits->max_depth) { + JS_ThrowTypeError(ctx, "maxDepth %u exceeded", limits->max_depth); + return JS_EXCEPTION; + } + + uint32_t length = (uint32_t)length64; + JSValue arr = JS_NewArray(ctx); + if (JS_IsException(arr)) { + return JS_EXCEPTION; + } + + for (uint32_t i = 0; i < length; i++) { + JSValue element = dv_decode_value(ctx, reader, limits, depth + 1); + if (JS_IsException(element)) { + JS_FreeValue(ctx, arr); + return JS_EXCEPTION; + } + if (JS_SetPropertyUint32(ctx, arr, i, element) < 0) { + JS_FreeValue(ctx, arr); + return JS_EXCEPTION; + } + } + + return arr; +} + +static JSValue dv_decode_map(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth, + uint8_t additional) { + uint64_t length64 = 0; + if (dv_read_length(reader, additional, &length64) != 0) { + return JS_EXCEPTION; + } + + if (length64 > limits->max_map_length) { + JS_ThrowTypeError(ctx, + "map entries exceed maxMapLength (%" PRIu64 " > %u)", + length64, + limits->max_map_length); + return JS_EXCEPTION; + } + + if (depth + 1 > limits->max_depth) { + JS_ThrowTypeError(ctx, "maxDepth %u exceeded", limits->max_depth); + return JS_EXCEPTION; + } + + uint32_t length = (uint32_t)length64; + JSValue obj = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + uint8_t *prev_key = NULL; + size_t prev_key_len = 0; + + for (uint32_t i = 0; i < length; i++) { + size_t key_start = reader->pos; + uint8_t key_initial; + if (dv_reader_read_u8(reader, &key_initial) != 0) { + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + uint8_t key_major = key_initial >> 5; + if (key_major != DV_CBOR_MAJOR_TEXT) { + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + JS_ThrowTypeError(ctx, "map keys must be text strings"); + return JS_EXCEPTION; + } + + JSValue key_val = dv_decode_text(ctx, reader, limits, key_initial & 0x1f); + if (JS_IsException(key_val)) { + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + size_t key_end = reader->pos; + size_t key_len = key_end - key_start; + const uint8_t *key_bytes = reader->data + key_start; + + if (prev_key) { + int cmp = dv_compare_encoded_keys(&(JSDvKeyEntry){.encoded_key = prev_key, .encoded_key_len = prev_key_len}, + &(JSDvKeyEntry){.encoded_key = (uint8_t *)key_bytes, .encoded_key_len = key_len}); + if (cmp == 0) { + JS_ThrowTypeError(ctx, "map contains duplicate key"); + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + if (cmp > 0) { + JS_ThrowTypeError(ctx, "map keys are not in canonical order"); + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + } + + if (prev_key) { + js_free(ctx, prev_key); + } + prev_key = NULL; + if (key_len > 0) { + prev_key = js_malloc(ctx, key_len); + if (!prev_key) { + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + memcpy(prev_key, key_bytes, key_len); + } + prev_key_len = key_len; + + JSValue decoded = dv_decode_value(ctx, reader, limits, depth + 1); + if (JS_IsException(decoded)) { + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + size_t key_str_len = 0; + const char *key_str = JS_ToCStringLen(ctx, &key_str_len, key_val); + if (!key_str) { + JS_FreeValue(ctx, decoded); + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + JSAtom atom = JS_NewAtomLen(ctx, key_str, key_str_len); + JS_FreeCString(ctx, key_str); + JS_FreeValue(ctx, key_val); + if (atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, decoded); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValue(ctx, obj, atom, decoded, JS_PROP_C_W_E) < 0) { + JS_FreeAtom(ctx, atom); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + JS_FreeAtom(ctx, atom); + } + + if (prev_key) { + js_free(ctx, prev_key); + } + return obj; +} + +static JSValue dv_decode_simple_or_float(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint8_t additional) { + (void)limits; + switch (additional) { + case 20: + return JS_FALSE; + case 21: + return JS_TRUE; + case 22: + return JS_NULL; + case 27: { + uint64_t bits = 0; + if (dv_reader_read_be(reader, 8, &bits) != 0) { + return JS_EXCEPTION; + } + union { + uint64_t u; + double d; + } u; + u.u = bits; + if (!isfinite(u.d)) { + JS_ThrowTypeError(ctx, "DV numbers must be finite"); + return JS_EXCEPTION; + } + double int_part; + if (modf(u.d, &int_part) == 0.0) { + JS_ThrowTypeError(ctx, "integers must use CBOR integer encoding"); + return JS_EXCEPTION; + } + if (u.d == 0 && signbit(u.d)) { + u.d = 0; + } + return JS_NewFloat64(ctx, u.d); + } + case 24: + case 25: + case 26: + JS_ThrowTypeError(ctx, "only float64 is allowed"); + return JS_EXCEPTION; + case 31: + JS_ThrowTypeError(ctx, "indefinite lengths are not allowed"); + return JS_EXCEPTION; + default: + JS_ThrowTypeError(ctx, "unsupported simple value %u", additional); + return JS_EXCEPTION; + } +} + +static JSValue dv_decode_value(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth) { + uint8_t initial = 0; + if (dv_reader_read_u8(reader, &initial) != 0) { + return JS_EXCEPTION; + } + + uint8_t major = initial >> 5; + uint8_t additional = initial & 0x1f; + + switch (major) { + case DV_CBOR_MAJOR_UINT: { + uint64_t value = 0; + if (dv_read_length(reader, additional, &value) != 0) { + return JS_EXCEPTION; + } + if (value > (uint64_t)dv_max_safe_int) { + JS_ThrowTypeError(ctx, "integer is outside safe range (%" PRIu64 " > %" PRIu64 ")", + value, + (uint64_t)dv_max_safe_int); + return JS_EXCEPTION; + } + return JS_NewInt64(ctx, (int64_t)value); + } + case DV_CBOR_MAJOR_NINT: { + uint64_t value = 0; + if (dv_read_length(reader, additional, &value) != 0) { + return JS_EXCEPTION; + } + if (value >= (uint64_t)dv_max_safe_int) { + JS_ThrowTypeError(ctx, "integer is outside safe range (-1 - %" PRIu64 " < %" PRId64 ")", + value, + dv_min_safe_int); + return JS_EXCEPTION; + } + int64_t neg = -1 - (int64_t)value; + return JS_NewInt64(ctx, neg); + } + case DV_CBOR_MAJOR_TEXT: + return dv_decode_text(ctx, reader, limits, additional); + case DV_CBOR_MAJOR_ARRAY: + return dv_decode_array(ctx, reader, limits, depth, additional); + case DV_CBOR_MAJOR_MAP: + return dv_decode_map(ctx, reader, limits, depth, additional); + case DV_CBOR_MAJOR_SIMPLE: + return dv_decode_simple_or_float(ctx, reader, limits, additional); + default: + JS_ThrowTypeError(ctx, "unsupported CBOR major type %u", major); + return JS_EXCEPTION; + } +} + +JSValue JS_DecodeDV(JSContext *ctx, + const uint8_t *data, + size_t length, + const JSDvLimits *maybe_limits) { + const JSDvLimits *limits = dv_limits_or_default(maybe_limits); + if (length > limits->max_encoded_bytes) { + JS_ThrowTypeError(ctx, + "encoded DV exceeds maxEncodedBytes (%zu > %u)", + length, + limits->max_encoded_bytes); + return JS_EXCEPTION; + } + + JSDvReader reader = { + .data = data, + .size = length, + .pos = 0, + .ctx = ctx, + }; + + JSValue result = dv_decode_value(ctx, &reader, limits, 0); + if (JS_IsException(result)) { + return result; + } + + if (reader.pos != reader.size) { + JS_FreeValue(ctx, result); + JS_ThrowTypeError(ctx, "unexpected trailing bytes after DV value"); + return JS_EXCEPTION; + } + + return result; +} + +void JS_FreeDVBuffer(JSContext *ctx, JSDvBuffer *buffer) { + if (!buffer || !buffer->data) { + return; + } + js_free(ctx, buffer->data); + buffer->data = NULL; + buffer->length = 0; +} + +const JSDvLimits JS_DV_LIMIT_DEFAULTS = { + .max_depth = 64, + .max_encoded_bytes = 1048576, + .max_string_bytes = 262144, + .max_array_length = 65535, + .max_map_length = 65535, +}; diff --git a/quickjs-host.c b/quickjs-host.c new file mode 100644 index 000000000..b46b69fcf --- /dev/null +++ b/quickjs-host.c @@ -0,0 +1,2652 @@ +#include "cutils.h" +#include "quickjs-host.h" +#include "quickjs-internal.h" +#include +#include +#include + +#define JS_HOST_ERROR_CODE_TRANSPORT "HOST_TRANSPORT" +#define JS_HOST_ERROR_TAG_TRANSPORT "host/transport" +#define JS_HOST_ERROR_CODE_ENVELOPE_INVALID "HOST_ENVELOPE_INVALID" +#define JS_HOST_ERROR_TAG_ENVELOPE_INVALID "host/envelope_invalid" + +static JSValue js_throw_host_error_str(JSContext *ctx, const char *code, const char *tag, JSValueConst details) +{ + JSValue ret; + JSAtom code_atom = JS_NewAtom(ctx, code); + JSAtom tag_atom = JS_NewAtom(ctx, tag); + + if (code_atom == JS_ATOM_NULL || tag_atom == JS_ATOM_NULL) { + if (code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, code_atom); + if (tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, tag_atom); + return JS_EXCEPTION; + } + + ret = JS_ThrowHostError(ctx, code_atom, tag_atom, details); + JS_FreeAtom(ctx, code_atom); + JS_FreeAtom(ctx, tag_atom); + return ret; +} + +JSValue JS_ThrowHostError(JSContext *ctx, JSAtom code_atom, JSAtom tag_atom, JSValueConst details) +{ + JSValue obj, name, msg, code_val, tag_val, details_val; + + obj = JS_NewError(ctx); + if (JS_IsException(obj)) + return JS_EXCEPTION; + + name = JS_NewString(ctx, "HostError"); + msg = JS_AtomToString(ctx, tag_atom); + code_val = JS_AtomToString(ctx, code_atom); + tag_val = JS_DupValue(ctx, msg); + details_val = JS_DupValue(ctx, details); + + if (JS_IsException(name) || JS_IsException(msg) || JS_IsException(code_val) || + JS_IsException(tag_val) || JS_IsException(details_val)) { + if (!JS_IsException(name)) + JS_FreeValue(ctx, name); + if (!JS_IsException(msg)) + JS_FreeValue(ctx, msg); + if (!JS_IsException(code_val)) + JS_FreeValue(ctx, code_val); + if (!JS_IsException(tag_val)) + JS_FreeValue(ctx, tag_val); + if (!JS_IsException(details_val)) + JS_FreeValue(ctx, details_val); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + + JS_DefinePropertyValueStr(ctx, obj, "name", name, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "message", msg, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "code", code_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "tag", tag_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "details", details_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_Throw(ctx, obj); + return JS_EXCEPTION; +} + +JSValue JS_ThrowHostTransportError(JSContext *ctx) +{ + return js_throw_host_error_str(ctx, + JS_HOST_ERROR_CODE_TRANSPORT, + JS_HOST_ERROR_TAG_TRANSPORT, + JS_UNDEFINED); +} + +static JSValue js_throw_host_envelope_invalid(JSContext *ctx) +{ + return js_throw_host_error_str(ctx, + JS_HOST_ERROR_CODE_ENVELOPE_INVALID, + JS_HOST_ERROR_TAG_ENVELOPE_INVALID, + JS_UNDEFINED); +} + +void JS_FreeHostResponse(JSContext *ctx, JSHostResponse *resp) +{ + if (!ctx || !resp) + return; + + if (!JS_IsUndefined(resp->ok)) + JS_FreeValue(ctx, resp->ok); + if (!JS_IsUndefined(resp->err_details)) + JS_FreeValue(ctx, resp->err_details); + if (resp->err_code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, resp->err_code_atom); + if (resp->err_tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, resp->err_tag_atom); + + resp->is_error = 0; + resp->units = 0; + resp->ok = JS_UNDEFINED; + resp->err_details = JS_UNDEFINED; + resp->err_code_atom = JS_ATOM_NULL; + resp->err_tag_atom = JS_ATOM_NULL; +} + +int JS_ParseHostResponse(JSContext *ctx, + const uint8_t *data, + size_t length, + const JSHostResponseValidation *validation, + JSHostResponse *out) +{ + JSHostResponse tmp; + JSValue envelope = JS_UNDEFINED; + JSValue ok_val = JS_UNDEFINED; + JSValue err_val = JS_UNDEFINED; + JSValue units_val = JS_UNDEFINED; + JSValue err_code_val = JS_UNDEFINED; + JSValue err_details_val = JS_UNDEFINED; + JSPropertyEnum *props = NULL; + JSPropertyEnum *err_props = NULL; + uint32_t props_len = 0; + uint32_t err_props_len = 0; + JSAtom ok_atom = JS_ATOM_NULL; + JSAtom err_atom = JS_ATOM_NULL; + JSAtom units_atom = JS_ATOM_NULL; + JSAtom code_atom = JS_ATOM_NULL; + JSAtom details_atom = JS_ATOM_NULL; + BOOL has_pending_exception = FALSE; + int ret = -1; + + if (!ctx || !validation || !out) + return -1; + + if (validation->error_count > 0 && !validation->errors) { + JS_ThrowTypeError(ctx, "host response errors table is required"); + return -1; + } + + tmp.is_error = 0; + tmp.units = 0; + tmp.ok = JS_UNDEFINED; + tmp.err_code_atom = JS_ATOM_NULL; + tmp.err_tag_atom = JS_ATOM_NULL; + tmp.err_details = JS_UNDEFINED; + + if (!data && length > 0) { + js_throw_host_envelope_invalid(ctx); + return -1; + } + + if (length > JS_DV_LIMIT_DEFAULTS.max_encoded_bytes) { + js_throw_host_envelope_invalid(ctx); + return -1; + } + + envelope = JS_DecodeDV(ctx, data, length, &JS_DV_LIMIT_DEFAULTS); + if (JS_IsException(envelope)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (!JS_IsObject(envelope)) + goto envelope_invalid; + + ok_atom = JS_NewAtom(ctx, "ok"); + err_atom = JS_NewAtom(ctx, "err"); + units_atom = JS_NewAtom(ctx, "units"); + code_atom = JS_NewAtom(ctx, "code"); + details_atom = JS_NewAtom(ctx, "details"); + if (ok_atom == JS_ATOM_NULL || err_atom == JS_ATOM_NULL || units_atom == JS_ATOM_NULL || + code_atom == JS_ATOM_NULL || details_atom == JS_ATOM_NULL) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (JS_GetOwnPropertyNames(ctx, &props, &props_len, envelope, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + for (uint32_t i = 0; i < props_len; i++) { + JSAtom atom = props[i].atom; + if (atom != ok_atom && atom != err_atom && atom != units_atom) { + goto envelope_invalid; + } + } + + ok_val = JS_GetProperty(ctx, envelope, ok_atom); + if (JS_IsException(ok_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + err_val = JS_GetProperty(ctx, envelope, err_atom); + if (JS_IsException(err_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + units_val = JS_GetProperty(ctx, envelope, units_atom); + if (JS_IsException(units_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + BOOL has_ok = !JS_IsUndefined(ok_val); + BOOL has_err = !JS_IsUndefined(err_val); + + if ((has_ok && has_err) || (!has_ok && !has_err)) + goto envelope_invalid; + + if (JS_IsUndefined(units_val)) + goto envelope_invalid; + + if (!JS_IsNumber(units_val)) + goto envelope_invalid; + + double units_d; + if (JS_ToFloat64(ctx, &units_d, units_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (!isfinite(units_d)) + goto envelope_invalid; + + if ((units_d == 0.0 && signbit(units_d)) || units_d < 0.0 || + units_d > (double)UINT32_MAX || units_d > (double)validation->max_units) + goto envelope_invalid; + + if (floor(units_d) != units_d) + goto envelope_invalid; + + tmp.units = (uint32_t)units_d; + + if (has_ok) { + tmp.is_error = 0; + tmp.ok = ok_val; + ok_val = JS_UNDEFINED; + } else { + if (!JS_IsObject(err_val)) + goto envelope_invalid; + + if (JS_GetOwnPropertyNames(ctx, &err_props, &err_props_len, err_val, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + for (uint32_t i = 0; i < err_props_len; i++) { + JSAtom atom = err_props[i].atom; + if (atom != code_atom && atom != details_atom) { + goto envelope_invalid; + } + } + + err_code_val = JS_GetProperty(ctx, err_val, code_atom); + if (JS_IsException(err_code_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (JS_IsUndefined(err_code_val)) + goto envelope_invalid; + + if (!JS_IsString(err_code_val)) + goto envelope_invalid; + + const char *code_str = JS_ToCString(ctx, err_code_val); + if (!code_str) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + JSAtom code_atom_val = JS_NewAtom(ctx, code_str); + JS_FreeCString(ctx, code_str); + if (code_atom_val == JS_ATOM_NULL) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + JSAtom tag_atom_val = JS_ATOM_NULL; + for (size_t i = 0; i < validation->error_count; i++) { + if (validation->errors[i].code_atom == code_atom_val) { + tag_atom_val = validation->errors[i].tag_atom; + break; + } + } + + if (tag_atom_val == JS_ATOM_NULL) { + JS_FreeAtom(ctx, code_atom_val); + goto envelope_invalid; + } + + tmp.is_error = 1; + tmp.err_code_atom = code_atom_val; + tmp.err_tag_atom = JS_DupAtom(ctx, tag_atom_val); + if (tmp.err_tag_atom == JS_ATOM_NULL) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + err_details_val = JS_GetProperty(ctx, err_val, details_atom); + if (JS_IsException(err_details_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (!JS_IsUndefined(err_details_val)) { + tmp.err_details = err_details_val; + err_details_val = JS_UNDEFINED; + } + JS_FreeValue(ctx, err_code_val); + err_code_val = JS_UNDEFINED; + } + + ret = 0; + *out = tmp; + tmp.ok = JS_UNDEFINED; + tmp.err_details = JS_UNDEFINED; + tmp.err_code_atom = JS_ATOM_NULL; + tmp.err_tag_atom = JS_ATOM_NULL; + goto done; + +envelope_invalid: + if (has_pending_exception) { + JSValue pending = JS_GetException(ctx); + if (!JS_IsException(pending)) { + BOOL is_out_of_gas = FALSE; + + if (JS_IsError(ctx, pending)) { + JSValue code_val = JS_GetPropertyStr(ctx, pending, "code"); + if (JS_IsException(code_val)) { + JSValue exc2 = JS_GetException(ctx); + if (!JS_IsException(exc2)) + JS_FreeValue(ctx, exc2); + JS_FreeValue(ctx, code_val); + } else { + const char *code_str = JS_ToCString(ctx, code_val); + if (code_str) { + if (strcmp(code_str, "OOG") == 0) + is_out_of_gas = TRUE; + JS_FreeCString(ctx, code_str); + } + JS_FreeValue(ctx, code_val); + } + } + + if (is_out_of_gas) { + JS_Throw(ctx, pending); + JS_SetUncatchableException(ctx, TRUE); + ret = -1; + JS_FreeHostResponse(ctx, &tmp); + goto done; + } + JS_FreeValue(ctx, pending); + } + } + + js_throw_host_envelope_invalid(ctx); + ret = -1; + JS_FreeHostResponse(ctx, &tmp); + +done: + if (props) + JS_FreePropertyEnum(ctx, props, props_len); + if (err_props) + JS_FreePropertyEnum(ctx, err_props, err_props_len); + if (ok_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, ok_atom); + if (err_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, err_atom); + if (units_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, units_atom); + if (code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, code_atom); + if (details_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, details_atom); + if (!JS_IsUndefined(envelope)) + JS_FreeValue(ctx, envelope); + if (!JS_IsUndefined(ok_val)) + JS_FreeValue(ctx, ok_val); + if (!JS_IsUndefined(err_val)) + JS_FreeValue(ctx, err_val); + if (!JS_IsUndefined(units_val)) + JS_FreeValue(ctx, units_val); + if (!JS_IsUndefined(err_code_val)) + JS_FreeValue(ctx, err_code_val); + if (!JS_IsUndefined(err_details_val)) + JS_FreeValue(ctx, err_details_val); + return ret; +} + +/* ------------------------------------------------------------------------- */ +/* Host manifest parsing and Host.v1 generation (T-040) */ + +extern void *js_malloc(JSContext *ctx, size_t size); +extern void js_free(JSContext *ctx, void *ptr); +extern void *js_realloc(JSContext *ctx, void *ptr, size_t size); + +typedef enum { + JS_HOST_SCHEMA_STRING, + JS_HOST_SCHEMA_DV, + JS_HOST_SCHEMA_NULL, +} JSHostSchemaType; + +typedef enum { + JS_HOST_EFFECT_READ, + JS_HOST_EFFECT_EMIT, + JS_HOST_EFFECT_MUTATE, +} JSHostEffect; + +typedef struct { + JSHostSchemaType type; + uint32_t utf8_max; /* 0 when not provided */ +} JSHostArgDef; + +typedef struct { + uint32_t fn_id; + size_t path_len; + char **path_segments; + char *name; + uint32_t arity; + JSHostEffect effect; + JSHostArgDef *args; + JSHostSchemaType return_type; + uint32_t gas_base; + uint32_t gas_k_arg_bytes; + uint32_t gas_k_ret_bytes; + uint32_t gas_k_units; + char *gas_schedule_id; + uint32_t max_request_bytes; + uint32_t max_response_bytes; + uint32_t max_units; + JSHostErrorEntry *errors; + size_t error_count; +} JSHostFunctionDef; + +struct JSHostManifest { + JSHostFunctionDef *functions; + size_t function_count; +}; + +typedef struct { + JSHostTapeRecord *records; + size_t capacity; + size_t count; + size_t head; +} JSHostTapeState; + +typedef struct JSHostManifestNode { + JSContext *ctx; + JSHostManifest manifest; + JSHostTapeState tape; + struct JSHostManifestNode *next; +} JSHostManifestNode; + +static JSHostManifestNode *js_host_manifest_list = NULL; + +static JSHostManifest *js_host_find_manifest(JSContext *ctx) +{ + JSHostManifestNode *node = js_host_manifest_list; + while (node) { + if (node->ctx == ctx) + return &node->manifest; + node = node->next; + } + return NULL; +} + +static JSHostManifestNode *js_host_find_manifest_node(JSContext *ctx) +{ + JSHostManifestNode *node = js_host_manifest_list; + while (node) { + if (node->ctx == ctx) + return node; + node = node->next; + } + return NULL; +} + +static JSHostTapeState *js_host_get_tape(JSContext *ctx) +{ + JSHostManifestNode *node = js_host_find_manifest_node(ctx); + if (!node) + return NULL; + return &node->tape; +} + +static void js_host_free_function(JSContext *ctx, JSHostFunctionDef *fn) +{ + if (!fn) + return; + + if (fn->path_segments) { + for (size_t i = 0; i < fn->path_len; i++) { + if (fn->path_segments[i]) + js_free(ctx, fn->path_segments[i]); + } + js_free(ctx, fn->path_segments); + } + + if (fn->args) + js_free(ctx, fn->args); + + if (fn->errors) { + for (size_t i = 0; i < fn->error_count; i++) { + if (fn->errors[i].code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, fn->errors[i].code_atom); + if (fn->errors[i].tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, fn->errors[i].tag_atom); + } + js_free(ctx, fn->errors); + } + + if (fn->gas_schedule_id) + js_free(ctx, fn->gas_schedule_id); + + if (fn->name) + js_free(ctx, fn->name); + + memset(fn, 0, sizeof(*fn)); +} + +static void js_host_manifest_clear(JSContext *ctx, JSHostManifest *manifest) +{ + if (!manifest) + return; + + if (manifest->functions) { + for (size_t i = 0; i < manifest->function_count; i++) + js_host_free_function(ctx, &manifest->functions[i]); + js_free(ctx, manifest->functions); + } + manifest->functions = NULL; + manifest->function_count = 0; +} + +static void js_host_tape_free(JSContext *ctx, JSHostTapeState *tape) +{ + if (!tape) + return; + + if (tape->records) + js_free(ctx, tape->records); + + tape->records = NULL; + tape->capacity = 0; + tape->count = 0; + tape->head = 0; +} + +static int js_host_calc_gas_charges(const JSHostFunctionDef *fn, + size_t req_len, + size_t resp_len, + uint32_t units, + uint64_t *out_pre, + uint64_t *out_post) +{ + uint64_t pre = 0; + uint64_t post = 0; + uint64_t arg_part = 0; + uint64_t resp_part = 0; + uint64_t unit_part = 0; + + if (!fn) + return -1; + + pre = fn->gas_base; + arg_part = (uint64_t)fn->gas_k_arg_bytes * (uint64_t)req_len; + if (arg_part > UINT64_MAX - pre) + return -1; + pre += arg_part; + + resp_part = (uint64_t)fn->gas_k_ret_bytes * (uint64_t)resp_len; + unit_part = (uint64_t)fn->gas_k_units * (uint64_t)units; + if (resp_part > UINT64_MAX - unit_part) + return -1; + post = resp_part + unit_part; + + if (out_pre) + *out_pre = pre; + if (out_post) + *out_post = post; + return 0; +} + +static void js_host_tape_append(JSHostTapeState *tape, const JSHostTapeRecord *record) +{ + if (!tape || !record || tape->capacity == 0 || !tape->records) + return; + + size_t idx = tape->head; + tape->records[idx] = *record; + if (tape->count < tape->capacity) + tape->count++; + tape->head = (tape->head + 1) % tape->capacity; +} + +void JS_FreeHostManifest(JSContext *ctx) +{ + JSHostManifestNode *prev = NULL; + JSHostManifestNode *node = js_host_manifest_list; + + while (node) { + if (node->ctx == ctx) { + if (prev) + prev->next = node->next; + else + js_host_manifest_list = node->next; + + js_host_manifest_clear(ctx, &node->manifest); + js_host_tape_free(ctx, &node->tape); + js_free(ctx, node); + return; + } + prev = node; + node = node->next; + } +} + +int JS_EnableHostTape(JSContext *ctx, size_t capacity) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + JSHostTapeRecord *records = NULL; + + if (!ctx) + return -1; + + if (!tape) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return -1; + } + + if (capacity > JS_HOST_TAPE_MAX_CAPACITY) { + JS_ThrowTypeError(ctx, "tape capacity exceeds max (%u)", JS_HOST_TAPE_MAX_CAPACITY); + return -1; + } + + if (capacity == 0) { + js_host_tape_free(ctx, tape); + return 0; + } + + records = js_mallocz(ctx, sizeof(JSHostTapeRecord) * capacity); + if (!records) + return -1; + + js_host_tape_free(ctx, tape); + tape->records = records; + tape->capacity = capacity; + tape->count = 0; + tape->head = 0; + return 0; +} + +int JS_ResetHostTape(JSContext *ctx) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + + if (!ctx) + return -1; + + if (!tape) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return -1; + } + + if (tape->records && tape->capacity > 0) + memset(tape->records, 0, sizeof(JSHostTapeRecord) * tape->capacity); + + tape->count = 0; + tape->head = 0; + return 0; +} + +size_t JS_GetHostTapeLength(JSContext *ctx) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + if (!tape) + return 0; + return tape->count; +} + +int JS_ReadHostTape(JSContext *ctx, JSHostTapeRecord *out_records, size_t max_records, size_t *out_count) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + size_t to_copy = 0; + + if (!ctx) + return -1; + + if (!tape) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return -1; + } + + if (out_count) + *out_count = tape->count; + + if (!out_records || max_records == 0 || tape->count == 0 || tape->capacity == 0 || !tape->records) + return 0; + + to_copy = tape->count < max_records ? tape->count : max_records; + size_t start = (tape->head + tape->capacity - tape->count) % tape->capacity; + for (size_t i = 0; i < to_copy; i++) { + size_t idx = (start + i) % tape->capacity; + out_records[i] = tape->records[idx]; + } + + return 0; +} + +static int js_host_manifest_error(JSContext *ctx, const char *path, const char *message) +{ + if (path && message) + JS_ThrowTypeError(ctx, "abi manifest invalid: %s (%s)", message, path); + else if (message) + JS_ThrowTypeError(ctx, "abi manifest invalid: %s", message); + else + JS_ThrowTypeError(ctx, "abi manifest invalid"); + return -1; +} + +static int js_host_expect_object(JSContext *ctx, JSValueConst val, const char *path) +{ + if (!JS_IsObject(val) || JS_IsArray(ctx, val)) + return js_host_manifest_error(ctx, path, "expected object"); + return 0; +} + +static int js_host_check_keys(JSContext *ctx, + JSValueConst obj, + const char **required, + size_t required_len, + const char **optional, + size_t optional_len, + const char *path) +{ + JSPropertyEnum *props = NULL; + uint32_t props_len = 0; + uint8_t *seen = NULL; + int ret = -1; + + if (JS_GetOwnPropertyNames(ctx, &props, &props_len, obj, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) + return -1; + + if (required_len > 0) { + seen = js_malloc(ctx, required_len); + if (!seen) { + JS_FreePropertyEnum(ctx, props, props_len); + return -1; + } + memset(seen, 0, required_len); + } + + for (uint32_t i = 0; i < props_len; i++) { + const char *name = JS_AtomToCString(ctx, props[i].atom); + BOOL allowed = FALSE; + + if (!name) + goto cleanup; + + for (size_t j = 0; j < required_len; j++) { + if (strcmp(name, required[j]) == 0) { + allowed = TRUE; + if (seen) + seen[j] = 1; + break; + } + } + + if (!allowed) { + for (size_t j = 0; j < optional_len; j++) { + if (strcmp(name, optional[j]) == 0) { + allowed = TRUE; + break; + } + } + } + + JS_FreeCString(ctx, name); + + if (!allowed) { + js_host_manifest_error(ctx, path, "unknown field"); + goto cleanup; + } + } + + for (size_t j = 0; j < required_len; j++) { + if (seen && !seen[j]) { + js_host_manifest_error(ctx, path, "missing required field"); + goto cleanup; + } + } + + ret = 0; + +cleanup: + if (props) + JS_FreePropertyEnum(ctx, props, props_len); + if (seen) + js_free(ctx, seen); + return ret; +} + +static int js_host_copy_non_empty_string(JSContext *ctx, + JSValueConst val, + const char *path, + char **out) +{ + const char *tmp; + char *copy; + + if (!JS_IsString(val)) + return js_host_manifest_error(ctx, path, "expected string"); + + tmp = JS_ToCString(ctx, val); + if (!tmp) + return -1; + + if (tmp[0] == '\0') { + JS_FreeCString(ctx, tmp); + return js_host_manifest_error(ctx, path, "string must be non-empty"); + } + + copy = js_malloc(ctx, strlen(tmp) + 1); + if (!copy) { + JS_FreeCString(ctx, tmp); + return -1; + } + + strcpy(copy, tmp); + JS_FreeCString(ctx, tmp); + *out = copy; + return 0; +} + +static int js_host_validate_uint32(JSContext *ctx, + JSValueConst val, + const char *path, + uint32_t min, + uint32_t max, + uint32_t *out) +{ + double d; + + if (!JS_IsNumber(val)) + return js_host_manifest_error(ctx, path, "expected integer"); + + if (JS_ToFloat64(ctx, &d, val)) + return -1; + + if (!isfinite(d) || floor(d) != d || (d == 0.0 && signbit(d)) || d < (double)min || d > (double)max) + return js_host_manifest_error(ctx, path, "value out of range"); + + *out = (uint32_t)d; + return 0; +} + +static int js_host_array_length(JSContext *ctx, JSValueConst arr, size_t *out_len) +{ + JSValue len_val = JS_UNDEFINED; + uint32_t len32 = 0; + int ret = -1; + + len_val = JS_GetPropertyStr(ctx, arr, "length"); + if (JS_IsException(len_val)) + return -1; + + if (JS_ToUint32(ctx, &len32, len_val)) { + JS_FreeValue(ctx, len_val); + return -1; + } + + JS_FreeValue(ctx, len_val); + *out_len = (size_t)len32; + ret = 0; + return ret; +} + +static int js_host_validate_schema(JSContext *ctx, + JSValueConst schema_val, + const char *path, + JSHostSchemaType *out) +{ + const char *required[] = {"type"}; + JSValue type_val = JS_UNDEFINED; + const char *type_str; + int ret = -1; + + if (js_host_expect_object(ctx, schema_val, path)) + return -1; + + if (js_host_check_keys(ctx, schema_val, required, 1, NULL, 0, path)) + return -1; + + type_val = JS_GetPropertyStr(ctx, schema_val, "type"); + if (JS_IsException(type_val)) + goto done; + + if (!JS_IsString(type_val)) { + js_host_manifest_error(ctx, path, "schema.type must be a string"); + goto done; + } + + type_str = JS_ToCString(ctx, type_val); + if (!type_str) + goto done; + + if (strcmp(type_str, "string") == 0) { + *out = JS_HOST_SCHEMA_STRING; + } else if (strcmp(type_str, "dv") == 0) { + *out = JS_HOST_SCHEMA_DV; + } else if (strcmp(type_str, "null") == 0) { + *out = JS_HOST_SCHEMA_NULL; + } else { + js_host_manifest_error(ctx, path, "unsupported schema type"); + JS_FreeCString(ctx, type_str); + goto done; + } + + JS_FreeCString(ctx, type_str); + ret = 0; + +done: + if (!JS_IsUndefined(type_val)) + JS_FreeValue(ctx, type_val); + return ret; +} + +static int js_host_validate_js_path(JSContext *ctx, + JSValueConst path_val, + const char *path_desc, + JSHostFunctionDef *out_fn) +{ + size_t len = 0; + char **segments = NULL; + JSValue entry = JS_UNDEFINED; + int ret = -1; + + if (!JS_IsArray(ctx, path_val)) + return js_host_manifest_error(ctx, path_desc, "js_path must be an array"); + + if (js_host_array_length(ctx, path_val, &len)) + return -1; + + if (len == 0) + return js_host_manifest_error(ctx, path_desc, "js_path must contain at least one segment"); + + segments = js_malloc(ctx, sizeof(char *) * len); + if (!segments) + return -1; + memset(segments, 0, sizeof(char *) * len); + + for (size_t i = 0; i < len; i++) { + char key_buf[64]; + snprintf(key_buf, sizeof(key_buf), "%s[%zu]", path_desc, i); + + entry = JS_GetPropertyUint32(ctx, path_val, (uint32_t)i); + if (JS_IsException(entry)) + goto cleanup; + + if (js_host_copy_non_empty_string(ctx, entry, key_buf, &segments[i])) + goto cleanup; + + JS_FreeValue(ctx, entry); + entry = JS_UNDEFINED; + + const char *seg = segments[i]; + for (const char *p = seg; *p; p++) { + char c = *p; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '_' || c == '-')) { + js_host_manifest_error(ctx, key_buf, "js_path segment must match [A-Za-z0-9_-]+"); + goto cleanup; + } + } + + if (strcmp(seg, "__proto__") == 0 || strcmp(seg, "prototype") == 0 || strcmp(seg, "constructor") == 0) { + js_host_manifest_error(ctx, key_buf, "js_path segment is forbidden"); + goto cleanup; + } + } + + out_fn->path_segments = segments; + out_fn->path_len = len; + ret = 0; + +cleanup: + if (ret != 0 && segments) { + for (size_t i = 0; i < len; i++) { + if (segments[i]) + js_free(ctx, segments[i]); + } + js_free(ctx, segments); + } + if (!JS_IsUndefined(entry)) + JS_FreeValue(ctx, entry); + return ret; +} + +static int js_host_validate_error_codes(JSContext *ctx, + JSValueConst errors_val, + const char *path, + JSHostFunctionDef *out_fn) +{ + JSValue entry = JS_UNDEFINED; + JSValue code_val = JS_UNDEFINED; + JSValue tag_val = JS_UNDEFINED; + char *code_str = NULL; + char *tag_str = NULL; + int ret = -1; + + if (!JS_IsArray(ctx, errors_val)) + return js_host_manifest_error(ctx, path, "error_codes must be an array"); + + if (js_host_array_length(ctx, errors_val, &out_fn->error_count)) + return -1; + + if (out_fn->error_count == 0) { + out_fn->errors = NULL; + return 0; + } + + out_fn->errors = js_malloc(ctx, sizeof(JSHostErrorEntry) * out_fn->error_count); + if (!out_fn->errors) + return -1; + memset(out_fn->errors, 0, sizeof(JSHostErrorEntry) * out_fn->error_count); + + for (size_t i = 0; i < out_fn->error_count; i++) { + char entry_path[96]; + snprintf(entry_path, sizeof(entry_path), "%s[%zu]", path, i); + + entry = JS_GetPropertyUint32(ctx, errors_val, (uint32_t)i); + if (JS_IsException(entry)) + goto cleanup; + + const char *required[] = {"code", "tag"}; + if (js_host_expect_object(ctx, entry, entry_path) || + js_host_check_keys(ctx, entry, required, 2, NULL, 0, entry_path)) + goto cleanup; + + code_val = JS_GetPropertyStr(ctx, entry, "code"); + if (JS_IsException(code_val)) + goto cleanup; + if (js_host_copy_non_empty_string(ctx, code_val, entry_path, &code_str)) + goto cleanup; + + tag_val = JS_GetPropertyStr(ctx, entry, "tag"); + if (JS_IsException(tag_val)) + goto cleanup; + if (js_host_copy_non_empty_string(ctx, tag_val, entry_path, &tag_str)) + goto cleanup; + + if (strcmp(code_str, JS_HOST_ERROR_CODE_TRANSPORT) == 0 || + strcmp(code_str, JS_HOST_ERROR_CODE_ENVELOPE_INVALID) == 0) { + js_host_manifest_error(ctx, entry_path, "reserved error code"); + goto cleanup; + } + + if (i > 0) { + JSAtom prev_atom = out_fn->errors[i - 1].code_atom; + const char *prev = JS_AtomToCString(ctx, prev_atom); + if (!prev) + goto cleanup; + int cmp = strcmp(prev, code_str); + JS_FreeCString(ctx, prev); + if (cmp >= 0) { + js_host_manifest_error(ctx, path, "error_codes must be sorted and unique"); + goto cleanup; + } + } + + out_fn->errors[i].code_atom = JS_NewAtom(ctx, code_str); + out_fn->errors[i].tag_atom = JS_NewAtom(ctx, tag_str); + if (out_fn->errors[i].code_atom == JS_ATOM_NULL || + out_fn->errors[i].tag_atom == JS_ATOM_NULL) + goto cleanup; + + js_free(ctx, code_str); + js_free(ctx, tag_str); + code_str = NULL; + tag_str = NULL; + + JS_FreeValue(ctx, entry); + JS_FreeValue(ctx, code_val); + JS_FreeValue(ctx, tag_val); + entry = code_val = tag_val = JS_UNDEFINED; + } + + ret = 0; + +cleanup: + if (code_str) + js_free(ctx, code_str); + if (tag_str) + js_free(ctx, tag_str); + if (!JS_IsUndefined(entry)) + JS_FreeValue(ctx, entry); + if (!JS_IsUndefined(code_val)) + JS_FreeValue(ctx, code_val); + if (!JS_IsUndefined(tag_val)) + JS_FreeValue(ctx, tag_val); + if (ret != 0 && out_fn->errors) { + for (size_t i = 0; i < out_fn->error_count; i++) { + if (out_fn->errors[i].code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, out_fn->errors[i].code_atom); + if (out_fn->errors[i].tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, out_fn->errors[i].tag_atom); + } + js_free(ctx, out_fn->errors); + out_fn->errors = NULL; + } + return ret; +} + +static int js_host_validate_limits(JSContext *ctx, + JSValueConst limits_val, + const char *path, + JSHostFunctionDef *out_fn) +{ + const char *required[] = {"max_request_bytes", "max_response_bytes", "max_units"}; + const char *optional[] = {"arg_utf8_max"}; + JSValue max_req = JS_UNDEFINED; + JSValue max_resp = JS_UNDEFINED; + JSValue max_units = JS_UNDEFINED; + JSValue arg_utf8 = JS_UNDEFINED; + int ret = -1; + + if (js_host_expect_object(ctx, limits_val, path)) + return -1; + + if (js_host_check_keys(ctx, limits_val, required, 3, optional, 1, path)) + return -1; + + max_req = JS_GetPropertyStr(ctx, limits_val, "max_request_bytes"); + if (JS_IsException(max_req)) + goto done; + if (js_host_validate_uint32(ctx, max_req, path, 1, JS_DV_LIMIT_DEFAULTS.max_encoded_bytes, &out_fn->max_request_bytes)) + goto done; + + max_resp = JS_GetPropertyStr(ctx, limits_val, "max_response_bytes"); + if (JS_IsException(max_resp)) + goto done; + if (js_host_validate_uint32(ctx, max_resp, path, 1, JS_DV_LIMIT_DEFAULTS.max_encoded_bytes, &out_fn->max_response_bytes)) + goto done; + + max_units = JS_GetPropertyStr(ctx, limits_val, "max_units"); + if (JS_IsException(max_units)) + goto done; + if (js_host_validate_uint32(ctx, max_units, path, 0, UINT32_MAX, &out_fn->max_units)) + goto done; + + arg_utf8 = JS_GetPropertyStr(ctx, limits_val, "arg_utf8_max"); + if (JS_IsException(arg_utf8)) + goto done; + + if (!JS_IsUndefined(arg_utf8)) { + size_t len = 0; + if (!JS_IsArray(ctx, arg_utf8)) { + js_host_manifest_error(ctx, path, "arg_utf8_max must be an array when present"); + goto done; + } + if (js_host_array_length(ctx, arg_utf8, &len)) + goto done; + if (len != out_fn->arity) { + js_host_manifest_error(ctx, path, "arg_utf8_max length must equal arity"); + goto done; + } + + for (size_t i = 0; i < out_fn->arity; i++) { + JSValue limit_val = JS_GetPropertyUint32(ctx, arg_utf8, (uint32_t)i); + if (JS_IsException(limit_val)) + goto done; + uint32_t utf8_limit = 0; + char idx_path[96]; + snprintf(idx_path, sizeof(idx_path), "%s[%zu]", path, i); + + if (js_host_validate_uint32(ctx, + limit_val, + idx_path, + 1, + JS_DV_LIMIT_DEFAULTS.max_string_bytes, + &utf8_limit)) { + JS_FreeValue(ctx, limit_val); + goto done; + } + + JS_FreeValue(ctx, limit_val); + + if (out_fn->args[i].type != JS_HOST_SCHEMA_STRING) { + js_host_manifest_error(ctx, path, "arg_utf8_max may only be used with string arguments"); + goto done; + } + out_fn->args[i].utf8_max = utf8_limit; + } + } + + ret = 0; + +done: + if (!JS_IsUndefined(max_req)) + JS_FreeValue(ctx, max_req); + if (!JS_IsUndefined(max_resp)) + JS_FreeValue(ctx, max_resp); + if (!JS_IsUndefined(max_units)) + JS_FreeValue(ctx, max_units); + if (!JS_IsUndefined(arg_utf8)) + JS_FreeValue(ctx, arg_utf8); + return ret; +} + +static int js_host_build_function_name(JSContext *ctx, JSHostFunctionDef *fn) +{ + const char *prefix = "Host.v1"; + size_t total = strlen(prefix) + 1; /* null terminator */ + + for (size_t i = 0; i < fn->path_len; i++) + total += 1 + strlen(fn->path_segments[i]); /* dot + segment */ + + fn->name = js_malloc(ctx, total); + if (!fn->name) + return -1; + + strcpy(fn->name, prefix); + for (size_t i = 0; i < fn->path_len; i++) { + strcat(fn->name, "."); + strcat(fn->name, fn->path_segments[i]); + } + return 0; +} + +static int js_host_validate_function(JSContext *ctx, + JSValueConst fn_val, + const char *path, + JSHostFunctionDef *out_fn) +{ + const char *required[] = {"fn_id", "js_path", "effect", "arity", "arg_schema", "return_schema", "gas", "limits", "error_codes"}; + JSValue fn_id = JS_UNDEFINED; + JSValue js_path = JS_UNDEFINED; + JSValue effect = JS_UNDEFINED; + JSValue arity = JS_UNDEFINED; + JSValue arg_schema = JS_UNDEFINED; + JSValue return_schema = JS_UNDEFINED; + JSValue gas = JS_UNDEFINED; + JSValue limits = JS_UNDEFINED; + JSValue error_codes = JS_UNDEFINED; + const char *eff_str = NULL; + char *schedule_id_str = NULL; + int ret = -1; + + if (js_host_expect_object(ctx, fn_val, path)) + return -1; + + if (js_host_check_keys(ctx, fn_val, required, sizeof(required) / sizeof(required[0]), NULL, 0, path)) + return -1; + + fn_id = JS_GetPropertyStr(ctx, fn_val, "fn_id"); + if (JS_IsException(fn_id)) + goto done; + if (js_host_validate_uint32(ctx, fn_id, path, 1, UINT32_MAX, &out_fn->fn_id)) + goto done; + + js_path = JS_GetPropertyStr(ctx, fn_val, "js_path"); + if (JS_IsException(js_path)) + goto done; + if (js_host_validate_js_path(ctx, js_path, "js_path", out_fn)) + goto done; + + effect = JS_GetPropertyStr(ctx, fn_val, "effect"); + if (JS_IsException(effect)) + goto done; + if (!JS_IsString(effect)) { + js_host_manifest_error(ctx, path, "effect must be a string"); + goto done; + } + eff_str = JS_ToCString(ctx, effect); + if (!eff_str) + goto done; + if (strcmp(eff_str, "READ") == 0) { + out_fn->effect = JS_HOST_EFFECT_READ; + } else if (strcmp(eff_str, "EMIT") == 0) { + out_fn->effect = JS_HOST_EFFECT_EMIT; + } else if (strcmp(eff_str, "MUTATE") == 0) { + out_fn->effect = JS_HOST_EFFECT_MUTATE; + } else { + js_host_manifest_error(ctx, path, "unsupported effect"); + goto done; + } + JS_FreeCString(ctx, eff_str); + eff_str = NULL; + + arity = JS_GetPropertyStr(ctx, fn_val, "arity"); + if (JS_IsException(arity)) + goto done; + if (js_host_validate_uint32(ctx, arity, path, 0, UINT32_MAX, &out_fn->arity)) + goto done; + + arg_schema = JS_GetPropertyStr(ctx, fn_val, "arg_schema"); + if (JS_IsException(arg_schema)) + goto done; + if (!JS_IsArray(ctx, arg_schema)) { + js_host_manifest_error(ctx, path, "arg_schema must be an array"); + goto done; + } + + size_t arg_len = 0; + if (js_host_array_length(ctx, arg_schema, &arg_len)) + goto done; + if (arg_len != out_fn->arity) { + js_host_manifest_error(ctx, path, "arg_schema length must equal arity"); + goto done; + } + + if (out_fn->arity > 0) { + out_fn->args = js_malloc(ctx, sizeof(JSHostArgDef) * out_fn->arity); + if (!out_fn->args) + goto done; + memset(out_fn->args, 0, sizeof(JSHostArgDef) * out_fn->arity); + } + + for (size_t i = 0; i < out_fn->arity; i++) { + JSValue schema_entry = JS_GetPropertyUint32(ctx, arg_schema, (uint32_t)i); + if (JS_IsException(schema_entry)) + goto done; + char schema_path[96]; + snprintf(schema_path, sizeof(schema_path), "arg_schema[%zu]", i); + if (js_host_validate_schema(ctx, schema_entry, schema_path, &out_fn->args[i].type)) { + JS_FreeValue(ctx, schema_entry); + goto done; + } + JS_FreeValue(ctx, schema_entry); + } + + return_schema = JS_GetPropertyStr(ctx, fn_val, "return_schema"); + if (JS_IsException(return_schema)) + goto done; + if (js_host_validate_schema(ctx, return_schema, "return_schema", &out_fn->return_type)) + goto done; + + gas = JS_GetPropertyStr(ctx, fn_val, "gas"); + if (JS_IsException(gas)) + goto done; + const char *gas_required[] = {"schedule_id", "base", "k_arg_bytes", "k_ret_bytes", "k_units"}; + if (js_host_expect_object(ctx, gas, "gas") || + js_host_check_keys(ctx, gas, gas_required, 5, NULL, 0, "gas")) + goto done; + + JSValue base = JS_GetPropertyStr(ctx, gas, "base"); + JSValue k_arg = JS_GetPropertyStr(ctx, gas, "k_arg_bytes"); + JSValue k_ret = JS_GetPropertyStr(ctx, gas, "k_ret_bytes"); + JSValue k_units = JS_GetPropertyStr(ctx, gas, "k_units"); + JSValue schedule_id = JS_GetPropertyStr(ctx, gas, "schedule_id"); + if (JS_IsException(base) || JS_IsException(k_arg) || JS_IsException(k_ret) || + JS_IsException(k_units) || JS_IsException(schedule_id)) { + JS_FreeValue(ctx, base); + JS_FreeValue(ctx, k_arg); + JS_FreeValue(ctx, k_ret); + JS_FreeValue(ctx, k_units); + JS_FreeValue(ctx, schedule_id); + goto done; + } + + if (js_host_validate_uint32(ctx, base, "gas.base", 0, UINT32_MAX, &out_fn->gas_base) || + js_host_validate_uint32(ctx, k_arg, "gas.k_arg_bytes", 0, UINT32_MAX, &out_fn->gas_k_arg_bytes) || + js_host_validate_uint32(ctx, k_ret, "gas.k_ret_bytes", 0, UINT32_MAX, &out_fn->gas_k_ret_bytes) || + js_host_validate_uint32(ctx, k_units, "gas.k_units", 0, UINT32_MAX, &out_fn->gas_k_units) || + js_host_copy_non_empty_string(ctx, schedule_id, "gas.schedule_id", &schedule_id_str)) { + JS_FreeValue(ctx, base); + JS_FreeValue(ctx, k_arg); + JS_FreeValue(ctx, k_ret); + JS_FreeValue(ctx, k_units); + JS_FreeValue(ctx, schedule_id); + goto done; + } + out_fn->gas_schedule_id = schedule_id_str; + schedule_id_str = NULL; + + JS_FreeValue(ctx, base); + JS_FreeValue(ctx, k_arg); + JS_FreeValue(ctx, k_ret); + JS_FreeValue(ctx, k_units); + JS_FreeValue(ctx, schedule_id); + + limits = JS_GetPropertyStr(ctx, fn_val, "limits"); + if (JS_IsException(limits)) + goto done; + if (js_host_validate_limits(ctx, limits, "limits", out_fn)) + goto done; + + error_codes = JS_GetPropertyStr(ctx, fn_val, "error_codes"); + if (JS_IsException(error_codes)) + goto done; + if (js_host_validate_error_codes(ctx, error_codes, "error_codes", out_fn)) + goto done; + + /* Gas overflow guard: base + (req * k_arg) + (resp * k_ret) + (units * k_units) */ + { + uint64_t total = out_fn->gas_base; + uint64_t arg_part = (uint64_t)out_fn->gas_k_arg_bytes * (uint64_t)out_fn->max_request_bytes; + uint64_t ret_part = (uint64_t)out_fn->gas_k_ret_bytes * (uint64_t)out_fn->max_response_bytes; + uint64_t unit_part = (uint64_t)out_fn->gas_k_units * (uint64_t)out_fn->max_units; + + if (arg_part > UINT64_MAX - total || + ret_part > UINT64_MAX - total - arg_part || + unit_part > UINT64_MAX - total - arg_part - ret_part) { + js_host_manifest_error(ctx, "gas", "gas charges overflow uint64 bounds"); + goto done; + } + } + + if (js_host_build_function_name(ctx, out_fn)) + goto done; + + ret = 0; + +done: + if (!JS_IsUndefined(fn_id)) + JS_FreeValue(ctx, fn_id); + if (!JS_IsUndefined(js_path)) + JS_FreeValue(ctx, js_path); + if (!JS_IsUndefined(effect)) + JS_FreeValue(ctx, effect); + if (!JS_IsUndefined(arity)) + JS_FreeValue(ctx, arity); + if (!JS_IsUndefined(arg_schema)) + JS_FreeValue(ctx, arg_schema); + if (!JS_IsUndefined(return_schema)) + JS_FreeValue(ctx, return_schema); + if (!JS_IsUndefined(gas)) + JS_FreeValue(ctx, gas); + if (!JS_IsUndefined(limits)) + JS_FreeValue(ctx, limits); + if (!JS_IsUndefined(error_codes)) + JS_FreeValue(ctx, error_codes); + if (eff_str) + JS_FreeCString(ctx, eff_str); + if (schedule_id_str) + js_free(ctx, schedule_id_str); + + if (ret != 0) { + if (out_fn->name) { + js_free(ctx, out_fn->name); + out_fn->name = NULL; + } + } + return ret; +} + +static BOOL js_host_paths_conflict(const JSHostFunctionDef *a, const JSHostFunctionDef *b) +{ + size_t len = a->path_len < b->path_len ? a->path_len : b->path_len; + for (size_t i = 0; i < len; i++) { + if (strcmp(a->path_segments[i], b->path_segments[i]) != 0) + return FALSE; + } + return TRUE; +} + +static int js_host_validate_manifest(JSContext *ctx, JSValueConst manifest_val, JSHostManifest *out_manifest) +{ + const char *required[] = {"abi_id", "abi_version", "functions"}; + JSValue abi_id = JS_UNDEFINED; + JSValue abi_version = JS_UNDEFINED; + JSValue functions = JS_UNDEFINED; + char *abi_id_str = NULL; + int ret = -1; + + if (js_host_expect_object(ctx, manifest_val, "manifest")) + return -1; + + if (js_host_check_keys(ctx, manifest_val, required, 3, NULL, 0, "manifest")) + return -1; + + abi_id = JS_GetPropertyStr(ctx, manifest_val, "abi_id"); + if (JS_IsException(abi_id)) + goto done; + if (js_host_copy_non_empty_string(ctx, abi_id, "manifest.abi_id", &abi_id_str)) + goto done; + if (strcmp(abi_id_str, "Host.v1") != 0) { + js_host_manifest_error(ctx, "manifest.abi_id", "unsupported abi_id (expected Host.v1)"); + goto done; + } + + abi_version = JS_GetPropertyStr(ctx, manifest_val, "abi_version"); + if (JS_IsException(abi_version)) + goto done; + uint32_t version = 0; + if (js_host_validate_uint32(ctx, abi_version, "manifest.abi_version", 1, UINT32_MAX, &version)) + goto done; + if (version != 1) { + js_host_manifest_error(ctx, "manifest.abi_version", "unsupported abi_version (expected 1)"); + goto done; + } + + functions = JS_GetPropertyStr(ctx, manifest_val, "functions"); + if (JS_IsException(functions)) + goto done; + if (!JS_IsArray(ctx, functions)) { + js_host_manifest_error(ctx, "manifest.functions", "functions must be an array"); + goto done; + } + + if (js_host_array_length(ctx, functions, &out_manifest->function_count)) + goto done; + if (out_manifest->function_count == 0) { + js_host_manifest_error(ctx, "manifest.functions", "functions must contain at least one entry"); + goto done; + } + + out_manifest->functions = js_malloc(ctx, sizeof(JSHostFunctionDef) * out_manifest->function_count); + if (!out_manifest->functions) + goto done; + memset(out_manifest->functions, 0, sizeof(JSHostFunctionDef) * out_manifest->function_count); + + uint32_t prev_fn_id = 0; + for (size_t i = 0; i < out_manifest->function_count; i++) { + JSValue fn_val = JS_GetPropertyUint32(ctx, functions, (uint32_t)i); + if (JS_IsException(fn_val)) + goto done; + + char fn_path[96]; + snprintf(fn_path, sizeof(fn_path), "manifest.functions[%zu]", i); + + if (js_host_validate_function(ctx, fn_val, fn_path, &out_manifest->functions[i])) { + JS_FreeValue(ctx, fn_val); + goto done; + } + + JS_FreeValue(ctx, fn_val); + + if (i > 0 && out_manifest->functions[i].fn_id <= prev_fn_id) { + js_host_manifest_error(ctx, "manifest.functions", "functions must be sorted by ascending fn_id"); + goto done; + } + prev_fn_id = out_manifest->functions[i].fn_id; + } + + for (size_t i = 0; i < out_manifest->function_count; i++) { + for (size_t j = i + 1; j < out_manifest->function_count; j++) { + if (js_host_paths_conflict(&out_manifest->functions[i], &out_manifest->functions[j])) { + js_host_manifest_error(ctx, "manifest.functions", "js_path collision detected"); + goto done; + } + } + } + + ret = 0; + +done: + if (abi_id_str) + js_free(ctx, abi_id_str); + if (!JS_IsUndefined(abi_id)) + JS_FreeValue(ctx, abi_id); + if (!JS_IsUndefined(abi_version)) + JS_FreeValue(ctx, abi_version); + if (!JS_IsUndefined(functions)) + JS_FreeValue(ctx, functions); + + if (ret != 0) + js_host_manifest_clear(ctx, out_manifest); + return ret; +} + +static int js_host_namespace_push(JSContext *ctx, + JSValue ns, + JSValue **out, + size_t *count, + size_t *capacity) +{ + void *ptr = JS_VALUE_GET_PTR(ns); + for (size_t i = 0; i < *count; i++) { + if (JS_VALUE_GET_PTR((*out)[i]) == ptr) + return 0; + } + + if (*count >= *capacity) { + size_t new_cap = (*capacity == 0) ? 8 : (*capacity * 2); + JSValue *resized = js_realloc(ctx, *out, sizeof(JSValue) * new_cap); + if (!resized) + return -1; + *out = resized; + *capacity = new_cap; + } + + (*out)[*count] = JS_DupValue(ctx, ns); + (*count)++; + return 0; +} + +static int js_host_get_or_create_child(JSContext *ctx, + JSValue parent, + const char *name, + JSValue *out_child, + JSValue **namespaces, + size_t *ns_count, + size_t *ns_capacity) +{ + JSValue child = JS_UNDEFINED; + JSAtom atom = JS_NewAtom(ctx, name); + int ret = -1; + + if (atom == JS_ATOM_NULL) + return -1; + + child = JS_GetProperty(ctx, parent, atom); + if (JS_IsException(child)) + goto done; + + if (JS_IsUndefined(child)) { + child = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(child)) + goto done; + if (JS_DefinePropertyValue(ctx, + parent, + atom, + JS_DupValue(ctx, child), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) + goto done; + } else if (!JS_IsObject(child)) { + js_host_manifest_error(ctx, name, "Host namespace collision"); + goto done; + } + + if (js_host_namespace_push(ctx, child, namespaces, ns_count, ns_capacity)) + goto done; + + *out_child = child; + child = JS_UNDEFINED; + ret = 0; + +done: + if (atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, atom); + if (!JS_IsUndefined(child)) + JS_FreeValue(ctx, child); + return ret; +} + +static int js_host_charge_pre(JSContext *ctx, const JSHostFunctionDef *fn, size_t req_len) +{ + uint64_t charge = fn->gas_base; + uint64_t arg_part = (uint64_t)fn->gas_k_arg_bytes * (uint64_t)req_len; + + if (arg_part > UINT64_MAX - charge) { + JS_ThrowTypeError(ctx, "host_call gas overflow"); + return -1; + } + + charge += arg_part; + return JS_UseGas(ctx, charge); +} + +static int js_host_charge_post(JSContext *ctx, const JSHostFunctionDef *fn, size_t resp_len, uint32_t units) +{ + uint64_t charge = 0; + uint64_t resp_part = (uint64_t)fn->gas_k_ret_bytes * (uint64_t)resp_len; + uint64_t unit_part = (uint64_t)fn->gas_k_units * (uint64_t)units; + + if (resp_part > UINT64_MAX - charge || unit_part > UINT64_MAX - charge - resp_part) { + JS_ThrowTypeError(ctx, "host_call gas overflow"); + return -1; + } + + charge += resp_part + unit_part; + return JS_UseGas(ctx, charge); +} + +static JSValue js_host_call_wrapper(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv, + int magic) +{ + JSHostManifest *manifest = js_host_find_manifest(ctx); + JSDvBuffer req_buf = {0}; + JSValue args_array = JS_UNDEFINED; + JSHostResponse resp; + JSHostCallResult result = {0}; + JSValue ret = JS_EXCEPTION; + JSHostTapeState *tape_state = js_host_get_tape(ctx); + int tape_enabled = tape_state && tape_state->capacity > 0 && tape_state->records; + uint8_t tape_req_hash[32]; + uint8_t tape_resp_hash[32]; + uint32_t tape_req_len = 0; + uint32_t tape_resp_len = 0; + uint64_t tape_gas_pre = 0; + uint64_t tape_gas_post = 0; + int tape_charge_failed = 0; + + if (!manifest) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return JS_EXCEPTION; + } + + if (magic < 0 || (size_t)magic >= manifest->function_count) { + JS_ThrowTypeError(ctx, "invalid host function binding"); + return JS_EXCEPTION; + } + + const JSHostFunctionDef *fn = &manifest->functions[magic]; + + if (argc != (int)fn->arity) { + JS_ThrowTypeError(ctx, "%s expects %u arguments", fn->name ? fn->name : "Host function", fn->arity); + return JS_EXCEPTION; + } + + for (size_t i = 0; i < fn->arity; i++) { + const JSHostArgDef *arg = &fn->args[i]; + JSValueConst val = argv[i]; + + switch (arg->type) { + case JS_HOST_SCHEMA_STRING: { + if (!JS_IsString(val)) { + JS_ThrowTypeError(ctx, "%s argument %zu must be a string", fn->name ? fn->name : "Host function", i + 1); + return JS_EXCEPTION; + } + if (arg->utf8_max > 0) { + size_t utf8_len = 0; + const char *str = JS_ToCStringLen2(ctx, &utf8_len, val, 0); + if (!str) + return JS_EXCEPTION; + JS_FreeCString(ctx, str); + if (utf8_len > arg->utf8_max) { + JS_ThrowTypeError(ctx, + "%s argument %zu exceeds utf8 limit (%zu > %u)", + fn->name ? fn->name : "Host function", + i + 1, + utf8_len, + arg->utf8_max); + return JS_EXCEPTION; + } + } + break; + } + case JS_HOST_SCHEMA_DV: + break; + case JS_HOST_SCHEMA_NULL: + if (!JS_IsNull(val)) { + JS_ThrowTypeError(ctx, "%s argument %zu must be null", fn->name ? fn->name : "Host function", i + 1); + return JS_EXCEPTION; + } + break; + } + } + + if (JS_RunGCCheckpoint(ctx)) + return JS_EXCEPTION; + + args_array = JS_NewArray(ctx); + if (JS_IsException(args_array)) + return JS_EXCEPTION; + + for (size_t i = 0; i < fn->arity; i++) { + if (JS_SetPropertyUint32(ctx, args_array, (uint32_t)i, JS_DupValue(ctx, argv[i])) < 0) { + JS_FreeValue(ctx, args_array); + return JS_EXCEPTION; + } + } + + JSDvLimits dv_limits = JS_DV_LIMIT_DEFAULTS; + dv_limits.max_encoded_bytes = fn->max_request_bytes; + + if (JS_EncodeDV(ctx, args_array, &dv_limits, &req_buf)) { + JS_FreeValue(ctx, args_array); + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + if (tape_enabled) { + memset(tape_req_hash, 0, sizeof(tape_req_hash)); + memset(tape_resp_hash, 0, sizeof(tape_resp_hash)); + tape_req_len = (uint32_t)req_buf.length; + if (js_host_calc_gas_charges(fn, req_buf.length, 0, 0, &tape_gas_pre, NULL)) + tape_gas_pre = 0; + js_sha256(req_buf.data, req_buf.length, tape_req_hash); + } + + JS_FreeValue(ctx, args_array); + args_array = JS_UNDEFINED; + + if (js_host_charge_pre(ctx, fn, req_buf.length)) { + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + if (JS_RunGCCheckpoint(ctx)) { + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + if (JS_HostCall(ctx, + fn->fn_id, + req_buf.data, + req_buf.length, + fn->max_request_bytes, + fn->max_response_bytes, + &result)) { + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + JS_FreeDVBuffer(ctx, &req_buf); + + if (tape_enabled) { + tape_resp_len = (uint32_t)result.length; + js_sha256(result.data, result.length, tape_resp_hash); + } + + JSHostResponseValidation validation = { + .max_units = fn->max_units, + .errors = fn->errors, + .error_count = fn->error_count, + }; + + if (JS_ParseHostResponse(ctx, result.data, result.length, &validation, &resp)) + return JS_EXCEPTION; + + if (tape_enabled) { + if (js_host_calc_gas_charges(fn, tape_req_len, tape_resp_len, resp.units, NULL, &tape_gas_post)) + tape_gas_post = 0; + } + + tape_charge_failed = js_host_charge_post(ctx, fn, result.length, resp.units); + + if (tape_enabled) { + JSHostTapeRecord rec = {0}; + rec.fn_id = fn->fn_id; + rec.req_len = tape_req_len; + rec.resp_len = tape_resp_len; + rec.units = resp.units; + rec.gas_pre = tape_gas_pre; + rec.gas_post = tape_gas_post; + rec.is_error = resp.is_error; + rec.charge_failed = tape_charge_failed; + memcpy(rec.req_hash, tape_req_hash, sizeof(rec.req_hash)); + memcpy(rec.resp_hash, tape_resp_hash, sizeof(rec.resp_hash)); + js_host_tape_append(tape_state, &rec); + } + + if (tape_charge_failed) { + JS_FreeHostResponse(ctx, &resp); + return JS_EXCEPTION; + } + + if (JS_RunGCCheckpoint(ctx)) { + JS_FreeHostResponse(ctx, &resp); + return JS_EXCEPTION; + } + + if (resp.is_error) { + ret = JS_ThrowHostError(ctx, resp.err_code_atom, resp.err_tag_atom, resp.err_details); + JS_FreeHostResponse(ctx, &resp); + return ret; + } + + switch (fn->return_type) { + case JS_HOST_SCHEMA_STRING: + if (!JS_IsString(resp.ok)) { + JS_FreeHostResponse(ctx, &resp); + return js_throw_host_envelope_invalid(ctx); + } + break; + case JS_HOST_SCHEMA_NULL: + if (!JS_IsNull(resp.ok)) { + JS_FreeHostResponse(ctx, &resp); + return js_throw_host_envelope_invalid(ctx); + } + break; + case JS_HOST_SCHEMA_DV: + break; + } + + ret = resp.ok; + resp.ok = JS_UNDEFINED; + JS_FreeHostResponse(ctx, &resp); + return ret; +} + +static int js_host_install_functions(JSContext *ctx, JSHostManifest *manifest) +{ + JSValue global = JS_UNDEFINED; + JSValue host = JS_UNDEFINED; + JSValue host_v1 = JS_UNDEFINED; + JSValue *namespaces = NULL; + size_t ns_count = 0; + size_t ns_capacity = 0; + int ret = -1; + + global = JS_GetGlobalObject(ctx); + if (JS_IsException(global)) + goto done; + + if (js_host_get_or_create_child(ctx, global, "Host", &host, &namespaces, &ns_count, &ns_capacity)) + goto done; + + if (js_host_get_or_create_child(ctx, host, "v1", &host_v1, &namespaces, &ns_count, &ns_capacity)) + goto done; + + for (size_t i = 0; i < manifest->function_count; i++) { + JSValue current = JS_DupValue(ctx, host_v1); + if (JS_IsException(current)) + goto done; + + JSValue next = JS_UNDEFINED; + JSHostFunctionDef *fn = &manifest->functions[i]; + + for (size_t j = 0; j + 1 < fn->path_len; j++) { + if (js_host_get_or_create_child(ctx, + current, + fn->path_segments[j], + &next, + &namespaces, + &ns_count, + &ns_capacity)) { + JS_FreeValue(ctx, current); + goto done; + } + JS_FreeValue(ctx, current); + current = next; + next = JS_UNDEFINED; + } + + JSAtom fn_atom = JS_NewAtom(ctx, fn->path_segments[fn->path_len - 1]); + if (fn_atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, current); + goto done; + } + + JSValue cfunc = JS_NewCFunctionMagic(ctx, + js_host_call_wrapper, + fn->path_segments[fn->path_len - 1], + fn->arity, + JS_CFUNC_generic_magic, + (int)i); + if (JS_IsException(cfunc)) { + JS_FreeAtom(ctx, fn_atom); + JS_FreeValue(ctx, current); + goto done; + } + + if (JS_DefinePropertyValue(ctx, + current, + fn_atom, + cfunc, + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeAtom(ctx, fn_atom); + JS_FreeValue(ctx, current); + goto done; + } + + JS_FreeAtom(ctx, fn_atom); + JS_FreeValue(ctx, current); + } + + for (size_t i = 0; i < ns_count; i++) { + if (JS_PreventExtensions(ctx, namespaces[i]) < 0) + goto done; + } + + ret = 0; + +done: + if (namespaces) { + for (size_t i = 0; i < ns_count; i++) + JS_FreeValue(ctx, namespaces[i]); + js_free(ctx, namespaces); + } + if (!JS_IsUndefined(host_v1)) + JS_FreeValue(ctx, host_v1); + if (!JS_IsUndefined(host)) + JS_FreeValue(ctx, host); + if (!JS_IsUndefined(global)) + JS_FreeValue(ctx, global); + return ret; +} + +int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_t manifest_size) +{ + JSValue manifest_val = JS_UNDEFINED; + JSHostManifest manifest = {0}; + JSHostManifestNode *node = NULL; + int ret = -1; + + if (!ctx || !manifest_bytes || manifest_size == 0) { + JS_ThrowTypeError(ctx, "abi manifest is required"); + return -1; + } + + if (js_host_find_manifest(ctx)) { + JS_ThrowTypeError(ctx, "abi manifest is already initialized"); + return -1; + } + + manifest_val = JS_DecodeDV(ctx, manifest_bytes, manifest_size, &JS_DV_LIMIT_DEFAULTS); + if (JS_IsException(manifest_val)) + return -1; + + if (js_host_validate_manifest(ctx, manifest_val, &manifest)) + goto done; + + node = js_mallocz(ctx, sizeof(JSHostManifestNode)); + if (!node) + goto done; + + node->ctx = ctx; + node->manifest = manifest; + node->tape = (JSHostTapeState){0}; + node->next = NULL; + memset(&manifest, 0, sizeof(manifest)); /* ownership moved */ + + if (js_host_install_functions(ctx, &node->manifest)) + goto done; + + node->next = js_host_manifest_list; + js_host_manifest_list = node; + node = NULL; + + ret = 0; + +done: + if (!JS_IsUndefined(manifest_val)) + JS_FreeValue(ctx, manifest_val); + if (node) { + js_host_manifest_clear(ctx, &node->manifest); + js_free(ctx, node); + } else if (ret != 0) { + js_host_manifest_clear(ctx, &manifest); + } + return ret; +} + +/* Ergonomic globals (T-041) */ + +static int js_freeze_value(JSContext *ctx, JSValueConst val) +{ + JSPropertyEnum *props = NULL; + uint32_t props_len = 0; + int ret = -1; + + if (!JS_IsObject(val)) + return 0; + + if (JS_GetOwnPropertyNames(ctx, &props, &props_len, val, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) + return -1; + + for (uint32_t i = 0; i < props_len; i++) { + JSAtom atom = props[i].atom; + JSValue prop_val = JS_GetProperty(ctx, val, atom); + int flags; + + if (JS_IsException(prop_val)) + goto done; + + if (js_freeze_value(ctx, prop_val)) { + JS_FreeValue(ctx, prop_val); + goto done; + } + + flags = JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE | JS_PROP_HAS_ENUMERABLE; + if (props[i].is_enumerable) + flags |= JS_PROP_ENUMERABLE; + + if (JS_DefinePropertyValue(ctx, val, atom, JS_DupValue(ctx, prop_val), flags) < 0) { + JS_FreeValue(ctx, prop_val); + goto done; + } + + JS_FreeValue(ctx, prop_val); + } + + if (JS_IsArray(ctx, val)) { + JSAtom length_atom = JS_NewAtom(ctx, "length"); + JSValue length_val = JS_UNDEFINED; + + if (length_atom == JS_ATOM_NULL) + goto done; + + length_val = JS_GetProperty(ctx, val, length_atom); + if (JS_IsException(length_val)) { + JS_FreeAtom(ctx, length_atom); + goto done; + } + + if (JS_DefinePropertyValue(ctx, + val, + length_atom, + JS_DupValue(ctx, length_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeAtom(ctx, length_atom); + JS_FreeValue(ctx, length_val); + goto done; + } + + JS_FreeAtom(ctx, length_atom); + JS_FreeValue(ctx, length_val); + } + + if (JS_PreventExtensions(ctx, val) < 0) + goto done; + + ret = 0; + +done: + if (props) + JS_FreePropertyEnum(ctx, props, props_len); + return ret; +} + +static int js_canon_clone_and_freeze(JSContext *ctx, JSValueConst input, JSValue *out) +{ + JSDvBuffer buf = {0}; + JSDvLimits limits = JS_DV_LIMIT_DEFAULTS; + JSValue decoded = JS_UNDEFINED; + + if (!out) + return -1; + + if (JS_RunGCCheckpoint(ctx)) + return -1; + + if (JS_EncodeDV(ctx, input, &limits, &buf)) + return -1; + + decoded = JS_DecodeDV(ctx, buf.data, buf.length, &limits); + JS_FreeDVBuffer(ctx, &buf); + if (JS_IsException(decoded)) + return -1; + + if (js_freeze_value(ctx, decoded)) { + JS_FreeValue(ctx, decoded); + return -1; + } + + *out = decoded; + return 0; +} + +static int js_context_copy_and_freeze(JSContext *ctx, + JSValueConst source, + JSAtom atom, + JSValue *out) +{ + JSValue tmp = JS_UNDEFINED; + JSValue dup = JS_UNDEFINED; + + if (!out) + return -1; + + tmp = JS_GetProperty(ctx, source, atom); + if (JS_IsException(tmp)) + return -1; + + if (!JS_IsUndefined(tmp)) { + dup = JS_DupValue(ctx, tmp); + if (js_freeze_value(ctx, dup)) { + JS_FreeValue(ctx, dup); + JS_FreeValue(ctx, tmp); + return -1; + } + *out = dup; + } + + JS_FreeValue(ctx, tmp); + return 0; +} + +static int js_decode_context_blob(JSContext *ctx, + const uint8_t *context_blob, + size_t context_blob_size, + JSValue *out_event, + JSValue *out_event_canonical, + JSValue *out_steps) +{ + JSValue decoded = JS_UNDEFINED; + JSAtom event_atom = JS_ATOM_NULL; + JSAtom event_canonical_atom = JS_ATOM_NULL; + JSAtom steps_atom = JS_ATOM_NULL; + int ret = -1; + + if (!out_event || !out_event_canonical || !out_steps) + return -1; + + *out_event = JS_NULL; + *out_event_canonical = JS_NULL; + *out_steps = JS_NULL; + + if (!context_blob || context_blob_size == 0) + return 0; + + decoded = JS_DecodeDV(ctx, context_blob, context_blob_size, &JS_DV_LIMIT_DEFAULTS); + if (JS_IsException(decoded)) + return -1; + + if (!JS_IsObject(decoded) || JS_IsArray(ctx, decoded)) { + JS_FreeValue(ctx, decoded); + JS_ThrowTypeError(ctx, "context blob must decode to an object"); + return -1; + } + + event_atom = JS_NewAtom(ctx, "event"); + event_canonical_atom = JS_NewAtom(ctx, "eventCanonical"); + steps_atom = JS_NewAtom(ctx, "steps"); + if (event_atom == JS_ATOM_NULL || event_canonical_atom == JS_ATOM_NULL || steps_atom == JS_ATOM_NULL) + goto done; + + if (js_context_copy_and_freeze(ctx, decoded, event_atom, out_event)) + goto done; + if (js_context_copy_and_freeze(ctx, decoded, event_canonical_atom, out_event_canonical)) + goto done; + if (js_context_copy_and_freeze(ctx, decoded, steps_atom, out_steps)) + goto done; + + ret = 0; + +done: + if (event_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, event_atom); + if (event_canonical_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, event_canonical_atom); + if (steps_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, steps_atom); + if (!JS_IsUndefined(decoded)) + JS_FreeValue(ctx, decoded); + return ret; +} + +static JSValue js_document_wrapper(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv, + int magic, + JSValue *func_data) +{ + (void)this_val; + (void)magic; + + if (!func_data || !JS_IsFunction(ctx, func_data[0])) { + JS_ThrowTypeError(ctx, "Host.v1.document binding is missing"); + return JS_EXCEPTION; + } + + return JS_Call(ctx, func_data[0], JS_UNDEFINED, argc, argv); +} + +static JSValue js_canon_unwrap(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv) +{ + JSValue clone = JS_UNDEFINED; + + (void)this_val; + + if (argc != 1) { + JS_ThrowTypeError(ctx, "canon.unwrap expects 1 argument"); + return JS_EXCEPTION; + } + + if (js_canon_clone_and_freeze(ctx, argv[0], &clone)) + return JS_EXCEPTION; + + return clone; +} + +static JSValue js_canon_at(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv) +{ + JSValue canonical = JS_UNDEFINED; + JSValue current = JS_UNDEFINED; + JSValue ret = JS_UNDEFINED; + uint32_t path_len = 0; + BOOL missing = FALSE; + + (void)this_val; + + if (argc < 2) { + JS_ThrowTypeError(ctx, "canon.at expects a value and a path array"); + return JS_EXCEPTION; + } + + if (!JS_IsArray(ctx, argv[1])) { + JS_ThrowTypeError(ctx, "canon.at path must be an array"); + return JS_EXCEPTION; + } + + if (js_canon_clone_and_freeze(ctx, argv[0], &canonical)) + return JS_EXCEPTION; + + { + JSValue len_val = JS_GetPropertyStr(ctx, argv[1], "length"); + if (JS_IsException(len_val)) { + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + if (JS_ToUint32(ctx, &path_len, len_val)) { + JS_FreeValue(ctx, len_val); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + JS_FreeValue(ctx, len_val); + } + + current = JS_DupValue(ctx, canonical); + + for (uint32_t i = 0; i < path_len; i++) { + JSValue segment = JS_GetPropertyUint32(ctx, argv[1], i); + + if (JS_IsException(segment)) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (!JS_IsObject(current)) { + JS_FreeValue(ctx, segment); + missing = TRUE; + break; + } + + if (JS_IsString(segment)) { + size_t utf8_len = 0; + const char *prop = JS_ToCStringLen2(ctx, &utf8_len, segment, 0); + JSValue next = JS_UNDEFINED; + + if (!prop) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (utf8_len > JS_DV_LIMIT_DEFAULTS.max_string_bytes) { + JS_FreeCString(ctx, prop); + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path segment exceeds string limit"); + return JS_EXCEPTION; + } + + next = JS_GetPropertyStr(ctx, current, prop); + JS_FreeCString(ctx, prop); + JS_FreeValue(ctx, segment); + + if (JS_IsException(next)) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (JS_IsUndefined(next)) { + JS_FreeValue(ctx, next); + missing = TRUE; + break; + } + + JS_FreeValue(ctx, current); + current = next; + } else { + BOOL is_number = JS_IsNumber(segment); + BOOL is_bigint = JS_IsBigInt(ctx, segment); + double idx_d = 0; + int64_t index = 0; + JSValue next = JS_UNDEFINED; + + if (!is_number && !is_bigint) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path elements must be strings or integers"); + return JS_EXCEPTION; + } + + if (is_number) { + if (JS_ToFloat64(ctx, &idx_d, segment)) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (!isfinite(idx_d) || floor(idx_d) != idx_d || (idx_d == 0.0 && signbit(idx_d))) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path elements must be strings or integers"); + return JS_EXCEPTION; + } + + index = (int64_t)idx_d; + } else { + if (JS_ToInt64Ext(ctx, &index, segment)) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path elements must be strings or integers"); + return JS_EXCEPTION; + } + } + + JS_FreeValue(ctx, segment); + + if (index < 0 || (uint64_t)index >= JS_DV_LIMIT_DEFAULTS.max_array_length) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path index is out of range"); + return JS_EXCEPTION; + } + + if (!JS_IsArray(ctx, current)) { + missing = TRUE; + break; + } + + next = JS_GetPropertyUint32(ctx, current, (uint32_t)index); + if (JS_IsException(next)) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (JS_IsUndefined(next)) { + JS_FreeValue(ctx, next); + missing = TRUE; + break; + } + + JS_FreeValue(ctx, current); + current = next; + } + } + + if (missing) { + JS_FreeValue(ctx, current); + current = JS_UNDEFINED; + ret = JS_UNDEFINED; + } else { + ret = current; + current = JS_UNDEFINED; + } + + JS_FreeValue(ctx, canonical); + if (!JS_IsUndefined(current)) + JS_FreeValue(ctx, current); + return ret; +} + +int JS_InitErgonomicGlobals(JSContext *ctx, const uint8_t *context_blob, size_t context_blob_size) +{ + JSValue global = JS_UNDEFINED; + JSValue host = JS_UNDEFINED; + JSValue host_v1 = JS_UNDEFINED; + JSValue document_ns = JS_UNDEFINED; + JSValue document_get = JS_UNDEFINED; + JSValue document_get_canonical = JS_UNDEFINED; + JSValue document_fn = JS_UNDEFINED; + JSValue document_canonical_fn = JS_UNDEFINED; + JSValue canon_obj = JS_UNDEFINED; + JSValue canon_unwrap_fn = JS_UNDEFINED; + JSValue canon_at_fn = JS_UNDEFINED; + JSValue event_val = JS_NULL; + JSValue event_canonical_val = JS_NULL; + JSValue steps_val = JS_NULL; + JSValueConst doc_funcs[1]; + int ret = -1; + + if (!ctx) + return -1; + + if (!js_host_find_manifest(ctx)) { + JS_ThrowTypeError(ctx, "abi manifest must be initialized before installing ergonomic globals"); + return -1; + } + + if (js_decode_context_blob(ctx, context_blob, context_blob_size, &event_val, &event_canonical_val, &steps_val)) + goto done; + + global = JS_GetGlobalObject(ctx); + if (JS_IsException(global)) + goto done; + + host = JS_GetPropertyStr(ctx, global, "Host"); + if (JS_IsException(host)) + goto done; + + host_v1 = JS_GetPropertyStr(ctx, host, "v1"); + if (JS_IsException(host_v1)) + goto done; + + document_ns = JS_GetPropertyStr(ctx, host_v1, "document"); + if (JS_IsException(document_ns)) + goto done; + + document_get = JS_GetPropertyStr(ctx, document_ns, "get"); + if (JS_IsException(document_get)) + goto done; + + document_get_canonical = JS_GetPropertyStr(ctx, document_ns, "getCanonical"); + if (JS_IsException(document_get_canonical)) + goto done; + + if (!JS_IsFunction(ctx, document_get) || !JS_IsFunction(ctx, document_get_canonical)) { + JS_ThrowTypeError(ctx, "Host.v1.document bindings are missing"); + goto done; + } + + doc_funcs[0] = JS_DupValue(ctx, document_get); + document_fn = JS_NewCFunctionData(ctx, js_document_wrapper, 1, 0, 1, doc_funcs); + JS_FreeValue(ctx, (JSValue)doc_funcs[0]); + if (JS_IsException(document_fn)) + goto done; + + doc_funcs[0] = JS_DupValue(ctx, document_get_canonical); + document_canonical_fn = JS_NewCFunctionData(ctx, js_document_wrapper, 1, 0, 1, doc_funcs); + JS_FreeValue(ctx, (JSValue)doc_funcs[0]); + if (JS_IsException(document_canonical_fn)) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "document", + JS_DupValue(ctx, document_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + document_fn, + "canonical", + JS_DupValue(ctx, document_canonical_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_PreventExtensions(ctx, document_fn) < 0) + goto done; + if (JS_PreventExtensions(ctx, document_canonical_fn) < 0) + goto done; + + canon_obj = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(canon_obj)) + goto done; + + canon_unwrap_fn = JS_NewCFunction(ctx, js_canon_unwrap, "unwrap", 1); + if (JS_IsException(canon_unwrap_fn)) + goto done; + + canon_at_fn = JS_NewCFunction(ctx, js_canon_at, "at", 2); + if (JS_IsException(canon_at_fn)) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + canon_obj, + "unwrap", + JS_DupValue(ctx, canon_unwrap_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + canon_obj, + "at", + JS_DupValue(ctx, canon_at_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_PreventExtensions(ctx, canon_unwrap_fn) < 0) + goto done; + if (JS_PreventExtensions(ctx, canon_at_fn) < 0) + goto done; + if (JS_PreventExtensions(ctx, canon_obj) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "canon", + JS_DupValue(ctx, canon_obj), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "event", + JS_DupValue(ctx, event_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "eventCanonical", + JS_DupValue(ctx, event_canonical_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "steps", + JS_DupValue(ctx, steps_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + ret = 0; + +done: + if (!JS_IsUndefined(host_v1)) + JS_FreeValue(ctx, host_v1); + if (!JS_IsUndefined(host)) + JS_FreeValue(ctx, host); + if (!JS_IsUndefined(global)) + JS_FreeValue(ctx, global); + if (!JS_IsUndefined(document_ns)) + JS_FreeValue(ctx, document_ns); + if (!JS_IsUndefined(document_get)) + JS_FreeValue(ctx, document_get); + if (!JS_IsUndefined(document_get_canonical)) + JS_FreeValue(ctx, document_get_canonical); + if (!JS_IsUndefined(document_fn)) + JS_FreeValue(ctx, document_fn); + if (!JS_IsUndefined(document_canonical_fn)) + JS_FreeValue(ctx, document_canonical_fn); + if (!JS_IsUndefined(canon_obj)) + JS_FreeValue(ctx, canon_obj); + if (!JS_IsUndefined(canon_unwrap_fn)) + JS_FreeValue(ctx, canon_unwrap_fn); + if (!JS_IsUndefined(canon_at_fn)) + JS_FreeValue(ctx, canon_at_fn); + if (!JS_IsUndefined(event_val)) + JS_FreeValue(ctx, event_val); + if (!JS_IsUndefined(event_canonical_val)) + JS_FreeValue(ctx, event_canonical_val); + if (!JS_IsUndefined(steps_val)) + JS_FreeValue(ctx, steps_val); + return ret; +} diff --git a/quickjs-host.h b/quickjs-host.h new file mode 100644 index 000000000..1b67157a3 --- /dev/null +++ b/quickjs-host.h @@ -0,0 +1,62 @@ +#ifndef QUICKJS_HOST_H +#define QUICKJS_HOST_H + +#include "quickjs.h" + +typedef struct JSHostErrorEntry { + JSAtom code_atom; + JSAtom tag_atom; +} JSHostErrorEntry; + +typedef struct JSHostResponseValidation { + uint32_t max_units; + const JSHostErrorEntry *errors; + size_t error_count; +} JSHostResponseValidation; + +typedef struct JSHostResponse { + int is_error; + uint32_t units; + JSValue ok; + JSAtom err_code_atom; + JSAtom err_tag_atom; + JSValue err_details; +} JSHostResponse; + +typedef struct JSHostManifest JSHostManifest; + +/* Host response envelope helpers (T-039). */ +JSValue JS_ThrowHostError(JSContext *ctx, JSAtom code_atom, JSAtom tag_atom, JSValueConst details); +JSValue JS_ThrowHostTransportError(JSContext *ctx); +int JS_ParseHostResponse(JSContext *ctx, + const uint8_t *data, + size_t length, + const JSHostResponseValidation *validation, + JSHostResponse *out); +void JS_FreeHostResponse(JSContext *ctx, JSHostResponse *resp); +int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_t manifest_size); +int JS_InitErgonomicGlobals(JSContext *ctx, const uint8_t *context_blob, size_t context_blob_size); +void JS_FreeHostManifest(JSContext *ctx); + +/* Optional host-call tape (T-043). */ +#define JS_HOST_TAPE_MAX_CAPACITY 1024 + +typedef struct JSHostTapeRecord { + uint32_t fn_id; + uint32_t req_len; + uint32_t resp_len; + uint32_t units; + uint64_t gas_pre; + uint64_t gas_post; + int is_error; + int charge_failed; + uint8_t req_hash[32]; + uint8_t resp_hash[32]; +} JSHostTapeRecord; + +int JS_EnableHostTape(JSContext *ctx, size_t capacity); +int JS_ResetHostTape(JSContext *ctx); +size_t JS_GetHostTapeLength(JSContext *ctx); +int JS_ReadHostTape(JSContext *ctx, JSHostTapeRecord *out_records, size_t max_records, size_t *out_count); + +#endif /* QUICKJS_HOST_H */ diff --git a/quickjs-internal.h b/quickjs-internal.h new file mode 100644 index 000000000..942ca0801 --- /dev/null +++ b/quickjs-internal.h @@ -0,0 +1,11 @@ +#ifndef QUICKJS_INTERNAL_H +#define QUICKJS_INTERNAL_H + +#include +#include + +/* Internal helpers not exposed in the public QuickJS API surface. */ +void js_sha256(const uint8_t *data, size_t len, uint8_t out_hash[32]); +void js_sha256_to_hex(const uint8_t hash[32], char out_hex[65]); + +#endif /* QUICKJS_INTERNAL_H */ diff --git a/quickjs-sha256.c b/quickjs-sha256.c new file mode 100644 index 000000000..3cb4896b7 --- /dev/null +++ b/quickjs-sha256.c @@ -0,0 +1,163 @@ +#include +#include +#include + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} JSSHA256Context; + +static const uint32_t js_sha256_k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2}; + +#define JS_SHA256_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define JS_SHA256_CH(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define JS_SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define JS_SHA256_EP0(x) (JS_SHA256_ROTR(x, 2) ^ JS_SHA256_ROTR(x, 13) ^ JS_SHA256_ROTR(x, 22)) +#define JS_SHA256_EP1(x) (JS_SHA256_ROTR(x, 6) ^ JS_SHA256_ROTR(x, 11) ^ JS_SHA256_ROTR(x, 25)) +#define JS_SHA256_SIG0(x) (JS_SHA256_ROTR(x, 7) ^ JS_SHA256_ROTR(x, 18) ^ ((x) >> 3)) +#define JS_SHA256_SIG1(x) (JS_SHA256_ROTR(x, 17) ^ JS_SHA256_ROTR(x, 19) ^ ((x) >> 10)) + +static void js_sha256_transform(JSSHA256Context *ctx, const uint8_t data[64]) +{ + uint32_t m[64]; + uint32_t a, b, c, d, e, f, g, h; + size_t i; + + for(i = 0; i < 16; i++) { + m[i] = ((uint32_t)data[i * 4] << 24) | ((uint32_t)data[i * 4 + 1] << 16) | + ((uint32_t)data[i * 4 + 2] << 8) | ((uint32_t)data[i * 4 + 3]); + } + + for(; i < 64; i++) { + m[i] = JS_SHA256_SIG1(m[i - 2]) + m[i - 7] + JS_SHA256_SIG0(m[i - 15]) + m[i - 16]; + } + + a = ctx->state[0]; + b = ctx->state[1]; + c = ctx->state[2]; + d = ctx->state[3]; + e = ctx->state[4]; + f = ctx->state[5]; + g = ctx->state[6]; + h = ctx->state[7]; + + for(i = 0; i < 64; i++) { + uint32_t t1 = h + JS_SHA256_EP1(e) + JS_SHA256_CH(e, f, g) + js_sha256_k[i] + m[i]; + uint32_t t2 = JS_SHA256_EP0(a) + JS_SHA256_MAJ(a, b, c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; + ctx->state[5] += f; + ctx->state[6] += g; + ctx->state[7] += h; +} + +static void js_sha256_init(JSSHA256Context *ctx) +{ + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; + ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; + ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; + ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; + ctx->state[7] = 0x5be0cd19; +} + +static void js_sha256_update(JSSHA256Context *ctx, const uint8_t *data, size_t len) +{ + for(size_t i = 0; i < len; i++) { + ctx->data[ctx->datalen] = data[i]; + ctx->datalen++; + if(ctx->datalen == 64) { + js_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void js_sha256_final(JSSHA256Context *ctx, uint8_t hash[32]) +{ + uint32_t i = ctx->datalen; + + if(ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while(i < 56) { + ctx->data[i++] = 0x00; + } + } else { + ctx->data[i++] = 0x80; + while(i < 64) { + ctx->data[i++] = 0x00; + } + js_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (uint8_t)(ctx->bitlen); + ctx->data[62] = (uint8_t)(ctx->bitlen >> 8); + ctx->data[61] = (uint8_t)(ctx->bitlen >> 16); + ctx->data[60] = (uint8_t)(ctx->bitlen >> 24); + ctx->data[59] = (uint8_t)(ctx->bitlen >> 32); + ctx->data[58] = (uint8_t)(ctx->bitlen >> 40); + ctx->data[57] = (uint8_t)(ctx->bitlen >> 48); + ctx->data[56] = (uint8_t)(ctx->bitlen >> 56); + js_sha256_transform(ctx, ctx->data); + + for(i = 0; i < 4; i++) { + hash[i] = (uint8_t)((ctx->state[0] >> (24 - i * 8)) & 0xff); + hash[i + 4] = (uint8_t)((ctx->state[1] >> (24 - i * 8)) & 0xff); + hash[i + 8] = (uint8_t)((ctx->state[2] >> (24 - i * 8)) & 0xff); + hash[i + 12] = (uint8_t)((ctx->state[3] >> (24 - i * 8)) & 0xff); + hash[i + 16] = (uint8_t)((ctx->state[4] >> (24 - i * 8)) & 0xff); + hash[i + 20] = (uint8_t)((ctx->state[5] >> (24 - i * 8)) & 0xff); + hash[i + 24] = (uint8_t)((ctx->state[6] >> (24 - i * 8)) & 0xff); + hash[i + 28] = (uint8_t)((ctx->state[7] >> (24 - i * 8)) & 0xff); + } +} + +void js_sha256(const uint8_t *data, size_t len, uint8_t out_hash[32]) +{ + JSSHA256Context ctx; + js_sha256_init(&ctx); + js_sha256_update(&ctx, data, len); + js_sha256_final(&ctx, out_hash); +} + +void js_sha256_to_hex(const uint8_t hash[32], char out_hex[65]) +{ + static const char hex_alphabet[] = "0123456789abcdef"; + for(size_t i = 0; i < 32; i++) { + out_hex[i * 2] = hex_alphabet[hash[i] >> 4]; + out_hex[i * 2 + 1] = hex_alphabet[hash[i] & 0x0f]; + } + out_hex[64] = '\0'; +} diff --git a/quickjs.c b/quickjs.c index 6f461d69d..2d13d3401 100644 --- a/quickjs.c +++ b/quickjs.c @@ -42,7 +42,9 @@ #include "cutils.h" #include "list.h" +#include "quickjs-internal.h" #include "quickjs.h" +#include "quickjs-host.h" #include "libregexp.h" #include "libunicode.h" #include "dtoa.h" @@ -234,6 +236,7 @@ typedef enum { } JSGCPhaseEnum; typedef enum OPCodeEnum OPCodeEnum; +typedef struct JSGasTraceData JSGasTraceData; struct JSRuntime { JSMallocFunctions mf; @@ -260,6 +263,9 @@ struct JSRuntime { struct list_head tmp_obj_list; /* used during GC */ JSGCPhaseEnum gc_phase : 8; size_t malloc_gc_threshold; + BOOL deterministic_mode : 8; + BOOL det_gc_pending : 8; + uint64_t det_gc_alloc_bytes; struct list_head weakref_list; /* list of JSWeakRefHeader.link */ #ifdef DUMP_LEAKS struct list_head string_list; /* list of JSString.link */ @@ -274,6 +280,10 @@ struct JSRuntime { BOOL current_exception_is_uncatchable : 8; /* true if inside an out of memory error, to avoid recursing */ BOOL in_out_of_memory : 8; + /* true if inside out-of-gas handling to avoid recursion */ + BOOL in_out_of_gas : 8; + /* true if inside a host_call to prevent reentrancy */ + BOOL in_host_call : 8; struct JSStackFrame *current_stack_frame; @@ -282,6 +292,8 @@ struct JSRuntime { JSHostPromiseRejectionTracker *host_promise_rejection_tracker; void *host_promise_rejection_tracker_opaque; + JSHostCallFunc *host_call_func; + void *host_call_opaque; struct list_head job_list; /* list of JSJobEntry.link */ @@ -489,6 +501,23 @@ struct JSContext { const char *input, size_t input_len, const char *filename, int flags, int scope_idx); void *user_opaque; + + uint64_t gas_limit; + uint64_t gas_remaining; + uint32_t gas_version; + + uint8_t *abi_manifest_bytes; + size_t abi_manifest_size; + uint8_t abi_manifest_hash[32]; + char abi_manifest_hash_hex[65]; + uint8_t *deterministic_context_blob; + size_t deterministic_context_blob_size; + + uint8_t *host_call_resp_buf; + uint32_t host_call_resp_capacity; + + JSGasTraceData *gas_trace; + BOOL deterministic_mode; }; typedef union JSFloat64Union { @@ -1085,6 +1114,94 @@ enum OPCodeEnum { OP_TEMP_END, }; +static const uint16_t js_opcode_gas_cost[OP_COUNT] = { +#define FMT(f) +#define DEF(id, size, n_pop, n_push, f) [OP_ ## id] = 1, +#define def(id, size, n_pop, n_push, f) +#include "quickjs-opcode.h" +#undef def +#undef DEF +#undef FMT +}; + +static inline uint16_t js_get_opcode_gas_cost(uint8_t opcode) +{ + if (opcode < OP_COUNT) + return js_opcode_gas_cost[opcode]; + return 0; +} + +struct JSGasTraceData { + BOOL enabled; + uint64_t opcode_count_total; + uint64_t opcode_gas; + uint64_t builtin_array_cb_base_count; + uint64_t builtin_array_cb_base_gas; + uint64_t builtin_array_cb_per_element_count; + uint64_t builtin_array_cb_per_element_gas; + uint64_t allocation_count; + uint64_t allocation_bytes; + uint64_t allocation_gas; +}; + +static void js_gas_trace_reset_counts(JSGasTraceData *trace) +{ + BOOL enabled = trace->enabled; + memset(trace, 0, sizeof(*trace)); + trace->enabled = enabled; +} + +static JSGasTraceData *js_gas_trace_or_null(JSContext *ctx) +{ + if (!ctx || !ctx->gas_trace || !ctx->gas_trace->enabled) + return NULL; + return ctx->gas_trace; +} + +static JSGasTraceData *js_gas_trace_ensure(JSContext *ctx) +{ + if (!ctx->gas_trace) { + ctx->gas_trace = js_mallocz_rt(ctx->rt, sizeof(JSGasTraceData)); + } + return ctx->gas_trace; +} + +static void js_gas_trace_record_opcode(JSContext *ctx, uint8_t opcode, uint16_t gas_cost) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + if (!trace) + return; + + trace->opcode_count_total++; + trace->opcode_gas += gas_cost; +} + +static void js_gas_trace_record_array_cb(JSContext *ctx, uint16_t gas_cost, BOOL per_element) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + if (!trace) + return; + + if (per_element) { + trace->builtin_array_cb_per_element_count++; + trace->builtin_array_cb_per_element_gas += gas_cost; + } else { + trace->builtin_array_cb_base_count++; + trace->builtin_array_cb_base_gas += gas_cost; + } +} + +static void js_gas_trace_record_allocation(JSContext *ctx, size_t size, uint64_t gas_cost) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + if (!trace) + return; + + trace->allocation_count++; + trace->allocation_bytes += size; + trace->allocation_gas += gas_cost; +} + static int JS_InitAtoms(JSRuntime *rt); static JSAtom __JS_NewAtomInit(JSRuntime *rt, const char *str, int len, int atom_type); @@ -1354,6 +1471,9 @@ static JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT; static void js_trigger_gc(JSRuntime *rt, size_t size) { BOOL force_gc; + + if (rt->deterministic_mode) + return; #ifdef FORCE_GC_AT_MALLOC force_gc = TRUE; #else @@ -1371,6 +1491,50 @@ static void js_trigger_gc(JSRuntime *rt, size_t size) } } +#define JS_GAS_ALLOC_BASE 3 +#define JS_GAS_ALLOC_PER_BYTE_SHIFT 4 +#define JS_DET_GC_THRESHOLD_BYTES (512 * 1024) + +static uint64_t js_gas_allocation_cost(size_t size) +{ + const uint64_t unit = UINT64_C(1) << JS_GAS_ALLOC_PER_BYTE_SHIFT; + uint64_t units; + + if (size == 0) { + units = 0; + } else if (size > UINT64_MAX - (unit - 1)) { + units = UINT64_MAX; + } else { + units = ((uint64_t)size + (unit - 1)) >> JS_GAS_ALLOC_PER_BYTE_SHIFT; + } + + if (units > UINT64_MAX - JS_GAS_ALLOC_BASE) + return UINT64_MAX; + return JS_GAS_ALLOC_BASE + units; +} + +static int js_charge_gas_allocation_ctx(JSContext *ctx, size_t size) +{ + JSRuntime *rt = ctx->rt; + uint64_t gas_cost; + + if (rt->in_out_of_gas || rt->current_exception_is_uncatchable) + return 0; + + if (rt->deterministic_mode) { + rt->det_gc_alloc_bytes += size; + if (rt->det_gc_alloc_bytes >= JS_DET_GC_THRESHOLD_BYTES) + rt->det_gc_pending = TRUE; + } + + gas_cost = js_gas_allocation_cost(size); + if (JS_UseGas(ctx, gas_cost)) + return -1; + + js_gas_trace_record_allocation(ctx, size, gas_cost); + return 0; +} + static size_t js_malloc_usable_size_unknown(const void *ptr) { return 0; @@ -1409,9 +1573,12 @@ void *js_mallocz_rt(JSRuntime *rt, size_t size) void *js_malloc(JSContext *ctx, size_t size) { void *ptr; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ptr = js_malloc_rt(ctx->rt, size); if (unlikely(!ptr)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } return ptr; @@ -1421,9 +1588,12 @@ void *js_malloc(JSContext *ctx, size_t size) void *js_mallocz(JSContext *ctx, size_t size) { void *ptr; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ptr = js_mallocz_rt(ctx->rt, size); if (unlikely(!ptr)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } return ptr; @@ -1438,9 +1608,12 @@ void js_free(JSContext *ctx, void *ptr) void *js_realloc(JSContext *ctx, void *ptr, size_t size) { void *ret; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ret = js_realloc_rt(ctx->rt, ptr, size); if (unlikely(!ret && size != 0)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } return ret; @@ -1450,9 +1623,12 @@ void *js_realloc(JSContext *ctx, void *ptr, size_t size) void *js_realloc2(JSContext *ctx, void *ptr, size_t size, size_t *pslack) { void *ret; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ret = js_realloc_rt(ctx->rt, ptr, size); if (unlikely(!ret && size != 0)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } if (pslack) { @@ -1697,6 +1873,7 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque) JS_UpdateStackTop(rt); rt->current_exception = JS_UNINITIALIZED; + rt->in_out_of_gas = FALSE; return rt; fail: @@ -1714,6 +1891,16 @@ void JS_SetRuntimeOpaque(JSRuntime *rt, void *opaque) rt->user_opaque = opaque; } +int JS_SetHostCallDispatcher(JSRuntime *rt, JSHostCallFunc *func, void *opaque) +{ + if (!rt) + return -1; + + rt->host_call_func = func; + rt->host_call_opaque = opaque; + return 0; +} + /* default memory allocation functions with memory limitation */ static size_t js_def_malloc_usable_size(const void *ptr) { @@ -1945,6 +2132,11 @@ static JSString *js_alloc_string_rt(JSRuntime *rt, int max_len, int is_wide_char static JSString *js_alloc_string(JSContext *ctx, int max_len, int is_wide_char) { JSString *p; + size_t alloc_size = sizeof(JSString) + + (((size_t)max_len << is_wide_char) + 1 - is_wide_char); + + if (js_charge_gas_allocation_ctx(ctx, alloc_size)) + return NULL; p = js_alloc_string_rt(ctx->rt, max_len, is_wide_char); if (unlikely(!p)) { JS_ThrowOutOfMemory(ctx); @@ -2187,6 +2379,9 @@ JSContext *JS_NewContextRaw(JSRuntime *rt) ctx->iterator_ctor = JS_NULL; ctx->regexp_ctor = JS_NULL; ctx->promise_ctor = JS_NULL; + ctx->gas_limit = JS_GAS_UNLIMITED; + ctx->gas_remaining = JS_GAS_UNLIMITED; + ctx->gas_version = JS_GAS_VERSION_LATEST; init_list_head(&ctx->loaded_modules); if (JS_AddIntrinsicBasicObjects(ctx)) { @@ -2221,6 +2416,861 @@ JSContext *JS_NewContext(JSRuntime *rt) return ctx; } +enum { + JS_DETERMINISTIC_DISABLED_EVAL = 1, + JS_DETERMINISTIC_DISABLED_FUNCTION = 2, + JS_DETERMINISTIC_DISABLED_RANDOM = 3, + JS_DETERMINISTIC_DISABLED_PROMISE = 4, + JS_DETERMINISTIC_DISABLED_REGEXP = 5, + JS_DETERMINISTIC_DISABLED_PROXY = 6, + JS_DETERMINISTIC_DISABLED_TYPED_ARRAY = 7, + JS_DETERMINISTIC_DISABLED_ARRAY_BUFFER = 8, + JS_DETERMINISTIC_DISABLED_SHARED_ARRAY_BUFFER = 9, + JS_DETERMINISTIC_DISABLED_DATAVIEW = 10, + JS_DETERMINISTIC_DISABLED_WEBASSEMBLY = 11, + JS_DETERMINISTIC_DISABLED_ATOMICS = 12, + JS_DETERMINISTIC_DISABLED_CONSOLE = 13, + JS_DETERMINISTIC_DISABLED_PRINT = 14, + JS_DETERMINISTIC_DISABLED_JSON_PARSE = 15, + JS_DETERMINISTIC_DISABLED_JSON_STRINGIFY = 16, + JS_DETERMINISTIC_DISABLED_ARRAY_SORT = 17, +}; + +static const char *js_get_disabled_name(int magic) +{ + switch (magic) { + case JS_DETERMINISTIC_DISABLED_ARRAY_SORT: + return "Array.prototype.sort"; + case JS_DETERMINISTIC_DISABLED_JSON_STRINGIFY: + return "JSON.stringify"; + case JS_DETERMINISTIC_DISABLED_JSON_PARSE: + return "JSON.parse"; + case JS_DETERMINISTIC_DISABLED_PRINT: + return "print"; + case JS_DETERMINISTIC_DISABLED_CONSOLE: + return "console"; + case JS_DETERMINISTIC_DISABLED_ATOMICS: + return "Atomics"; + case JS_DETERMINISTIC_DISABLED_WEBASSEMBLY: + return "WebAssembly"; + case JS_DETERMINISTIC_DISABLED_DATAVIEW: + return "DataView"; + case JS_DETERMINISTIC_DISABLED_SHARED_ARRAY_BUFFER: + return "SharedArrayBuffer"; + case JS_DETERMINISTIC_DISABLED_ARRAY_BUFFER: + return "ArrayBuffer"; + case JS_DETERMINISTIC_DISABLED_TYPED_ARRAY: + return "Typed arrays"; + case JS_DETERMINISTIC_DISABLED_PROXY: + return "Proxy"; + case JS_DETERMINISTIC_DISABLED_REGEXP: + return "RegExp"; + case JS_DETERMINISTIC_DISABLED_PROMISE: + return "Promise"; + case JS_DETERMINISTIC_DISABLED_RANDOM: + return "Math.random"; + case JS_DETERMINISTIC_DISABLED_FUNCTION: + return "Function"; + case JS_DETERMINISTIC_DISABLED_EVAL: + default: + return "eval"; + } +} + +static JSValue js_deterministic_disabled(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + const char *name = js_get_disabled_name(magic); + if (magic == JS_DETERMINISTIC_DISABLED_TYPED_ARRAY) + return JS_ThrowTypeError(ctx, "%s are disabled in deterministic mode", name); + return JS_ThrowTypeError(ctx, "%s is disabled in deterministic mode", name); +} + +static int js_deterministic_define_disabled_global(JSContext *ctx, const char *name, + int length, int magic) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, name, length, + JS_CFUNC_constructor_or_func_magic, magic); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValueStr(ctx, ctx->global_obj, name, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + +static JSValue js_deterministic_compile_regexp(JSContext *ctx, JSValueConst pattern, + JSValueConst flags) +{ + (void)pattern; + (void)flags; + return JS_ThrowTypeError(ctx, "RegExp is disabled in deterministic mode"); +} + +static int js_deterministic_disable_eval(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "eval", 1, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_EVAL); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_eval, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) { + JS_FreeValue(ctx, fn); + return -1; + } + + JS_FreeValue(ctx, ctx->eval_obj); + ctx->eval_obj = JS_UNDEFINED; + JS_FreeValue(ctx, fn); + return 0; +} + +static int js_deterministic_disable_function(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "Function", 1, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_FUNCTION); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_Function, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + +static int js_deterministic_disable_random(JSContext *ctx) +{ + JSValue math, fn; + int ret; + + math = JS_GetProperty(ctx, ctx->global_obj, JS_ATOM_Math); + if (JS_IsException(math)) + return -1; + if (!JS_IsObject(math)) { + JS_FreeValue(ctx, math); + return -1; + } + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "random", 0, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_RANDOM); + if (JS_IsException(fn)) { + JS_FreeValue(ctx, math); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, math, "random", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + JS_FreeValue(ctx, fn); + JS_FreeValue(ctx, math); + if (ret < 0) + return -1; + + return 0; +} + +static int js_deterministic_disable_regexp(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "RegExp", 2, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_REGEXP); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_RegExp, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) { + JS_FreeValue(ctx, fn); + return -1; + } + + JS_FreeValue(ctx, ctx->regexp_ctor); + ctx->regexp_ctor = JS_DupValue(ctx, fn); + ctx->compile_regexp = js_deterministic_compile_regexp; + + JS_FreeValue(ctx, fn); + return 0; +} + +static int js_deterministic_disable_proxy(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "Proxy", 2, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_PROXY); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_Proxy, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + +static int js_deterministic_disable_promise(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "Promise", 1, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_PROMISE); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_Promise, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + /* Ensure async internals see the disabled ctor */ + JS_FreeValue(ctx, ctx->promise_ctor); + ctx->promise_ctor = JS_DupValue(ctx, fn); + + /* Mirror common Promise statics to the same disabled stub */ + JS_DefinePropertyValueStr(ctx, fn, "resolve", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "reject", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "all", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "race", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "any", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "allSettled", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + +static int js_deterministic_disable_typed_arrays(JSContext *ctx) +{ + static const struct { + const char *name; + int length; + int magic; + } entries[] = { + { "ArrayBuffer", 1, JS_DETERMINISTIC_DISABLED_ARRAY_BUFFER }, + { "SharedArrayBuffer", 1, JS_DETERMINISTIC_DISABLED_SHARED_ARRAY_BUFFER }, + { "DataView", 3, JS_DETERMINISTIC_DISABLED_DATAVIEW }, + { "Uint8Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Uint8ClampedArray", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Int8Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Uint16Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Int16Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Uint32Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Int32Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "BigInt64Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "BigUint64Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Float16Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Float32Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Float64Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + }; + + for (size_t i = 0; i < countof(entries); i++) { + if (js_deterministic_define_disabled_global(ctx, entries[i].name, + entries[i].length, entries[i].magic)) + return -1; + } + return 0; +} + +static int js_deterministic_disable_webassembly(JSContext *ctx) +{ + return js_deterministic_define_disabled_global(ctx, "WebAssembly", 1, + JS_DETERMINISTIC_DISABLED_WEBASSEMBLY); +} + +static int js_deterministic_disable_atomics(JSContext *ctx) +{ + return js_deterministic_define_disabled_global(ctx, "Atomics", 3, + JS_DETERMINISTIC_DISABLED_ATOMICS); +} + +static int js_deterministic_disable_console(JSContext *ctx) +{ + JSValue console; + + console = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(console)) + return -1; + + static const char *const methods[] = { "log", "info", "warn", "error", "debug" }; + for (size_t i = 0; i < countof(methods); i++) { + JSValue fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "console", 1, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_CONSOLE); + if (JS_IsException(fn)) { + JS_FreeValue(ctx, console); + return -1; + } + if (JS_DefinePropertyValueStr(ctx, console, methods[i], JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeValue(ctx, fn); + JS_FreeValue(ctx, console); + return -1; + } + JS_FreeValue(ctx, fn); + } + + if (JS_DefinePropertyValueStr(ctx, ctx->global_obj, "console", JS_DupValue(ctx, console), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeValue(ctx, console); + return -1; + } + + JS_FreeValue(ctx, console); + return 0; +} + +static int js_deterministic_disable_print(JSContext *ctx) +{ + return js_deterministic_define_disabled_global(ctx, "print", 1, + JS_DETERMINISTIC_DISABLED_PRINT); +} + +static int js_deterministic_disable_json(JSContext *ctx) +{ + JSValue json, parse_fn, stringify_fn; + int ret; + + json = JS_GetProperty(ctx, ctx->global_obj, JS_ATOM_JSON); + if (JS_IsException(json)) + return -1; + if (!JS_IsObject(json)) { + JS_FreeValue(ctx, json); + return -1; + } + + parse_fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "parse", 2, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_JSON_PARSE); + if (JS_IsException(parse_fn)) { + JS_FreeValue(ctx, json); + return -1; + } + stringify_fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "stringify", 3, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_JSON_STRINGIFY); + if (JS_IsException(stringify_fn)) { + JS_FreeValue(ctx, parse_fn); + JS_FreeValue(ctx, json); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, json, "parse", JS_DupValue(ctx, parse_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + ret = JS_DefinePropertyValueStr(ctx, json, "stringify", JS_DupValue(ctx, stringify_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + JS_FreeValue(ctx, parse_fn); + JS_FreeValue(ctx, stringify_fn); + JS_FreeValue(ctx, json); + return 0; + +fail: + JS_FreeValue(ctx, parse_fn); + JS_FreeValue(ctx, stringify_fn); + JS_FreeValue(ctx, json); + return -1; +} + +static int js_deterministic_disable_array_sort(JSContext *ctx) +{ + JSValue array_ctor, array_proto, sort_fn; + int ret; + + array_ctor = JS_GetProperty(ctx, ctx->global_obj, JS_ATOM_Array); + if (JS_IsException(array_ctor)) + return -1; + if (!JS_IsObject(array_ctor)) { + JS_FreeValue(ctx, array_ctor); + return -1; + } + + array_proto = JS_GetProperty(ctx, array_ctor, JS_ATOM_prototype); + if (JS_IsException(array_proto)) { + JS_FreeValue(ctx, array_ctor); + return -1; + } + JS_FreeValue(ctx, array_ctor); + if (!JS_IsObject(array_proto)) { + JS_FreeValue(ctx, array_proto); + return -1; + } + + sort_fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "sort", 1, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_ARRAY_SORT); + if (JS_IsException(sort_fn)) { + JS_FreeValue(ctx, array_proto); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, array_proto, "sort", JS_DupValue(ctx, sort_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + JS_FreeValue(ctx, sort_fn); + JS_FreeValue(ctx, array_proto); + if (ret < 0) + return -1; + return 0; +} + +static int js_deterministic_init_host(JSContext *ctx) +{ + JSValue host_ns, host_v1; + int ret; + + host_ns = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(host_ns)) + return -1; + + host_v1 = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(host_v1)) { + JS_FreeValue(ctx, host_ns); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, host_ns, "v1", JS_DupValue(ctx, host_v1), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + ret = JS_DefinePropertyValueStr(ctx, ctx->global_obj, "Host", JS_DupValue(ctx, host_ns), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + JS_FreeValue(ctx, host_ns); + JS_FreeValue(ctx, host_v1); + + return 0; + +fail: + JS_FreeValue(ctx, host_ns); + JS_FreeValue(ctx, host_v1); + return -1; +} + +static int js_deterministic_init_context(JSContext *ctx) +{ + if (JS_AddIntrinsicBaseObjects(ctx) || + JS_AddIntrinsicEval(ctx) || + JS_AddIntrinsicJSON(ctx) || + JS_AddIntrinsicMapSet(ctx) || + js_deterministic_disable_eval(ctx) || + js_deterministic_disable_function(ctx) || + js_deterministic_disable_regexp(ctx) || + js_deterministic_disable_proxy(ctx) || + js_deterministic_disable_random(ctx) || + js_deterministic_disable_promise(ctx) || + js_deterministic_disable_typed_arrays(ctx) || + js_deterministic_disable_atomics(ctx) || + js_deterministic_disable_console(ctx) || + js_deterministic_disable_print(ctx) || + js_deterministic_disable_json(ctx) || + js_deterministic_disable_array_sort(ctx) || + js_deterministic_disable_webassembly(ctx) || + js_deterministic_init_host(ctx)) { + return -1; + } + ctx->random_state = 1; /* deterministic seed */ + ctx->deterministic_mode = TRUE; + return 0; +} + +#ifdef __EMSCRIPTEN__ +__attribute__((import_module("host"), import_name("host_call"))) +uint32_t js_wasm_import_host_call(uint32_t fn_id, uint32_t req_ptr, uint32_t req_len, + uint32_t resp_ptr, uint32_t resp_capacity); + +static uint32_t js_wasm_host_call(JSContext *ctx, uint32_t fn_id, const uint8_t *req_ptr, + uint32_t req_len, uint8_t *resp_ptr, + uint32_t resp_capacity, void *opaque) +{ + (void)ctx; + (void)opaque; + return js_wasm_import_host_call(fn_id, + (uint32_t)(uintptr_t)req_ptr, + req_len, + (uint32_t)(uintptr_t)resp_ptr, + resp_capacity); +} +#endif + +int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx) +{ + JSRuntime *rt; + JSContext *ctx; + + if (!out_rt || !out_ctx) + return -1; + + *out_rt = NULL; + *out_ctx = NULL; + + rt = JS_NewRuntime(); + if (!rt) + return -1; + + rt->deterministic_mode = TRUE; + rt->det_gc_pending = FALSE; + rt->det_gc_alloc_bytes = 0; +#ifdef __EMSCRIPTEN__ + rt->host_call_func = js_wasm_host_call; +#else + rt->host_call_func = NULL; +#endif + rt->host_call_opaque = NULL; + rt->in_host_call = FALSE; + JS_SetGCThreshold(rt, (size_t)-1); + + ctx = JS_NewContextRaw(rt); + if (!ctx) { + JS_FreeRuntime(rt); + return -1; + } + + if (js_deterministic_init_context(ctx)) { + JS_FreeContext(ctx); + JS_FreeRuntime(rt); + return -1; + } + + *out_rt = rt; + *out_ctx = ctx; + return 0; +} + +static JSValue JS_ThrowManifestError(JSContext *ctx, const char *code, const char *message) +{ + JSValue obj, name, msg, code_val; + + obj = JS_NewError(ctx); + if (JS_IsException(obj)) + return JS_EXCEPTION; + + name = JS_NewString(ctx, "ManifestError"); + msg = JS_NewString(ctx, message); + code_val = JS_NewString(ctx, code); + if (JS_IsException(name) || JS_IsException(msg) || JS_IsException(code_val)) { + if (!JS_IsException(name)) + JS_FreeValue(ctx, name); + if (!JS_IsException(msg)) + JS_FreeValue(ctx, msg); + if (!JS_IsException(code_val)) + JS_FreeValue(ctx, code_val); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + + JS_DefinePropertyValue(ctx, obj, JS_ATOM_name, name, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValue(ctx, obj, JS_ATOM_message, msg, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "code", code_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_Throw(ctx, obj); + return JS_EXCEPTION; +} + +static int js_hex_nibble(int c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + return -1; +} + +static int js_parse_hash_hex(JSContext *ctx, const char *hex, uint8_t *out, size_t out_size) +{ + size_t hex_len, i; + + if (!hex || !out) + return -1; + + hex_len = strlen(hex); + if (hex_len != out_size * 2) { + JS_ThrowTypeError(ctx, "abi manifest hash must be 64 lowercase hex characters"); + return -1; + } + + for(i = 0; i < out_size; i++) { + int high = js_hex_nibble(hex[i * 2]); + int low = js_hex_nibble(hex[i * 2 + 1]); + if (high < 0 || low < 0) { + JS_ThrowTypeError(ctx, "abi manifest hash must be 64 lowercase hex characters"); + return -1; + } + out[i] = (uint8_t)((high << 4) | low); + } + + return 0; +} + +int JS_InitDeterministicContext(JSContext *ctx, const JSDeterministicInitOptions *options) +{ + uint8_t computed_hash[32]; + uint8_t expected_hash[32]; + uint8_t *manifest_copy = NULL; + uint8_t *context_copy = NULL; + + if (!ctx || !options) + return -1; + + if (ctx->abi_manifest_bytes) { + JS_ThrowTypeError(ctx, "abi manifest is already initialized"); + return -1; + } + + if (!options->manifest_bytes || options->manifest_size == 0) { + JS_ThrowTypeError(ctx, "abi manifest is required"); + return -1; + } + + if (options->manifest_size > JS_DETERMINISTIC_MAX_MANIFEST_BYTES) { + JS_ThrowTypeError(ctx, "abi manifest exceeds maximum size"); + return -1; + } + + if (!options->manifest_hash_hex) { + JS_ThrowTypeError(ctx, "abi manifest hash is required"); + return -1; + } + + if (js_parse_hash_hex(ctx, options->manifest_hash_hex, expected_hash, sizeof(expected_hash)) != 0) + return -1; + + if (options->context_blob_size > 0 && !options->context_blob) { + JS_ThrowTypeError(ctx, "context blob is required when context_blob_size is set"); + return -1; + } + + js_sha256(options->manifest_bytes, options->manifest_size, computed_hash); + if (memcmp(computed_hash, expected_hash, sizeof(expected_hash)) != 0) { + JS_ThrowManifestError(ctx, "ABI_MANIFEST_HASH_MISMATCH", "abi manifest hash mismatch"); + return -1; + } + + manifest_copy = js_malloc_rt(ctx->rt, options->manifest_size); + if (!manifest_copy) { + JS_ThrowOutOfMemory(ctx); + return -1; + } + memcpy(manifest_copy, options->manifest_bytes, options->manifest_size); + + if (options->context_blob && options->context_blob_size > 0) { + if (options->context_blob_size > JS_DETERMINISTIC_MAX_CONTEXT_BLOB_BYTES) { + js_free_rt(ctx->rt, manifest_copy); + JS_ThrowTypeError(ctx, "context blob exceeds maximum size"); + return -1; + } + + context_copy = js_malloc_rt(ctx->rt, options->context_blob_size); + if (!context_copy) { + js_free_rt(ctx->rt, manifest_copy); + JS_ThrowOutOfMemory(ctx); + return -1; + } + memcpy(context_copy, options->context_blob, options->context_blob_size); + } + + if (JS_InitHostFromManifest(ctx, manifest_copy, options->manifest_size) != 0) { + js_free_rt(ctx->rt, manifest_copy); + if (context_copy) + js_free_rt(ctx->rt, context_copy); + return -1; + } + + if (context_copy && options->context_blob_size > 0) { + if (JS_InitErgonomicGlobals(ctx, context_copy, options->context_blob_size) != 0) { + JS_FreeHostManifest(ctx); + js_free_rt(ctx->rt, manifest_copy); + js_free_rt(ctx->rt, context_copy); + return -1; + } + } + + memcpy(ctx->abi_manifest_hash, computed_hash, sizeof(computed_hash)); + js_sha256_to_hex(computed_hash, ctx->abi_manifest_hash_hex); + ctx->abi_manifest_bytes = manifest_copy; + ctx->abi_manifest_size = options->manifest_size; + ctx->deterministic_context_blob = context_copy; + ctx->deterministic_context_blob_size = context_copy ? options->context_blob_size : 0; + JS_SetGasLimit(ctx, options->gas_limit); + + return 0; +} + +static int js_reserve_host_response_buffer(JSContext *ctx, uint32_t capacity) +{ + uint8_t *new_buf; + + if (ctx->host_call_resp_capacity >= capacity) + return 0; + + new_buf = js_realloc_rt(ctx->rt, ctx->host_call_resp_buf, capacity); + if (!new_buf) { + JS_ThrowOutOfMemory(ctx); + return -1; + } + + ctx->host_call_resp_buf = new_buf; + ctx->host_call_resp_capacity = capacity; + return 0; +} + +int JS_HostCall(JSContext *ctx, + uint32_t fn_id, + const uint8_t *req_bytes, + size_t req_len, + uint32_t max_request_bytes, + uint32_t max_response_bytes, + JSHostCallResult *out_result) +{ + JSRuntime *rt; + uint32_t resp_len, resp_capacity, req_len32; + uint32_t dv_limit; + const uint8_t *req_ptr; + + if (!ctx || !out_result) + return -1; + + out_result->data = NULL; + out_result->length = 0; + + rt = ctx->rt; + dv_limit = JS_DV_LIMIT_DEFAULTS.max_encoded_bytes; + + if (!rt->host_call_func) { + JS_ThrowTypeError(ctx, "host_call dispatcher is not configured"); + return -1; + } + + if (fn_id == 0) { + JS_ThrowTypeError(ctx, "host_call fn_id must be >= 1"); + return -1; + } + + if (max_request_bytes == 0) { + JS_ThrowTypeError(ctx, "host_call max_request_bytes must be > 0"); + return -1; + } + + if (max_request_bytes > dv_limit) { + JS_ThrowTypeError(ctx, "host_call max_request_bytes exceeds DV limit"); + return -1; + } + + if (max_response_bytes == 0) { + JS_ThrowTypeError(ctx, "host_call max_response_bytes must be > 0"); + return -1; + } + + if (max_response_bytes > dv_limit) { + JS_ThrowTypeError(ctx, "host_call max_response_bytes exceeds DV limit"); + return -1; + } + + if (req_len > (size_t)max_request_bytes) { + JS_ThrowTypeError(ctx, "host_call request exceeds max_request_bytes"); + return -1; + } + + if (req_len > (size_t)dv_limit) { + JS_ThrowTypeError(ctx, "host_call request exceeds DV limit"); + return -1; + } + + if (req_len > UINT32_MAX) { + JS_ThrowTypeError(ctx, "host_call request length overflow"); + return -1; + } + + if (req_len > 0 && !req_bytes) { + JS_ThrowTypeError(ctx, "host_call request pointer is null"); + return -1; + } + + if (rt->in_host_call) { + JS_ThrowTypeError(ctx, "host_call is already in progress"); + return -1; + } + + resp_capacity = max_response_bytes; + if (js_reserve_host_response_buffer(ctx, resp_capacity)) + return -1; + + rt->in_host_call = TRUE; + req_len32 = (uint32_t)req_len; + req_ptr = req_bytes ? req_bytes : NULL; + resp_len = rt->host_call_func(ctx, fn_id, req_ptr, req_len32, + ctx->host_call_resp_buf, resp_capacity, + rt->host_call_opaque); + rt->in_host_call = FALSE; + + if (JS_HasException(ctx)) + return -1; + + if (resp_len == JS_HOST_CALL_TRANSPORT_ERROR || resp_len > resp_capacity) { + JS_ThrowHostTransportError(ctx); + return -1; + } + + out_result->data = ctx->host_call_resp_buf; + out_result->length = resp_len; + return 0; +} + void *JS_GetContextOpaque(JSContext *ctx) { return ctx->user_opaque; @@ -2231,6 +3281,148 @@ void JS_SetContextOpaque(JSContext *ctx, void *opaque) ctx->user_opaque = opaque; } +static JSValue JS_ThrowOutOfGas(JSContext *ctx) +{ + JSRuntime *rt = ctx->rt; + JSValue obj, name, message, code; + + if (rt->in_out_of_gas) + return JS_EXCEPTION; + + rt->in_out_of_gas = TRUE; + obj = JS_NewError(ctx); + if (JS_IsException(obj)) + goto fail; + + name = JS_NewString(ctx, "OutOfGas"); + message = JS_NewString(ctx, "out of gas"); + code = JS_NewString(ctx, "OOG"); + if (JS_IsException(name) || JS_IsException(message) || JS_IsException(code)) { + if (!JS_IsException(name)) + JS_FreeValue(ctx, name); + if (!JS_IsException(message)) + JS_FreeValue(ctx, message); + if (!JS_IsException(code)) + JS_FreeValue(ctx, code); + JS_FreeValue(ctx, obj); + goto fail; + } + + JS_DefinePropertyValue(ctx, obj, JS_ATOM_name, name, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValue(ctx, obj, JS_ATOM_message, message, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "code", code, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_Throw(ctx, obj); + JS_SetUncatchableException(ctx, TRUE); + rt->in_out_of_gas = FALSE; + return JS_EXCEPTION; + +fail: + rt->in_out_of_gas = FALSE; + return JS_EXCEPTION; +} + +void JS_SetGasLimit(JSContext *ctx, uint64_t gas_limit) +{ + ctx->gas_limit = gas_limit; + ctx->gas_remaining = gas_limit; +} + +uint64_t JS_GetGasRemaining(JSContext *ctx) +{ + return ctx->gas_remaining; +} + +uint64_t JS_GetGasLimit(JSContext *ctx) +{ + return ctx->gas_limit; +} + +uint32_t JS_GetGasVersion(JSContext *ctx) +{ + return ctx->gas_version; +} + +int JS_EnableGasTrace(JSContext *ctx, int enabled) +{ + JSGasTraceData *trace; + + if (!ctx) + return -1; + + trace = js_gas_trace_ensure(ctx); + if (!trace) + return -1; + + js_gas_trace_reset_counts(trace); + trace->enabled = enabled ? TRUE : FALSE; + return 0; +} + +int JS_ResetGasTrace(JSContext *ctx) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + + if (!trace) + return -1; + + js_gas_trace_reset_counts(trace); + return 0; +} + +int JS_ReadGasTrace(JSContext *ctx, JSGasTrace *out_trace) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + + if (!trace || !out_trace) + return -1; + + out_trace->opcode_count = trace->opcode_count_total; + out_trace->opcode_gas = trace->opcode_gas; + out_trace->builtin_array_cb_base_count = trace->builtin_array_cb_base_count; + out_trace->builtin_array_cb_base_gas = trace->builtin_array_cb_base_gas; + out_trace->builtin_array_cb_per_element_count = trace->builtin_array_cb_per_element_count; + out_trace->builtin_array_cb_per_element_gas = trace->builtin_array_cb_per_element_gas; + out_trace->allocation_count = trace->allocation_count; + out_trace->allocation_bytes = trace->allocation_bytes; + out_trace->allocation_gas = trace->allocation_gas; + + return 0; +} + +int JS_UseGas(JSContext *ctx, uint64_t amount) +{ + if (ctx->gas_limit == JS_GAS_UNLIMITED) + return 0; + if (amount == 0) + return 0; + if (amount > ctx->gas_remaining) { + ctx->gas_remaining = 0; + JS_ThrowOutOfGas(ctx); + return -1; + } + ctx->gas_remaining -= amount; + return 0; +} + +int JS_RunGCCheckpoint(JSContext *ctx) +{ + JSRuntime *rt = ctx->rt; + + if (rt->current_exception_is_uncatchable || rt->in_out_of_gas) + return 0; + + if (rt->deterministic_mode && !rt->det_gc_pending) + return 0; + + JS_RunGC(rt); + rt->det_gc_pending = FALSE; + rt->det_gc_alloc_bytes = 0; + return 0; +} + /* set the new value and free the old value after (freeing the value can reallocate the object data) */ static inline void set_value(JSContext *ctx, JSValue *pval, JSValue new_val) @@ -2370,6 +3562,7 @@ void JS_FreeContext(JSContext *ctx) } #endif + JS_FreeHostManifest(ctx); js_free_modules(ctx, JS_FREE_MODULE_ALL); JS_FreeValue(ctx, ctx->global_obj); @@ -2400,8 +3593,17 @@ void JS_FreeContext(JSContext *ctx) js_free_shape_null(ctx->rt, ctx->regexp_shape); js_free_shape_null(ctx->rt, ctx->regexp_result_shape); + if (ctx->abi_manifest_bytes) + js_free_rt(ctx->rt, ctx->abi_manifest_bytes); + if (ctx->deterministic_context_blob) + js_free_rt(ctx->rt, ctx->deterministic_context_blob); + if (ctx->host_call_resp_buf) + js_free_rt(ctx->rt, ctx->host_call_resp_buf); + list_del(&ctx->link); remove_gc_object(&ctx->header); + if (ctx->gas_trace) + js_free_rt(ctx->rt, ctx->gas_trace); js_free_rt(ctx->rt, ctx); } @@ -17369,7 +18571,7 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, size_t alloca_size; #if !DIRECT_DISPATCH -#define SWITCH(pc) switch (opcode = *pc++) +#define DISPATCH() switch (opcode) #define CASE(op) case op #define DEFAULT default #define BREAK break @@ -17384,10 +18586,10 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, #include "quickjs-opcode.h" [ OP_COUNT ... 255 ] = &&case_default }; -#define SWITCH(pc) goto *dispatch_table[opcode = *pc++]; +#define DISPATCH() goto *dispatch_table[opcode] #define CASE(op) case_ ## op #define DEFAULT case_default -#define BREAK SWITCH(pc) +#define BREAK goto dispatch_next #endif if (js_poll_interrupts(caller_ctx)) @@ -17480,8 +18682,17 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, for(;;) { int call_argc; JSValue *call_argv; + opcode = *pc++; + uint16_t gas_cost = js_get_opcode_gas_cost(opcode); + if (unlikely(JS_UseGas(ctx, gas_cost) != 0)) + goto exception; + js_gas_trace_record_opcode(ctx, opcode, gas_cost); - SWITCH(pc) { + #if !DIRECT_DISPATCH + DISPATCH() { + #else + DISPATCH(); + #endif CASE(OP_push_i32): *sp++ = JS_NewInt32(ctx, get_u32(pc)); pc += 4; @@ -20061,7 +21272,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, JS_ThrowInternalError(ctx, "invalid opcode: pc=%u opcode=0x%02x", (int)(pc - b->byte_code_buf - 1), opcode); goto exception; +#if !DIRECT_DISPATCH } +#endif +#if DIRECT_DISPATCH + dispatch_next: + ; +#endif } exception: if (is_backtrace_needed(ctx, rt->current_exception)) { @@ -40435,6 +41652,10 @@ static JSValue js_function_proto(JSContext *ctx, JSValueConst this_val, static JSValue js_function_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv, int magic) { + if (unlikely(ctx->deterministic_mode)) { + return JS_ThrowTypeError(ctx, "Function constructor is disabled in deterministic mode"); + } + JSFunctionKindEnum func_kind = magic; int i, n, ret; JSValue s, proto, obj = JS_UNDEFINED; @@ -41477,6 +42698,9 @@ static JSValue js_array_concat(JSContext *ctx, JSValueConst this_val, #define special_filter 4 #define special_TA 8 +#define JS_GAS_ARRAY_CB_BASE 5 +#define JS_GAS_ARRAY_CB_PER_ELEMENT 2 + static JSValue js_typed_array___speciesCreate(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); @@ -41490,8 +42714,12 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, int64_t len, k, n; int present; + obj = JS_UNDEFINED; ret = JS_UNDEFINED; val = JS_UNDEFINED; + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_BASE) != 0)) + goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_BASE, FALSE); if (special & special_TA) { obj = JS_DupValue(ctx, this_val); len = js_typed_array_get_length_unsafe(ctx, obj); @@ -41546,6 +42774,9 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, n = 0; for(k = 0; k < len; k++) { + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) + goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT, TRUE); if (special & special_TA) { val = JS_GetPropertyInt64(ctx, obj, k); if (JS_IsException(val)) @@ -41647,8 +42878,12 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, int64_t len, k, k1; int present; + obj = JS_UNDEFINED; acc = JS_UNDEFINED; val = JS_UNDEFINED; + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_BASE) != 0)) + goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_BASE, FALSE); if (special & special_TA) { obj = JS_DupValue(ctx, this_val); len = js_typed_array_get_length_unsafe(ctx, obj); @@ -41669,6 +42904,9 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, acc = JS_DupValue(ctx, argv[1]); } else { for(;;) { + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) + goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT, TRUE); if (k >= len) { JS_ThrowTypeError(ctx, "empty array"); goto exception; @@ -41691,6 +42929,9 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, } for (; k < len; k++) { k1 = (special & special_reduceRight) ? len - k - 1 : k; + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) + goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT, TRUE); if (special & special_TA) { val = JS_GetPropertyInt64(ctx, obj, k1); if (JS_IsException(val)) diff --git a/quickjs.h b/quickjs.h index 92cc000d0..5c817d80a 100644 --- a/quickjs.h +++ b/quickjs.h @@ -343,10 +343,16 @@ static inline JSValue __JS_NewShortBigInt(JSContext *ctx, int64_t d) promise. Only allowed with JS_EVAL_TYPE_GLOBAL */ #define JS_EVAL_FLAG_ASYNC (1 << 7) +#define JS_DETERMINISTIC_MAX_MANIFEST_BYTES 1048576 +#define JS_DETERMINISTIC_MAX_CONTEXT_BLOB_BYTES 1048576 + typedef JSValue JSCFunction(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); typedef JSValue JSCFunctionMagic(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); typedef JSValue JSCFunctionData(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data); +#define JS_GAS_VERSION_LATEST 1 +#define JS_GAS_UNLIMITED UINT64_C(0xffffffffffffffff) + typedef struct JSMallocState { size_t malloc_count; size_t malloc_size; @@ -380,13 +386,88 @@ void JS_SetRuntimeOpaque(JSRuntime *rt, void *opaque); typedef void JS_MarkFunc(JSRuntime *rt, JSGCObjectHeader *gp); void JS_MarkValue(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); void JS_RunGC(JSRuntime *rt); +int JS_RunGCCheckpoint(JSContext *ctx); JS_BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj); JSContext *JS_NewContext(JSRuntime *rt); +int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx); +typedef struct JSDeterministicInitOptions { + const uint8_t *manifest_bytes; + size_t manifest_size; + const char *manifest_hash_hex; + const uint8_t *context_blob; + size_t context_blob_size; + uint64_t gas_limit; +} JSDeterministicInitOptions; +int JS_InitDeterministicContext(JSContext *ctx, const JSDeterministicInitOptions *options); void JS_FreeContext(JSContext *s); JSContext *JS_DupContext(JSContext *ctx); void *JS_GetContextOpaque(JSContext *ctx); void JS_SetContextOpaque(JSContext *ctx, void *opaque); +void JS_SetGasLimit(JSContext *ctx, uint64_t gas_limit); +uint64_t JS_GetGasRemaining(JSContext *ctx); +uint64_t JS_GetGasLimit(JSContext *ctx); +uint32_t JS_GetGasVersion(JSContext *ctx); +typedef struct JSGasTrace { + uint64_t opcode_count; + uint64_t opcode_gas; + uint64_t builtin_array_cb_base_count; + uint64_t builtin_array_cb_base_gas; + uint64_t builtin_array_cb_per_element_count; + uint64_t builtin_array_cb_per_element_gas; + uint64_t allocation_count; + uint64_t allocation_bytes; + uint64_t allocation_gas; +} JSGasTrace; +int JS_EnableGasTrace(JSContext *ctx, int enabled); +int JS_ResetGasTrace(JSContext *ctx); +int JS_ReadGasTrace(JSContext *ctx, JSGasTrace *out_trace); +int JS_UseGas(JSContext *ctx, uint64_t amount); + +#define JS_HOST_CALL_TRANSPORT_ERROR UINT32_C(0xffffffff) + +typedef struct JSHostCallResult { + uint8_t *data; + uint32_t length; +} JSHostCallResult; +/* data points to an internal scratch buffer owned by the context; valid until the next JS_HostCall or context free */ + +typedef uint32_t JSHostCallFunc(JSContext *ctx, + uint32_t fn_id, + const uint8_t *req_ptr, + uint32_t req_len, + uint8_t *resp_ptr, + uint32_t resp_capacity, + void *opaque); + +int JS_SetHostCallDispatcher(JSRuntime *rt, JSHostCallFunc *func, void *opaque); +int JS_HostCall(JSContext *ctx, + uint32_t fn_id, + const uint8_t *req_bytes, + size_t req_len, + uint32_t max_request_bytes, + uint32_t max_response_bytes, + JSHostCallResult *out_result); + +typedef struct JSDvLimits { + uint32_t max_depth; + uint32_t max_encoded_bytes; + uint32_t max_string_bytes; + uint32_t max_array_length; + uint32_t max_map_length; +} JSDvLimits; + +typedef struct JSDvBuffer { + uint8_t *data; + size_t length; +} JSDvBuffer; + +extern const JSDvLimits JS_DV_LIMIT_DEFAULTS; + +int JS_EncodeDV(JSContext *ctx, JSValueConst value, const JSDvLimits *limits, JSDvBuffer *out_buffer); +JSValue JS_DecodeDV(JSContext *ctx, const uint8_t *data, size_t length, const JSDvLimits *limits); +void JS_FreeDVBuffer(JSContext *ctx, JSDvBuffer *buffer); + JSRuntime *JS_GetRuntime(JSContext *ctx); void JS_SetClassProto(JSContext *ctx, JSClassID class_id, JSValue obj); JSValue JS_GetClassProto(JSContext *ctx, JSClassID class_id);