From 03cf09d8e7e71aded730aaa473fc489c4d30dba6 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Sun, 29 Mar 2026 17:43:13 -0700 Subject: [PATCH] improve runtime compatibility for modern SSR/RSC workloads and builtin modules fix JSON/promise/abort/collection edge cases while simplifying server, import-meta, path, and builtin lookup internals --- examples/results.txt | 8 +- include/ant.h | 4 +- include/common.h | 1 + include/esm/builtin_bundle.h | 17 +- include/esm/loader.h | 2 +- include/gc/modules.h | 1 + include/gc/roots.h | 2 +- include/modules/collections.h | 6 +- include/modules/timer.h | 2 + include/modules/tls.h | 8 + include/modules/uri.h | 8 + include/net/connection.h | 31 +- include/net/listener.h | 39 +- meson/deps/meson.build | 4 +- src/ant.c | 390 ++++++---- src/builtins/node/{http.cjs => http.mjs} | 68 +- src/builtins/node/https.mjs | 132 ++++ src/builtins/node/querystring.mjs | 80 +++ src/errors.c | 18 +- src/esm/builtin_bundle.c | 30 +- src/esm/loader.c | 69 +- src/gc/objects.c | 4 + src/main.c | 8 +- src/modules/abort.c | 27 + src/modules/buffer.c | 46 ++ src/modules/collections.c | 296 +++++--- src/modules/fs.c | 88 ++- src/modules/globals.c | 1 - src/modules/json.c | 182 +++-- src/modules/net.c | 85 ++- src/modules/path.c | 670 ++++++++++++++---- src/modules/request.c | 41 +- src/modules/server.c | 99 ++- src/modules/structured-clone.c | 18 +- src/modules/timer.c | 206 ++++++ src/modules/tls.c | 272 +++++++ src/modules/uri.c | 8 +- src/net/connection.c | 100 ++- src/net/listener.c | 90 ++- src/silver/compiler.c | 8 +- src/silver/engine.c | 1 - src/streams/readable.c | 90 ++- src/tools/gen_builtin_bundle.js | 29 +- src/types/modules/fs.d.ts | 6 + src/types/modules/path.d.ts | 38 +- tests/await_reactpromise_shape.mjs | 35 + tests/await_thenable.mjs | 11 + tests/http_incomingmessage_async_iter.mjs | 22 + tests/json_parse_flight_reviver.cjs | 22 + ...son_stringify_nested_function_replacer.cjs | 19 + tests/json_stringify_root_replacer.js | 16 + tests/promise_expando.cjs | 19 + tests/proxy_has_own.cjs | 28 + tests/react_like_async_iterable_buffer.js | 96 +++ tests/react_like_async_iterable_controller.js | 132 ++++ tests/react_like_bigint_parse.js | 16 + tests/react_like_fetch_row_parser.cjs | 181 +++++ tests/react_like_http_mixed_row_parser.cjs | 193 +++++ tests/react_like_stream_controller.js | 39 + tests/react_server_action_encode_trace.mjs | 135 ++++ tests/server_reference_bind_trace.mjs | 67 ++ tests/test_map.js | 18 + 62 files changed, 3709 insertions(+), 673 deletions(-) create mode 100644 include/modules/tls.h rename src/builtins/node/{http.cjs => http.mjs} (87%) create mode 100644 src/builtins/node/https.mjs create mode 100644 src/builtins/node/querystring.mjs create mode 100644 src/modules/tls.c create mode 100644 tests/await_reactpromise_shape.mjs create mode 100644 tests/await_thenable.mjs create mode 100644 tests/http_incomingmessage_async_iter.mjs create mode 100644 tests/json_parse_flight_reviver.cjs create mode 100644 tests/json_stringify_nested_function_replacer.cjs create mode 100644 tests/json_stringify_root_replacer.js create mode 100644 tests/promise_expando.cjs create mode 100644 tests/proxy_has_own.cjs create mode 100644 tests/react_like_async_iterable_buffer.js create mode 100644 tests/react_like_async_iterable_controller.js create mode 100644 tests/react_like_bigint_parse.js create mode 100644 tests/react_like_fetch_row_parser.cjs create mode 100644 tests/react_like_http_mixed_row_parser.cjs create mode 100644 tests/react_like_stream_controller.js create mode 100644 tests/react_server_action_encode_trace.mjs create mode 100644 tests/server_reference_bind_trace.mjs diff --git a/examples/results.txt b/examples/results.txt index 6455d7b..db385c4 100644 --- a/examples/results.txt +++ b/examples/results.txt @@ -460,7 +460,7 @@ compat-table/es6/Function.name.symbol-keyed.js: failed compat-table/es6/Function.name.variable.js: failed compat-table/es6/Map.Symbol.species.js: OK compat-table/es6/Map.constructor-accepts-null.js: OK -compat-table/es6/Map.constructor-arguments.js: failed +compat-table/es6/Map.constructor-arguments.js: OK compat-table/es6/Map.constructor-invokes-set.js: failed compat-table/es6/Map.constructor-requires-new.js: OK compat-table/es6/Map.iterator-closing.js: failed @@ -527,7 +527,7 @@ compat-table/es6/Proxy.handler.defineProperty.js: failed compat-table/es6/Proxy.handler.deleteProperty.invariants.js: OK compat-table/es6/Proxy.handler.deleteProperty.js: OK compat-table/es6/Proxy.handler.getOwnPropertyDescriptor.invariants.js: failed -compat-table/es6/Proxy.handler.getOwnPropertyDescriptor.js: TypeError: Cannot read properties of undefined (reading 'value') +compat-table/es6/Proxy.handler.getOwnPropertyDescriptor.js: failed compat-table/es6/Proxy.handler.getPrototypeOf.invariants.js: failed compat-table/es6/Proxy.handler.getPrototypeOf.js: failed compat-table/es6/Proxy.handler.get.instances.js: failed @@ -887,7 +887,7 @@ compat-table/es6/misc.Proxy.deleteProperty.Array.splice.js: failed compat-table/es6/misc.Proxy.deleteProperty.Array.unshift.js: failed compat-table/es6/misc.Proxy.getOwnPropertyDescriptor.Function.bind.js: TypeError: bind requires a function compat-table/es6/misc.Proxy.getOwnPropertyDescriptor.Object.assign.js: failed -compat-table/es6/misc.Proxy.getOwnPropertyDescriptor.Object.hasOwnProperty.js: failed +compat-table/es6/misc.Proxy.getOwnPropertyDescriptor.Object.hasOwnProperty.js: OK compat-table/es6/misc.Proxy.getOwnPropertyDescriptor.Set.js: failed compat-table/es6/misc.Proxy.get.Array.concat.js: OK compat-table/es6/misc.Proxy.get.Array.from.js: failed @@ -1072,7 +1072,7 @@ compat-table/es6/typed-arrays.prototype.filter.js: failed compat-table/es6/typed-arrays.prototype.findIndex.js: failed compat-table/es6/typed-arrays.prototype.find.js: failed compat-table/es6/typed-arrays.prototype.forEach.js: failed -compat-table/es6/typed-arrays.prototype.indexOf.js: failed +compat-table/es6/typed-arrays.prototype.indexOf.js: OK compat-table/es6/typed-arrays.prototype.join.js: OK compat-table/es6/typed-arrays.prototype.keys.js: OK compat-table/es6/typed-arrays.prototype.lastIndexOf.js: failed diff --git a/include/ant.h b/include/ant.h index c47cf07..921e01f 100644 --- a/include/ant.h +++ b/include/ant.h @@ -125,9 +125,11 @@ ant_value_t js_tostring_val(ant_t *js, ant_value_t value); uint8_t vtype(ant_value_t val); size_t vdata(ant_value_t val); -ant_object_t *js_obj_ptr(ant_value_t val); bool js_is_constructor(ant_t *js, ant_value_t value); +ant_object_t *js_obj_ptr(ant_value_t val); +ant_value_t js_obj_from_ptr(ant_object_t *obj); + double js_getnum(ant_value_t val); char *js_getstr(ant_t *js, ant_value_t val, size_t *len); diff --git a/include/common.h b/include/common.h index 7dc241d..b4d2b34 100644 --- a/include/common.h +++ b/include/common.h @@ -75,6 +75,7 @@ typedef enum { SLOT_HEADERS_GUARD, SLOT_REQUEST_HEADERS, SLOT_REQUEST_SIGNAL, + SLOT_REQUEST_ABORT_REASON, SLOT_REQUEST_BODY_STREAM, SLOT_RESPONSE_HEADERS, SLOT_RESPONSE_BODY_STREAM, diff --git a/include/esm/builtin_bundle.h b/include/esm/builtin_bundle.h index 4cd90b4..c1a55c5 100644 --- a/include/esm/builtin_bundle.h +++ b/include/esm/builtin_bundle.h @@ -8,14 +8,21 @@ #include typedef struct { - const char *specifier; - const char *source_name; const uint8_t *code; size_t code_len; ant_module_format_t format; -} ant_builtin_bundle_entry_t; +} ant_builtin_bundle_module_t; + +typedef struct { + const char *specifier; + size_t specifier_len; + const char *source_name; + size_t module_id; +} ant_builtin_bundle_alias_t; + +bool esm_has_builtin_scheme(const char *specifier); -bool esm_is_builtin_specifier(const char *specifier); -const ant_builtin_bundle_entry_t *esm_lookup_builtin_bundle(const char *specifier, size_t spec_len); +const ant_builtin_bundle_alias_t *esm_lookup_builtin_alias(const char *specifier, size_t spec_len); +const ant_builtin_bundle_module_t *esm_lookup_builtin_module(size_t module_id); #endif diff --git a/include/esm/loader.h b/include/esm/loader.h index 30e2630..1076763 100644 --- a/include/esm/loader.h +++ b/include/esm/loader.h @@ -36,6 +36,6 @@ ant_value_t js_esm_eval_module_source( ant_value_t js_esm_import_sync_cstr(ant_t *js, const char *specifier, size_t spec_len); ant_value_t js_esm_resolve_specifier(ant_t *js, ant_value_t specifier, const char *base_path); ant_value_t js_esm_import_sync_cstr_from(ant_t *js, const char *specifier, size_t spec_len, const char *base_path); -ant_value_t js_esm_import_dynamic(ant_t *js, ant_value_t specifier, ant_value_t *out_tla_promise); +ant_value_t js_esm_import_dynamic(ant_t *js, ant_value_t specifier, const char *base_path, ant_value_t *out_tla_promise); #endif diff --git a/include/gc/modules.h b/include/gc/modules.h index 3d206db..bdde63e 100644 --- a/include/gc/modules.h +++ b/include/gc/modules.h @@ -29,5 +29,6 @@ void gc_mark_transform_streams(ant_t *js, gc_mark_fn mark); void gc_mark_codec_streams(ant_t *js, gc_mark_fn mark); void gc_mark_compression_streams(ant_t *js, gc_mark_fn mark); void gc_mark_zlib(ant_t *js, gc_mark_fn mark); +void gc_mark_abort_signal_object(ant_t *js, ant_value_t signal, gc_mark_fn mark); #endif diff --git a/include/gc/roots.h b/include/gc/roots.h index 6e8ec76..2804658 100644 --- a/include/gc/roots.h +++ b/include/gc/roots.h @@ -5,7 +5,7 @@ #include #include -#define GC_MAX_STATIC_ROOTS 64 +#define GC_MAX_STATIC_ROOTS 128 typedef void (*gc_root_visitor_t)(ant_t *js, ant_value_t v); diff --git a/include/modules/collections.h b/include/modules/collections.h index b8d82cf..d7166e5 100644 --- a/include/modules/collections.h +++ b/include/modules/collections.h @@ -6,7 +6,8 @@ #include "modules/symbol.h" typedef struct map_entry { - char *key; + unsigned char *key; + size_t key_len; ant_value_t key_val; ant_value_t value; UT_hash_handle hh; @@ -14,7 +15,8 @@ typedef struct map_entry { typedef struct set_entry { ant_value_t value; - char *key; + unsigned char *key; + size_t key_len; UT_hash_handle hh; } set_entry_t; diff --git a/include/modules/timer.h b/include/modules/timer.h index 55af51b..cc6396f 100644 --- a/include/modules/timer.h +++ b/include/modules/timer.h @@ -3,6 +3,8 @@ #include "types.h" +ant_value_t timers_promises_library(ant_t *js); + void init_timer_module(void); void process_microtasks(ant_t *js); void process_immediates(ant_t *js); diff --git a/include/modules/tls.h b/include/modules/tls.h new file mode 100644 index 0000000..6005a4e --- /dev/null +++ b/include/modules/tls.h @@ -0,0 +1,8 @@ +#ifndef ANT_INTERNAL_TLS_MODULE_H +#define ANT_INTERNAL_TLS_MODULE_H + +#include "types.h" + +ant_value_t internal_tls_library(ant_t *js); + +#endif diff --git a/include/modules/uri.h b/include/modules/uri.h index 5c7353e..00205a8 100644 --- a/include/modules/uri.h +++ b/include/modules/uri.h @@ -1,6 +1,14 @@ #ifndef URI_H #define URI_H +#include "types.h" + void init_uri_module(void); +ant_value_t js_encodeURI(ant_t *js, ant_value_t *args, int nargs); +ant_value_t js_decodeURI(ant_t *js, ant_value_t *args, int nargs); + +ant_value_t js_encodeURIComponent(ant_t *js, ant_value_t *args, int nargs); +ant_value_t js_decodeURIComponent(ant_t *js, ant_value_t *args, int nargs); + #endif diff --git a/include/net/connection.h b/include/net/connection.h index a67e0bc..d9f5766 100644 --- a/include/net/connection.h +++ b/include/net/connection.h @@ -9,34 +9,63 @@ typedef struct ant_listener_s ant_listener_t; typedef struct ant_conn_s ant_conn_t; +typedef enum { + ANT_CONN_KIND_TCP = 0, + ANT_CONN_KIND_PIPE, +} ant_conn_kind_t; + struct ant_conn_s { - uv_tcp_t handle; + ant_conn_kind_t kind; + + union { + uv_tcp_t tcp; + uv_pipe_t pipe; + } handle; + uv_timer_t timer; ant_listener_t *listener; + void *user_data; char *buffer; + size_t buffer_len; size_t buffer_cap; + size_t buffer_offset; + uint64_t timeout_ms; uint64_t bytes_read; uint64_t bytes_written; + int close_handles; bool closing; bool read_paused; bool read_eof; bool has_local_addr; bool has_remote_addr; + char local_addr[64]; char remote_addr[64]; char local_family[8]; char remote_family[8]; + int local_port; int remote_port; struct ant_conn_s *next; }; +static inline uv_stream_t *ant_conn_stream(ant_conn_t *conn) { + if (!conn) return NULL; + switch (conn->kind) { + case ANT_CONN_KIND_PIPE: return (uv_stream_t *)&conn->handle.pipe; + case ANT_CONN_KIND_TCP: + default: return (uv_stream_t *)&conn->handle.tcp; + } +} + void ant_conn_start(ant_conn_t *conn); int ant_conn_accept(ant_conn_t *conn, uv_stream_t *server_stream); + ant_conn_t *ant_conn_create_tcp(ant_listener_t *listener, uint64_t timeout_ms); +ant_conn_t *ant_conn_create_pipe(ant_listener_t *listener, uint64_t timeout_ms); #endif diff --git a/include/net/listener.h b/include/net/listener.h index a2ee5af..1ba2355 100644 --- a/include/net/listener.h +++ b/include/net/listener.h @@ -9,6 +9,11 @@ typedef struct ant_listener_s ant_listener_t; typedef struct ant_conn_s ant_conn_t; +typedef enum { + ANT_LISTENER_KIND_TCP = 0, + ANT_LISTENER_KIND_PIPE, +} ant_listener_kind_t; + typedef void (*ant_conn_write_cb)( ant_conn_t *conn, int status, @@ -27,18 +32,37 @@ typedef struct { struct ant_listener_s { uv_loop_t *loop; - uv_tcp_t handle; + ant_listener_kind_t kind; + + union { + uv_tcp_t tcp; + uv_pipe_t pipe; + } handle; + ant_conn_t *connections; ant_listener_callbacks_t callbacks; + void *user_data; uint64_t idle_timeout_ms; + int port; int backlog; + + char *path; bool started; bool closing; bool closed; }; +static inline uv_stream_t *ant_listener_stream(ant_listener_t *listener) { + if (!listener) return NULL; + switch (listener->kind) { + case ANT_LISTENER_KIND_PIPE: return (uv_stream_t *)&listener->handle.pipe; + case ANT_LISTENER_KIND_TCP: + default: return (uv_stream_t *)&listener->handle.tcp; + } +} + int ant_listener_listen_tcp( ant_listener_t *listener, uv_loop_t *loop, @@ -50,8 +74,20 @@ int ant_listener_listen_tcp( void *user_data ); +int ant_listener_listen_pipe( + ant_listener_t *listener, + uv_loop_t *loop, + const char *path, + int backlog, + uint64_t idle_timeout_ms, + const ant_listener_callbacks_t *callbacks, + void *user_data +); + void ant_listener_stop(ant_listener_t *listener, bool force); void *ant_listener_get_user_data(const ant_listener_t *listener); +void ant_listener_ref(ant_listener_t *listener); +void ant_listener_unref(ant_listener_t *listener); int ant_conn_local_port(const ant_conn_t *conn); int ant_listener_port(const ant_listener_t *listener); @@ -82,6 +118,7 @@ const char *ant_conn_local_addr(const ant_conn_t *conn); const char *ant_conn_remote_addr(const ant_conn_t *conn); const char *ant_conn_local_family(const ant_conn_t *conn); const char *ant_conn_remote_family(const ant_conn_t *conn); +const char *ant_listener_path(const ant_listener_t *listener); size_t ant_conn_buffer_len(const ant_conn_t *conn); ant_listener_t *ant_conn_listener(const ant_conn_t *conn); diff --git a/meson/deps/meson.build b/meson/deps/meson.build index b185f3d..f90fd05 100644 --- a/meson/deps/meson.build +++ b/meson/deps/meson.build @@ -211,10 +211,10 @@ ant_deps = [ libffi_dep, uuid_dep, crprintf_dep, llhttp, pcre2_dep, libuv_dep, base64_dep, argtable3_dep, tlsuv_dep, libsodium_dep, - yyjson_dep, minicoro_dep, uuidv7_dep, + yyjson_dep, minicoro_dep, uuidv7_dep, uriparser_dep, utf8proc_dep, openssl_dep, zlib_dep, brotli_common_dep, brotli_dec_dep, - brotli_enc_dep, uthash_dep, lmdb_dep, + brotli_enc_dep, uthash_dep, lmdb_dep, nghttp2_dep, ] + mbedtls_dep + win_deps if get_option('jit') diff --git a/src/ant.c b/src/ant.c index 06b9b59..58c29a3 100644 --- a/src/ant.c +++ b/src/ant.c @@ -253,6 +253,11 @@ ant_object_t *js_obj_ptr(ant_value_t v) { return (ant_object_t *)(uintptr_t)vdata(as_obj); } +ant_value_t js_obj_from_ptr(ant_object_t *obj) { + if (!obj) return js_mkundef(); + return mkval(T_OBJ, (uintptr_t)obj); +} + void js_mark_constructor(ant_value_t value, bool is_constructor) { ant_object_t *obj = js_obj_ptr(value); if (obj) obj->is_constructor = is_constructor ? 1u : 0u; @@ -613,6 +618,7 @@ static inline ant_value_t lkp_val(ant_t *js, ant_value_t obj, const char *buf, s static inline ant_value_t lkp_sym_proto_val(ant_t *js, ant_value_t obj, ant_offset_t sym_off); static size_t tostr(ant_t *js, ant_value_t value, char *buf, size_t len); static size_t strpromise(ant_t *js, ant_value_t value, char *buf, size_t len); + static ant_value_t js_call_valueOf(ant_t *js, ant_value_t value); static ant_value_t js_call_toString(ant_t *js, ant_value_t value); static ant_value_t js_call_method(ant_t *js, ant_value_t obj, const char *method, size_t method_len, ant_value_t *args, int nargs); @@ -620,22 +626,18 @@ static ant_value_t js_call_method(ant_t *js, ant_value_t obj, const char *method static inline bool is_slot_prop(ant_offset_t header); static inline ant_offset_t next_prop(ant_offset_t header); -static ant_value_t builtin_Object(ant_t *js, ant_value_t *args, int nargs); static ant_value_t builtin_promise_then(ant_t *js, ant_value_t *args, int nargs); -static ant_value_t string_split_impl(ant_t *js, ant_value_t str, ant_value_t *args, int nargs); - static ant_value_t proxy_get(ant_t *js, ant_value_t proxy, const char *key, size_t key_len); static ant_value_t proxy_get_val(ant_t *js, ant_value_t proxy, ant_value_t key_val); static ant_value_t proxy_set(ant_t *js, ant_value_t proxy, const char *key, size_t key_len, ant_value_t value); static ant_value_t proxy_has(ant_t *js, ant_value_t proxy, const char *key, size_t key_len); static ant_value_t proxy_has_val(ant_t *js, ant_value_t proxy, ant_value_t key_val); +static ant_value_t proxy_get_own_property_descriptor(ant_t *js, ant_value_t proxy, ant_value_t key_val); +static ant_value_t proxy_has_own(ant_t *js, ant_value_t proxy, ant_value_t key_val); static ant_value_t proxy_delete(ant_t *js, ant_value_t proxy, const char *key, size_t key_len); static ant_value_t proxy_delete_val(ant_t *js, ant_value_t proxy, ant_value_t key_val); -static ant_value_t get_prototype_for_type(ant_t *js, uint8_t type); static ant_value_t get_ctor_proto(ant_t *js, const char *name, size_t len); -ant_offset_t lkp_interned(ant_t *js, ant_value_t obj, const char *search_intern, size_t len); -static ant_offset_t get_array_length(ant_t *js, ant_value_t arr); static inline void array_len_set(ant_t *js, ant_value_t obj, ant_offset_t new_len); typedef struct { ant_value_t handle; bool is_new; } ctor_t; @@ -872,9 +874,20 @@ static size_t add_indent(char *buf, size_t len, int level) { } const char *get_str_prop(ant_t *js, ant_value_t obj, const char *key, ant_offset_t klen, ant_offset_t *out_len) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, obj); ant_value_t v = lkp_val(js, obj, key, klen); - if (vtype(v) != T_STR) return NULL; - return (const char *)(uintptr_t)(vstr(js, v, out_len)); + + GC_ROOT_PIN(js, v); + if (vtype(v) != T_STR) { + GC_ROOT_RESTORE(js, root_mark); + return NULL; + } + + const char *str = (const char *)(uintptr_t)(vstr(js, v, out_len)); + GC_ROOT_RESTORE(js, root_mark); + + return str; } static bool is_small_array(ant_t *js, ant_value_t obj, int *elem_count) { @@ -1717,26 +1730,38 @@ static void rope_flatten_into(ant_t *js, ant_value_t str, char *dest, ant_offset ant_value_t rope_flatten(ant_t *js, ant_value_t rope) { assert(vtype(rope) == T_STR); if (!str_is_heap_rope(rope)) return rope; + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, rope); ant_value_t cached = rope_cached_flat(rope); - if (vtype(cached) == T_STR && !str_is_heap_rope(cached)) return cached; + GC_ROOT_PIN(js, cached); + if (vtype(cached) == T_STR && !str_is_heap_rope(cached)) { + GC_ROOT_RESTORE(js, root_mark); + return cached; + } ant_offset_t total_len = rope_len(rope); - char *buf = (char *)ant_calloc(total_len + 1); - if (!buf) return js_mkerr(js, "oom"); + + if (!buf) { + GC_ROOT_RESTORE(js, root_mark); + return js_mkerr(js, "oom"); + } ant_offset_t pos = 0; rope_flatten_into(js, rope, buf, &pos); buf[pos] = '\0'; ant_value_t flat = js_mkstr(js, buf, pos); + GC_ROOT_PIN(js, flat); free(buf); if (!is_err(flat)) { rope_set_cached_flat(rope, flat); } + GC_ROOT_RESTORE(js, root_mark); return flat; } @@ -4074,8 +4099,6 @@ ant_value_t do_string_op(ant_t *js, uint8_t op, ant_value_t l, ant_value_t r) { typedef enum { ITER_CONTINUE, ITER_BREAK, ITER_ERROR } iter_action_t; typedef iter_action_t (*iter_callback_t)(ant_t *js, ant_value_t value, void *ctx, ant_value_t *out); -static ant_value_t iter_foreach(ant_t *js, ant_value_t iterable, iter_callback_t cb, void *ctx); - static bool js_try_call_method(ant_t *js, ant_value_t obj, const char *method, size_t method_len, ant_value_t *args, int nargs, ant_value_t *out_result) { ant_value_t getter = js_mkundef(); bool has_getter = false; @@ -4906,12 +4929,18 @@ static ant_value_t builtin_function_bind(ant_t *js, ant_value_t *args, int nargs bound_closure->func = orig->func; bound_closure->call_flags = orig->call_flags; - bound_closure->upvalues = orig->upvalues; - if (orig->upvalues) bound_closure->call_flags |= SV_CALL_BORROWED_UPVALS; + bound_closure->upvalues = NULL; bound_closure->bound_this = this_arg; bound_closure->bound_args = js_mkundef(); bound_closure->super_val = orig->super_val; bound_closure->func_obj = bound_func; + + if (orig->func && orig->func->upvalue_count > 0 && orig->upvalues) { + size_t upvalue_bytes = sizeof(sv_upvalue_t *) * (size_t)orig->func->upvalue_count; + bound_closure->upvalues = malloc(upvalue_bytes); + if (!bound_closure->upvalues) return js_mkerr(js, "oom"); + memcpy(bound_closure->upvalues, orig->upvalues, upvalue_bytes); + } if (bound_argc > 0) bound_closure->call_flags |= SV_CALL_HAS_BOUND_ARGS; @@ -5324,72 +5353,108 @@ static ant_value_t builtin_object_keys(ant_t *js, ant_value_t *args, int nargs) } static ant_value_t for_in_keys_add(ant_t *js, ant_value_t out, ant_value_t seen, ant_value_t key) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, out); + GC_ROOT_PIN(js, seen); + GC_ROOT_PIN(js, key); + if (vtype(key) != T_STR) return js_mkundef(); ant_offset_t key_len = 0; ant_offset_t key_off = vstr(js, key, &key_len); const char *key_ptr = (const char *)(uintptr_t)(key_off); - if (is_internal_prop(key_ptr, key_len)) - return js_mkundef(); - if (lkp(js, seen, key_ptr, key_len) != 0) - return js_mkundef(); + if (is_internal_prop(key_ptr, key_len)) goto done; + if (lkp(js, seen, key_ptr, key_len) != 0) goto done; ant_value_t mark = setprop_cstr(js, seen, key_ptr, key_len, js_true); - if (is_err(mark)) return mark; + if (is_err(mark)) { + GC_ROOT_RESTORE(js, root_mark); + return mark; + } js_arr_push(js, out, key); +done: + GC_ROOT_RESTORE(js, root_mark); return js_mkundef(); } static ant_value_t for_in_keys_add_string_indices(ant_t *js, ant_value_t out, ant_value_t seen, ant_value_t str) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, out); + GC_ROOT_PIN(js, seen); + GC_ROOT_PIN(js, str); + ant_offset_t slen = vstrlen(js, str); for (ant_offset_t i = 0; i < slen; i++) { char idx[16]; size_t idx_len = uint_to_str(idx, sizeof(idx), (uint64_t)i); + ant_value_t key = js_mkstr(js, idx, idx_len); + GC_ROOT_PIN(js, key); + ant_value_t r = for_in_keys_add(js, out, seen, key); - if (is_err(r)) return r; + if (is_err(r)) { GC_ROOT_RESTORE(js, root_mark); return r; } } + + GC_ROOT_RESTORE(js, root_mark); return js_mkundef(); } ant_value_t js_for_in_keys(ant_t *js, ant_value_t obj) { + GC_ROOT_SAVE(root_mark, js); uint8_t t = vtype(obj); ant_value_t out = mkarr(js); - if (t == T_NULL || t == T_UNDEF) return out; + ant_value_t result = out; + + GC_ROOT_PIN(js, obj); + GC_ROOT_PIN(js, out); + + if (t == T_NULL || t == T_UNDEF) goto done; ant_value_t seen = mkobj(js, 0); - ant_value_t result = js_mkundef(); + GC_ROOT_PIN(js, seen); if (t == T_STR) { result = for_in_keys_add_string_indices(js, out, seen, obj); - if (is_err(result)) return result; - return out; + if (is_err(result)) goto done; + result = out; + goto done; } if (t == T_OBJ) { ant_value_t prim = get_slot(obj, SLOT_PRIMITIVE); + GC_ROOT_PIN(js, prim); if (vtype(prim) == T_STR) { result = for_in_keys_add_string_indices(js, out, seen, prim); - if (is_err(result)) return result; + if (is_err(result)) goto done; + result = out; } } - if (t == T_OBJ || t == T_ARR || t == T_FUNC) { - ant_value_t own_keys = object_enum(js, obj, OBJ_ENUM_KEYS); - if (is_err(own_keys)) return own_keys; - if (vtype(own_keys) == T_ARR) { - ant_offset_t own_len = js_arr_len(js, own_keys); - for (ant_offset_t i = 0; i < own_len; i++) { - ant_value_t key = js_arr_get(js, own_keys, i); - result = for_in_keys_add(js, out, seen, key); - if (is_err(result)) return result; - } - } + if (t != T_OBJ && t != T_ARR && t != T_FUNC) goto done; + ant_value_t own_keys = object_enum(js, obj, OBJ_ENUM_KEYS); + + GC_ROOT_PIN(js, own_keys); + if (is_err(own_keys)) { + result = own_keys; + goto done; } - return out; + if (vtype(own_keys) != T_ARR) goto done; + ant_offset_t own_len = js_arr_len(js, own_keys); + + for (ant_offset_t i = 0; i < own_len; i++) { + ant_value_t key = js_arr_get(js, own_keys, i); + GC_ROOT_PIN(js, key); + result = for_in_keys_add(js, out, seen, key); + if (is_err(result)) goto done; + result = out; + } + +done: + GC_ROOT_RESTORE(js, root_mark); + return result; } static ant_value_t builtin_object_values(ant_t *js, ant_value_t *args, int nargs) { @@ -5501,19 +5566,20 @@ static ant_value_t builtin_object_create(ant_t *js, ant_value_t *args, int nargs if (nargs >= 2 && vtype(args[1]) == T_OBJ) { ant_value_t props = args[1]; ant_iter_t iter = js_prop_iter_begin(js, props); + const char *key = NULL; size_t klen = 0; ant_value_t descriptor = js_mkundef(); + while (js_prop_iter_next(&iter, &key, &klen, &descriptor)) { - if (vtype(descriptor) == T_OBJ) { - ant_offset_t val_off = lkp(js, descriptor, "value", 5); - if (val_off != 0) { - ant_value_t val = propref_load(js, val_off); - ant_value_t key_str = js_mkstr(js, key, klen); - js_setprop(js, obj, key_str, val); - } - } + if (vtype(descriptor) != T_OBJ) continue; + ant_offset_t val_off = lkp(js, descriptor, "value", 5); + if (val_off == 0) continue; + ant_value_t val = propref_load(js, val_off); + ant_value_t key_str = js_mkstr(js, key, klen); + js_setprop(js, obj, key_str, val); } + js_prop_iter_end(&iter); } @@ -5537,8 +5603,9 @@ static ant_value_t builtin_object_hasOwn(ant_t *js, ant_value_t *args, int nargs } if (t != T_OBJ && t != T_ARR && t != T_FUNC) return mkval(T_BOOL, 0); - ant_value_t as_obj = js_as_obj(obj); + if (is_proxy(as_obj)) return proxy_has_own(js, as_obj, key); + ant_offset_t key_len, key_off = vstr(js, key, &key_len); const char *key_str = (char *)(uintptr_t)(key_off); @@ -6124,10 +6191,13 @@ static ant_value_t builtin_object_getOwnPropertyDescriptor(ant_t *js, ant_value_ } ant_value_t as_obj = js_as_obj(obj); + if (is_proxy(as_obj)) { + return proxy_get_own_property_descriptor(js, as_obj, key); + } ant_offset_t sym_off = is_sym ? (ant_offset_t)vdata(key) : 0; - prop_meta_t sym_meta; - prop_meta_t str_meta; + prop_meta_t sym_meta; prop_meta_t str_meta; + bool has_sym_meta = is_sym ? lookup_symbol_prop_meta(as_obj, sym_off, &sym_meta) : false; bool has_str_meta = is_sym ? false : lookup_string_prop_meta(js, as_obj, key_str, (size_t)key_len, &str_meta); @@ -6135,13 +6205,14 @@ static ant_value_t builtin_object_getOwnPropertyDescriptor(ant_t *js, ant_value_ if (prop_off == 0 && !(is_sym ? has_sym_meta : has_str_meta)) { return js_mkundef(); } - - ant_value_t result = js_mkobj(js); bool has_getter = false; bool has_setter = false; + + ant_value_t result = js_mkobj(js); ant_value_t getter = js_mkundef(); ant_value_t setter = js_mkundef(); + bool writable = true; bool enumerable = true; bool configurable = true; @@ -6165,12 +6236,8 @@ static ant_value_t builtin_object_getOwnPropertyDescriptor(ant_t *js, ant_value_ } if (has_getter || has_setter) { - if (has_getter) { - js_setprop(js, result, js_mkstr(js, "get", 3), getter); - } - if (has_setter) { - js_setprop(js, result, js_mkstr(js, "set", 3), setter); - } + if (has_getter) js_setprop(js, result, js_mkstr(js, "get", 3), getter); + if (has_setter) js_setprop(js, result, js_mkstr(js, "set", 3), setter); js_setprop(js, result, js_mkstr(js, "enumerable", 10), js_bool(enumerable)); js_setprop(js, result, js_mkstr(js, "configurable", 12), js_bool(configurable)); } else { @@ -6313,6 +6380,8 @@ static ant_value_t builtin_object_hasOwnProperty(ant_t *js, ant_value_t *args, i if (t != T_OBJ && t != T_ARR && t != T_FUNC) return mkval(T_BOOL, 0); ant_value_t as_obj = js_as_obj(obj); + + if (is_proxy(as_obj)) return proxy_has_own(js, as_obj, key); bool is_arr_obj = array_obj_ptr(as_obj) != NULL; if (vtype(key) == T_SYMBOL) { @@ -6459,24 +6528,23 @@ static ant_value_t builtin_object_toString(ant_t *js, ant_value_t *args, int nar } if (!tag) { - if (is_object_type(obj) && get_slot(obj, SLOT_ERROR_BRAND) == js_true) { - tag = "Error"; tag_len = 5; - } else switch (t) { - case T_UNDEF: tag = "Undefined"; tag_len = 9; break; - case T_NULL: tag = "Null"; tag_len = 4; break; - case T_BOOL: tag = "Boolean"; tag_len = 7; break; - case T_NUM: tag = "Number"; tag_len = 6; break; - case T_STR: tag = "String"; tag_len = 6; break; - case T_ARR: tag = "Array"; tag_len = 5; break; - case T_FUNC: tag = "Function"; tag_len = 8; break; - case T_CFUNC: tag = "Function"; tag_len = 8; break; - case T_ERR: tag = "Error"; tag_len = 5; break; - case T_BIGINT: tag = "BigInt"; tag_len = 6; break; - case T_PROMISE: tag = "Promise"; tag_len = 7; break; - case T_OBJ: tag = "Object"; tag_len = 6; break; - default: tag = "Unknown"; tag_len = 7; break; - } - } + if (is_object_type(obj) && get_slot(obj, SLOT_ERROR_BRAND) == js_true) { + tag = "Error"; tag_len = 5; + } else switch (t) { + case T_UNDEF: tag = "Undefined"; tag_len = 9; break; + case T_NULL: tag = "Null"; tag_len = 4; break; + case T_BOOL: tag = "Boolean"; tag_len = 7; break; + case T_NUM: tag = "Number"; tag_len = 6; break; + case T_STR: tag = "String"; tag_len = 6; break; + case T_ARR: tag = "Array"; tag_len = 5; break; + case T_FUNC: tag = "Function"; tag_len = 8; break; + case T_CFUNC: tag = "Function"; tag_len = 8; break; + case T_ERR: tag = "Error"; tag_len = 5; break; + case T_BIGINT: tag = "BigInt"; tag_len = 6; break; + case T_PROMISE: tag = "Promise"; tag_len = 7; break; + case T_OBJ: tag = "Object"; tag_len = 6; break; + default: tag = "Unknown"; tag_len = 7; break; + }} char static_buf[64]; string_builder_t sb; @@ -9870,6 +9938,11 @@ ant_value_t js_promise_then(ant_t *js, ant_value_t promise, ant_value_t on_fulfi return result; } +static inline ant_value_t js_get_thenable_then(ant_t *js, ant_value_t value) { + if (!is_object_type(value)) return js_mkundef(); + return js_getprop_fallback(js, value, "then"); +} + js_await_result_t js_promise_await_coroutine(ant_t *js, ant_value_t promise, coroutine_t *coro) { js_await_result_t result = { .state = JS_AWAIT_PENDING, @@ -9877,27 +9950,27 @@ js_await_result_t js_promise_await_coroutine(ant_t *js, ant_value_t promise, cor }; if (vtype(promise) != T_PROMISE || !coro) return result; - ant_promise_state_t *pd = get_promise_data(js, promise, false); + if (!pd) { result.state = JS_AWAIT_FULFILLED; return result; } - if (pd->state == 1) { - result.state = JS_AWAIT_FULFILLED; - result.value = pd->value; - return result; - } - - if (pd->state == 2) { - if (pd->unhandled_reported) js_fire_rejection_handled(js, promise, pd->value); - pd->has_rejection_handler = true; - pd->unhandled_reported = false; - result.state = JS_AWAIT_REJECTED; - result.value = pd->value; - return result; - } + // if (pd->state == 1) { + // result.state = JS_AWAIT_FULFILLED; + // result.value = pd->value; + // return result; + // } + // + // if (pd->state == 2) { + // if (pd->unhandled_reported) js_fire_rejection_handled(js, promise, pd->value); + // pd->has_rejection_handler = true; + // pd->unhandled_reported = false; + // result.state = JS_AWAIT_REJECTED; + // result.value = pd->value; + // return result; + // } promise_handler_t h = { js_mkundef(), js_mkundef(), js_mkundef(), coro }; if (!promise_handler_append(pd, &h)) { @@ -9910,7 +9983,9 @@ js_await_result_t js_promise_await_coroutine(ant_t *js, ant_value_t promise, cor pd->has_rejection_handler = true; pd->unhandled_reported = false; - gc_root_pending_promise(js_obj_ptr(js_as_obj(promise))); + if (pd->state == 0) gc_root_pending_promise(js_obj_ptr(js_as_obj(promise))); + else queue_promise_trigger(js, promise); + return result; } @@ -9924,41 +9999,47 @@ void js_process_promise_handlers(ant_t *js, ant_value_t promise) { uint32_t len = promise_handler_count(pd); if (len == 0) return; + gc_root_pending_promise(pobj); pd->processing = true; for (uint32_t i = 0; i < len; i++) { promise_handler_t *h = promise_handler_at(pd, i); if (!h) continue; + if (h->await_coro) { settle_and_resume_coroutine(js, h->await_coro, val, state != 1); continue; } - - ant_value_t handler = (state == 1) ? h->onFulfilled : h->onRejected; - if (vtype(handler) == T_FUNC || vtype(handler) == T_CFUNC) { - ant_value_t res; - if (vtype(handler) == T_CFUNC) { - ant_value_t (*fn)(ant_t *, ant_value_t *, int) = (ant_value_t(*)(ant_t *, ant_value_t *, int)) vdata(handler); - res = fn(js, &val, 1); - } else { - ant_value_t call_args[] = { val }; - res = sv_vm_call(js->vm, js, handler, js_mkundef(), call_args, 1, NULL, false); - } - - if (is_err(res)) { - ant_value_t reject_val = js->thrown_value; - if (vtype(reject_val) == T_UNDEF) reject_val = res; - js->thrown_exists = false; - js->thrown_value = js_mkundef(); - js->thrown_stack = js_mkundef(); - js_reject_promise(js, h->nextPromise, reject_val); - } else js_resolve_promise(js, h->nextPromise, res); - } else { + ant_value_t handler = (state == 1) ? h->onFulfilled : h->onRejected; + if (vtype(handler) != T_FUNC && vtype(handler) != T_CFUNC) { if (state == 1) js_resolve_promise(js, h->nextPromise, val); else js_reject_promise(js, h->nextPromise, val); + continue; } + + ant_value_t res = js_mkundef(); + if (vtype(handler) == T_CFUNC) { + ant_value_t (*fn)(ant_t *, ant_value_t *, int) = (ant_value_t(*)(ant_t *, ant_value_t *, int))vdata(handler); + res = fn(js, &val, 1); + } else { + ant_value_t call_args[] = { val }; + res = sv_vm_call(js->vm, js, handler, js_mkundef(), call_args, 1, NULL, false); + } + + if (!is_err(res)) { + js_resolve_promise(js, h->nextPromise, res); + continue; + } + + ant_value_t reject_val = js->thrown_value; + if (vtype(reject_val) == T_UNDEF) reject_val = res; + + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + js->thrown_stack = js_mkundef(); + js_reject_promise(js, h->nextPromise, reject_val); } pd->processing = false; @@ -10039,7 +10120,7 @@ void js_resolve_promise(ant_t *js, ant_value_t p, ant_value_t val) { return; } - ant_value_t then_prop = js_get(js, val, "then"); + ant_value_t then_prop = js_get_thenable_then(js, val); GC_ROOT_PIN(js, then_prop); if (vtype(then_prop) == T_FUNC || vtype(then_prop) == T_CFUNC) { @@ -10335,8 +10416,8 @@ static ant_value_t finally_fulfilled_wrapper(ant_t *js, ant_value_t *args, int n return ret; } - if (vtype(result) == T_OBJ) { - ant_value_t then_fn = js_get(js, result, "then"); + if (is_object_type(result)) { + ant_value_t then_fn = js_get_thenable_then(js, result); GC_ROOT_PIN(js, then_fn); if (vtype(then_fn) == T_FUNC || vtype(then_fn) == T_CFUNC) { @@ -10393,8 +10474,8 @@ static ant_value_t finally_rejected_wrapper(ant_t *js, ant_value_t *args, int na return ret; } - if (vtype(result) == T_OBJ) { - ant_value_t then_prop = js_get(js, result, "then"); + if (is_object_type(result)) { + ant_value_t then_prop = js_get_thenable_then(js, result); GC_ROOT_PIN(js, then_prop); if (vtype(then_prop) == T_FUNC || vtype(then_prop) == T_CFUNC) { @@ -11154,11 +11235,22 @@ static ant_value_t builtin_import_tla_resolve(ant_t *js, ant_value_t *args, int return get_slot(me, SLOT_DATA); } +static inline bool js_has_module_filename(const char *filename) { + return filename && filename[0]; +} + +static const char *js_get_current_module_filename(ant_t *js) { + const char *active = js_module_eval_active_filename(js); + if (js_has_module_filename(active)) return active; + return js->filename; +} + static ant_value_t builtin_import(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "import() requires a string specifier"); + const char *base_path = js_get_current_module_filename(js); ant_value_t tla_promise = js_mkundef(); - ant_value_t ns = js_esm_import_dynamic(js, args[0], &tla_promise); + ant_value_t ns = js_esm_import_dynamic(js, args[0], base_path, &tla_promise); if (is_err(ns)) return builtin_Promise_reject(js, &ns, 1); if (vtype(tla_promise) == T_PROMISE) { @@ -11207,16 +11299,16 @@ static ant_value_t js_get_current_import_meta(ant_t *js) { static ant_value_t builtin_import_meta_resolve(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "import.meta.resolve() requires a string specifier"); - ant_value_t import_meta = js_get_current_import_meta(js); - if (vtype(import_meta) == T_OBJ) { - ant_value_t filename = js_get(js, import_meta, "filename"); + const char *base_path = NULL; + ant_value_t import_meta = js_getthis(js); - if (vtype(filename) == T_STR) { - ant_offset_t n = 0; ant_offset_t off = vstr(js, filename, &n); - return js_esm_resolve_specifier(js, args[0], (const char *)(uintptr_t)(off)); - }} - - return js_esm_resolve_specifier(js, args[0], NULL); + if (vtype(import_meta) == T_OBJ) { + ant_value_t filename = js_get(js, import_meta, "filename"); + if (vtype(filename) == T_STR) base_path = js_getstr(js, filename, NULL); + } + + if (!js_has_module_filename(base_path)) base_path = js_get_current_module_filename(js); + return js_esm_resolve_specifier(js, args[0], base_path); } static inline void js_set_import_meta_special_dirname( @@ -11264,7 +11356,7 @@ ant_value_t js_create_import_meta(ant_t *js, const char *filename, bool is_main) if (is_err(import_meta)) return import_meta; bool is_url = esm_is_url(filename); - bool is_builtin = esm_is_builtin_specifier(filename); + bool is_builtin = esm_has_builtin_scheme(filename); ant_value_t url_val = (is_url || is_builtin) ? js_mkstr(js, filename, strlen(filename)) @@ -11639,6 +11731,38 @@ static ant_value_t proxy_has_val(ant_t *js, ant_value_t proxy, ant_value_t key_v return js_false; } +static ant_value_t proxy_get_own_property_descriptor(ant_t *js, ant_value_t proxy, ant_value_t key_val) { + ant_proxy_state_t *data = get_proxy_data(proxy); + if (!data) return js_mkundef(); + + if (data->revoked) + return throw_proxy_error(js, "Cannot perform 'getOwnPropertyDescriptor' on a proxy that has been revoked"); + + ant_value_t target = data->target; + ant_value_t handler = data->handler; + ant_offset_t trap_off = vtype(handler) == T_OBJ ? lkp(js, handler, "getOwnPropertyDescriptor", 24) : 0; + + if (trap_off != 0) { + ant_value_t trap = propref_load(js, trap_off); + if (vtype(trap) == T_FUNC || vtype(trap) == T_CFUNC) { + ant_value_t args[2] = { target, key_val }; + ant_value_t result = sv_vm_call(js->vm, js, trap, js_mkundef(), args, 2, NULL, false); + if (is_err(result)) return result; + if (vtype(result) == T_UNDEF || vtype(result) == T_OBJ) return result; + return js_mkerr_typed(js, JS_ERR_TYPE, "'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined"); + } + } + + ant_value_t args[2] = { target, key_val }; + return builtin_object_getOwnPropertyDescriptor(js, args, 2); +} + +static ant_value_t proxy_has_own(ant_t *js, ant_value_t proxy, ant_value_t key_val) { + ant_value_t desc = proxy_get_own_property_descriptor(js, proxy, key_val); + if (is_err(desc)) return desc; + return js_bool(vtype(desc) != T_UNDEF); +} + static ant_value_t proxy_delete_val(ant_t *js, ant_value_t proxy, ant_value_t key_val) { ant_proxy_state_t *data = get_proxy_data(proxy); if (!data) return js_true; @@ -12864,9 +12988,15 @@ void js_check_unhandled_rejections(ant_t *js) { print_uncaught_throw(js); js_destroy(js); exit(1); } + + GC_ROOT_SAVE(root_mark, js); + ant_value_t reason = pd->value; + GC_ROOT_PIN(js, p); GC_ROOT_PIN(js, reason); - if (!js_fire_unhandled_rejection(js, p, pd->value)) - print_unhandled_promise_rejection(js, pd->value); + if (!js_fire_unhandled_rejection(js, p, reason)) + print_unhandled_promise_rejection(js, reason); + + GC_ROOT_RESTORE(js, root_mark); pd->unhandled_reported = true; } diff --git a/src/builtins/node/http.cjs b/src/builtins/node/http.mjs similarity index 87% rename from src/builtins/node/http.cjs rename to src/builtins/node/http.mjs index 3389a3e..9dce12b 100644 --- a/src/builtins/node/http.cjs +++ b/src/builtins/node/http.mjs @@ -1,10 +1,10 @@ -const net = require('node:net'); -const httpMetadata = require('ant:internal/http_metadata'); -const httpParser = require('ant:internal/http_parser'); -const httpWriter = require('ant:internal/http_writer'); +import net from 'node:net'; -const METHODS = httpMetadata.METHODS; -const STATUS_CODES = httpMetadata.STATUS_CODES; +import httpParser from 'ant:internal/http_parser'; +import httpWriter from 'ant:internal/http_writer'; + +import { EventEmitter } from 'node:events'; +import { STATUS_CODES } from 'ant:internal/http_metadata'; function createHeadersObject() { return Object.create(null); @@ -106,7 +106,7 @@ function handleClientError(server, socket, error) { } } -class IncomingMessage extends EventEmitter { +export class IncomingMessage extends EventEmitter { constructor(socket, parsed) { super(); this.socket = socket; @@ -124,18 +124,55 @@ class IncomingMessage extends EventEmitter { this.destroyed = false; this.readableEnded = false; this._body = bufferFrom(parsed.body); + this._bodyConsumed = false; + } + + _takeBody() { + if (this._bodyConsumed) return Buffer.alloc(0); + this._bodyConsumed = true; + return this._body; } _deliverBody() { if (this.destroyed || this.complete) return; - if (this._body.length > 0) this.emit('data', this._body); + const body = this._takeBody(); + if (body.length > 0) this.emit('data', body); this.complete = true; this.readableEnded = true; this.emit('end'); this.emit('close'); } + [Symbol.asyncIterator]() { + const body = this._takeBody(); + let emitted = false; + let finished = false; + + return { + next: async () => { + if (finished) return { done: true, value: undefined }; + if (!emitted && body.length > 0) { + emitted = true; + return { done: false, value: body }; + } + finished = true; + this.complete = true; + this.readableEnded = true; + return { done: true, value: undefined }; + }, + return: async () => { + finished = true; + this.complete = true; + this.readableEnded = true; + return { done: true, value: undefined }; + }, + [Symbol.asyncIterator]() { + return this; + } + }; + } + destroy(error) { if (this.destroyed) return this; this.destroyed = true; @@ -150,7 +187,7 @@ class IncomingMessage extends EventEmitter { } } -class ServerResponse extends EventEmitter { +export class ServerResponse extends EventEmitter { constructor(req, socket, socketState) { super(); this.req = req; @@ -311,7 +348,7 @@ class ServerResponse extends EventEmitter { } } -class Server extends net.Server { +export class Server extends net.Server { constructor(options, requestListener) { const serverOptions = typeof options === 'function' ? undefined : options; const onRequest = typeof options === 'function' ? options : requestListener; @@ -395,15 +432,8 @@ class Server extends net.Server { } } -function createServer(options, requestListener) { +export function createServer(options, requestListener) { return new Server(options, requestListener); } -module.exports = { - METHODS, - STATUS_CODES, - IncomingMessage, - Server, - ServerResponse, - createServer -}; +export { METHODS, STATUS_CODES } from 'ant:internal/http_metadata'; diff --git a/src/builtins/node/https.mjs b/src/builtins/node/https.mjs new file mode 100644 index 0000000..e2e5f36 --- /dev/null +++ b/src/builtins/node/https.mjs @@ -0,0 +1,132 @@ +import * as http from 'node:http'; +import tls from 'ant:internal/tls'; + +const kSecureContext = Symbol('ant.internal.https.secureContext'); + +const TLS_CONTEXT_OPTION_KEYS = ['allowPartialChain', 'ca', 'cert', 'key']; + +const TLS_SERVER_OPTION_KEYS = [ + 'ALPNProtocols', + 'SNICallback', + 'allowPartialChain', + 'ca', + 'cert', + 'ciphers', + 'clientCertEngine', + 'crl', + 'dhparam', + 'ecdhCurve', + 'honorCipherOrder', + 'key', + 'maxVersion', + 'minVersion', + 'passphrase', + 'pfx', + 'rejectUnauthorized', + 'requestCert', + 'secureOptions', + 'secureProtocol', + 'sessionIdContext', + 'sigalgs', + 'ticketKeys' +]; + +function createPlainObject() { + return Object.create(null); +} + +function splitServerOptions(options) { + const httpOptions = createPlainObject(); + const tlsOptions = createPlainObject(); + let hasTlsOptions = false; + + if (!options || typeof options !== 'object') { + return { httpOptions: undefined, tlsOptions: undefined, hasTlsOptions: false }; + } + + Object.keys(options).forEach(key => { + if (TLS_SERVER_OPTION_KEYS.includes(key)) { + tlsOptions[key] = options[key]; + hasTlsOptions = true; + return; + } + + httpOptions[key] = options[key]; + }); + + return { + httpOptions, + tlsOptions: hasTlsOptions ? tlsOptions : undefined, + hasTlsOptions + }; +} + +function toContextOptions(options) { + const contextOptions = createPlainObject(); + + if (!options) return contextOptions; + TLS_CONTEXT_OPTION_KEYS.forEach(key => { + if (options[key] !== undefined) contextOptions[key] = options[key]; + }); + return contextOptions; +} + +function maybeCreateSecureContext(options) { + if (!options) return undefined; + return tls.createContext(toContextOptions(options)); +} + +function normalizeCreateServerArgs(options, requestListener) { + if (typeof options === 'function') { + return { + serverOptions: undefined, + requestListener: options + }; + } + + return { + serverOptions: options, + requestListener + }; +} + +function clientNotImplemented() { + throw new Error('node:https client transport is not implemented yet'); +} + +// compatibility stub only +export class Server extends http.Server { + constructor(options, requestListener) { + const normalized = normalizeCreateServerArgs(options, requestListener); + const split = splitServerOptions(normalized.serverOptions); + + super(split.httpOptions, normalized.requestListener); + this[kSecureContext] = split.hasTlsOptions ? maybeCreateSecureContext(split.tlsOptions) : undefined; + } +} + +export function createSecureContext(options) { + return tls.createContext(options); +} + +export function isSecureContext(value) { + return tls.isContext(value); +} + +export function setConfigPath(path) { + return tls.setConfigPath(path); +} + +export function createServer(options, requestListener) { + return new Server(options, requestListener); +} + +export function request() { + clientNotImplemented(); +} + +export function get() { + clientNotImplemented(); +} + +export { METHODS, STATUS_CODES } from 'ant:internal/http_metadata'; diff --git a/src/builtins/node/querystring.mjs b/src/builtins/node/querystring.mjs new file mode 100644 index 0000000..26b3d30 --- /dev/null +++ b/src/builtins/node/querystring.mjs @@ -0,0 +1,80 @@ +const plusPattern = /\+/g; +const defaultMaxKeys = 1000; + +export const escape = value => encodeURIComponent(String(value)); +export const normalizeComponent = value => String(value).replace(plusPattern, '%20'); + +export function unescape(value) { + const input = normalizeComponent(value); + + try { + return decodeURIComponent(input); + } catch { + return input; + } +} + +const getOptions = options => (options && typeof options === 'object' ? options : null); + +const appendValue = (target, key, value) => { + if (target[key] === undefined) { + target[key] = value; + return; + } + + target[key] = Array.isArray(target[key]) ? [...target[key], value] : [target[key], value]; +}; + +export function parse(input, sep = '&', eq = '=', options = undefined) { + const result = Object.create(null); + const source = String(input || ''); + const parsedOptions = getOptions(options); + const maxKeys = typeof parsedOptions?.maxKeys === 'number' ? parsedOptions.maxKeys : defaultMaxKeys; + const decode = typeof parsedOptions?.decodeURIComponent === 'function' ? parsedOptions.decodeURIComponent : unescape; + + if (source === '') return result; + + let count = 0; + for (const entry of source.split(sep)) { + if (entry === '') continue; + if (maxKeys > 0 && count >= maxKeys) break; + + const index = entry.indexOf(eq); + const rawKey = index >= 0 ? entry.slice(0, index) : entry; + const rawValue = index >= 0 ? entry.slice(index + eq.length) : ''; + const key = decode(normalizeComponent(rawKey)); + const value = decode(normalizeComponent(rawValue)); + + appendValue(result, key, value); + count += 1; + } + + return result; +} + +export function stringify(obj, sep = '&', eq = '=', options = undefined) { + const parsedOptions = getOptions(options); + const encode = typeof parsedOptions?.encodeURIComponent === 'function' ? parsedOptions.encodeURIComponent : escape; + + if (obj === null || obj === undefined) return ''; + const parts = []; + + for (const key of Object.keys(obj)) { + const value = obj[key]; + const encodedKey = encode(key); + + if (Array.isArray(value)) { + for (const item of value) parts.push(encodedKey + eq + encode(item ?? '')); + continue; + } + + parts.push(encodedKey + eq + encode(value ?? '')); + } + + return parts.join(sep); +} + +export const encode = stringify; +export const decode = parse; + +export default { parse, stringify, escape, unescape, encode, decode }; diff --git a/src/errors.c b/src/errors.c index 63470b0..e136f84 100644 --- a/src/errors.c +++ b/src/errors.c @@ -22,10 +22,11 @@ typedef struct { char *buf; size_t size; } errbuf_t; static void print_error_value(ant_t *js, ant_value_t value, ant_value_t fallback_stack, const char *prefix) { + ant_value_t obj = is_err(value) ? js_as_obj(value) : value; const char *stack = NULL; - if (vtype(value) == T_OBJ) - stack = get_str_prop(js, value, "stack", 5, NULL); + if (vtype(obj) == T_OBJ) + stack = get_str_prop(js, obj, "stack", 5, NULL); if (!stack && vtype(fallback_stack) == T_STR) { ant_offset_t slen; @@ -39,13 +40,16 @@ static void print_error_value(ant_t *js, ant_value_t value, ant_value_t fallback fputs(stack, stderr); size_t n = strlen(stack); if (n == 0 || stack[n - 1] != '\n') fputc('\n', stderr); - } else if (vtype(value) == T_OBJ) { - const char *name = get_str_prop(js, value, "name", 4, NULL); - const char *msg = get_str_prop(js, value, "message", 7, NULL); + } else if (vtype(obj) == T_OBJ) { + const char *name = get_str_prop(js, obj, "name", 4, NULL); + const char *msg = get_str_prop(js, obj, "message", 7, NULL); + if (name && msg) fprintf(stderr, "%s%s%s: %s%s%s\n", C_RED, name, C_RESET, C_BOLD, msg, C_RESET); else if (name) fprintf(stderr, "%s%s%s\n", C_RED, name, C_RESET); - else fprintf(stderr, "%s\n", js_str(js, value)); - } else fprintf(stderr, "%s\n", js_str(js, value)); + else fprintf(stderr, "[object Error]\n"); + } + + else fprintf(stderr, "%s\n", js_str(js, value)); } bool print_uncaught_throw(ant_t *js) { diff --git a/src/esm/builtin_bundle.c b/src/esm/builtin_bundle.c index 210235e..4f49b1d 100644 --- a/src/esm/builtin_bundle.c +++ b/src/esm/builtin_bundle.c @@ -3,22 +3,30 @@ #include "esm/builtin_bundle.h" #include "builtin_bundle_data.h" -bool esm_is_builtin_specifier(const char *specifier) { +bool esm_has_builtin_scheme(const char *specifier) { if (!specifier) return false; - return - strncmp(specifier, "node:", 5) == 0 || - strncmp(specifier, "ant:", 4) == 0; + + if (specifier[0] == 'n') return strncmp(specifier, "node:", 5) == 0; + if (specifier[0] == 'a') return strncmp(specifier, "ant:" , 4) == 0; + + return false; } -const ant_builtin_bundle_entry_t *esm_lookup_builtin_bundle(const char *specifier, size_t spec_len) { - size_t i = 0; + +const ant_builtin_bundle_alias_t *esm_lookup_builtin_alias(const char *specifier, size_t spec_len) { if (!specifier) return NULL; - - for (i = 0; i < ant_builtin_bundle_data_count; i++) { - const ant_builtin_bundle_entry_t *entry = &ant_builtin_bundle_data[i]; - size_t entry_len = strlen(entry->specifier); - if (entry_len == spec_len && memcmp(entry->specifier, specifier, spec_len) == 0) return entry; + + for (size_t i = 0; i < ant_builtin_bundle_alias_count; i++) { + const ant_builtin_bundle_alias_t *alias = &ant_builtin_bundle_aliases[i]; + if (alias->specifier_len != spec_len) continue; + if (memcmp(alias->specifier, specifier, spec_len) != 0) continue; + return alias; } return NULL; } + +const ant_builtin_bundle_module_t *esm_lookup_builtin_module(size_t module_id) { + if (module_id >= ant_builtin_bundle_module_count) return NULL; + return &ant_builtin_bundle_modules[module_id]; +} diff --git a/src/esm/loader.c b/src/esm/loader.c index 076ed48..e9a5e54 100644 --- a/src/esm/loader.c +++ b/src/esm/loader.c @@ -8,6 +8,7 @@ #include "modules/json.h" #include "modules/napi.h" +#include "modules/uri.h" #include "errors.h" #include "gc/modules.h" @@ -73,7 +74,7 @@ static int esm_dynamic_import_depth = 0; static char *esm_resolve_node_module(const char *specifier, const char *base_path); static char *esm_canonicalize_path(const char *path); -static char *esm_file_url_to_path(const char *specifier) { +static char *esm_file_url_to_path(ant_t *js, const char *specifier) { if (!specifier || strncmp(specifier, "file:", 5) != 0) return NULL; const char *p = specifier + 5; @@ -81,12 +82,19 @@ static char *esm_file_url_to_path(const char *specifier) { else if (strncmp(p, "//localhost/", 12) == 0) p += 11; if (*p == '\0') return NULL; - return strdup(p); + + ant_value_t encoded = js_mkstr(js, p, strlen(p)); + ant_value_t decoded = js_decodeURI(js, &encoded, 1); + + size_t len = 0; + char *str = js_getstr(js, decoded, &len); + + return str ? strndup(str, len) : NULL; } static char *esm_make_cache_key(const char *module_key) { if (!module_key) return NULL; - if (esm_is_builtin_specifier(module_key)) return strdup(module_key); + if (esm_has_builtin_scheme(module_key)) return strdup(module_key); if (esm_is_data_url(module_key)) return strdup(module_key); if (esm_is_url(module_key)) return strdup(module_key); return esm_canonicalize_path(module_key); @@ -1176,28 +1184,36 @@ ant_value_t js_esm_import_sync_cstr_from( size_t spec_len, const char *base_path ) { - const ant_builtin_bundle_entry_t *bundle = NULL; + const ant_builtin_bundle_alias_t *bundle = NULL; + const ant_builtin_bundle_module_t *module = NULL; + char *spec_copy = strndup(specifier, spec_len); if (!spec_copy) return js_mkerr(js, "oom"); - char *file_url_path = esm_file_url_to_path(spec_copy); + char *file_url_path = esm_file_url_to_path(js, spec_copy); if (file_url_path) { free(spec_copy); spec_copy = file_url_path; spec_len = strlen(spec_copy); } - bundle = esm_lookup_builtin_bundle(spec_copy, spec_len); + bundle = esm_lookup_builtin_alias(spec_copy, spec_len); if (bundle) { + module = esm_lookup_builtin_module(bundle->module_id); + if (!module) { + free(spec_copy); + return js_mkerr(js, "Invalid builtin module id"); + } + ant_value_t ns = esm_get_or_load( - js, - spec_copy, + js, spec_copy, bundle->source_name, - bundle->specifier, - bundle->format, - bundle->code, - bundle->code_len + bundle->source_name, + module->format, + module->code, + module->code_len ); + free(spec_copy); return ns; } @@ -1250,11 +1266,11 @@ ant_value_t js_esm_import_sync(ant_t *js, ant_value_t specifier) { return js_esm_import_sync_from(js, specifier, NULL); } -ant_value_t js_esm_import_dynamic(ant_t *js, ant_value_t specifier, ant_value_t *out_tla_promise) { +ant_value_t js_esm_import_dynamic(ant_t *js, ant_value_t specifier, const char *base_path, ant_value_t *out_tla_promise) { *out_tla_promise = js_mkundef(); esm_dynamic_import_depth++; - ant_value_t ns = js_esm_import_sync(js, specifier); + ant_value_t ns = js_esm_import_sync_from(js, specifier, base_path); esm_dynamic_import_depth--; if (is_err(ns)) return ns; @@ -1271,14 +1287,17 @@ ant_value_t js_esm_import_dynamic(ant_t *js, ant_value_t specifier, ant_value_t } ant_value_t js_esm_make_file_url(ant_t *js, const char *path) { - size_t url_len = strlen(path) + 8; - char *url = malloc(url_len); - if (!url) return js_mkerr(js, "oom"); - - snprintf(url, url_len, "file://%s", path); - ant_value_t val = js_mkstr(js, url, strlen(url)); - free(url); - return val; + size_t path_len = strlen(path); + size_t raw_len = 7 + path_len; + + char *raw = malloc(raw_len + 1); + if (!raw) return js_mkerr(js, "oom"); + + snprintf(raw, raw_len + 1, "file://%s", path); + ant_value_t raw_val = js_mkstr(js, raw, raw_len); + free(raw); + + return js_encodeURI(js, &raw_val, 1); } void gc_mark_esm(ant_t *js, gc_mark_fn mark) { @@ -1290,7 +1309,7 @@ HASH_ITER(hh, global_module_cache.modules, mod, tmp) { }} ant_value_t js_esm_resolve_specifier(ant_t *js, ant_value_t specifier, const char *base_path) { - const ant_builtin_bundle_entry_t *bundle = NULL; + const ant_builtin_bundle_alias_t *bundle = NULL; if (vtype(specifier) != T_STR) { return js_mkerr(js, "import.meta.resolve() requires a string specifier"); } @@ -1301,9 +1320,9 @@ ant_value_t js_esm_resolve_specifier(ant_t *js, ant_value_t specifier, const cha char *spec_copy = strndup(spec_str, (size_t)spec_len); if (!spec_copy) return js_mkerr(js, "oom"); - bundle = esm_lookup_builtin_bundle(spec_copy, (size_t)spec_len); + bundle = esm_lookup_builtin_alias(spec_copy, (size_t)spec_len); if (bundle) { - ant_value_t result = js_mkstr(js, bundle->specifier, strlen(bundle->specifier)); + ant_value_t result = js_mkstr(js, bundle->source_name, strlen(bundle->source_name)); free(spec_copy); return result; } diff --git a/src/gc/objects.c b/src/gc/objects.c index 185afde..bcb128a 100644 --- a/src/gc/objects.c +++ b/src/gc/objects.c @@ -296,6 +296,10 @@ static void gc_scan_obj(ant_t *js, ant_object_t *obj) { gc_mark_value(js, obj->proxy_state->target); gc_mark_value(js, obj->proxy_state->handler); } + + gc_mark_abort_signal_object( + js, js_obj_from_ptr(obj), gc_mark_value + ); } static void gc_drain_mark_stack(ant_t *js) { diff --git a/src/main.c b/src/main.c index 528f8ab..a320799 100644 --- a/src/main.c +++ b/src/main.c @@ -70,6 +70,7 @@ #include "modules/util.h" #include "modules/async_hooks.h" #include "modules/net.h" +#include "modules/tls.h" #include "modules/http_metadata.h" #include "modules/http_parser.h" #include "modules/http_writer.h" @@ -615,6 +616,7 @@ int main(int argc, char *argv[]) { ant_register_library(ffi_library, "ant:ffi", NULL); ant_register_library(lmdb_library, "ant:lmdb", NULL); + ant_register_library(internal_tls_library, "ant:internal/tls", NULL); ant_register_library(internal_http_parser_library, "ant:internal/http_parser", NULL); ant_register_library(internal_http_writer_library, "ant:internal/http_writer", NULL); ant_register_library(internal_http_metadata_library, "ant:internal/http_metadata", NULL); @@ -627,7 +629,6 @@ int main(int argc, char *argv[]) { ant_standard_library("buffer", buffer_library); ant_standard_library("path", path_library); ant_standard_library("fs", fs_library); - ant_standard_library("fs/promises", fs_promises_library); ant_standard_library("os", os_library); ant_standard_library("url", url_library); ant_standard_library("perf_hooks", perf_hooks_library); @@ -636,12 +637,15 @@ int main(int argc, char *argv[]) { ant_standard_library("events", events_library); ant_standard_library("tty", tty_library); ant_standard_library("readline", readline_library); - ant_standard_library("readline/promises", readline_promises_library); ant_standard_library("child_process", child_process_library); ant_standard_library("worker_threads", worker_threads_library); ant_standard_library("async_hooks", async_hooks_library); ant_standard_library("v8", v8_library); ant_standard_library("zlib", zlib_library); + + ant_standard_library("fs/promises", fs_promises_library); + ant_standard_library("timers/promises", timers_promises_library); + ant_standard_library("readline/promises", readline_promises_library); ant_value_t snapshot_result = ant_load_snapshot(js); if (vtype(snapshot_result) == T_ERR) { diff --git a/src/modules/abort.c b/src/modules/abort.c index f69d940..9343b35 100644 --- a/src/modules/abort.c +++ b/src/modules/abort.c @@ -50,6 +50,12 @@ static abort_signal_data_t *get_signal_data(ant_value_t obj) { return (abort_signal_data_t *)(uintptr_t)js_getnum(slot); } +static abort_signal_data_t *get_signal_data_if_signal_object(ant_value_t obj) { + if (!g_initialized || !is_object_type(obj)) return NULL; + if (js_get_slot(obj, SLOT_PROTO) != g_signal_proto) return NULL; + return get_signal_data(obj); +} + static ant_value_t make_abort_error(ant_t *js) { return make_dom_exception(js, "signal is aborted without reason", "AbortError"); } @@ -418,6 +424,27 @@ void gc_mark_abort(ant_t *js, gc_mark_fn mark) { if (!e->closed) mark(js, e->signal); } +void gc_mark_abort_signal_object(ant_t *js, ant_value_t signal, gc_mark_fn mark) { + abort_signal_data_t *data = get_signal_data_if_signal_object(signal); + + if (!data) return; + mark(js, data->reason); + + unsigned int listener_count = utarray_len(data->listeners); + for (unsigned int i = 0; i < listener_count; i++) { + abort_listener_t *entry = (abort_listener_t *)utarray_eltptr(data->listeners, i); + if (!entry) continue; + mark(js, entry->callback); + } + + unsigned int follower_count = utarray_len(data->followers); + for (unsigned int i = 0; i < follower_count; i++) { + ant_value_t *follower = (ant_value_t *)utarray_eltptr(data->followers, i); + if (!follower) continue; + mark(js, *follower); + } +} + bool abort_signal_is_aborted(ant_value_t signal) { abort_signal_data_t *data = get_signal_data(signal); return data && data->aborted; diff --git a/src/modules/buffer.c b/src/modules/buffer.c index e1d423f..488f4ee 100644 --- a/src/modules/buffer.c +++ b/src/modules/buffer.c @@ -1858,6 +1858,51 @@ static ant_value_t js_typedarray_join(ant_t *js, ant_value_t *args, int nargs) { return typedarray_join_with(js, js_getthis(js), sep, sep_len); } +static ant_value_t js_typedarray_indexOf(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t this_val = js_getthis(js); + ant_value_t ta_data_val = js_get_slot(this_val, SLOT_BUFFER); + + TypedArrayData *ta_data = (TypedArrayData *)js_gettypedarray(ta_data_val); + if (!ta_data || !ta_data->buffer || ta_data->buffer->is_detached) return js_mknum(-1); + + size_t len = ta_data->length; + if (len == 0 || nargs < 1) return js_mknum(-1); + + int64_t from_index = 0; + if (nargs > 1 && vtype(args[1]) != T_UNDEF) { + from_index = (int64_t)js_to_number(js, args[1]); + if (from_index < 0) { + from_index += (int64_t)len; + if (from_index < 0) from_index = 0; + }} + + if ((size_t)from_index >= len) return js_mknum(-1); + uint8_t *data = ta_data->buffer->data + ta_data->byte_offset; + double needle_num = js_to_number(js, args[0]); + + for (size_t i = (size_t)from_index; i < len; i++) { + bool match = false; + switch (ta_data->type) { + case TYPED_ARRAY_INT8: match = ((int8_t *)data)[i] == (int8_t)needle_num; break; + case TYPED_ARRAY_UINT8: + case TYPED_ARRAY_UINT8_CLAMPED: match = data[i] == (uint8_t)needle_num; break; + case TYPED_ARRAY_INT16: match = ((int16_t *)data)[i] == (int16_t)needle_num; break; + case TYPED_ARRAY_UINT16: match = ((uint16_t *)data)[i] == (uint16_t)needle_num; break; + case TYPED_ARRAY_INT32: match = ((int32_t *)data)[i] == (int32_t)needle_num; break; + case TYPED_ARRAY_UINT32: match = ((uint32_t *)data)[i] == (uint32_t)needle_num; break; + case TYPED_ARRAY_FLOAT16: match = half_to_double(((uint16_t *)data)[i]) == needle_num; break; + case TYPED_ARRAY_FLOAT32: match = ((float *)data)[i] == (float)needle_num; break; + case TYPED_ARRAY_FLOAT64: match = ((double *)data)[i] == needle_num; break; + case TYPED_ARRAY_BIGINT64: match = ((int64_t *)data)[i] == (int64_t)needle_num; break; + case TYPED_ARRAY_BIGUINT64: match = ((uint64_t *)data)[i] == (uint64_t)needle_num; break; + default: break; + } + if (match) return js_mknum((double)i); + } + + return js_mknum(-1); +} + // Buffer.prototype.toString(encoding) static ant_value_t js_buffer_slice(ant_t *js, ant_value_t *args, int nargs) { return js_typedarray_subarray(js, args, nargs); @@ -2174,6 +2219,7 @@ void init_buffer_module() { js_set(js, typedarray_proto, "with", js_mkfun(js_typedarray_with)); js_set(js, typedarray_proto, "toString", js_mkfun(js_typedarray_toString)); js_set(js, typedarray_proto, "join", js_mkfun(js_typedarray_join)); + js_set(js, typedarray_proto, "indexOf", js_mkfun(js_typedarray_indexOf)); js_set_sym(js, typedarray_proto, get_toStringTag_sym(), js_mkstr(js, "TypedArray", 10)); g_typedarray_iter_proto = js_mkobj(js); diff --git a/src/modules/collections.c b/src/modules/collections.c index 1800348..eccc948 100644 --- a/src/modules/collections.c +++ b/src/modules/collections.c @@ -10,12 +10,19 @@ #include "silver/engine.h" #include "descriptors.h" +#include "modules/bigint.h" #include "modules/collections.h" #include "modules/symbol.h" ant_value_t g_map_iter_proto = 0; ant_value_t g_set_iter_proto = 0; +typedef struct { + unsigned char stack[32]; + unsigned char *bytes; + size_t len; +} collection_key_t; + static ant_value_t normalize_map_key(ant_value_t key) { if (vtype(key) == T_NUM) { double d = tod(key); @@ -24,13 +31,154 @@ static ant_value_t normalize_map_key(ant_value_t key) { return key; } -static const char *ant_value_to_key(ant_t *js, ant_value_t val) { - if (vtype(val) == T_STR) { - ant_offset_t len; - ant_offset_t off = vstr(js, val, &len); - return (char *)(uintptr_t)(off); +static void collection_key_reset(collection_key_t *key) { + key->bytes = key->stack; + key->len = 0; +} + +static void collection_key_free(collection_key_t *key) { + if (key->bytes != key->stack) free(key->bytes); + collection_key_reset(key); +} + +static bool collection_key_reserve(collection_key_t *key, size_t len) { + if (len <= sizeof(key->stack)) return true; + unsigned char *heap = malloc(len); + if (!heap) return false; + key->bytes = heap; + return true; +} + +static bool collection_key_init(ant_t *js, ant_value_t input, collection_key_t *out) { + collection_key_reset(out); + + ant_value_t key = normalize_map_key(input); + uint8_t tag = (uint8_t)vtype(key); + + if (vtype(key) == T_STR) { + size_t str_len = 0; + const char *str = js_getstr(js, key, &str_len); + out->len = 1 + str_len; + if (!collection_key_reserve(out, out->len)) return false; + out->bytes[0] = tag; + if (str_len > 0) memcpy(out->bytes + 1, str, str_len); + return true; + } + + if (vtype(key) == T_BIGINT) { + size_t str_len = bigint_digits_len(js, key) + (bigint_is_negative(js, key) ? 1 : 0); + out->len = 1 + str_len; + if (!collection_key_reserve(out, out->len)) return false; + out->bytes[0] = tag; + if (str_len > 0) strbigint(js, key, (char *)(out->bytes + 1), str_len + 1); + return true; + } + + out->len = 1 + sizeof(ant_value_t); + if (!collection_key_reserve(out, out->len)) return false; + out->bytes[0] = tag; + memcpy(out->bytes + 1, &key, sizeof(ant_value_t)); + + return true; +} + +static map_entry_t *map_find_entry(ant_t *js, map_entry_t **map_ptr, ant_value_t key_val) { + collection_key_t key; + if (!collection_key_init(js, key_val, &key)) return NULL; + + map_entry_t *entry = NULL; + HASH_FIND(hh, *map_ptr, key.bytes, key.len, entry); + collection_key_free(&key); + + return entry; +} + +static set_entry_t *set_find_entry(ant_t *js, set_entry_t **set_ptr, ant_value_t value) { + collection_key_t key; + if (!collection_key_init(js, value, &key)) return NULL; + + set_entry_t *entry = NULL; + HASH_FIND(hh, *set_ptr, key.bytes, key.len, entry); + collection_key_free(&key); + + return entry; +} + +static bool map_store_entry( + ant_t *js, + map_entry_t **map_ptr, + ant_value_t raw_key, + ant_value_t key_val, + ant_value_t value +) { + collection_key_t key; + if (!collection_key_init(js, raw_key, &key)) return false; + + map_entry_t *entry = NULL; + HASH_FIND(hh, *map_ptr, key.bytes, key.len, entry); + if (entry) { + entry->key_val = key_val; + entry->value = value; + collection_key_free(&key); + return true; + } + + entry = ant_calloc(sizeof(map_entry_t)); + if (!entry) { + collection_key_free(&key); + return false; } - return js_str(js, val); + + entry->key = malloc(key.len); + if (!entry->key) { + collection_key_free(&key); + free(entry); + return false; + } + + memcpy(entry->key, key.bytes, key.len); + entry->key_len = key.len; + entry->key_val = key_val; + entry->value = value; + + HASH_ADD_KEYPTR(hh, *map_ptr, entry->key, entry->key_len, entry); + collection_key_free(&key); + + return true; +} + +static bool set_store_entry(ant_t *js, set_entry_t **set_ptr, ant_value_t value) { + collection_key_t key; + if (!collection_key_init(js, value, &key)) return false; + + set_entry_t *entry = NULL; + HASH_FIND(hh, *set_ptr, key.bytes, key.len, entry); + if (entry) { + collection_key_free(&key); + return true; + } + + entry = ant_calloc(sizeof(set_entry_t)); + if (!entry) { + collection_key_free(&key); + return false; + } + + entry->key = malloc(key.len); + if (!entry->key) { + collection_key_free(&key); + free(entry); + return false; + } + + memcpy(entry->key, key.bytes, key.len); + entry->key_len = key.len; + entry->value = value; + + HASH_ADD_KEYPTR(hh, *set_ptr, entry->key, entry->key_len, entry); + collection_key_free(&key); + + return true; } map_entry_t **get_map_from_obj(ant_t *js, ant_value_t obj) { @@ -79,23 +227,10 @@ static ant_value_t map_set(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; map_entry_t **map_ptr = get_map_from_obj(js, this_val); if (!map_ptr) return js_mkerr(js, "Invalid Map object"); - const char *key_str = ant_value_to_key(js, args[0]); ant_value_t key_val = normalize_map_key(args[0]); - map_entry_t *entry; - - HASH_FIND_STR(*map_ptr, key_str, entry); - if (entry) { - entry->key_val = key_val; - entry->value = args[1]; - } else { - entry = ant_calloc(sizeof(map_entry_t)); - if (!entry) return js_mkerr(js, "out of memory"); - entry->key = strdup(key_str); - entry->key_val = key_val; - entry->value = args[1]; - HASH_ADD_STR(*map_ptr, key, entry); - } + if (!map_store_entry(js, map_ptr, args[0], key_val, args[1])) + return js_mkerr(js, "out of memory"); return this_val; } @@ -106,11 +241,8 @@ static ant_value_t map_get(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; map_entry_t **map_ptr = get_map_from_obj(js, this_val); if (!map_ptr) return js_mkundef(); - - const char *key_str = ant_value_to_key(js, args[0]); - - map_entry_t *entry; - HASH_FIND_STR(*map_ptr, key_str, entry); + + map_entry_t *entry = map_find_entry(js, map_ptr, args[0]); return entry ? entry->value : js_mkundef(); } @@ -121,10 +253,7 @@ static ant_value_t map_has(ant_t *js, ant_value_t *args, int nargs) { map_entry_t **map_ptr = get_map_from_obj(js, this_val); if (!map_ptr) return js_false; - const char *key_str = ant_value_to_key(js, args[0]); - - map_entry_t *entry; - HASH_FIND_STR(*map_ptr, key_str, entry); + map_entry_t *entry = map_find_entry(js, map_ptr, args[0]); return js_bool(entry != NULL); } @@ -135,10 +264,7 @@ static ant_value_t map_delete(ant_t *js, ant_value_t *args, int nargs) { map_entry_t **map_ptr = get_map_from_obj(js, this_val); if (!map_ptr) return js_false; - const char *key_str = ant_value_to_key(js, args[0]); - - map_entry_t *entry; - HASH_FIND_STR(*map_ptr, key_str, entry); + map_entry_t *entry = map_find_entry(js, map_ptr, args[0]); if (entry) { HASH_DEL(*map_ptr, entry); free(entry->key); @@ -308,18 +434,8 @@ static ant_value_t set_add(ant_t *js, ant_value_t *args, int nargs) { set_entry_t **set_ptr = get_set_from_obj(js, this_val); if (!set_ptr) return js_mkerr(js, "Invalid Set object"); - const char *key_str = ant_value_to_key(js, args[0]); - - set_entry_t *entry; - HASH_FIND_STR(*set_ptr, key_str, entry); - - if (!entry) { - entry = ant_calloc(sizeof(set_entry_t)); - if (!entry) return js_mkerr(js, "out of memory"); - entry->value = args[0]; - entry->key = strdup(key_str); - HASH_ADD_KEYPTR(hh, *set_ptr, entry->key, strlen(entry->key), entry); - } + if (!set_store_entry(js, set_ptr, args[0])) + return js_mkerr(js, "out of memory"); return this_val; } @@ -330,11 +446,8 @@ static ant_value_t set_has(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; set_entry_t **set_ptr = get_set_from_obj(js, this_val); if (!set_ptr) return js_false; - - const char *key_str = ant_value_to_key(js, args[0]); - - set_entry_t *entry; - HASH_FIND_STR(*set_ptr, key_str, entry); + + set_entry_t *entry = set_find_entry(js, set_ptr, args[0]); return js_bool(entry != NULL); } @@ -344,10 +457,8 @@ static ant_value_t set_delete(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; set_entry_t **set_ptr = get_set_from_obj(js, this_val); if (!set_ptr) return js_false; - - const char *key_str = ant_value_to_key(js, args[0]); - set_entry_t *entry; - HASH_FIND_STR(*set_ptr, key_str, entry); + + set_entry_t *entry = set_find_entry(js, set_ptr, args[0]); if (entry) { HASH_DEL(*set_ptr, entry); @@ -427,7 +538,7 @@ static ant_value_t weakmap_set(ant_t *js, ant_value_t *args, int nargs) { weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val); if (!wm_ptr) return js_mkerr(js, "Invalid WeakMap object"); - if (vtype(args[0]) != T_OBJ) + if (!is_object_type(args[0])) return js_mkerr(js, "WeakMap key must be an object"); ant_value_t key_obj = args[0]; @@ -453,8 +564,7 @@ static ant_value_t weakmap_get(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val); if (!wm_ptr) return js_mkundef(); - - if (vtype(args[0]) != T_OBJ) return js_mkundef(); + if (!is_object_type(args[0])) return js_mkundef(); ant_value_t key_obj = args[0]; weakmap_entry_t *entry; @@ -468,8 +578,7 @@ static ant_value_t weakmap_has(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val); if (!wm_ptr) return js_false; - - if (vtype(args[0]) != T_OBJ) return js_false; + if (!is_object_type(args[0])) return js_false; ant_value_t key_obj = args[0]; weakmap_entry_t *entry; @@ -483,8 +592,7 @@ static ant_value_t weakmap_delete(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val); if (!wm_ptr) return js_false; - - if (vtype(args[0]) != T_OBJ) return js_false; + if (!is_object_type(args[0])) return js_false; ant_value_t key_obj = args[0]; weakmap_entry_t *entry; @@ -504,7 +612,7 @@ static ant_value_t weakset_add(ant_t *js, ant_value_t *args, int nargs) { weakset_entry_t **ws_ptr = get_weakset_from_obj(js, this_val); if (!ws_ptr) return js_mkerr(js, "Invalid WeakSet object"); - if (vtype(args[0]) != T_OBJ) + if (!is_object_type(args[0])) return js_mkerr(js, "WeakSet value must be an object"); ant_value_t value_obj = args[0]; @@ -528,8 +636,7 @@ static ant_value_t weakset_has(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; weakset_entry_t **ws_ptr = get_weakset_from_obj(js, this_val); if (!ws_ptr) return js_false; - - if (vtype(args[0]) != T_OBJ) return js_false; + if (!is_object_type(args[0])) return js_false; ant_value_t value_obj = args[0]; weakset_entry_t *entry; @@ -543,8 +650,7 @@ static ant_value_t weakset_delete(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_val = js->this_val; weakset_entry_t **ws_ptr = get_weakset_from_obj(js, this_val); if (!ws_ptr) return js_false; - - if (vtype(args[0]) != T_OBJ) return js_false; + if (!is_object_type(args[0])) return js_false; ant_value_t value_obj = args[0]; weakset_entry_t *entry; @@ -559,7 +665,7 @@ static ant_value_t weakset_delete(ant_t *js, ant_value_t *args, int nargs) { } static ant_value_t builtin_WeakRef(ant_t *js, ant_value_t *args, int nargs) { - if (nargs < 1 || vtype(args[0]) != T_OBJ) { + if (nargs < 1 || !is_object_type(args[0])) { return js_mkerr(js, "WeakRef target must be an object"); } @@ -695,21 +801,13 @@ static ant_value_t map_groupBy(ant_t *js, ant_value_t *args, int nargs) { js_mkundef(), cb_args, 2, NULL, false) ); - map_entry_t *entry; - ant_value_t group; - if (is_err(key)) return key; - const char *key_str = ant_value_to_key(js, key); - HASH_FIND_STR(*map_head, key_str, entry); - + map_entry_t *entry = map_find_entry(js, map_head, key); + ant_value_t group; + if (entry) group = entry->value; else { group = js_mkarr(js); - entry = ant_calloc(sizeof(map_entry_t)); - if (!entry) return js_mkerr(js, "out of memory"); - entry->key = strdup(key_str); - entry->key_val = key; - entry->value = group; - HASH_ADD_STR(*map_head, key, entry); + if (!map_store_entry(js, map_head, key, key, group)) return js_mkerr(js, "out of memory"); } js_arr_push(js, group, val); @@ -756,22 +854,8 @@ static ant_value_t builtin_Map(ant_t *js, ant_value_t *args, int nargs) { ant_value_t key = normalize_map_key(js_arr_get(js, entry, 0)); ant_value_t value = js_arr_get(js, entry, 1); - const char *key_str = ant_value_to_key(js, key); - - map_entry_t *map_entry; - HASH_FIND_STR(*map_head, key_str, map_entry); - if (map_entry) { - map_entry->key_val = key; - map_entry->value = value; - continue; - } - - map_entry = ant_calloc(sizeof(map_entry_t)); - if (!map_entry) return js_mkerr(js, "out of memory"); - map_entry->key = strdup(key_str); - map_entry->key_val = key; - map_entry->value = value; - HASH_ADD_STR(*map_head, key, map_entry); + if (!map_store_entry(js, map_head, key, key, value)) + return js_mkerr(js, "out of memory"); } return map_obj; @@ -808,17 +892,7 @@ static ant_value_t builtin_Set(ant_t *js, ant_value_t *args, int nargs) { for (ant_offset_t i = 0; i < length; i++) { ant_value_t value = js_arr_get(js, iterable, i); - const char *key_str = ant_value_to_key(js, value); - - set_entry_t *entry; - HASH_FIND_STR(*set_head, key_str, entry); - if (entry) continue; - - entry = ant_calloc(sizeof(set_entry_t)); - if (!entry) return js_mkerr(js, "out of memory"); - entry->value = value; - entry->key = strdup(key_str); - HASH_ADD_KEYPTR(hh, *set_head, entry->key, strlen(entry->key), entry); + if (!set_store_entry(js, set_head, value)) return js_mkerr(js, "out of memory"); } return set_obj; @@ -862,11 +936,11 @@ static ant_value_t builtin_WeakMap(ant_t *js, ant_value_t *args, int nargs) { ant_value_t key = js_arr_get(js, entry, 0); ant_value_t value = js_arr_get(js, entry, 1); - - if (vtype(key) != T_OBJ) return js_mkerr(js, "WeakMap key must be an object"); + if (!is_object_type(key)) return js_mkerr(js, "WeakMap key must be an object"); weakmap_entry_t *wm_entry; HASH_FIND(hh, *wm_head, &key, sizeof(ant_value_t), wm_entry); + if (wm_entry) { wm_entry->value = value; continue; @@ -874,6 +948,7 @@ static ant_value_t builtin_WeakMap(ant_t *js, ant_value_t *args, int nargs) { wm_entry = ant_calloc(sizeof(weakmap_entry_t)); if (!wm_entry) return js_mkerr(js, "out of memory"); + wm_entry->key_obj = key; wm_entry->value = value; HASH_ADD(hh, *wm_head, key_obj, sizeof(ant_value_t), wm_entry); @@ -913,8 +988,7 @@ static ant_value_t builtin_WeakSet(ant_t *js, ant_value_t *args, int nargs) { for (ant_offset_t i = 0; i < length; i++) { ant_value_t value = js_arr_get(js, iterable, i); - - if (vtype(value) != T_OBJ) return js_mkerr(js, "WeakSet value must be an object"); + if (!is_object_type(value)) return js_mkerr(js, "WeakSet value must be an object"); weakset_entry_t *entry; HASH_FIND(hh, *ws_head, &value, sizeof(ant_value_t), entry); diff --git a/src/modules/fs.c b/src/modules/fs.c index 427a003..10631f9 100644 --- a/src/modules/fs.c +++ b/src/modules/fs.c @@ -148,6 +148,7 @@ typedef enum { FS_OP_EXISTS, FS_OP_READDIR, FS_OP_ACCESS, + FS_OP_REALPATH, FS_OP_WRITE_FD, FS_OP_OPEN, FS_OP_CLOSE @@ -222,6 +223,8 @@ static void complete_request(fs_request_t *req) { else result = fs_read_to_uint8array(req->js, req->data, req->data_len); } else if (req->op_type == FS_OP_READ_BYTES && req->data) result = js_mkstr(req->js, req->data, req->data_len); + else if (req->op_type == FS_OP_REALPATH && req->data) + result = js_mkstr(req->js, req->data, req->data_len); js_resolve_promise(req->js, req->promise, result); } @@ -422,6 +425,31 @@ static void on_stat_complete(uv_fs_t *uv_req) { free_fs_request(req); } +static void on_realpath_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0 || !uv_req->ptr) { + req->failed = 1; + req->error_msg = strdup(uv_strerror((int)uv_req->result)); + req->completed = 1; + complete_request(req); + return; + } + + req->data = strdup((const char *)uv_req->ptr); + if (!req->data) { + req->failed = 1; + req->error_msg = strdup("Out of memory"); + req->completed = 1; + complete_request(req); + return; + } + + req->data_len = strlen(req->data); + req->completed = 1; + complete_request(req); +} + static void on_exists_complete(uv_fs_t *uv_req) { fs_request_t *req = (fs_request_t *)uv_req->data; ant_value_t result = js_bool(uv_req->result >= 0); @@ -1304,6 +1332,61 @@ static ant_value_t builtin_fs_access(ant_t *js, ant_value_t *args, int nargs) { return req->promise; } +static ant_value_t builtin_fs_realpathSync(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "realpathSync() requires a path argument"); + + ant_value_t path_val = fs_coerce_path(js, args[0]); + if (vtype(path_val) != T_STR) return js_mkerr(js, "realpathSync() path must be a string"); + + size_t path_len = 0; + const char *path = js_getstr(js, path_val, &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + char *resolved = realpath(path, NULL); + if (!resolved) return js_mkerr(js, "realpathSync failed for '%s'", path); + + ant_value_t result = js_mkstr(js, resolved, strlen(resolved)); + free(resolved); + return result; +} + +static ant_value_t builtin_fs_realpath(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "realpath() requires a path argument"); + + ant_value_t path_val = fs_coerce_path(js, args[0]); + if (vtype(path_val) != T_STR) return js_mkerr(js, "realpath() path must be a string"); + + size_t path_len = 0; + const char *path = js_getstr(js, path_val, &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_REALPATH; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + if (!req->path) { + free_fs_request(req); + return js_mkerr(js, "Out of memory"); + } + + utarray_push_back(pending_requests, &req); + int result = uv_fs_realpath(uv_default_loop(), &req->uv_req, req->path, on_realpath_complete); + + if (result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(result)); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + static ant_value_t builtin_fs_readdirSync(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "readdirSync() requires a path argument"); @@ -1873,6 +1956,7 @@ static void fs_set_promise_methods(ant_t *js, ant_value_t lib) { js_set(js, lib, "exists", js_mkfun(builtin_fs_exists)); js_set(js, lib, "access", js_mkfun(builtin_fs_access)); js_set(js, lib, "readdir", js_mkfun(builtin_fs_readdir)); + js_set(js, lib, "realpath", js_mkfun(builtin_fs_realpath)); } static ant_value_t fs_make_constants(ant_t *js) { @@ -1903,6 +1987,7 @@ static ant_value_t builtin_fs_promises_getter(ant_t *js, ant_value_t *args, int ant_value_t fs_library(ant_t *js) { ant_value_t lib = js_mkobj(js); + ant_value_t realpath_sync = js_mkfun(builtin_fs_realpathSync); fs_set_promise_methods(js, lib); js_set(js, lib, "readFileSync", js_mkfun(builtin_fs_readFileSync)); @@ -1923,6 +2008,8 @@ ant_value_t fs_library(ant_t *js) { js_set(js, lib, "existsSync", js_mkfun(builtin_fs_existsSync)); js_set(js, lib, "accessSync", js_mkfun(builtin_fs_accessSync)); js_set(js, lib, "readdirSync", js_mkfun(builtin_fs_readdirSync)); + js_set(js, lib, "realpathSync", realpath_sync); + js_set(js, realpath_sync, "native", realpath_sync); js_set_getter_desc( js, lib, @@ -1959,4 +2046,3 @@ void gc_mark_fs(ant_t *js, gc_mark_fn mark) { if (reqp && *reqp) { mark(js, (*reqp)->promise); } } } - diff --git a/src/modules/globals.c b/src/modules/globals.c index 1318b0e..1ba8bb3 100644 --- a/src/modules/globals.c +++ b/src/modules/globals.c @@ -106,5 +106,4 @@ void init_globals_module(void) { js_set(js, global, "reportError", js_mkfun(js_report_error)); js_set_descriptor(js, global, "reportError", 11, JS_DESC_W | JS_DESC_C); - } diff --git a/src/modules/json.c b/src/modules/json.c index eee792f..2268507 100644 --- a/src/modules/json.c +++ b/src/modules/json.c @@ -5,6 +5,7 @@ #include #include +#include "gc.h" #include "utf8.h" #include "errors.h" #include "runtime.h" @@ -40,9 +41,11 @@ static ant_value_t yyjson_to_jsval(ant_t *js, yyjson_val *val) { size_t idx, max; yyjson_val *item; - yyjson_arr_foreach(val, idx, max, item) - js_arr_push(js, arr, yyjson_to_jsval(js, item)); - + yyjson_arr_foreach(val, idx, max, item) { + ant_value_t elem = yyjson_to_jsval(js, item); + js_arr_push(js, arr, elem); + } + return arr; } @@ -53,18 +56,18 @@ static ant_value_t yyjson_to_jsval(ant_t *js, yyjson_val *val) { json_key_entry_t *hash = NULL, *entry, *tmp; yyjson_obj_foreach(val, idx, max, key, item) { - const char *k = yyjson_get_str(key); - size_t klen = yyjson_get_len(key); - ant_value_t v = yyjson_to_jsval(js, item); - - HASH_FIND(hh, hash, k, klen, entry); - if (entry) js_saveval(js, entry->prop_off, v); else { - ant_offset_t off = js_mkprop_fast_off(js, obj, k, klen, v); - entry = malloc(sizeof(json_key_entry_t)); - entry->key = k; entry->key_len = klen; entry->prop_off = off; - HASH_ADD_KEYPTR(hh, hash, entry->key, entry->key_len, entry); - } - } + const char *k = yyjson_get_str(key); + + size_t klen = yyjson_get_len(key); + ant_value_t v = yyjson_to_jsval(js, item); + + HASH_FIND(hh, hash, k, klen, entry); + if (entry) js_saveval(js, entry->prop_off, v); else { + ant_offset_t off = js_mkprop_fast_off(js, obj, k, klen, v); + entry = malloc(sizeof(json_key_entry_t)); + entry->key = k; entry->key_len = klen; entry->prop_off = off; + HASH_ADD_KEYPTR(hh, hash, entry->key, entry->key_len, entry); + }} HASH_ITER(hh, hash, entry, tmp) { HASH_DEL(hash, entry); free(entry); @@ -188,6 +191,66 @@ static ant_value_t apply_reviver( const char *key, ant_value_t reviver ); +static ant_value_t json_apply_tojson( + ant_t *js, + const char *key, + ant_value_t val, + json_cycle_ctx *ctx +) { + if (!is_special_object(val)) return val; + ant_value_t toJSON = js_get(js, val, "toJSON"); + + if (is_err(toJSON)) { + json_capture_error(ctx, toJSON); + return js_mkundef(); + } + + if (!is_callable(toJSON)) return val; + ant_value_t args[1] = { js_mkstr(js, key, strlen(key)) }; + + ant_value_t transformed = sv_vm_call( + js->vm, js, + toJSON, val, + args, 1, NULL, false + ); + + if (is_err(transformed)) { + json_capture_error(ctx, transformed); + return js_mkundef(); + } + + return transformed; +} + +static ant_value_t json_apply_replacer( + ant_t *js, + const char *key, + ant_value_t val, + json_cycle_ctx *ctx +) { + if (!is_callable(ctx->replacer_func)) return val; + ant_value_t args[2] = { js_mkstr(js, key, strlen(key)), val }; + + ant_value_t transformed = sv_vm_call( + js->vm, js, + ctx->replacer_func, ctx->holder, + args, 2, NULL, false + ); + + if (is_err(transformed)) { + json_capture_error(ctx, transformed); + return js_mkundef(); + } + + return transformed; +} + +static inline ant_value_t json_create_root_holder(ant_t *js, ant_value_t value) { + ant_value_t holder = js_mkobj(js); + if (!is_err(holder)) js_set(js, holder, "", value); + return holder; +} + static yyjson_mut_val *json_array_to_yyjson( ant_t *js, yyjson_mut_doc *doc, ant_value_t val, json_cycle_ctx *ctx ) { @@ -241,9 +304,7 @@ static yyjson_mut_val *json_object_to_yyjson( ctx->holder = saved_holder; return NULL; } - int ptype = vtype(prop); - if (ptype == T_UNDEF || ptype == T_FUNC) continue; - + yyjson_mut_val *jval = ant_value_to_yyjson_with_key(js, doc, key, prop, ctx, 0); if (json_has_abort(ctx)) { ctx->holder = saved_holder; @@ -262,25 +323,13 @@ static yyjson_mut_val *ant_value_to_yyjson_impl(ant_t *js, yyjson_mut_doc *doc, int type = vtype(val); yyjson_mut_val *result = NULL; - if (is_special_object(val)) { - ant_value_t toJSON = js_get(js, val, "toJSON"); - if (is_err(toJSON)) { - json_capture_error(ctx, toJSON); - return NULL; - } - - if (vtype(toJSON) == T_FUNC) { - ant_value_t r = sv_vm_call(js->vm, js, toJSON, js_mkundef(), &val, 1, NULL, false); - if (vtype(r) == T_ERR) { json_capture_error(ctx, r); return NULL; } - return ant_value_to_yyjson_impl(js, doc, r, ctx, in_array); - }} - switch (type) { case T_NULL: return yyjson_mut_null(doc); case T_BOOL: return yyjson_mut_bool(doc, val == js_true); case T_UNDEF: return in_array ? yyjson_mut_null(doc) : YYJSON_SKIP_VALUE; case T_FUNC: return in_array ? yyjson_mut_null(doc) : YYJSON_SKIP_VALUE; + case T_SYMBOL: return in_array ? yyjson_mut_null(doc) : YYJSON_SKIP_VALUE; case T_NUM: { double num = js_getnum(val); @@ -317,19 +366,13 @@ static yyjson_mut_val *ant_value_to_yyjson_with_key( ant_t *js, yyjson_mut_doc *doc, const char *key, ant_value_t val, json_cycle_ctx *ctx, int in_array ) { - if (vtype(ctx->replacer_func) != T_FUNC) - return ant_value_to_yyjson_impl(js, doc, val, ctx, in_array); - - ant_value_t key_str = js_mkstr(js, key, strlen(key)); - ant_value_t call_args[2] = { key_str, val }; - ant_value_t transformed = sv_vm_call(js->vm, js, ctx->replacer_func, js_mkundef(), call_args, 2, NULL, false); - - if (vtype(transformed) == T_ERR) { - json_capture_error(ctx, transformed); - return NULL; - } - - return ant_value_to_yyjson_impl(js, doc, transformed, ctx, in_array); + val = json_apply_tojson(js, key, val, ctx); + if (json_has_abort(ctx)) return NULL; + + val = json_apply_replacer(js, key, val, ctx); + if (json_has_abort(ctx)) return NULL; + + return ant_value_to_yyjson_impl(js, doc, val, ctx, in_array); } static yyjson_mut_val *ant_value_to_yyjson(ant_t *js, yyjson_mut_doc *doc, ant_value_t val, json_cycle_ctx *ctx) { @@ -338,20 +381,29 @@ static yyjson_mut_val *ant_value_to_yyjson(ant_t *js, yyjson_mut_doc *doc, ant_v static ant_value_t apply_reviver_call(ant_t *js, ant_value_t holder, const char *key, ant_value_t reviver) { ant_value_t key_str = js_mkstr(js, key, strlen(key)); - ant_value_t call_args[2] = { key_str, js_get(js, holder, key) }; - return sv_vm_call(js->vm, js, reviver, holder, call_args, 2, NULL, false); + ant_value_t current_value = js_get(js, holder, key); + ant_value_t call_args[2] = { key_str, current_value }; + + ant_value_t result = sv_vm_call( + js->vm, js, reviver, holder, + call_args, 2, NULL, false + ); + + return result; } static void apply_reviver_to_array(ant_t *js, ant_value_t value, ant_value_t reviver) { ant_offset_t length = js_arr_len(js, value); for (ant_offset_t i = 0; i < length; i++) { - char idxstr[32]; - size_t idx_len = uint_to_str(idxstr, sizeof(idxstr), (uint64_t)i); - ant_value_t new_elem = apply_reviver(js, value, idxstr, reviver); - if (vtype(new_elem) == T_UNDEF) js_delete_prop(js, value, idxstr, idx_len); - else js_set(js, value, idxstr, new_elem); - } + char idxstr[32]; + size_t idx_len = uint_to_str(idxstr, sizeof(idxstr), (uint64_t)i); + ant_value_t new_elem = apply_reviver(js, value, idxstr, reviver); + if (vtype(new_elem) == T_UNDEF) js_delete_prop(js, value, idxstr, idx_len); + else { + ant_value_t key_val = js_mkstr(js, idxstr, idx_len); + js_setprop(js, value, key_val, new_elem); + }} } static void apply_reviver_to_object(ant_t *js, ant_value_t value, ant_value_t reviver) { @@ -382,23 +434,30 @@ static ant_value_t apply_reviver(ant_t *js, ant_value_t holder, const char *key, ant_value_t js_json_parse(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "JSON.parse() requires at least 1 argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "JSON.parse() argument must be a string"); + bool saved_gc_disabled = gc_disabled; size_t len; char *json_str = js_getstr(js, args[0], &len); + gc_disabled = true; yyjson_doc *doc = yyjson_read(json_str, len, 0); - if (!doc) return js_mkerr_typed(js, JS_ERR_SYNTAX, "JSON.parse: unexpected character"); + + if (!doc) { + gc_disabled = saved_gc_disabled; + return js_mkerr_typed(js, JS_ERR_SYNTAX, "JSON.parse: unexpected character"); + } ant_value_t result = yyjson_to_jsval(js, yyjson_doc_get_root(doc)); yyjson_doc_free(doc); - if (nargs >= 2 && vtype(args[1]) == T_FUNC) { + if (nargs >= 2 && is_callable(args[1])) { ant_value_t reviver = args[1]; ant_value_t root = js_mkobj(js); js_set(js, root, "", result); result = apply_reviver(js, root, "", reviver); } + gc_disabled = saved_gc_disabled; return result; } @@ -424,6 +483,7 @@ static yyjson_write_flag get_write_flags(ant_value_t *args, int nargs) { ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { ant_value_t result; yyjson_mut_doc *doc = NULL; + bool saved_gc_disabled = gc_disabled; json_cycle_ctx ctx = { .js = js, @@ -435,10 +495,10 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { char *json_str = NULL; size_t len; + ant_value_t root_holder = js_mkundef(); if (nargs < 1) return js_mkerr(js, "JSON.stringify() requires at least 1 argument"); int top_type = vtype(args[0]); - if (top_type == T_UNDEF || top_type == T_FUNC || top_type == T_SYMBOL) return js_mkundef(); if (nargs < 2 && top_type == T_STR) { size_t byte_len = 0; @@ -453,10 +513,12 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { return result; } + + gc_disabled = true; if (nargs >= 2) { ant_value_t replacer = args[1]; - if (vtype(replacer) == T_FUNC) ctx.replacer_func = replacer; + if (is_callable(replacer)) ctx.replacer_func = replacer; else if (is_special_object(replacer)) { ant_value_t len_val = js_get(js, replacer, "length"); @@ -468,6 +530,13 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { doc = yyjson_mut_doc_new(NULL); if (!doc) return js_mkerr(js, "JSON.stringify() failed: out of memory"); + + root_holder = json_create_root_holder(js, args[0]); + if (is_err(root_holder)) { + result = root_holder; + goto cleanup; + } + ctx.holder = root_holder; yyjson_mut_val *root = ant_value_to_yyjson(js, doc, args[0], &ctx); @@ -498,6 +567,7 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { result = js_mkstr(js, json_str, len); cleanup: + gc_disabled = saved_gc_disabled; free(json_str); free(ctx.stack); yyjson_mut_doc_free(doc); diff --git a/src/modules/net.c b/src/modules/net.c index b773718..3679e8f 100644 --- a/src/modules/net.c +++ b/src/modules/net.c @@ -52,6 +52,7 @@ struct net_server_s { net_socket_t *sockets; struct net_server_s *next_active; char *host; + char *path; int port; int backlog; int max_connections; @@ -66,6 +67,7 @@ struct net_server_s { struct net_listen_args_s { const char *host; + const char *path; int port; int backlog; ant_value_t callback; @@ -251,9 +253,8 @@ static bool net_parse_listen_args(ant_t *js, ant_value_t *args, int nargs, net_l if (vtype(args[0]) == T_OBJ) { ant_value_t value = js_get(js, args[0], "path"); - if (vtype(value) != T_UNDEF) { - out->error = net_not_implemented(js, "IPC server listen"); - return false; + if (vtype(value) == T_STR) { + out->path = js_getstr(js, value, NULL); } value = js_get(js, args[0], "port"); @@ -263,15 +264,20 @@ static bool net_parse_listen_args(ant_t *js, ant_value_t *args, int nargs, net_l size_t len = 0; out->host = js_getstr(js, value, &len); } + value = js_get(js, args[0], "backlog"); if (vtype(value) == T_NUM) out->backlog = (int)js_getnum(value); if (nargs > 1 && is_callable(args[1])) out->callback = args[1]; + return true; } if (vtype(args[0]) == T_STR) { - out->error = net_not_implemented(js, "IPC server listen"); - return false; + out->path = js_getstr(js, args[0], NULL); + if (nargs > 1 && vtype(args[1]) == T_NUM) out->backlog = (int)js_getnum(args[1]); + if (nargs > 2 && is_callable(args[2])) out->callback = args[2]; + else if (nargs > 1 && is_callable(args[1])) out->callback = args[1]; + return true; } if (is_callable(args[0])) { @@ -760,21 +766,52 @@ static ant_value_t js_net_server_ctor(ant_t *js, ant_value_t *args, int nargs) { return server->obj; } +static ant_value_t net_server_bind_listener( + ant_t *js, + net_server_t *server, + const net_listen_args_t *parsed, + const ant_listener_callbacks_t *callbacks +) { + uv_loop_t *loop = uv_default_loop(); + int rc = 0; + + if (parsed->path) { + server->path = strdup(parsed->path); + if (!server->path) return js_mkerr_typed(js, JS_ERR_TYPE, "Out of memory"); + + rc = ant_listener_listen_pipe( + &server->listener, loop, server->path, + server->backlog, 0, callbacks, server + ); + } else { + const char *host = parsed->host; + net_server_parse_host(host, &host); + + free(server->host); + server->host = strdup(host ? host : "0.0.0.0"); + if (!server->host) return js_mkerr_typed(js, JS_ERR_TYPE, "Out of memory"); + + rc = ant_listener_listen_tcp( + &server->listener, loop, server->host, parsed->port, + server->backlog, 0, callbacks, server + ); + } + + if (rc != 0) return js_mkerr_typed(js, JS_ERR_TYPE, "%s", uv_strerror(rc)); + return js_mkundef(); +} + static ant_value_t js_net_server_listen(ant_t *js, ant_value_t *args, int nargs) { net_server_t *server = net_require_server(js, js_getthis(js)); ant_listener_callbacks_t callbacks = {0}; net_listen_args_t parsed; - const char *host = NULL; - int rc = 0; if (!server) return js->thrown_value; if (server->listening) return js_mkerr_typed(js, JS_ERR_TYPE, "Server is already listening"); if (!net_parse_listen_args(js, args, nargs, &parsed)) return parsed.error; - host = parsed.host; - net_server_parse_host(host, &host); - free(server->host); - server->host = strdup(host ? host : "0.0.0.0"); + free(server->path); + server->path = NULL; server->port = parsed.port; server->backlog = parsed.backlog > 0 ? parsed.backlog : server->backlog; @@ -786,17 +823,8 @@ static ant_value_t js_net_server_listen(ant_t *js, ant_value_t *args, int nargs) callbacks.on_conn_close = net_server_on_conn_close; callbacks.on_listener_close = net_server_on_listener_close; - rc = ant_listener_listen_tcp( - &server->listener, - uv_default_loop(), - server->host, - parsed.port, - server->backlog, - 0, - &callbacks, - server - ); - if (rc != 0) return js_mkerr_typed(js, JS_ERR_TYPE, "%s", uv_strerror(rc)); + ant_value_t bind_result = net_server_bind_listener(js, server, &parsed, &callbacks); + if (is_err(bind_result)) return bind_result; server->port = ant_listener_port(&server->listener); server->listening = true; @@ -813,7 +841,6 @@ static ant_value_t js_net_server_close(ant_t *js, ant_value_t *args, int nargs) net_server_t *server = net_require_server(js, js_getthis(js)); if (!server) return js->thrown_value; - if (nargs > 0 && is_callable(args[0])) net_add_listener(js, server->obj, "close", args[0], true); if (!server->listening && !server->closing) { @@ -833,8 +860,10 @@ static ant_value_t js_net_server_close(ant_t *js, ant_value_t *args, int nargs) static ant_value_t js_net_server_address(ant_t *js, ant_value_t *args, int nargs) { net_server_t *server = net_require_server(js, js_getthis(js)); ant_value_t out = js_mknull(); + if (!server) return js->thrown_value; if (!server || !server->listening) return out; + if (server->path) return js_mkstr(js, server->path, strlen(server->path)); out = js_mkobj(js); js_set(js, out, "port", js_mknum(server->port)); @@ -856,16 +885,14 @@ static ant_value_t js_net_server_getConnections(ant_t *js, ant_value_t *args, in static ant_value_t js_net_server_ref(ant_t *js, ant_value_t *args, int nargs) { net_server_t *server = net_require_server(js, js_getthis(js)); if (!server) return js->thrown_value; - if (server && !uv_is_closing((uv_handle_t *)&server->listener.handle)) - uv_ref((uv_handle_t *)&server->listener.handle); + if (server) ant_listener_ref(&server->listener); return js_getthis(js); } static ant_value_t js_net_server_unref(ant_t *js, ant_value_t *args, int nargs) { net_server_t *server = net_require_server(js, js_getthis(js)); if (!server) return js->thrown_value; - if (server && !uv_is_closing((uv_handle_t *)&server->listener.handle)) - uv_unref((uv_handle_t *)&server->listener.handle); + if (server) ant_listener_unref(&server->listener); return js_getthis(js); } @@ -948,7 +975,6 @@ ant_value_t net_library(ant_t *js) { ant_value_t lib = js_mkobj(js); net_init_constructors(js); - js_set(js, lib, "Server", g_net_server_ctor); js_set(js, lib, "Socket", g_net_socket_ctor); js_set(js, lib, "createServer", js_mkfun(js_net_createServer)); @@ -963,6 +989,7 @@ ant_value_t net_library(ant_t *js) { js_set(js, lib, "setDefaultAutoSelectFamilyAttemptTimeout", js_mkfun(js_net_setDefaultAutoSelectFamilyAttemptTimeout)); js_set(js, lib, "default", lib); js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "net", 3)); + return lib; } @@ -977,7 +1004,7 @@ void gc_mark_net(ant_t *js, gc_mark_fn mark) { for (server = g_active_servers; server; server = server->next_active) mark(js, server->obj); - + for (socket = g_active_sockets; socket; socket = socket->next_active) { mark(js, socket->obj); if (vtype(socket->encoding) != T_UNDEF) mark(js, socket->encoding); diff --git a/src/modules/path.c b/src/modules/path.c index c9ff84a..78efda4 100644 --- a/src/modules/path.c +++ b/src/modules/path.c @@ -1,5 +1,6 @@ #include // IWYU pragma: keep +#include #include #include #include @@ -23,141 +24,291 @@ #define PATH_DELIMITER ':' #endif -static char* normalize_separators(const char *path, size_t len) { +typedef enum { + PATH_STYLE_POSIX = 1, + PATH_STYLE_WIN32 = 2, +} path_style_t; + +static path_style_t path_host_style(void) { +#ifdef _WIN32 + return PATH_STYLE_WIN32; +#else + return PATH_STYLE_POSIX; +#endif +} + +static path_style_t path_current_style(ant_t *js) { + ant_value_t data = js_get_slot(js->current_func, SLOT_DATA); + if (vtype(data) == T_NUM) { + int style = (int)js_getnum(data); + if (style == PATH_STYLE_WIN32) return PATH_STYLE_WIN32; + if (style == PATH_STYLE_POSIX) return PATH_STYLE_POSIX; + } + return path_host_style(); +} + +static bool path_style_is_windows(path_style_t style) { + return style == PATH_STYLE_WIN32; +} + +static bool path_is_sep(path_style_t style, char ch) { + if (style == PATH_STYLE_WIN32) return ch == '\\' || ch == '/'; + return ch == '/'; +} + +static char path_sep_char(path_style_t style) { + return style == PATH_STYLE_WIN32 ? '\\' : '/'; +} + +static const char *path_sep_str(path_style_t style) { + return style == PATH_STYLE_WIN32 ? "\\" : "/"; +} + +static const char *path_delimiter_str(path_style_t style) { + return style == PATH_STYLE_WIN32 ? ";" : ":"; +} + +static bool path_has_drive_letter(const char *path, size_t len) { + return len >= 2 && isalpha((unsigned char)path[0]) && path[1] == ':'; +} + +static bool path_is_absolute_style(path_style_t style, const char *path, size_t len) { + if (!path || len == 0) return false; + if (style == PATH_STYLE_WIN32) { + if (path_is_sep(style, path[0])) return true; + return len >= 3 && path_has_drive_letter(path, len) && path_is_sep(style, path[2]); + } + return path[0] == '/'; +} + +static char *path_normalize_separators(path_style_t style, const char *path, size_t len) { char *result = malloc(len + 1); if (!result) return NULL; - + for (size_t i = 0; i < len; i++) { - if (path[i] == '\\' || path[i] == '/') { - result[i] = PATH_SEP; - } else { - result[i] = path[i]; - } + if (style == PATH_STYLE_WIN32 && (path[i] == '\\' || path[i] == '/')) + result[i] = '\\'; + else result[i] = path[i]; } + result[len] = '\0'; return result; } +static size_t path_root_length(path_style_t style, const char *path, size_t len) { + if (!path || len == 0) return 0; + if (style != PATH_STYLE_WIN32) return path[0] == '/' ? 1 : 0; + + if (len >= 2 && path_is_sep(style, path[0]) && path_is_sep(style, path[1])) { + size_t i = 2; + int parts = 0; + + while (i < len) { + while (i < len && path_is_sep(style, path[i])) i++; + + size_t start = i; + while (i < len && !path_is_sep(style, path[i])) i++; + + if (i == start) continue; + parts++; + + if (parts == 2) { + while (i < len && path_is_sep(style, path[i])) i++; + return i; + }} + + return 2; + } + + if (path_has_drive_letter(path, len)) + return (len >= 3 && path_is_sep(style, path[2])) ? 3 : 2; + + return path_is_sep(style, path[0]) ? 1 : 0; +} + +static bool path_is_drive_relative(path_style_t style, const char *path, size_t len) { + return style == PATH_STYLE_WIN32 && path_has_drive_letter(path, len) && !(len >= 3 && path_is_sep(style, path[2])); +} + +typedef struct { + char *from_norm; + char *to_norm; + char **from_segs; + char **to_segs; + size_t *from_lens; + size_t *to_lens; + int from_count; + int to_count; + size_t from_root_len; + size_t to_root_len; + int common; + char *result; + size_t result_cap; + size_t pos; +} path_relative_ctx_t; + +static size_t path_trimmed_end(path_style_t style, const char *path, size_t len) { + size_t root_len = path_root_length(style, path, len); + + while (len > root_len && path_is_sep(style, path[len - 1])) len--; + return len; +} + +static size_t path_basename_start(path_style_t style, const char *path, size_t len) { + size_t root_len = path_root_length(style, path, len); + size_t end = path_trimmed_end(style, path, len); + size_t start = end; + + while (start > root_len && !path_is_sep(style, path[start - 1])) start--; + return start; +} + +static ant_value_t path_make_string(ant_t *js, const char *src, size_t len) { + return js_mkstr(js, src, len); +} + static ant_value_t builtin_path_basename(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); if (nargs < 1) return js_mkerr(js, "basename() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "basename() path must be a string"); size_t path_len; char *path = js_getstr(js, args[0], &path_len); if (!path || path_len == 0) return js_mkstr(js, "", 0); - - char *path_copy = strndup(path, path_len); - if (!path_copy) return js_mkerr(js, "Out of memory"); - char *base = basename(path_copy); + + size_t end = path_trimmed_end(style, path, path_len); + size_t start = path_basename_start(style, path, path_len); + const char *base = path + start; + size_t base_len = end > start ? end - start : 0; if (nargs >= 2 && vtype(args[1]) == T_STR) { size_t ext_len; char *ext = js_getstr(js, args[1], &ext_len); - if (ext && ext_len > 0) { - size_t base_len = strlen(base); - if (base_len >= ext_len && strcmp(base + base_len - ext_len, ext) == 0) base[base_len - ext_len] = '\0'; + if (ext && ext_len > 0 && base_len >= ext_len) { + if (memcmp(base + base_len - ext_len, ext, ext_len) == 0) base_len -= ext_len; } } - ant_value_t result = js_mkstr(js, base, strlen(base)); - free(path_copy); - return result; + return path_make_string(js, base, base_len); } // path.dirname(path) static ant_value_t builtin_path_dirname(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); + if (nargs < 1) return js_mkerr(js, "dirname() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "dirname() path must be a string"); size_t path_len; char *path = js_getstr(js, args[0], &path_len); + if (!path || path_len == 0) return js_mkstr(js, ".", 1); + char *normalized = path_normalize_separators(style, path, path_len); - char *path_copy = strndup(path, path_len); - if (!path_copy) return js_mkerr(js, "Out of memory"); + size_t root_len = 0; + size_t end = 0; + size_t cut = 0; - char *dir = dirname(path_copy); - ant_value_t result = js_mkstr(js, dir, strlen(dir)); - free(path_copy); + if (!normalized) return js_mkerr(js, "Out of memory"); + root_len = path_root_length(style, normalized, path_len); + end = path_trimmed_end(style, normalized, path_len); + cut = end; + + while (cut > root_len && !path_is_sep(style, normalized[cut - 1])) cut--; + while (cut > root_len && path_is_sep(style, normalized[cut - 1])) cut--; + + if (cut == 0) { + free(normalized); + return js_mkstr(js, ".", 1); + } + + if (cut < root_len) cut = root_len; + + ant_value_t result = path_make_string(js, normalized, cut); + free(normalized); return result; } // path.extname(path) static ant_value_t builtin_path_extname(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); + if (nargs < 1) return js_mkerr(js, "extname() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "extname() path must be a string"); size_t path_len; char *path = js_getstr(js, args[0], &path_len); if (!path || path_len == 0) return js_mkstr(js, "", 0); - - char *path_copy = strndup(path, path_len); - if (!path_copy) return js_mkerr(js, "Out of memory"); - - char *base = basename(path_copy); - char *last_dot = strrchr(base, '.'); - - if (last_dot && last_dot != base) { - ant_value_t result = js_mkstr(js, last_dot, strlen(last_dot)); - free(path_copy); - return result; + + size_t start = path_basename_start(style, path, path_len); + size_t end = path_trimmed_end(style, path, path_len); + const char *base = path + start; + size_t base_len = end > start ? end - start : 0; + + for (size_t i = base_len; i > 0; i--) { + if (base[i - 1] != '.') continue; + if (i - 1 == 0) break; + return path_make_string(js, base + i - 1, base_len - (i - 1)); } - - free(path_copy); + return js_mkstr(js, "", 0); } -static int is_dotdot(char *seg, size_t len) { +static int is_dotdot(const char *seg, size_t len) { return len == 2 && seg[0] == '.' && seg[1] == '.'; } -static char* normalize_path_full(const char *path, size_t path_len) { +static char* normalize_path_full(path_style_t style, const char *path, size_t path_len) { char *normalized = NULL; char **segments = NULL; size_t *seg_lens = NULL; + char *result = NULL; + char sep = path_sep_char(style); + + size_t root_len = 0; + bool drive_relative = false; if (path_len == 0) return strdup("."); - normalized = normalize_separators(path, path_len); + normalized = path_normalize_separators(style, path, path_len); if (!normalized) goto fail; segments = malloc(path_len * sizeof(char*)); seg_lens = malloc(path_len * sizeof(size_t)); if (!segments || !seg_lens) goto fail; - int is_absolute = (normalized[0] == PATH_SEP); + root_len = path_root_length(style, normalized, path_len); + drive_relative = path_is_drive_relative(style, normalized, path_len); int seg_count = 0; - char *start = is_absolute ? normalized + 1 : normalized; + char *start = normalized + root_len; char *seg_start = start; for (char *p = start; ; p++) { - if (*p != PATH_SEP && *p != '\0') continue; + if (!path_is_sep(style, *p) && *p != '\0') continue; size_t len = p - seg_start; if (len == 0 || (len == 1 && seg_start[0] == '.')) goto next; if (is_dotdot(seg_start, len)) { - if (seg_count > 0 && !is_dotdot(segments[seg_count-1], seg_lens[seg_count-1])) - seg_count--; - else if (!is_absolute) - goto add_segment; + if (seg_count > 0 && !is_dotdot(segments[seg_count - 1], seg_lens[seg_count - 1])) seg_count--; + else if (root_len == 0 || drive_relative) goto add_segment; goto next; } -add_segment: + add_segment: segments[seg_count] = seg_start; seg_lens[seg_count] = len; seg_count++; -next: + next: if (*p == '\0') break; seg_start = p + 1; } - size_t result_len = is_absolute ? 1 : 0; + size_t result_len = root_len; for (int i = 0; i < seg_count; i++) { result_len += seg_lens[i]; if (i < seg_count - 1) result_len++; @@ -168,12 +319,16 @@ next: if (!result) goto fail; size_t pos = 0; - if (is_absolute) result[pos++] = PATH_SEP; + if (root_len > 0) { + memcpy(result + pos, normalized, root_len); + pos += root_len; + } for (int i = 0; i < seg_count; i++) { + if (pos > 0 && result[pos - 1] != sep && !(drive_relative && pos == root_len)) + result[pos++] = sep; memcpy(result + pos, segments[i], seg_lens[i]); pos += seg_lens[i]; - if (i < seg_count - 1) result[pos++] = PATH_SEP; } if (pos == 0) result[pos++] = '.'; @@ -193,6 +348,7 @@ fail: // path.normalize(path) static ant_value_t builtin_path_normalize(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); if (nargs < 1) return js_mkerr(js, "normalize() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "normalize() path must be a string"); @@ -200,7 +356,7 @@ static ant_value_t builtin_path_normalize(ant_t *js, ant_value_t *args, int narg char *path = js_getstr(js, args[0], &path_len); if (!path || path_len == 0) return js_mkstr(js, ".", 1); - char *result = normalize_path_full(path, path_len); + char *result = normalize_path_full(style, path, path_len); if (!result) return js_mkerr(js, "Out of memory"); ant_value_t ret = js_mkstr(js, result, strlen(result)); @@ -210,6 +366,8 @@ static ant_value_t builtin_path_normalize(ant_t *js, ant_value_t *args, int narg // path.join(...paths) static ant_value_t builtin_path_join(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); + char sep = path_sep_char(style); if (nargs < 1) return js_mkstr(js, ".", 1); size_t total_len = 0; @@ -225,14 +383,12 @@ static ant_value_t builtin_path_join(ant_t *js, ant_value_t *args, int nargs) { int valid_segments = 0; for (int i = 0; i < nargs; i++) { - if (vtype(args[i]) == T_STR) { - segments[valid_segments] = js_getstr(js, args[i], &lengths[valid_segments]); - if (segments[valid_segments] && lengths[valid_segments] > 0) { - total_len += lengths[valid_segments] + 1; // +1 for separator - valid_segments++; - } - } - } + if (vtype(args[i]) == T_STR) { + segments[valid_segments] = js_getstr(js, args[i], &lengths[valid_segments]); + if (segments[valid_segments] && lengths[valid_segments] > 0) { + total_len += lengths[valid_segments] + 1; // +1 for separator + valid_segments++; + }}} if (valid_segments == 0) { free(segments); @@ -249,15 +405,15 @@ static ant_value_t builtin_path_join(ant_t *js, ant_value_t *args, int nargs) { size_t pos = 0; for (int i = 0; i < valid_segments; i++) { - if (i > 0 && pos > 0 && result[pos - 1] != PATH_SEP) { - result[pos++] = PATH_SEP; + if (i > 0 && pos > 0 && result[pos - 1] != sep) { + result[pos++] = sep; } size_t start = 0; - if (i > 0 && segments[i][0] == PATH_SEP) start = 1; + if (i > 0 && path_is_sep(style, segments[i][0])) start = 1; size_t seg_len = lengths[i]; - if (seg_len > start && segments[i][seg_len - 1] == PATH_SEP) seg_len--; + while (seg_len > start && path_is_sep(style, segments[i][seg_len - 1])) seg_len--; if (seg_len > start) { memcpy(result + pos, segments[i] + start, seg_len - start); @@ -267,7 +423,7 @@ static ant_value_t builtin_path_join(ant_t *js, ant_value_t *args, int nargs) { result[pos] = '\0'; - char *normalized = normalize_path_full(result, pos); + char *normalized = normalize_path_full(style, result, pos); free(result); free(segments); free(lengths); @@ -281,47 +437,122 @@ static ant_value_t builtin_path_join(ant_t *js, ant_value_t *args, int nargs) { // path.resolve(...paths) static ant_value_t builtin_path_resolve(ant_t *js, ant_value_t *args, int nargs) { - char cwd[PATH_MAX]; - if (getcwd(cwd, sizeof(cwd)) == NULL) { - return js_mkerr(js, "Failed to get current working directory"); - } + path_style_t style = path_current_style(js); - char *result = strdup(cwd); - if (!result) return js_mkerr(js, "Out of memory"); + char cwd[PATH_MAX]; + char *cwd_norm = NULL; + char *joined = NULL; + char *normalized = NULL; + char drive_prefix[3] = {0}; - for (int i = 0; i < nargs; i++) { + bool saw_absolute = false; + char sep = path_sep_char(style); + + if (getcwd(cwd, sizeof(cwd)) == NULL) + return js_mkerr(js, "Failed to get current working directory"); + + cwd_norm = path_normalize_separators(style, cwd, strlen(cwd)); + if (!cwd_norm) return js_mkerr(js, "Out of memory"); + + for (int i = nargs - 1; i >= 0; i--) { + char *next = NULL; + size_t seg_len = 0; + size_t joined_len = 0; + size_t next_len = 0; + char *segment = NULL; + size_t prefix_len = 0; + if (vtype(args[i]) != T_STR) continue; - - size_t seg_len; - char *segment = js_getstr(js, args[i], &seg_len); + + segment = js_getstr(js, args[i], &seg_len); if (!segment || seg_len == 0) continue; - - if (segment[0] == PATH_SEP) { - free(result); - result = strndup(segment, seg_len); - if (!result) return js_mkerr(js, "Out of memory"); + + if (style == PATH_STYLE_WIN32 && path_is_drive_relative(style, segment, seg_len)) { + if (drive_prefix[0] == '\0') { + drive_prefix[0] = segment[0]; + drive_prefix[1] = ':'; + drive_prefix[2] = '\0'; + } + prefix_len = 2; + } + + if (!joined) { + joined = strndup(segment + prefix_len, seg_len - prefix_len); + if (!joined) { + free(cwd_norm); + return js_mkerr(js, "Out of memory"); + } } else { - size_t result_len = strlen(result); - size_t new_len = result_len + seg_len + 2; - char *new_result = malloc(new_len); - if (!new_result) { - free(result); + joined_len = strlen(joined); + next_len = (seg_len - prefix_len) + 1 + joined_len + 1; + next = malloc(next_len); + if (!next) { + free(cwd_norm); + free(joined); return js_mkerr(js, "Out of memory"); } - - snprintf(new_result, new_len, "%s%c%.*s", result, PATH_SEP, (int)seg_len, segment); - free(result); - result = new_result; + + snprintf(next, next_len, "%.*s%c%s", (int)(seg_len - prefix_len), segment + prefix_len, sep, joined); + free(joined); + joined = next; + } + + if (path_is_absolute_style(style, segment, seg_len)) { + saw_absolute = true; + break; } } - - ant_value_t ret = js_mkstr(js, result, strlen(result)); - free(result); + + if (!joined) { + if (style == PATH_STYLE_WIN32 && drive_prefix[0] != '\0') { + size_t cwd_len = strlen(cwd_norm); + joined = malloc(2 + cwd_len + 1); + if (!joined) { + free(cwd_norm); + return js_mkerr(js, "Out of memory"); + } + snprintf(joined, 2 + cwd_len + 1, "%s%s", drive_prefix, cwd_norm); + } else { + joined = strdup(cwd_norm); + if (!joined) { + free(cwd_norm); + return js_mkerr(js, "Out of memory"); + } + } + } else if (!saw_absolute) { + size_t cwd_len = strlen(cwd_norm); + size_t joined_len = strlen(joined); + size_t prefix_len = (style == PATH_STYLE_WIN32 && drive_prefix[0] != '\0') ? 2 : 0; + char *next = malloc(prefix_len + cwd_len + 1 + joined_len + 1); + if (!next) { + free(cwd_norm); + free(joined); + return js_mkerr(js, "Out of memory"); + } + + if (prefix_len > 0) + snprintf(next, prefix_len + cwd_len + 1 + joined_len + 1, "%s%s%c%s", drive_prefix, cwd_norm, sep, joined); + else snprintf(next, prefix_len + cwd_len + 1 + joined_len + 1, "%s%c%s", cwd_norm, sep, joined); + free(joined); + joined = next; + } + + normalized = normalize_path_full(style, joined, strlen(joined)); + free(cwd_norm); + free(joined); + if (!normalized) return js_mkerr(js, "Out of memory"); + + ant_value_t ret = js_mkstr(js, normalized, strlen(normalized)); + free(normalized); return ret; } // path.relative(from, to) static ant_value_t builtin_path_relative(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); + path_relative_ctx_t rel = {0}; + + char sep = path_sep_char(style); if (nargs < 2) return js_mkerr(js, "relative() requires from and to arguments"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "relative() from must be a string"); if (vtype(args[1]) != T_STR) return js_mkerr(js, "relative() to must be a string"); @@ -332,30 +563,121 @@ static ant_value_t builtin_path_relative(ant_t *js, ant_value_t *args, int nargs if (!from || !to) return js_mkerr(js, "Failed to get arguments"); if (from_len == to_len && strncmp(from, to, from_len) == 0) return js_mkstr(js, "", 0); - - return js_mkstr(js, to, to_len); + + rel.from_norm = normalize_path_full(style, from, from_len); + rel.to_norm = normalize_path_full(style, to, to_len); + if (!rel.from_norm || !rel.to_norm) goto relative_fail; + + rel.from_root_len = path_root_length(style, rel.from_norm, strlen(rel.from_norm)); + rel.to_root_len = path_root_length(style, rel.to_norm, strlen(rel.to_norm)); + + if (style == PATH_STYLE_WIN32) { + if (rel.from_root_len != rel.to_root_len || strncasecmp(rel.from_norm, rel.to_norm, rel.from_root_len) != 0) { + ant_value_t out = js_mkstr(js, rel.to_norm, strlen(rel.to_norm)); + free(rel.from_norm); + free(rel.to_norm); + return out; + } + } else if (rel.from_root_len != rel.to_root_len || strncmp(rel.from_norm, rel.to_norm, rel.from_root_len) != 0) { + ant_value_t out = js_mkstr(js, rel.to_norm, strlen(rel.to_norm)); + free(rel.from_norm); + free(rel.to_norm); + return out; + } + + rel.from_segs = malloc(strlen(rel.from_norm) * sizeof(char *)); + rel.to_segs = malloc(strlen(rel.to_norm) * sizeof(char *)); + rel.from_lens = malloc(strlen(rel.from_norm) * sizeof(size_t)); + rel.to_lens = malloc(strlen(rel.to_norm) * sizeof(size_t)); + if (!rel.from_segs || !rel.to_segs || !rel.from_lens || !rel.to_lens) goto relative_fail; + + for (size_t i = rel.from_root_len, start = rel.from_root_len;; i++) { + if (rel.from_norm[i] != '\0' && !path_is_sep(style, rel.from_norm[i])) continue; + if (i > start) { + rel.from_segs[rel.from_count] = rel.from_norm + start; + rel.from_lens[rel.from_count++] = i - start; + } + if (rel.from_norm[i] == '\0') break; + start = i + 1; + } + + for (size_t i = rel.to_root_len, start = rel.to_root_len;; i++) { + if (rel.to_norm[i] != '\0' && !path_is_sep(style, rel.to_norm[i])) continue; + if (i > start) { + rel.to_segs[rel.to_count] = rel.to_norm + start; + rel.to_lens[rel.to_count++] = i - start; + } + if (rel.to_norm[i] == '\0') break; + start = i + 1; + } + + while (rel.common < rel.from_count && rel.common < rel.to_count) { + bool equal = false; + if (rel.from_lens[rel.common] == rel.to_lens[rel.common]) { + equal = style == PATH_STYLE_WIN32 + ? strncasecmp(rel.from_segs[rel.common], rel.to_segs[rel.common], rel.from_lens[rel.common]) == 0 + : strncmp(rel.from_segs[rel.common], rel.to_segs[rel.common], rel.from_lens[rel.common]) == 0; + } + if (!equal) break; + rel.common++; + } + + rel.result_cap = strlen(rel.to_norm) + (size_t)(rel.from_count - rel.common) * 3 + 2; + rel.result = malloc(rel.result_cap); + if (!rel.result) goto relative_fail; + + for (int i = rel.common; i < rel.from_count; i++) { + if (rel.pos > 0) rel.result[rel.pos++] = sep; + rel.result[rel.pos++] = '.'; + rel.result[rel.pos++] = '.'; + } + + for (int i = rel.common; i < rel.to_count; i++) { + if (rel.pos > 0) rel.result[rel.pos++] = sep; + memcpy(rel.result + rel.pos, rel.to_segs[i], rel.to_lens[i]); + rel.pos += rel.to_lens[i]; + } + + if (rel.pos == 0) rel.result[rel.pos++] = '.'; + rel.result[rel.pos] = '\0'; + + ant_value_t out = js_mkstr(js, rel.result, rel.pos); + free(rel.result); + free(rel.from_norm); + free(rel.to_norm); + free(rel.from_segs); + free(rel.to_segs); + free(rel.from_lens); + free(rel.to_lens); + return out; + +relative_fail: + free(rel.result); + free(rel.from_norm); + free(rel.to_norm); + free(rel.from_segs); + free(rel.to_segs); + free(rel.from_lens); + free(rel.to_lens); + return js_mkerr(js, "Out of memory"); } // path.isAbsolute(path) static ant_value_t builtin_path_isAbsolute(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); if (nargs < 1) return js_mkerr(js, "isAbsolute() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "isAbsolute() path must be a string"); size_t path_len; char *path = js_getstr(js, args[0], &path_len); if (!path || path_len == 0) return js_false; - -#ifdef _WIN32 - if (path_len >= 2 && path[1] == ':') return js_true; - if (path_len >= 2 && path[0] == '\\' && path[1] == '\\') return js_true; - return js_false; -#else - return js_bool(path[0] == '/'); -#endif + + return js_bool(path_is_absolute_style(style, path, path_len)); } // path.parse(path) static ant_value_t builtin_path_parse(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); if (nargs < 1) return js_mkerr(js, "parse() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "parse() path must be a string"); @@ -364,40 +686,51 @@ static ant_value_t builtin_path_parse(ant_t *js, ant_value_t *args, int nargs) { if (!path) return js_mkerr(js, "Failed to get path string"); ant_value_t result = js_mkobj(js); - - if (path_len > 0 && path[0] == PATH_SEP) { - js_set(js, result, "root", js_mkstr(js, PATH_SEP_STR, 1)); - } else { - js_set(js, result, "root", js_mkstr(js, "", 0)); + char *normalized = path_normalize_separators(style, path, path_len); + size_t root_len = 0; + size_t start = 0; + size_t end = 0; + const char *base = NULL; + size_t base_len = 0; + if (!normalized) return js_mkerr(js, "Out of memory"); + + root_len = path_root_length(style, normalized, path_len); + js_set(js, result, "root", path_make_string(js, normalized, root_len)); + + end = path_trimmed_end(style, normalized, path_len); + start = path_basename_start(style, normalized, path_len); + base = normalized + start; + base_len = end > start ? end - start : 0; + + if (start == 0) js_set(js, result, "dir", js_mkstr(js, ".", 1)); + else { + size_t dir_len = start; + while (dir_len > root_len && path_is_sep(style, normalized[dir_len - 1])) dir_len--; + js_set(js, result, "dir", path_make_string(js, normalized, dir_len)); } - - char *path_copy = strndup(path, path_len); - if (!path_copy) return js_mkerr(js, "Out of memory"); - char *dir = dirname(path_copy); - js_set(js, result, "dir", js_mkstr(js, dir, strlen(dir))); - free(path_copy); - - path_copy = strndup(path, path_len); - if (!path_copy) return js_mkerr(js, "Out of memory"); - char *base = basename(path_copy); - js_set(js, result, "base", js_mkstr(js, base, strlen(base))); - - char *last_dot = strrchr(base, '.'); - if (last_dot && last_dot != base && *(last_dot + 1) != '\0') { - js_set(js, result, "ext", js_mkstr(js, last_dot, strlen(last_dot))); - size_t name_len = last_dot - base; - js_set(js, result, "name", js_mkstr(js, base, name_len)); - } else { - js_set(js, result, "ext", js_mkstr(js, "", 0)); - js_set(js, result, "name", js_mkstr(js, base, strlen(base))); + + js_set(js, result, "base", path_make_string(js, base, base_len)); + + for (size_t i = base_len; i > 0; i--) { + if (base[i - 1] != '.') continue; + if (i - 1 == 0) break; + js_set(js, result, "ext", path_make_string(js, base + i - 1, base_len - (i - 1))); + js_set(js, result, "name", path_make_string(js, base, i - 1)); + free(normalized); + return result; } - - free(path_copy); + + js_set(js, result, "ext", js_mkstr(js, "", 0)); + js_set(js, result, "name", path_make_string(js, base, base_len)); + free(normalized); return result; } // path.format(pathObject) static ant_value_t builtin_path_format(ant_t *js, ant_value_t *args, int nargs) { + path_style_t style = path_current_style(js); + + char sep = path_sep_char(style); if (nargs < 1) return js_mkerr(js, "format() requires a path object argument"); if (!is_special_object(args[0])) return js_mkerr(js, "format() argument must be an object"); @@ -418,8 +751,8 @@ static ant_value_t builtin_path_format(ant_t *js, ant_value_t *args, int nargs) if (str && len > 0 && pos + len < PATH_MAX) { memcpy(result + pos, str, len); pos += len; - if (result[pos - 1] != PATH_SEP && pos < PATH_MAX - 1) { - result[pos++] = PATH_SEP; + if (result[pos - 1] != sep && pos < PATH_MAX - 1) { + result[pos++] = sep; } } } else if (vtype(root_val) == T_STR) { @@ -460,23 +793,68 @@ static ant_value_t builtin_path_format(ant_t *js, ant_value_t *args, int nargs) return js_mkstr(js, result, pos); } +typedef struct { + ant_value_t basename; + ant_value_t dirname; + ant_value_t extname; + ant_value_t join; + ant_value_t normalize; + ant_value_t resolve; + ant_value_t relative; + ant_value_t isAbsolute; + ant_value_t parse; + ant_value_t format; +} path_api_t; + +static path_api_t path_build_api_for_style(ant_t *js, path_style_t style) { +ant_value_t style_value = js_mknum((double)style); +return (path_api_t){ + .basename = js_heavy_mkfun(js, builtin_path_basename, style_value), + .dirname = js_heavy_mkfun(js, builtin_path_dirname, style_value), + .extname = js_heavy_mkfun(js, builtin_path_extname, style_value), + .join = js_heavy_mkfun(js, builtin_path_join, style_value), + .normalize = js_heavy_mkfun(js, builtin_path_normalize, style_value), + .resolve = js_heavy_mkfun(js, builtin_path_resolve, style_value), + .relative = js_heavy_mkfun(js, builtin_path_relative, style_value), + .isAbsolute = js_heavy_mkfun(js, builtin_path_isAbsolute, style_value), + .parse = js_heavy_mkfun(js, builtin_path_parse, style_value), + .format = js_heavy_mkfun(js, builtin_path_format, style_value) +};} + +static void path_apply_api(ant_t *js, ant_value_t target, const path_api_t *api) { + js_set(js, target, "basename", api->basename); + js_set(js, target, "dirname", api->dirname); + js_set(js, target, "extname", api->extname); + js_set(js, target, "join", api->join); + js_set(js, target, "normalize", api->normalize); + js_set(js, target, "resolve", api->resolve); + js_set(js, target, "relative", api->relative); + js_set(js, target, "isAbsolute", api->isAbsolute); + js_set(js, target, "parse", api->parse); + js_set(js, target, "format", api->format); +} + +static ant_value_t path_make_variant(ant_t *js, const path_api_t *api, path_style_t style) { + ant_value_t variant = js_mkobj(js); + path_apply_api(js, variant, api); + + js_set(js, variant, "sep", js_mkstr(js, path_sep_str(style), 1)); + js_set(js, variant, "delimiter", js_mkstr(js, path_delimiter_str(style), 1)); + + return variant; +} + ant_value_t path_library(ant_t *js) { - ant_value_t lib = js_mkobj(js); - - js_set(js, lib, "basename", js_mkfun(builtin_path_basename)); - js_set(js, lib, "dirname", js_mkfun(builtin_path_dirname)); - js_set(js, lib, "extname", js_mkfun(builtin_path_extname)); - js_set(js, lib, "join", js_mkfun(builtin_path_join)); - js_set(js, lib, "normalize", js_mkfun(builtin_path_normalize)); - js_set(js, lib, "resolve", js_mkfun(builtin_path_resolve)); - js_set(js, lib, "relative", js_mkfun(builtin_path_relative)); - js_set(js, lib, "isAbsolute", js_mkfun(builtin_path_isAbsolute)); - js_set(js, lib, "parse", js_mkfun(builtin_path_parse)); - js_set(js, lib, "format", js_mkfun(builtin_path_format)); - - js_set(js, lib, "sep", js_mkstr(js, PATH_SEP_STR, 1)); - char delimiter_str[2] = {PATH_DELIMITER, '\0'}; - js_set(js, lib, "delimiter", js_mkstr(js, delimiter_str, 1)); + path_api_t api = path_build_api_for_style(js, path_host_style()); + path_api_t posix_api = path_build_api_for_style(js, PATH_STYLE_POSIX); + path_api_t win32_api = path_build_api_for_style(js, PATH_STYLE_WIN32); + + ant_value_t lib = path_make_variant(js, &api, path_host_style()); + ant_value_t posix = path_make_variant(js, &posix_api, PATH_STYLE_POSIX); + ant_value_t win32 = path_make_variant(js, &win32_api, PATH_STYLE_WIN32); + + js_set(js, lib, "posix", posix); + js_set(js, lib, "win32", win32); js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "path", 4)); return lib; diff --git a/src/modules/request.c b/src/modules/request.c index 143e13f..062f09a 100644 --- a/src/modules/request.c +++ b/src/modules/request.c @@ -46,8 +46,13 @@ ant_value_t request_get_signal(ant_t *js, ant_value_t obj) { if (vtype(signal) != T_UNDEF) return signal; signal = abort_signal_create_dependent(js, js_mkundef()); + if (is_err(signal)) return signal; + ant_value_t abort_reason = js_get_slot(obj, SLOT_REQUEST_ABORT_REASON); + + if (vtype(abort_reason) != T_UNDEF) signal_do_abort(js, signal, abort_reason); js_set_slot_wb(js, obj, SLOT_REQUEST_SIGNAL, signal); + return signal; } @@ -67,33 +72,14 @@ static void data_free(request_data_t *d) { free(d); } -static request_data_t *data_new(void) { - request_data_t *d = calloc(1, sizeof(request_data_t)); - if (!d) return NULL; - d->method = strdup("GET"); - d->referrer = strdup("client"); - d->referrer_policy = strdup(""); - d->mode = strdup("cors"); - d->credentials = strdup("same-origin"); - d->cache = strdup("default"); - d->redirect = strdup("follow"); - d->integrity = strdup(""); - if (!d->method || !d->referrer || !d->referrer_policy || - !d->mode || !d->credentials || !d->cache || !d->redirect || !d->integrity) { - data_free(d); - return NULL; - } - return d; -} - -static request_data_t *data_new_server(const char *method) { +static request_data_t *data_new_with(const char *method, const char *mode) { request_data_t *d = calloc(1, sizeof(request_data_t)); if (!d) return NULL; d->method = strdup(method ? method : "GET"); d->referrer = strdup("client"); d->referrer_policy = strdup(""); - d->mode = strdup("same-origin"); + d->mode = strdup(mode); d->credentials = strdup("same-origin"); d->cache = strdup("default"); d->redirect = strdup("follow"); @@ -112,6 +98,9 @@ static request_data_t *data_new_server(const char *method) { return d; } +static request_data_t *data_new(void) { return data_new_with("GET", "cors"); } +static request_data_t *data_new_server(const char *method) { return data_new_with(method, "same-origin"); } + static ant_value_t request_create_object(ant_t *js, request_data_t *req, ant_value_t headers_obj, bool create_signal) { ant_value_t obj = js_mkobj(js); ant_value_t hdrs = is_object_type(headers_obj) @@ -130,6 +119,8 @@ static ant_value_t request_create_object(ant_t *js, request_data_t *req, ant_val headers_apply_guard(hdrs); js_set_slot_wb(js, obj, SLOT_REQUEST_HEADERS, hdrs); + js_set_slot(obj, SLOT_REQUEST_ABORT_REASON, js_mkundef()); + js_set_slot_wb(js, obj, SLOT_REQUEST_SIGNAL, create_signal ? abort_signal_create_dependent(js, js_mkundef()) : js_mkundef() @@ -891,6 +882,7 @@ static ant_value_t js_request_clone(ant_t *js, ant_value_t *args, int nargs) { js_set_slot(obj, SLOT_DATA, ANT_PTR(nd)); js_set_slot_wb(js, obj, SLOT_REQUEST_HEADERS, new_headers); + js_set_slot(obj, SLOT_REQUEST_ABORT_REASON, js_mkundef()); js_set_slot_wb(js, obj, SLOT_REQUEST_SIGNAL, new_signal); ant_value_t src_stream = js_get_slot(this, SLOT_REQUEST_BODY_STREAM); @@ -928,7 +920,10 @@ static ant_value_t request_new_from_input( *out_src = NULL; *out_input_signal = js_mkundef(); - if (vtype(input) == T_OBJ) src = get_data(input); + if ( + vtype(input) == T_OBJ && + js_check_brand(input, BRAND_REQUEST) + ) src = get_data(input); if (!src) { size_t ulen = 0; @@ -1244,6 +1239,7 @@ static ant_value_t js_request_ctor(ant_t *js, ant_value_t *args, int nargs) { js_set_slot(obj, SLOT_BRAND, js_mknum(BRAND_REQUEST)); js_set_slot(obj, SLOT_DATA, ANT_PTR(req)); + js_set_slot(obj, SLOT_REQUEST_ABORT_REASON, js_mkundef()); signal = abort_signal_create_dependent(js, input_signal); if (is_err(signal)) { data_free(req); return signal; } @@ -1321,6 +1317,7 @@ ant_value_t request_create_from_input_init(ant_t *js, ant_value_t input, ant_val js_set_proto_init(obj, g_request_proto); js_set_slot(obj, SLOT_BRAND, js_mknum(BRAND_REQUEST)); js_set_slot(obj, SLOT_DATA, ANT_PTR(req)); + js_set_slot(obj, SLOT_REQUEST_ABORT_REASON, js_mkundef()); signal = abort_signal_create_dependent(js, input_signal); if (is_err(signal)) { data_free(req); return signal; } diff --git a/src/modules/server.c b/src/modules/server.c index 140dde3..295fb78 100644 --- a/src/modules/server.c +++ b/src/modules/server.c @@ -21,11 +21,14 @@ #include "http/http1_writer.h" #include "modules/assert.h" +#include "modules/abort.h" #include "modules/buffer.h" #include "modules/headers.h" #include "modules/request.h" #include "modules/response.h" #include "modules/server.h" +#include "modules/timer.h" +#include "modules/domexception.h" typedef struct server_runtime_s server_runtime_t; typedef struct server_request_s server_request_t; @@ -98,6 +101,8 @@ struct server_runtime_s { server_request_t *requests; char *hostname; + char *unix_path; + uint64_t request_timeout_ms; uint64_t idle_timeout_ms; @@ -287,6 +292,56 @@ static ant_value_t server_call_fetch(server_runtime_t *server, ant_value_t reque return result; } +static ant_value_t server_abort_signal_task(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t payload = js_get_slot(js->current_func, SLOT_DATA); + ant_value_t signal = 0; ant_value_t reason = 0; + if (vtype(payload) != T_OBJ) return js_mkundef(); + + signal = js_get(js, payload, "signal"); + reason = js_get(js, payload, "reason"); + + if (abort_signal_is_signal(signal) && !abort_signal_is_aborted(signal)) + signal_do_abort(js, signal, reason); + + return js_mkundef(); +} + +static void server_queue_abort_signal(ant_t *js, ant_value_t signal, ant_value_t reason) { + ant_value_t callback = 0; + ant_value_t payload = 0; + + if (!abort_signal_is_signal(signal) || abort_signal_is_aborted(signal)) return; + payload = js_mkobj(js); + if (is_err(payload)) return; + + js_set(js, payload, "signal", signal); + js_set(js, payload, "reason", reason); + callback = js_heavy_mkfun(js, server_abort_signal_task, payload); + + queue_microtask(js, callback); +} + +static void server_abort_request(server_request_t *req, const char *message) { + ant_t *js = NULL; + + ant_value_t signal = 0; ant_value_t reason = 0; ant_value_t abort_reason = 0; + if (!req || !req->server || !is_object_type(req->request_obj)) return; + + js = req->server->js; + abort_reason = js_get_slot(req->request_obj, SLOT_REQUEST_ABORT_REASON); + if (vtype(abort_reason) != T_UNDEF) return; + + reason = make_dom_exception(js, + message ? message : "The request was aborted", "AbortError" + ); + + js_set_slot_wb(js, req->request_obj, SLOT_REQUEST_ABORT_REASON, reason); + signal = js_get_slot(req->request_obj, SLOT_REQUEST_SIGNAL); + + if (!abort_signal_is_signal(signal) || abort_signal_is_aborted(signal)) return; + server_queue_abort_signal(js, signal, reason); +} + static bool server_response_chunk(server_request_t *req, ant_value_t value, const uint8_t **out, size_t *len) { ant_value_t slot = js_get_slot(value, SLOT_BUFFER); TypedArrayData *ta = (TypedArrayData *)js_gettypedarray(slot); @@ -853,6 +908,7 @@ static void server_on_conn_close(ant_conn_t *conn, void *user_data) { if (!uv_is_closing((uv_handle_t *)&cs->drain_timer)) uv_close((uv_handle_t *)&cs->drain_timer, server_on_drain_timer_close); if (cs->active_req) { + server_abort_request(cs->active_req, "Client disconnected"); cs->active_req->conn = NULL; cs->active_req = NULL; server_request_release(&cs->request); @@ -976,6 +1032,7 @@ ant_value_t server_start_from_export(ant_t *js, ant_value_t default_export) { .export_obj = default_export, .fetch_fn = js_get(js, default_export, "fetch"), .hostname = strdup("0.0.0.0"), + .unix_path = NULL, .port = 3000, .request_timeout_ms = 5000, .idle_timeout_ms = 5000, @@ -989,13 +1046,24 @@ ant_value_t server_start_from_export(ant_t *js, ant_value_t default_export) { unix_v = js_get(js, default_export, "unix"); tls_v = js_get(js, default_export, "tls"); + if (vtype(unix_v) != T_UNDEF && vtype(unix_v) != T_NULL) { - free(server->hostname); - free(server); - return js_mkerr_typed(js, JS_ERR_TYPE, "unix sockets are not implemented yet"); + if (vtype(unix_v) != T_STR) { + free(server->hostname); + free(server); + return js_mkerr_typed(js, JS_ERR_TYPE, "server unix must be a string"); + } + + server->unix_path = strdup(js_getstr(js, unix_v, NULL)); + if (!server->unix_path) { + free(server->hostname); + free(server); + return js_mkerr(js, "out of memory"); + } } if (vtype(tls_v) != T_UNDEF && vtype(tls_v) != T_NULL) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr_typed(js, JS_ERR_TYPE, "tls server config is not implemented yet"); @@ -1007,12 +1075,14 @@ ant_value_t server_start_from_export(ant_t *js, ant_value_t default_export) { if (vtype(port_v) != T_UNDEF && vtype(port_v) != T_NULL) { if (vtype(port_v) != T_NUM) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr_typed(js, JS_ERR_TYPE, "server port must be a number"); } server->port = (int)js_getnum(port_v); if (server->port < 0 || server->port > 65535) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr_typed(js, JS_ERR_RANGE, "server port must be between 0 and 65535"); @@ -1022,12 +1092,14 @@ ant_value_t server_start_from_export(ant_t *js, ant_value_t default_export) { if (vtype(hostname_v) != T_UNDEF && vtype(hostname_v) != T_NULL) { char *next_hostname = NULL; if (vtype(hostname_v) != T_STR) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr_typed(js, JS_ERR_TYPE, "server hostname must be a string"); } next_hostname = strdup(js_getstr(js, hostname_v, NULL)); if (!next_hostname) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr(js, "out of memory"); @@ -1039,12 +1111,14 @@ ant_value_t server_start_from_export(ant_t *js, ant_value_t default_export) { if (vtype(timeout_v) != T_UNDEF && vtype(timeout_v) != T_NULL) { double timeout = 0; if (vtype(timeout_v) != T_NUM) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr_typed(js, JS_ERR_TYPE, "server idleTimeout must be a number"); } timeout = js_getnum(timeout_v); if (timeout < 0) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr_typed(js, JS_ERR_RANGE, "server idleTimeout must be >= 0"); @@ -1064,13 +1138,22 @@ ant_value_t server_start_from_export(ant_t *js, ant_value_t default_export) { callbacks.on_conn_close = server_on_conn_close; callbacks.on_listener_close = server_on_listener_close; - rc = ant_listener_listen_tcp( - &server->listener, server->loop, - server->hostname, server->port, - 128, server->idle_timeout_ms, &callbacks, server - ); + if (server->unix_path) { + rc = ant_listener_listen_pipe( + &server->listener, server->loop, + server->unix_path, + 128, server->idle_timeout_ms, &callbacks, server + ); + } else { + rc = ant_listener_listen_tcp( + &server->listener, server->loop, + server->hostname, server->port, + 128, server->idle_timeout_ms, &callbacks, server + ); + } if (rc != 0) { + free(server->unix_path); free(server->hostname); free(server); return js_mkerr_typed(js, JS_ERR_TYPE, "%s", uv_strerror(rc)); diff --git a/src/modules/structured-clone.c b/src/modules/structured-clone.c index 980a84f..40a4d67 100644 --- a/src/modules/structured-clone.c +++ b/src/modules/structured-clone.c @@ -208,10 +208,15 @@ static ant_value_t sc_clone_rec(ant_t *js, ant_value_t val, sc_entry_t **seen, s map_entry_t *ne = ant_calloc(sizeof(map_entry_t)); if (!ne) return js_mkerr(js, "out of memory"); - ne->key = strdup(e->key); + + ne->key = (unsigned char *)strdup((const char *)e->key); ne->key_val = e->key_val; ne->value = vc; - HASH_ADD_STR(*new_head, key, ne); + + HASH_ADD_KEYPTR( + hh, *new_head, ne->key, + (unsigned)strlen((const char *)ne->key), ne + ); }} return clone; @@ -239,9 +244,14 @@ static ant_value_t sc_clone_rec(ant_t *js, ant_value_t val, sc_entry_t **seen, s set_entry_t *ne = ant_calloc(sizeof(set_entry_t)); if (!ne) return js_mkerr(js, "out of memory"); - ne->key = strdup(e->key); + + ne->key = (unsigned char *)strdup((const char *)e->key); ne->value = vc; - HASH_ADD_STR(*new_head, key, ne); + + HASH_ADD_KEYPTR( + hh, *new_head, ne->key, + (unsigned)strlen((const char *)ne->key), ne + ); }} return clone; diff --git a/src/modules/timer.c b/src/modules/timer.c index 93bc6ac..9a85634 100644 --- a/src/modules/timer.c +++ b/src/modules/timer.c @@ -12,6 +12,7 @@ #include "silver/engine.h" #include "gc/roots.h" #include "gc/modules.h" +#include "modules/abort.h" #include "modules/timer.h" #include "modules/symbol.h" @@ -285,6 +286,195 @@ static ant_value_t js_queue_microtask(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); } +static ant_value_t timers_promises_get_state(ant_t *js) { + return js_get_slot(js->current_func, SLOT_DATA); +} + +static ant_value_t timers_promises_abort_reason(ant_t *js, ant_value_t signal) { + ant_value_t reason = abort_signal_get_reason(signal); + if (vtype(reason) != T_UNDEF && vtype(reason) != T_NULL) return reason; + return js_mkerr_typed(js, JS_ERR_TYPE, "The operation was aborted"); +} + +static void timers_promises_remove_abort_listener(ant_t *js, ant_value_t state) { + ant_value_t signal = 0; + ant_value_t listener = 0; + + if (!is_object_type(state)) return; + + signal = js_get(js, state, "signal"); + listener = js_get(js, state, "abortListener"); + + if (abort_signal_is_signal(signal) && is_callable(listener)) + abort_signal_remove_listener(js, signal, listener); + + js_set(js, state, "abortListener", js_mkundef()); +} + +static void timers_promises_settle(ant_t *js, ant_value_t state, bool reject, ant_value_t value) { + ant_value_t settled = 0; + ant_value_t promise = 0; + + if (!is_object_type(state)) return; + + settled = js_get(js, state, "settled"); + if (js_truthy(js, settled)) return; + + js_set(js, state, "settled", js_true); + timers_promises_remove_abort_listener(js, state); + js_set(js, state, "handle", js_mkundef()); + + promise = js_get(js, state, "promise"); + if (reject) js_reject_promise(js, promise, value); + else js_resolve_promise(js, promise, value); +} + +static ant_value_t timers_promises_resolve(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = timers_promises_get_state(js); + ant_value_t value = js_mkundef(); + if (!is_object_type(state)) return js_mkundef(); + value = js_get(js, state, "value"); + timers_promises_settle(js, state, false, value); + return js_mkundef(); +} + +static ant_value_t timers_promises_on_abort(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = timers_promises_get_state(js); + ant_value_t signal = 0; + ant_value_t handle = 0; + ant_value_t is_immediate = 0; + ant_value_t reason = js_mkundef(); + ant_value_t clear_args[1]; + + if (!is_object_type(state)) return js_mkundef(); + + signal = js_get(js, state, "signal"); + handle = js_get(js, state, "handle"); + is_immediate = js_get(js, state, "isImmediate"); + + if (vtype(handle) != T_UNDEF && vtype(handle) != T_NULL) { + clear_args[0] = handle; + if (js_truthy(js, is_immediate)) js_clear_immediate(js, clear_args, 1); + else js_clear_timeout(js, clear_args, 1); + } + + if (abort_signal_is_signal(signal)) reason = timers_promises_abort_reason(js, signal); + else reason = js_mkerr_typed(js, JS_ERR_TYPE, "The operation was aborted"); + + timers_promises_settle(js, state, true, reason); + return js_mkundef(); +} + +static bool timers_promises_parse_options( + ant_t *js, + ant_value_t value, + ant_value_t *signal_out, + ant_value_t *error_out +) { + ant_value_t signal = js_mkundef(); + + if (signal_out) *signal_out = js_mkundef(); + if (error_out) *error_out = js_mkundef(); + + if (vtype(value) == T_UNDEF || vtype(value) == T_NULL) return true; + if (vtype(value) != T_OBJ) { + if (error_out) *error_out = js_mkerr_typed(js, JS_ERR_TYPE, "Timer options must be an object"); + return false; + } + + signal = js_get(js, value, "signal"); + if (vtype(signal) != T_UNDEF && vtype(signal) != T_NULL && !abort_signal_is_signal(signal)) { + if (error_out) *error_out = js_mkerr_typed(js, JS_ERR_TYPE, "options.signal must be an AbortSignal"); + return false; + } + + if (signal_out) *signal_out = signal; + return true; +} + +static ant_value_t timers_promises_schedule( + ant_t *js, + double delay_ms, + ant_value_t value, + ant_value_t signal, + bool is_immediate +) { + ant_value_t promise = js_mkpromise(js); + ant_value_t state = js_mkobj(js); + ant_value_t callback = 0; + ant_value_t handle = 0; + ant_value_t args[2]; + + if (abort_signal_is_signal(signal) && abort_signal_is_aborted(signal)) { + js_reject_promise(js, promise, timers_promises_abort_reason(js, signal)); + return promise; + } + + js_set(js, state, "promise", promise); + js_set(js, state, "value", value); + js_set(js, state, "signal", signal); + js_set(js, state, "abortListener", js_mkundef()); + js_set(js, state, "handle", js_mkundef()); + js_set(js, state, "settled", js_false); + js_set(js, state, "isImmediate", js_bool(is_immediate)); + + callback = js_heavy_mkfun(js, timers_promises_resolve, state); + if (is_immediate) handle = js_set_immediate(js, &callback, 1); + else { + args[0] = callback; + args[1] = js_mknum(delay_ms); + handle = js_set_timeout(js, args, 2); + } + + if (is_err(handle)) { + js_reject_promise(js, promise, handle); + return promise; + } + + js_set(js, state, "handle", handle); + + if (abort_signal_is_signal(signal)) { + ant_value_t listener = js_heavy_mkfun(js, timers_promises_on_abort, state); + js_set(js, state, "abortListener", listener); + abort_signal_add_listener(js, signal, listener); + } + + return promise; +} + +static ant_value_t js_timers_promises_setTimeout(ant_t *js, ant_value_t *args, int nargs) { + double delay_ms = nargs > 0 ? js_getnum(args[0]) : 0; + ant_value_t value = nargs > 1 ? args[1] : js_mkundef(); + ant_value_t options = nargs > 2 ? args[2] : js_mkundef(); + ant_value_t signal = js_mkundef(); + ant_value_t error = js_mkundef(); + + if (!timers_promises_parse_options(js, options, &signal, &error)) return error; + return timers_promises_schedule(js, delay_ms, value, signal, false); +} + +static ant_value_t js_timers_promises_setImmediate(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t value = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t options = nargs > 1 ? args[1] : js_mkundef(); + ant_value_t signal = js_mkundef(); + ant_value_t error = js_mkundef(); + + if (!timers_promises_parse_options(js, options, &signal, &error)) return error; + return timers_promises_schedule(js, 0, value, signal, true); +} + +static ant_value_t js_timers_promises_setInterval(ant_t *js, ant_value_t *args, int nargs) { + return js_mkerr_typed(js, JS_ERR_TYPE, "node:timers/promises setInterval() is not implemented yet"); +} + +static ant_value_t js_timers_promises_scheduler_wait(ant_t *js, ant_value_t *args, int nargs) { + return js_timers_promises_setTimeout(js, args, nargs); +} + +static ant_value_t js_timers_promises_scheduler_yield(ant_t *js, ant_value_t *args, int nargs) { + return js_timers_promises_setImmediate(js, args, nargs); +} + void queue_microtask(ant_t *js, ant_value_t callback) { microtask_entry_t *entry = ant_calloc(sizeof(microtask_entry_t)); if (entry == NULL) return; @@ -439,6 +629,22 @@ int has_pending_microtasks(void) { return timer_state.microtasks != NULL ? 1 : 0; } +// TODO: mostly stubbed +ant_value_t timers_promises_library(ant_t *js) { + ant_value_t lib = js_mkobj(js); + ant_value_t scheduler = js_mkobj(js); + + js_set(js, lib, "scheduler", scheduler); + js_set(js, lib, "setTimeout", js_mkfun(js_timers_promises_setTimeout)); + js_set(js, lib, "setImmediate", js_mkfun(js_timers_promises_setImmediate)); + js_set(js, lib, "setInterval", js_mkfun(js_timers_promises_setInterval)); + js_set(js, scheduler, "wait", js_mkfun(js_timers_promises_scheduler_wait)); + js_set(js, scheduler, "yield", js_mkfun(js_timers_promises_scheduler_yield)); + js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "timers/promises", 16)); + + return lib; +} + void init_timer_module() { ant_t *js = rt->js; diff --git a/src/modules/tls.c b/src/modules/tls.c new file mode 100644 index 0000000..b1ef220 --- /dev/null +++ b/src/modules/tls.c @@ -0,0 +1,272 @@ +#include // IWYU pragma: keep + +#include +#include +#include +#include + +#include +#include + +#include "ant.h" +#include "ptr.h" +#include "errors.h" + +#include "modules/tls.h" +#include "modules/buffer.h" + +typedef struct ant_tls_context_wrap_s { + ant_value_t obj; + tls_context *ctx; + tlsuv_private_key_t key; + tlsuv_certificate_t cert; + bool closed; +} ant_tls_context_wrap_t; + +enum { TLS_CONTEXT_NATIVE_TAG = 0x544c5343u }; +static ant_value_t g_tls_context_proto = 0; + +static void tls_context_free(ant_tls_context_wrap_t *wrap) { + if (!wrap || wrap->closed) return; + wrap->closed = true; + + if (wrap->cert && wrap->cert->free) wrap->cert->free(wrap->cert); + if (wrap->key && wrap->key->free) wrap->key->free(wrap->key); + if (wrap->ctx && wrap->ctx->free_ctx) wrap->ctx->free_ctx(wrap->ctx); + + wrap->cert = NULL; + wrap->key = NULL; + wrap->ctx = NULL; +} + +static ant_tls_context_wrap_t *tls_context_data(ant_value_t value) { + if (!js_check_native_tag(value, TLS_CONTEXT_NATIVE_TAG)) return NULL; + return (ant_tls_context_wrap_t *)js_get_native_ptr(value); +} + +static bool tls_value_bytes( + ant_t *js, + ant_value_t value, + const char **bytes_out, + size_t *len_out, + ant_value_t *error_out +) { + ant_value_t str_value = 0; + const uint8_t *buffer_bytes = NULL; + + if (error_out) *error_out = js_mkundef(); + if (bytes_out) *bytes_out = NULL; + if (len_out) *len_out = 0; + + if (vtype(value) == T_UNDEF || vtype(value) == T_NULL) return true; + if (buffer_source_get_bytes(js, value, &buffer_bytes, len_out)) { + if (bytes_out) *bytes_out = (const char *)buffer_bytes; + return true; + } + + str_value = js_tostring_val(js, value); + if (is_err(str_value)) { + if (error_out) *error_out = str_value; + return false; + } + + if (!bytes_out || !len_out) return true; + *bytes_out = js_getstr(js, str_value, len_out); + + if (!*bytes_out) { + if (error_out) *error_out = js_mkerr_typed(js, JS_ERR_TYPE, "Invalid TLS input"); + return false; + } + + return true; +} + +static ant_value_t tls_make_error(ant_t *js, tls_context *ctx, long code, const char *fallback) { + const char *message = fallback; + if (ctx && ctx->strerror) { + const char *detail = ctx->strerror(code); + if (detail && *detail) message = detail; + } + return js_mkerr_typed(js, JS_ERR_TYPE, "%s", message ? message : "TLS error"); +} + +static ant_value_t js_tls_context_close(ant_t *js, ant_value_t *args, int nargs) { + ant_tls_context_wrap_t *wrap = tls_context_data(js_getthis(js)); + if (!wrap) return js_getthis(js); + + tls_context_free(wrap); + js_set_native_ptr(wrap->obj, NULL); + js_set_native_tag(wrap->obj, 0); + return js_getthis(js); +} + +static void tls_init_context_proto(ant_t *js) { + if (g_tls_context_proto) return; + + g_tls_context_proto = js_mkobj(js); + js_set(js, g_tls_context_proto, "close", js_mkfun(js_tls_context_close)); +} + +static ant_value_t js_tls_create_context(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t options = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t obj = 0; + ant_value_t error = js_mkundef(); + + ant_tls_context_wrap_t *wrap = NULL; + ant_value_t ca_v = js_mkundef(); + ant_value_t key_v = js_mkundef(); + ant_value_t cert_v = js_mkundef(); + ant_value_t partial_v = js_mkundef(); + + const char *ca = NULL; + const char *key_data = NULL; + const char *cert_data = NULL; + + size_t ca_len = 0; + size_t key_len = 0; + size_t cert_len = 0; + int rc = 0; + + if (vtype(options) != T_UNDEF && vtype(options) != T_NULL && vtype(options) != T_OBJ) + return js_mkerr_typed(js, JS_ERR_TYPE, "TLS context options must be an object"); + + tls_init_context_proto(js); + + if (vtype(options) == T_OBJ) { + ca_v = js_get(js, options, "ca"); + key_v = js_get(js, options, "key"); + cert_v = js_get(js, options, "cert"); + partial_v = js_get(js, options, "allowPartialChain"); + } + + if (!tls_value_bytes(js, ca_v, &ca, &ca_len, &error)) return error; + if (!tls_value_bytes(js, key_v, &key_data, &key_len, &error)) return error; + if (!tls_value_bytes(js, cert_v, &cert_data, &cert_len, &error)) return error; + + wrap = calloc(1, sizeof(*wrap)); + if (!wrap) return js_mkerr_typed(js, JS_ERR_TYPE, "Out of memory"); + + wrap->ctx = default_tls_context(ca, ca_len); + if (!wrap->ctx) { + free(wrap); + return js_mkerr_typed(js, JS_ERR_TYPE, "Failed to create TLS context"); + } + + if (wrap->ctx->allow_partial_chain && js_truthy(js, partial_v)) { + rc = wrap->ctx->allow_partial_chain(wrap->ctx, 1); + if (rc != 0) { + ant_value_t err = tls_make_error(js, wrap->ctx, rc, "Failed to enable partial chain verification"); + tls_context_free(wrap); + free(wrap); + return err; + }} + + if (key_data) { + if (!wrap->ctx->load_key) { + tls_context_free(wrap); + free(wrap); + return js_mkerr_typed(js, JS_ERR_TYPE, "TLS engine does not support loading private keys"); + } + + rc = wrap->ctx->load_key(&wrap->key, key_data, key_len); + if (rc != 0) { + ant_value_t err = tls_make_error(js, wrap->ctx, rc, "Failed to load TLS private key"); + tls_context_free(wrap); + free(wrap); + return err; + } + + if (cert_data) { + if (!wrap->ctx->load_cert) { + tls_context_free(wrap); + free(wrap); + return js_mkerr_typed(js, JS_ERR_TYPE, "TLS engine does not support loading certificates"); + } + + rc = wrap->ctx->load_cert(&wrap->cert, cert_data, cert_len); + if (rc != 0) { + ant_value_t err = tls_make_error(js, wrap->ctx, rc, "Failed to load TLS certificate"); + tls_context_free(wrap); + free(wrap); + return err; + }} + + if (!wrap->ctx->set_own_cert) { + tls_context_free(wrap); + free(wrap); + return js_mkerr_typed(js, JS_ERR_TYPE, "TLS engine does not support own certificate configuration"); + } + + rc = wrap->ctx->set_own_cert(wrap->ctx, wrap->key, wrap->cert); + if (rc != 0) { + ant_value_t err = tls_make_error(js, wrap->ctx, rc, "Failed to configure TLS certificate"); + tls_context_free(wrap); + free(wrap); + return err; + } + } else if (cert_data) { + tls_context_free(wrap); + free(wrap); + return js_mkerr_typed(js, JS_ERR_TYPE, "TLS certificate requires a private key"); + } + + obj = js_mkobj(js); + js_set_proto_init(obj, g_tls_context_proto); + wrap->obj = obj; + + js_set_native_ptr(obj, wrap); + js_set_native_tag(obj, TLS_CONTEXT_NATIVE_TAG); + + return obj; +} + +static ant_value_t js_tls_is_context(ant_t *js, ant_value_t *args, int nargs) { + ant_tls_context_wrap_t *wrap = nargs > 0 ? tls_context_data(args[0]) : NULL; + return js_bool(wrap && !wrap->closed && wrap->ctx); +} + +static ant_value_t js_tls_set_config_path(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t str_value = js_mkundef(); + const char *path = NULL; + int rc = 0; + + if (nargs < 1 || vtype(args[0]) == T_UNDEF || vtype(args[0]) == T_NULL) { + rc = tlsuv_set_config_path(NULL); + if (rc != 0) return js_mkerr_typed(js, JS_ERR_TYPE, "%s", uv_strerror(rc)); + return js_mkundef(); + } + + str_value = js_tostring_val(js, args[0]); + if (is_err(str_value)) return str_value; + + path = js_getstr(js, str_value, NULL); + if (!path) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid TLS config path"); + + rc = tlsuv_set_config_path(path); + if (rc != 0) return js_mkerr_typed(js, JS_ERR_TYPE, "%s", uv_strerror(rc)); + + return js_mkundef(); +} + +ant_value_t internal_tls_library(ant_t *js) { + ant_value_t lib = js_mkobj(js); + ant_value_t version = js_mkstr(js, "unknown", 7); + + tls_context *ctx = default_tls_context(NULL, 0); + const char *version_str = NULL; + + if (ctx && ctx->version) { + version_str = ctx->version(); + if (version_str) version = js_mkstr(js, version_str, strlen(version_str)); + } + + if (ctx && ctx->free_ctx) ctx->free_ctx(ctx); + tls_init_context_proto(js); + + js_set(js, lib, "version", version); + js_set(js, lib, "createContext", js_mkfun(js_tls_create_context)); + js_set(js, lib, "isContext", js_mkfun(js_tls_is_context)); + js_set(js, lib, "setConfigPath", js_mkfun(js_tls_set_config_path)); + + return lib; +} diff --git a/src/modules/uri.c b/src/modules/uri.c index 1630e2f..44bdb55 100644 --- a/src/modules/uri.c +++ b/src/modules/uri.c @@ -74,7 +74,7 @@ static int decode_escape_sequence(const char *str, size_t len, size_t *pos, unsi } // encodeURIComponent() -static ant_value_t js_encodeURIComponent(ant_t *js, ant_value_t *args, int nargs) { +ant_value_t js_encodeURIComponent(ant_t *js, ant_value_t *args, int nargs) { ant_value_t result; char *out = NULL; @@ -127,7 +127,7 @@ malformed: } // encodeURI() -static ant_value_t js_encodeURI(ant_t *js, ant_value_t *args, int nargs) { +ant_value_t js_encodeURI(ant_t *js, ant_value_t *args, int nargs) { ant_value_t result; char *out = NULL; @@ -180,7 +180,7 @@ malformed: } // decodeURIComponent() -static ant_value_t js_decodeURIComponent(ant_t *js, ant_value_t *args, int nargs) { +ant_value_t js_decodeURIComponent(ant_t *js, ant_value_t *args, int nargs) { ant_value_t result; char *out = NULL; @@ -229,7 +229,7 @@ malformed: } // decodeURI() -static ant_value_t js_decodeURI(ant_t *js, ant_value_t *args, int nargs) { +ant_value_t js_decodeURI(ant_t *js, ant_value_t *args, int nargs) { ant_value_t result; char *out = NULL; diff --git a/src/net/connection.c b/src/net/connection.c index 737fec6..743fc1c 100644 --- a/src/net/connection.c +++ b/src/net/connection.c @@ -70,8 +70,9 @@ static bool ant_conn_store_addr( } static bool ant_conn_store_peer_addr(ant_conn_t *conn) { + if (!conn || conn->kind != ANT_CONN_KIND_TCP) return false; return ant_conn_store_addr( - &conn->handle, + &conn->handle.tcp, (int (*)(const uv_tcp_t *, struct sockaddr *, int *))uv_tcp_getpeername, conn->remote_addr, sizeof(conn->remote_addr), @@ -83,8 +84,9 @@ static bool ant_conn_store_peer_addr(ant_conn_t *conn) { } static bool ant_conn_store_local_addr(ant_conn_t *conn) { + if (!conn || conn->kind != ANT_CONN_KIND_TCP) return false; return ant_conn_store_addr( - &conn->handle, + &conn->handle.tcp, (int (*)(const uv_tcp_t *, struct sockaddr *, int *))uv_tcp_getsockname, conn->local_addr, sizeof(conn->local_addr), @@ -116,7 +118,6 @@ static void ant_conn_restart_timer(ant_conn_t *conn) { static void ant_conn_alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { ant_conn_t *conn = (ant_conn_t *)handle->data; - size_t need = 0; size_t next_cap = 0; char *next = NULL; @@ -126,10 +127,14 @@ static void ant_conn_alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf return; } - need = conn->buffer_len + suggested_size; - if (need > conn->buffer_cap) { + if (conn->buffer_len + suggested_size > conn->buffer_cap - conn->buffer_offset) { + if (conn->buffer_offset > 0 && conn->buffer_len > 0) memmove(conn->buffer, conn->buffer + conn->buffer_offset, conn->buffer_len); + else if (conn->buffer_offset > 0) conn->buffer_len = 0; + conn->buffer_offset = 0; + + if (conn->buffer_len + suggested_size > conn->buffer_cap) { next_cap = conn->buffer_cap ? conn->buffer_cap * 2 : ANT_CONN_READ_BUFFER_SIZE; - while (next_cap < need) next_cap *= 2; + while (next_cap < conn->buffer_len + suggested_size) next_cap *= 2; next = realloc(conn->buffer, next_cap); if (!next) { buf->base = NULL; @@ -138,10 +143,10 @@ static void ant_conn_alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf } conn->buffer = next; conn->buffer_cap = next_cap; - } + }} - buf->base = conn->buffer + conn->buffer_len; - buf->len = conn->buffer_cap - conn->buffer_len; + buf->base = conn->buffer + conn->buffer_offset + conn->buffer_len; + buf->len = conn->buffer_cap - conn->buffer_offset - conn->buffer_len; } static void ant_conn_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { @@ -152,7 +157,7 @@ static void ant_conn_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t if (nread < 0) { if (nread == UV_EOF) { conn->read_eof = true; - uv_read_stop((uv_stream_t *)&conn->handle); + uv_read_stop(ant_conn_stream(conn)); if (listener->callbacks.on_end) listener->callbacks.on_end(conn, listener->user_data); return; @@ -215,32 +220,66 @@ ant_conn_t *ant_conn_create_tcp(ant_listener_t *listener, uint64_t timeout_ms) { conn = calloc(1, sizeof(*conn)); if (!conn) return NULL; + conn->kind = ANT_CONN_KIND_TCP; conn->listener = listener; conn->timeout_ms = timeout_ms; conn->buffer_cap = ANT_CONN_READ_BUFFER_SIZE; conn->buffer = malloc(conn->buffer_cap); + if (!conn->buffer) { free(conn); return NULL; } - uv_tcp_init(listener->loop, &conn->handle); + uv_tcp_init(listener->loop, &conn->handle.tcp); uv_timer_init(listener->loop, &conn->timer); - conn->handle.data = conn; + conn->handle.tcp.data = conn; conn->timer.data = conn; + + return conn; +} + +ant_conn_t *ant_conn_create_pipe(ant_listener_t *listener, uint64_t timeout_ms) { + ant_conn_t *conn = NULL; + + if (!listener || !listener->loop) return NULL; + + conn = calloc(1, sizeof(*conn)); + if (!conn) return NULL; + + conn->kind = ANT_CONN_KIND_PIPE; + conn->listener = listener; + conn->timeout_ms = timeout_ms; + conn->buffer_cap = ANT_CONN_READ_BUFFER_SIZE; + conn->buffer = malloc(conn->buffer_cap); + + if (!conn->buffer) { + free(conn); + return NULL; + } + + uv_pipe_init(listener->loop, &conn->handle.pipe, 0); + uv_timer_init(listener->loop, &conn->timer); + conn->handle.pipe.data = conn; + conn->timer.data = conn; + return conn; } int ant_conn_accept(ant_conn_t *conn, uv_stream_t *server_stream) { if (!conn || !server_stream) return UV_EINVAL; - if (uv_accept(server_stream, (uv_stream_t *)&conn->handle) != 0) + if (uv_accept(server_stream, ant_conn_stream(conn)) != 0) return UV_ECONNABORTED; - ant_conn_store_peer_addr(conn); - ant_conn_store_local_addr(conn); + if (conn->kind == ANT_CONN_KIND_TCP) { + ant_conn_store_peer_addr(conn); + ant_conn_store_local_addr(conn); + } + conn->next = conn->listener->connections; conn->listener->connections = conn; + return 0; } @@ -248,7 +287,7 @@ void ant_conn_start(ant_conn_t *conn) { if (!conn || conn->closing) return; conn->read_paused = false; ant_conn_restart_timer(conn); - uv_read_start((uv_stream_t *)&conn->handle, ant_conn_alloc_cb, ant_conn_read_cb); + uv_read_start(ant_conn_stream(conn), ant_conn_alloc_cb, ant_conn_read_cb); } void ant_conn_set_user_data(ant_conn_t *conn, void *user_data) { @@ -264,7 +303,7 @@ ant_listener_t *ant_conn_listener(const ant_conn_t *conn) { } const char *ant_conn_buffer(const ant_conn_t *conn) { - return conn ? conn->buffer : NULL; + return conn ? conn->buffer + conn->buffer_offset : NULL; } size_t ant_conn_buffer_len(const ant_conn_t *conn) { @@ -320,14 +359,14 @@ int ant_conn_remote_port(const ant_conn_t *conn) { void ant_conn_pause_read(ant_conn_t *conn) { if (!conn || conn->closing) return; conn->read_paused = true; - uv_read_stop((uv_stream_t *)&conn->handle); + uv_read_stop(ant_conn_stream(conn)); } void ant_conn_resume_read(ant_conn_t *conn) { if (!conn || conn->closing || conn->read_eof || !conn->read_paused) return; conn->read_paused = false; ant_conn_restart_timer(conn); - uv_read_start((uv_stream_t *)&conn->handle, ant_conn_alloc_cb, ant_conn_read_cb); + uv_read_start(ant_conn_stream(conn), ant_conn_alloc_cb, ant_conn_read_cb); } void ant_conn_shutdown(ant_conn_t *conn) { @@ -342,7 +381,7 @@ void ant_conn_shutdown(ant_conn_t *conn) { return; } - rc = uv_shutdown(&req->req, (uv_stream_t *)&conn->handle, ant_conn_shutdown_cb); + rc = uv_shutdown(&req->req, ant_conn_stream(conn), ant_conn_shutdown_cb); if (rc != 0) { free(req); ant_conn_close(conn); @@ -351,34 +390,37 @@ void ant_conn_shutdown(ant_conn_t *conn) { void ant_conn_ref(ant_conn_t *conn) { if (!conn) return; - if (!uv_is_closing((uv_handle_t *)&conn->handle)) uv_ref((uv_handle_t *)&conn->handle); + if (!uv_is_closing((uv_handle_t *)ant_conn_stream(conn))) uv_ref((uv_handle_t *)ant_conn_stream(conn)); if (!uv_is_closing((uv_handle_t *)&conn->timer)) uv_ref((uv_handle_t *)&conn->timer); } void ant_conn_unref(ant_conn_t *conn) { if (!conn) return; - if (!uv_is_closing((uv_handle_t *)&conn->handle)) uv_unref((uv_handle_t *)&conn->handle); + if (!uv_is_closing((uv_handle_t *)ant_conn_stream(conn))) uv_unref((uv_handle_t *)ant_conn_stream(conn)); if (!uv_is_closing((uv_handle_t *)&conn->timer)) uv_unref((uv_handle_t *)&conn->timer); } int ant_conn_set_no_delay(ant_conn_t *conn, bool enable) { if (!conn || conn->closing) return UV_EINVAL; - return uv_tcp_nodelay(&conn->handle, enable ? 1 : 0); + if (conn->kind != ANT_CONN_KIND_TCP) return UV_ENOTSUP; + return uv_tcp_nodelay(&conn->handle.tcp, enable ? 1 : 0); } int ant_conn_set_keep_alive(ant_conn_t *conn, bool enable, unsigned int delay_secs) { if (!conn || conn->closing) return UV_EINVAL; - return uv_tcp_keepalive(&conn->handle, enable ? 1 : 0, delay_secs); + if (conn->kind != ANT_CONN_KIND_TCP) return UV_ENOTSUP; + return uv_tcp_keepalive(&conn->handle.tcp, enable ? 1 : 0, delay_secs); } void ant_conn_consume(ant_conn_t *conn, size_t len) { if (!conn || conn->buffer_len == 0 || len == 0) return; if (len >= conn->buffer_len) { + conn->buffer_offset = 0; conn->buffer_len = 0; return; } - memmove(conn->buffer, conn->buffer + len, conn->buffer_len - len); + conn->buffer_offset += len; conn->buffer_len -= len; } @@ -393,12 +435,12 @@ void ant_conn_close(ant_conn_t *conn) { conn->close_handles++; } - if (!uv_is_closing((uv_handle_t *)&conn->handle)) { - uv_close((uv_handle_t *)&conn->handle, ant_conn_close_cb); + if (!uv_is_closing((uv_handle_t *)ant_conn_stream(conn))) { + uv_close((uv_handle_t *)ant_conn_stream(conn), ant_conn_close_cb); conn->close_handles++; } - if (conn->close_handles == 0) ant_conn_close_cb((uv_handle_t *)&conn->handle); + if (conn->close_handles == 0) ant_conn_close_cb((uv_handle_t *)ant_conn_stream(conn)); } int ant_conn_write(ant_conn_t *conn, char *data, size_t len, ant_conn_write_cb cb, void *user_data) { @@ -422,7 +464,7 @@ int ant_conn_write(ant_conn_t *conn, char *data, size_t len, ant_conn_write_cb c wr->user_data = user_data; ant_conn_restart_timer(conn); - rc = uv_write(&wr->req, (uv_stream_t *)&conn->handle, &wr->buf, 1, ant_conn_write_cb_impl); + rc = uv_write(&wr->req, ant_conn_stream(conn), &wr->buf, 1, ant_conn_write_cb_impl); if (rc != 0) { free(wr->buf.base); free(wr); diff --git a/src/net/listener.c b/src/net/listener.c index 9aa6d4d..b141e59 100644 --- a/src/net/listener.c +++ b/src/net/listener.c @@ -3,6 +3,7 @@ #ifndef _WIN32 #include #include +#include #endif #include #include @@ -13,9 +14,18 @@ static void ant_listener_close_cb(uv_handle_t *handle) { ant_listener_t *listener = (ant_listener_t *)handle->data; if (!listener) return; + +#ifndef _WIN32 + if (listener->kind == ANT_LISTENER_KIND_PIPE && listener->path && listener->path[0] != '\0') + unlink(listener->path); +#endif + listener->closed = true; if (listener->callbacks.on_listener_close) listener->callbacks.on_listener_close(listener, listener->user_data); + + free(listener->path); + listener->path = NULL; } static void ant_listener_accept_cb(uv_stream_t *server_stream, int status) { @@ -24,7 +34,9 @@ static void ant_listener_accept_cb(uv_stream_t *server_stream, int status) { if (status < 0 || !listener || listener->closing) return; - conn = ant_conn_create_tcp(listener, listener->idle_timeout_ms); + conn = listener->kind == ANT_LISTENER_KIND_PIPE + ? ant_conn_create_pipe(listener, listener->idle_timeout_ms) + : ant_conn_create_tcp(listener, listener->idle_timeout_ms); if (!conn) return; if (ant_conn_accept(conn, server_stream) != 0) { @@ -60,27 +72,73 @@ int ant_listener_listen_tcp( listener->idle_timeout_ms = idle_timeout_ms; listener->port = port; listener->backlog = backlog > 0 ? backlog : 128; + listener->kind = ANT_LISTENER_KIND_TCP; if (callbacks) listener->callbacks = *callbacks; - uv_tcp_init(loop, &listener->handle); - listener->handle.data = listener; + uv_tcp_init(loop, &listener->handle.tcp); + listener->handle.tcp.data = listener; rc = uv_ip4_addr(hostname, port, &addr); if (rc != 0) return rc; - rc = uv_tcp_bind(&listener->handle, (const struct sockaddr *)&addr, 0); + rc = uv_tcp_bind(&listener->handle.tcp, (const struct sockaddr *)&addr, 0); if (rc != 0) return rc; - rc = uv_listen((uv_stream_t *)&listener->handle, listener->backlog, ant_listener_accept_cb); + rc = uv_listen((uv_stream_t *)&listener->handle.tcp, listener->backlog, ant_listener_accept_cb); if (rc != 0) return rc; listener->started = true; - if (port == 0 && uv_tcp_getsockname(&listener->handle, (struct sockaddr *)&addr, &addr_len) == 0) + if (port == 0 && uv_tcp_getsockname(&listener->handle.tcp, (struct sockaddr *)&addr, &addr_len) == 0) listener->port = ntohs(addr.sin_port); return 0; } +int ant_listener_listen_pipe( + ant_listener_t *listener, + uv_loop_t *loop, + const char *path, + int backlog, + uint64_t idle_timeout_ms, + const ant_listener_callbacks_t *callbacks, + void *user_data +) { + int rc = 0; + + if (!listener || !loop || !path || !*path) return UV_EINVAL; + memset(listener, 0, sizeof(*listener)); + + listener->loop = loop; + listener->user_data = user_data; + listener->idle_timeout_ms = idle_timeout_ms; + listener->backlog = backlog > 0 ? backlog : 128; + listener->kind = ANT_LISTENER_KIND_PIPE; + listener->path = strdup(path); + if (callbacks) listener->callbacks = *callbacks; + + if (!listener->path) return UV_ENOMEM; + + uv_pipe_init(loop, &listener->handle.pipe, 0); + listener->handle.pipe.data = listener; + + rc = uv_pipe_bind(&listener->handle.pipe, path); + if (rc != 0) { + free(listener->path); + listener->path = NULL; + return rc; + } + + rc = uv_listen((uv_stream_t *)&listener->handle.pipe, listener->backlog, ant_listener_accept_cb); + if (rc != 0) { + free(listener->path); + listener->path = NULL; + return rc; + } + + listener->started = true; + return 0; +} + void ant_listener_stop(ant_listener_t *listener, bool force) { ant_conn_t *conn = NULL; ant_conn_t *next = NULL; @@ -88,8 +146,8 @@ void ant_listener_stop(ant_listener_t *listener, bool force) { if (!listener) return; listener->closing = true; - if (listener->started && !uv_is_closing((uv_handle_t *)&listener->handle)) - uv_close((uv_handle_t *)&listener->handle, ant_listener_close_cb); + if (listener->started && !uv_is_closing((uv_handle_t *)ant_listener_stream(listener))) + uv_close((uv_handle_t *)ant_listener_stream(listener), ant_listener_close_cb); else if (!listener->started) listener->closed = true; if (force) { @@ -114,3 +172,19 @@ int ant_listener_port(const ant_listener_t *listener) { void *ant_listener_get_user_data(const ant_listener_t *listener) { return listener ? listener->user_data : NULL; } + +void ant_listener_ref(ant_listener_t *listener) { + if (!listener) return; + if (!listener->started) return; + if (!uv_is_closing((uv_handle_t *)ant_listener_stream(listener))) uv_ref((uv_handle_t *)ant_listener_stream(listener)); +} + +void ant_listener_unref(ant_listener_t *listener) { + if (!listener) return; + if (!listener->started) return; + if (!uv_is_closing((uv_handle_t *)ant_listener_stream(listener))) uv_unref((uv_handle_t *)ant_listener_stream(listener)); +} + +const char *ant_listener_path(const ant_listener_t *listener) { + return listener ? listener->path : NULL; +} diff --git a/src/silver/compiler.c b/src/silver/compiler.c index af7c425..260b23b 100644 --- a/src/silver/compiler.c +++ b/src/silver/compiler.c @@ -51,6 +51,7 @@ typedef struct { typedef struct sv_compiler { ant_t *js; + const char *filename; const char *source; ant_offset_t source_len; @@ -3906,6 +3907,7 @@ static void compile_class(sv_compiler_t *c, sv_ast_t *node) { c->computed_key_locals = computed_key_locals; sv_compiler_t comp = {0}; comp.js = c->js; + comp.filename = c->filename; comp.source = c->source; comp.source_len = c->source_len; comp.enclosing = c; @@ -4056,6 +4058,7 @@ static sv_func_t *compile_function_body( ) { sv_compiler_t comp = {0}; comp.js = enclosing->js; + comp.filename = enclosing->filename; comp.source = enclosing->source; comp.source_len = enclosing->source_len; comp.enclosing = enclosing; @@ -4441,7 +4444,7 @@ static sv_func_t *compile_function_body( func->is_generator = !!(node->flags & FN_GENERATOR); func->is_method = !!(node->flags & FN_METHOD); func->is_tla = comp.is_tla; - func->filename = enclosing->js->filename; + func->filename = enclosing->filename ? enclosing->filename : enclosing->js->filename; func->source_line = (int)node->line; if (node->str && node->len > 0) { char *name = code_arena_bump(node->len + 1); @@ -4696,6 +4699,7 @@ sv_func_t *sv_compile(ant_t *js, sv_ast_t *program, sv_compile_mode_t mode, cons sv_compiler_t root = {0}; root.js = js; + root.filename = js->filename; root.source = pin_source_text(source, source_len); root.source_len = source_len; root.mode = mode; @@ -4736,6 +4740,7 @@ sv_func_t *sv_compile_function(ant_t *js, const char *source, size_t len, bool i sv_compiler_t root = {0}; root.js = js; + root.filename = js->filename; root.source = pin_source_text(wrapped, (ant_offset_t)wrapped_len); root.source_len = (ant_offset_t)wrapped_len; root.mode = SV_COMPILE_SCRIPT; @@ -4803,6 +4808,7 @@ sv_func_t *sv_compile_function_with_params( sv_compiler_t root = {0}; root.js = js; + root.filename = js->filename; root.source = pin_source_text(body, (ant_offset_t)body_len); root.source_len = (ant_offset_t)body_len; root.mode = SV_COMPILE_SCRIPT; diff --git a/src/silver/engine.c b/src/silver/engine.c index f4cd849..2337da5 100644 --- a/src/silver/engine.c +++ b/src/silver/engine.c @@ -243,7 +243,6 @@ static inline ant_value_t sv_execute_entry_common( ant_value_t result = sv_execute_frame(vm, func, this_val, super_val, args, argc); if (out_this) *out_this = vm->frames[vm->fp].this; - if (!vm->suspended) vm->fp = saved_fp; return result; diff --git a/src/streams/readable.c b/src/streams/readable.c index 028d349..8313023 100644 --- a/src/streams/readable.c +++ b/src/streams/readable.c @@ -16,6 +16,7 @@ ant_value_t g_rs_proto; ant_value_t g_reader_proto; ant_value_t g_controller_proto; +ant_value_t g_rs_async_iter_proto; bool rs_is_stream(ant_value_t obj) { return js_check_brand(obj, BRAND_READABLE_STREAM) @@ -604,34 +605,39 @@ static ant_value_t js_rs_reader_read(ant_t *js, ant_value_t *args, int nargs) { return rs_default_reader_read(js, js->this_val); } -static ant_value_t js_rs_reader_release_lock(ant_t *js, ant_value_t *args, int nargs) { - ant_value_t stream_obj = rs_reader_stream(js->this_val); +static ant_value_t rs_release_reader_lock(ant_t *js, ant_value_t reader_obj) { + ant_value_t stream_obj = rs_reader_stream(reader_obj); if (!rs_is_stream(stream_obj)) return js_mkundef(); rs_stream_t *stream = rs_get_stream(stream_obj); - if (rs_reader_has_reqs(js, js->this_val)) { + if (rs_reader_has_reqs(js, reader_obj)) { ant_value_t release_err = js_make_error_silent(js, JS_ERR_TYPE, "Reader was released"); - rs_default_reader_error_read_requests(js, js->this_val, release_err); + rs_default_reader_error_read_requests(js, reader_obj, release_err); } ant_value_t new_closed = js_mkpromise(js); ant_value_t release_err = js_make_error_silent(js, JS_ERR_TYPE, "Reader was released"); if (stream->state == RS_STATE_READABLE) { - ant_value_t old_closed = rs_reader_closed(js->this_val); + ant_value_t old_closed = rs_reader_closed(reader_obj); js_reject_promise(js, old_closed, release_err); promise_mark_handled(old_closed); } js_reject_promise(js, new_closed, release_err); promise_mark_handled(new_closed); - js_set_slot(js->this_val, SLOT_RS_CLOSED, new_closed); + js_set_slot(reader_obj, SLOT_RS_CLOSED, new_closed); js_set_slot(stream_obj, SLOT_CTOR, js_mkundef()); - js_set_slot(js->this_val, SLOT_ENTRIES, js_mkundef()); + js_set_slot(reader_obj, SLOT_ENTRIES, js_mkundef()); + return js_mkundef(); } +static ant_value_t js_rs_reader_release_lock(ant_t *js, ant_value_t *args, int nargs) { + return rs_release_reader_lock(js, js->this_val); +} + static ant_value_t js_rs_reader_cancel(ant_t *js, ant_value_t *args, int nargs) { ant_value_t stream_obj = rs_reader_stream(js->this_val); if (!rs_is_stream(stream_obj)) { @@ -725,18 +731,56 @@ static ant_value_t js_rs_get_reader(ant_t *js, ant_value_t *args, int nargs) { return reader; } +static ant_value_t js_rs_async_iter_next(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t reader = js_get_slot(js->this_val, SLOT_DATA); + if (!rs_is_reader(reader)) return js_mkerr_typed( + js, JS_ERR_TYPE, "ReadableStream async iterator has no reader" + ); + return rs_default_reader_read(js, reader); +} + +static ant_value_t js_rs_async_iter_return(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t reader = js_get_slot(js->this_val, SLOT_DATA); + + if (!rs_is_reader(reader)) { + ant_value_t p = js_mkpromise(js); + js_resolve_promise(js, p, js_iter_result(js, false, js_mkundef())); + return p; + } + + ant_value_t stream_obj = rs_reader_stream(reader); + ant_value_t reason = (nargs > 0) ? args[0] : js_mkundef(); + + readable_stream_cancel(js, stream_obj, reason); + rs_release_reader_lock(js, reader); + + ant_value_t p = js_mkpromise(js); + js_resolve_promise(js, p, js_iter_result(js, false, js_mkundef())); + + return p; +} + static ant_value_t js_rs_values(ant_t *js, ant_value_t *args, int nargs) { - return js_mkerr_typed(js, JS_ERR_TYPE, "ReadableStream async iteration is not yet implemented"); + ant_value_t reader = js_rs_get_reader(js, NULL, 0); + if (is_err(reader)) return reader; + + ant_value_t iterator = js_mkobj(js); + js_set_proto_init(iterator, g_rs_async_iter_proto); + js_set_slot(iterator, SLOT_DATA, reader); + + return iterator; } static ant_value_t rs_start_resolve_handler(ant_t *js, ant_value_t *args, int nargs) { ant_value_t ctrl_obj = js_get_slot(js->current_func, SLOT_DATA); rs_controller_t *ctrl = rs_get_controller(ctrl_obj); + if (!ctrl) return js_mkundef(); ctrl->started = true; ctrl->pulling = false; ctrl->pull_again = false; rs_default_controller_call_pull_if_needed(js, ctrl_obj); + return js_mkundef(); } @@ -744,10 +788,15 @@ static ant_value_t rs_start_reject_handler(ant_t *js, ant_value_t *args, int nar ant_value_t ctrl_obj = js_get_slot(js->current_func, SLOT_DATA); rs_controller_t *ctrl = rs_get_controller(ctrl_obj); if (!ctrl) return js_mkundef(); + ant_value_t stream_obj = rs_ctrl_stream(ctrl_obj); rs_stream_t *stream = rs_get_stream(stream_obj); - if (stream && stream->state == RS_STATE_READABLE) - readable_stream_error(js, stream_obj, nargs > 0 ? args[0] : js_mkundef()); + + if (stream && stream->state == RS_STATE_READABLE) readable_stream_error( + js, stream_obj, + nargs > 0 ? args[0] : js_mkundef() + ); + return js_mkundef(); } @@ -780,6 +829,7 @@ static ant_value_t js_rs_ctor(ant_t *js, ant_value_t *args, int nargs) { return js_mkerr_typed(js, JS_ERR_TYPE, "ReadableStream constructor requires 'new'"); ant_value_t underlying_source = js_mkundef(); + bool is_bytes_source = false; if (nargs > 0 && !is_undefined(args[0])) { if (is_null(args[0])) return js_mkerr_typed(js, JS_ERR_TYPE, "The underlying source cannot be null"); @@ -796,9 +846,11 @@ static ant_value_t js_rs_ctor(ant_t *js, ant_value_t *args, int nargs) { } size_t tlen; const char *tstr = js_getstr(js, coerced, &tlen); - if (tstr && tlen == 5 && memcmp(tstr, "bytes", 5) == 0) - return js_mkerr_typed(js, JS_ERR_RANGE, "ReadableStream byte sources are not yet implemented"); - return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid type is specified"); + if (tstr && tlen == 5 && memcmp(tstr, "bytes", 5) == 0) { + // TODO: accept byte sources, but currently back them with the default + // controller path until BYOB readers/controllers are implemented + is_bytes_source = true; + } else return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid type is specified"); }} ant_value_t strategy = js_mkundef(); @@ -837,6 +889,9 @@ static ant_value_t js_rs_ctor(ant_t *js, ant_value_t *args, int nargs) { js_set_slot(obj, SLOT_DATA, ANT_PTR(st)); js_set_finalizer(obj, rs_stream_finalize); + if (is_bytes_source) + js_set(js, obj, "supportsBYOB", js_false); + ant_value_t pull_fn = js_mkundef(); ant_value_t cancel_fn = js_mkundef(); ant_value_t start_fn = js_mkundef(); @@ -925,6 +980,7 @@ void gc_mark_readable_streams(ant_t *js, void (*mark)(ant_t *, ant_value_t)) { mark(js, g_rs_proto); mark(js, g_reader_proto); mark(js, g_controller_proto); + mark(js, g_rs_async_iter_proto); } void init_readable_stream_module(void) { @@ -959,6 +1015,13 @@ void init_readable_stream_module(void) { js_set(js, g, "ReadableStreamDefaultReader", reader_ctor); js_set_descriptor(js, g, "ReadableStreamDefaultReader", 27, JS_DESC_W | JS_DESC_C); + g_rs_async_iter_proto = js_mkobj(js); + js_set(js, g_rs_async_iter_proto, "next", js_mkfun(js_rs_async_iter_next)); + js_set_descriptor(js, g_rs_async_iter_proto, "next", 4, JS_DESC_W | JS_DESC_C); + js_set(js, g_rs_async_iter_proto, "return", js_mkfun(js_rs_async_iter_return)); + js_set_descriptor(js, g_rs_async_iter_proto, "return", 6, JS_DESC_W | JS_DESC_C); + js_set_sym(js, g_rs_async_iter_proto, get_asyncIterator_sym(), js_mkfun(sym_this_cb)); + g_rs_proto = js_mkobj(js); js_set_getter_desc(js, g_rs_proto, "locked", 6, js_mkfun(js_rs_get_locked), JS_DESC_C); js_set(js, g_rs_proto, "cancel", js_mkfun(js_rs_cancel)); @@ -969,6 +1032,7 @@ void init_readable_stream_module(void) { js_set(js, g_rs_proto, "values", js_mkfun(js_rs_values)); js_set_descriptor(js, g_rs_proto, "values", 6, JS_DESC_W | JS_DESC_C); + js_set_sym(js, g_rs_proto, get_asyncIterator_sym(), js_get(js, g_rs_proto, "values")); js_set_sym(js, g_rs_proto, get_toStringTag_sym(), js_mkstr(js, "ReadableStream", 14)); ant_value_t rs_ctor = js_make_ctor(js, js_rs_ctor, g_rs_proto, "ReadableStream", 14); diff --git a/src/tools/gen_builtin_bundle.js b/src/tools/gen_builtin_bundle.js index eb327e8..a800221 100644 --- a/src/tools/gen_builtin_bundle.js +++ b/src/tools/gen_builtin_bundle.js @@ -21,6 +21,11 @@ function toSpecifier(rootDir, filePath) { throw new Error(`Unsupported builtin module path: ${relativePath}`); } +function toAliasSpecifiers(specifier) { + if (specifier.startsWith('node:')) return [specifier.slice('node:'.length)]; + return []; +} + function toFormat(filePath) { if (/\.(cts|cjs)$/u.test(filePath)) return 'MODULE_EVAL_FORMAT_CJS'; if (/\.(mts|mjs)$/u.test(filePath)) return 'MODULE_EVAL_FORMAT_ESM'; @@ -81,16 +86,25 @@ function generateHeader(rootDir, bundles) { lines.push(''); }); - lines.push('static const ant_builtin_bundle_entry_t ant_builtin_bundle_data[] = {'); + lines.push('static const ant_builtin_bundle_module_t ant_builtin_bundle_modules[] = {'); + bundles.forEach((bundle, index) => { + lines.push(` { ant_builtin_bundle_${index}, sizeof(ant_builtin_bundle_${index}), ${bundle.format} },`); + }); + lines.push('};'); + lines.push(''); + lines.push('static const ant_builtin_bundle_alias_t ant_builtin_bundle_aliases[] = {'); bundles.forEach((bundle, index) => { - lines.push( - ` { ${cString(bundle.specifier)}, ${cString(bundle.specifier)}, ant_builtin_bundle_${index}, sizeof(ant_builtin_bundle_${index}), ${bundle.format} },` - ); + for (const specifier of bundle.specifiers) { + lines.push(` { ${cString(specifier)}, ${specifier.length}, ${cString(bundle.specifier)}, ${index} },`); + } }); lines.push('};'); lines.push(''); - lines.push('static const size_t ant_builtin_bundle_data_count ='); - lines.push(' sizeof(ant_builtin_bundle_data) / sizeof(ant_builtin_bundle_data[0]);'); + lines.push('static const size_t ant_builtin_bundle_module_count ='); + lines.push(' sizeof(ant_builtin_bundle_modules) / sizeof(ant_builtin_bundle_modules[0]);'); + lines.push(''); + lines.push('static const size_t ant_builtin_bundle_alias_count ='); + lines.push(' sizeof(ant_builtin_bundle_aliases) / sizeof(ant_builtin_bundle_aliases[0]);'); lines.push(''); lines.push('#endif'); @@ -111,7 +125,8 @@ async function main() { const specifier = toSpecifier(builtinsRoot, entryPath); const format = toFormat(entryPath); const bytes = await bundleBuiltin(entryPath, format); - bundles.push({ entryPath, specifier, format, bytes }); + const specifiers = [specifier, ...toAliasSpecifiers(specifier)]; + bundles.push({ entryPath, specifier, specifiers, format, bytes }); } const header = generateHeader(builtinsRoot, bundles); diff --git a/src/types/modules/fs.d.ts b/src/types/modules/fs.d.ts index 8d81831..5e23f81 100644 --- a/src/types/modules/fs.d.ts +++ b/src/types/modules/fs.d.ts @@ -59,6 +59,11 @@ declare module 'fs' { function accessSync(path: string, mode?: number): void; function readdir(path: string): Promise; function readdirSync(path: string): string[]; + function realpath(path: string): Promise; + function realpathSync(path: string): string; + namespace realpathSync { + function native(path: string): string; + } } declare module 'ant:fs' { @@ -110,6 +115,7 @@ declare module 'fs/promises' { function exists(path: string): Promise; function access(path: string, mode?: number): Promise; function readdir(path: string): Promise; + function realpath(path: string): Promise; } declare module 'ant:fs/promises' { diff --git a/src/types/modules/path.d.ts b/src/types/modules/path.d.ts index 43a0482..92f7ca3 100644 --- a/src/types/modules/path.d.ts +++ b/src/types/modules/path.d.ts @@ -7,25 +7,23 @@ declare module 'path' { name: string; } - const sep: string; - const delimiter: string; - - function basename(path: string, ext?: string): string; - function dirname(path: string): string; - function extname(path: string): string; - function join(...paths: string[]): string; - function normalize(path: string): string; - function resolve(...paths: string[]): string; - function relative(from: string, to: string): string; - function isAbsolute(path: string): boolean; - function parse(path: string): ParsedPath; - function format(pathObject: Partial): string; -} - -declare module 'ant:path' { - export * from 'path'; -} + interface PathModule { + sep: string; + delimiter: string; + basename(path: string, ext?: string): string; + dirname(path: string): string; + extname(path: string): string; + join(...paths: string[]): string; + normalize(path: string): string; + resolve(...paths: string[]): string; + relative(from: string, to: string): string; + isAbsolute(path: string): boolean; + parse(path: string): ParsedPath; + format(pathObject: Partial): string; + posix: PathModule; + win32: PathModule; + } -declare module 'node:path' { - export * from 'path'; + const path: PathModule; + export = path; } diff --git a/tests/await_reactpromise_shape.mjs b/tests/await_reactpromise_shape.mjs new file mode 100644 index 0000000..3c47425 --- /dev/null +++ b/tests/await_reactpromise_shape.mjs @@ -0,0 +1,35 @@ +function ReactPromiseShape(value) { + this.value = value; +} + +ReactPromiseShape.prototype = Object.create(Promise.prototype); +ReactPromiseShape.prototype.constructor = ReactPromiseShape; +let lastThis = null; +ReactPromiseShape.prototype.then = function(resolve, _reject) { + lastThis = this; + resolve(this.value); +}; + +async function main() { + const first = new ReactPromiseShape([1]); + console.log(`first.then.type:${typeof first.then}`); + console.log(`first.hasOwn.then:${Object.prototype.hasOwnProperty.call(first, 'then')}`); + console.log(`proto.hasOwn.then:${Object.prototype.hasOwnProperty.call(ReactPromiseShape.prototype, 'then')}`); + console.log(`first.then.eq.proto:${first.then === ReactPromiseShape.prototype.then}`); + + const awaited = await first; + console.log(`await.protoThen.isUndefined:${awaited === undefined}`); + console.log(`await.protoThen.isArray:${Array.isArray(awaited)}`); + console.log(`await.protoThen.0:${String(awaited?.[0])}`); + console.log(`await.protoThen.thisEqInstance:${lastThis === first}`); + + const second = new ReactPromiseShape([2]); + lastThis = null; + const resolved = await Promise.resolve(second); + console.log(`promiseResolve.protoThen.isUndefined:${resolved === undefined}`); + console.log(`promiseResolve.protoThen.isArray:${Array.isArray(resolved)}`); + console.log(`promiseResolve.protoThen.0:${String(resolved?.[0])}`); + console.log(`promiseResolve.protoThen.thisEqInstance:${lastThis === second}`); +} + +await main(); diff --git a/tests/await_thenable.mjs b/tests/await_thenable.mjs new file mode 100644 index 0000000..388f9c8 --- /dev/null +++ b/tests/await_thenable.mjs @@ -0,0 +1,11 @@ +async function main() { + const value = await { + then(resolve) { + resolve(123); + } + }; + + console.log(`await.thenable:${value}`); +} + +await main(); diff --git a/tests/http_incomingmessage_async_iter.mjs b/tests/http_incomingmessage_async_iter.mjs new file mode 100644 index 0000000..4df8557 --- /dev/null +++ b/tests/http_incomingmessage_async_iter.mjs @@ -0,0 +1,22 @@ +import { createServer } from 'node:http'; + +const server = createServer(async (req, res) => { + const chunks = []; + + for await (const chunk of req) chunks.push(chunk); + + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end(Buffer.concat(chunks).toString('utf8')); +}); + +server.listen(0, async () => { + const address = server.address(); + const response = await fetch(`http://127.0.0.1:${address.port}/`, { + method: 'POST', + body: 'hello-body', + }); + + console.log(`status:${response.status}`); + console.log(`text:${await response.text()}`); + server.close(); +}); diff --git a/tests/json_parse_flight_reviver.cjs b/tests/json_parse_flight_reviver.cjs new file mode 100644 index 0000000..b45d449 --- /dev/null +++ b/tests/json_parse_flight_reviver.cjs @@ -0,0 +1,22 @@ +function flightReviver(key, value) { + if (typeof value === 'string' && value[0] === '$' && value.length > 1) { + return { + ref: value, + holderIsArray: Array.isArray(this), + holderKeys: Object.keys(this).join(',') + }; + } + + return value; +} + +const topLevelArray = JSON.parse('["$5","$8"]', flightReviver); +console.log(`top0:${topLevelArray[0].ref}:${topLevelArray[0].holderIsArray}:${topLevelArray[0].holderKeys}`); +console.log(`top1:${topLevelArray[1].ref}:${topLevelArray[1].holderIsArray}:${topLevelArray[1].holderKeys}`); + +const nested = JSON.parse('{"root":["$5","$8"]}', flightReviver); +console.log(`nested0:${nested.root[0].ref}:${nested.root[0].holderIsArray}:${nested.root[0].holderKeys}`); +console.log(`nested1:${nested.root[1].ref}:${nested.root[1].holderIsArray}:${nested.root[1].holderKeys}`); + +const escaped = JSON.parse('{"literal":"$$escaped"}', flightReviver); +console.log(`escaped:${escaped.literal.ref || escaped.literal}`); diff --git a/tests/json_stringify_nested_function_replacer.cjs b/tests/json_stringify_nested_function_replacer.cjs new file mode 100644 index 0000000..433236a --- /dev/null +++ b/tests/json_stringify_nested_function_replacer.cjs @@ -0,0 +1,19 @@ +const obj = { + action() { + return 1; + }, + child: { + inner() { + return 2; + }, + }, + missing: undefined, +}; + +const out = JSON.stringify(obj, (_key, value) => { + if (typeof value === 'function') return `[fn:${value.name || 'anonymous'}]`; + if (value === undefined) return '[undef]'; + return value; +}); + +console.log(out); diff --git a/tests/json_stringify_root_replacer.js b/tests/json_stringify_root_replacer.js new file mode 100644 index 0000000..254e16e --- /dev/null +++ b/tests/json_stringify_root_replacer.js @@ -0,0 +1,16 @@ +function show(label, value) { + const out = JSON.stringify(value, function (key, current) { + if (key === '') { + if (typeof current === 'function') return { root: 'function' }; + if (current === undefined) return { root: 'undefined' }; + if (typeof current === 'symbol') return { root: 'symbol' }; + } + return current; + }); + + console.log(`${label}:${out}`); +} + +show('fn', function demo() {}); +show('undef', undefined); +show('sym', Symbol.for('demo')); diff --git a/tests/promise_expando.cjs b/tests/promise_expando.cjs new file mode 100644 index 0000000..990ade6 --- /dev/null +++ b/tests/promise_expando.cjs @@ -0,0 +1,19 @@ +const p = Promise.resolve(123); + +console.log(`t0.status:${p.status}`); +p.status = 'pending'; +console.log(`t1.status:${p.status}`); +p.value = 123; +console.log(`t2.value:${p.value}`); + +p.then((value) => { + p.status = 'fulfilled'; + p.value = value; + console.log(`then.status:${p.status}`); + console.log(`then.value:${p.value}`); +}); + +Promise.resolve().then(() => { + console.log(`micro.status:${p.status}`); + console.log(`micro.value:${p.value}`); +}); diff --git a/tests/proxy_has_own.cjs b/tests/proxy_has_own.cjs new file mode 100644 index 0000000..828afa8 --- /dev/null +++ b/tests/proxy_has_own.cjs @@ -0,0 +1,28 @@ +const target = {}; +function makeClientCounter() { + return function ClientCounter() {}; +} + +const proxy = new Proxy(target, { + get(_target, key) { + if (key === 'ClientCounter') return makeClientCounter(); + return undefined; + }, + getOwnPropertyDescriptor(_target, key) { + if (key === 'ClientCounter') { + return { + value: makeClientCounter(), + writable: false, + configurable: false, + enumerable: false, + }; + } + return undefined; + }, +}); + +console.log(`get:${typeof proxy.ClientCounter}`); +console.log(`hasOwn:${Object.prototype.hasOwnProperty.call(proxy, 'ClientCounter')}`); +console.log(`objHasOwn:${Object.hasOwn(proxy, 'ClientCounter')}`); +const desc = Object.getOwnPropertyDescriptor(proxy, 'ClientCounter'); +console.log(`desc.value:${typeof desc?.value}`); diff --git a/tests/react_like_async_iterable_buffer.js b/tests/react_like_async_iterable_buffer.js new file mode 100644 index 0000000..8e553bf --- /dev/null +++ b/tests/react_like_async_iterable_buffer.js @@ -0,0 +1,96 @@ +class ReactPromise { + constructor(status, value, reason) { + this.status = status; + this.value = value; + this.reason = reason; + } +} + +function createPendingChunk(response) { + return new ReactPromise("pending", null, null); +} + +function createResolvedIteratorResultChunk(response, value, done) { + return new ReactPromise( + "resolved_model", + (done ? "{\"done\":true,\"value\":" : "{\"done\":false,\"value\":") + value + "}", + response + ); +} + +function resolveModelChunk(response, chunk, value) { + if ("pending" !== chunk.status) { + return chunk.reason.enqueueModel(value); + } + + chunk.status = "resolved_model"; + chunk.value = value; + chunk.reason = response; +} + +function resolveIteratorResultChunk(response, chunk, value, done) { + resolveModelChunk( + response, + chunk, + (done ? "{\"done\":true,\"value\":" : "{\"done\":false,\"value\":") + value + "}" + ); +} + +function startAsyncIterable(response) { + const buffer = []; + let closed = false; + let nextWriteIndex = 0; + + const iterable = {}; + iterable[Symbol.asyncIterator] = function () { + let nextReadIndex = 0; + return { + next(arg) { + if (arg !== undefined) throw new Error("bad arg"); + if (nextReadIndex === buffer.length) { + if (closed) { + return new ReactPromise("fulfilled", { done: true, value: undefined }, null); + } + buffer[nextReadIndex] = createPendingChunk(response); + } + return buffer[nextReadIndex++]; + } + }; + }; + + return { + buffer, + iterator: iterable[Symbol.asyncIterator](), + enqueueModel(value) { + if (nextWriteIndex === buffer.length) { + buffer[nextWriteIndex] = createResolvedIteratorResultChunk(response, value, false); + } else { + resolveIteratorResultChunk(response, buffer[nextWriteIndex], value, false); + } + nextWriteIndex++; + } + }; +} + +const response = { + seen: [], + enqueueModel(value) { + this.seen.push(value); + } +}; + +const state = startAsyncIterable(response); + +console.log(`len0:${state.buffer.length}`); +const first = state.iterator.next(); +console.log(`len1:${state.buffer.length}`); +console.log(`first.status:${first.status}`); + +state.enqueueModel("1"); +console.log(`len2:${state.buffer.length}`); +console.log(`slot0.status:${state.buffer[0].status}`); +console.log(`slot0.reasonType:${typeof state.buffer[0].reason}`); + +state.enqueueModel("2"); +console.log(`len3:${state.buffer.length}`); +console.log(`seen:${response.seen.join(",")}`); diff --git a/tests/react_like_async_iterable_controller.js b/tests/react_like_async_iterable_controller.js new file mode 100644 index 0000000..5cc0e97 --- /dev/null +++ b/tests/react_like_async_iterable_controller.js @@ -0,0 +1,132 @@ +class ReactPromise { + constructor(status, value, reason) { + this.status = status; + this.value = value; + this.reason = reason; + } +} + +function createPendingChunk() { + return new ReactPromise("pending", null, null); +} + +function createResolvedIteratorResultChunk(response, value, done) { + return new ReactPromise( + "resolved_model", + (done ? "{\"done\":true,\"value\":" : "{\"done\":false,\"value\":") + value + "}", + response + ); +} + +function resolveModelChunk(response, chunk, value) { + if ("pending" !== chunk.status) { + chunk.reason.enqueueModel(value); + return; + } + + chunk.status = "resolved_model"; + chunk.value = value; + chunk.reason = response; +} + +function resolveIteratorResultChunk(response, chunk, value, done) { + resolveModelChunk( + response, + chunk, + (done ? "{\"done\":true,\"value\":" : "{\"done\":false,\"value\":") + value + "}" + ); +} + +function startAsyncIterable(response, id, iterator) { + const buffer = []; + let closed = false; + let nextWriteIndex = 0; + const iterable = {}; + + iterable[Symbol.asyncIterator] = function () { + let nextReadIndex = 0; + return { + next(arg) { + if (arg !== undefined) throw new Error("bad arg"); + if (nextReadIndex === buffer.length) { + if (closed) { + return new ReactPromise("fulfilled", { done: true, value: undefined }, null); + } + buffer[nextReadIndex] = createPendingChunk(response); + } + return buffer[nextReadIndex++]; + } + }; + }; + + const controller = { + enqueueValue(value) { + if (nextWriteIndex === buffer.length) { + buffer[nextWriteIndex] = new ReactPromise("fulfilled", { done: false, value }, null); + } else { + const chunk = buffer[nextWriteIndex]; + chunk.status = "fulfilled"; + chunk.value = { done: false, value }; + chunk.reason = null; + } + nextWriteIndex++; + }, + enqueueModel(value) { + if (nextWriteIndex === buffer.length) { + buffer[nextWriteIndex] = createResolvedIteratorResultChunk(response, value, false); + } else { + resolveIteratorResultChunk(response, buffer[nextWriteIndex], value, false); + } + nextWriteIndex++; + }, + close(value) { + if (!closed) { + closed = true; + if (nextWriteIndex === buffer.length) { + buffer[nextWriteIndex] = createResolvedIteratorResultChunk(response, value, true); + } else { + resolveIteratorResultChunk(response, buffer[nextWriteIndex], value, true); + } + nextWriteIndex++; + } + }, + error(error) { + throw error; + } + }; + + response._chunks.set(id, new ReactPromise("fulfilled", iterator ? iterable[Symbol.asyncIterator]() : iterable, controller)); + return { buffer, controller }; +} + +function log(label, fn) { + try { + const out = fn(); + console.log(`${label}:${out}`); + } catch (error) { + console.log(`${label}:ERR:${error && error.message ? error.message : String(error)}`); + } +} + +const response = { _chunks: new Map() }; +const id = 1; + +startAsyncIterable(response, id, true); +const chunk = response._chunks.get(id); + +log("reasonType", () => typeof chunk.reason); +log("enqueueModelType", () => typeof chunk.reason.enqueueModel); + +resolveModelChunk(response, chunk, "\"1\""); +console.log(`bufferLength:${chunk.reason ? "controller" : "missing"}`); + +const response2 = { _chunks: new Map() }; +const id2 = 2; +response2._chunks.set(id2, new ReactPromise("resolved_model", "\"boot\"", response2)); +const badChunk = response2._chunks.get(id2); +log("badReasonType", () => typeof badChunk.reason); +log("badEnqueueModelType", () => typeof badChunk.reason.enqueueModel); +log("badResolve", () => { + resolveModelChunk(response2, badChunk, "\"next\""); + return "ok"; +}); diff --git a/tests/react_like_bigint_parse.js b/tests/react_like_bigint_parse.js new file mode 100644 index 0000000..19189fc --- /dev/null +++ b/tests/react_like_bigint_parse.js @@ -0,0 +1,16 @@ +const samples = [ + "$n0", + "$n1", + "$n123", + "$n-42", + "$n0x10", +]; + +for (const value of samples) { + try { + const parsed = BigInt(value.slice(2)); + console.log(`${value} -> ${parsed.toString()}`); + } catch (error) { + console.log(`${value} -> error:${error && error.message ? error.message : String(error)}`); + } +} diff --git a/tests/react_like_fetch_row_parser.cjs b/tests/react_like_fetch_row_parser.cjs new file mode 100644 index 0000000..8cadf3c --- /dev/null +++ b/tests/react_like_fetch_row_parser.cjs @@ -0,0 +1,181 @@ +const http = require('node:http'); + +class ReactPromise { + constructor(status, value, reason) { + this.status = status; + this.value = value; + this.reason = reason; + } +} + +function createPendingChunk() { + return new ReactPromise('pending', null, null); +} + +function createResolvedModelChunk(response, value) { + return new ReactPromise('resolved_model', value, response); +} + +function resolveModelChunk(response, chunk, value) { + if ('pending' !== chunk.status) { + chunk.reason.enqueueModel(value); + return; + } + chunk.status = 'resolved_model'; + chunk.value = value; + chunk.reason = response; +} + +function startAsyncIterable(response, id) { + const seen = []; + const controller = { + enqueueModel(value) { + seen.push(value); + }, + close(value) { + seen.push(`close:${value}`); + }, + error(error) { + seen.push(`error:${error && error.message ? error.message : String(error)}`); + } + }; + response.controllers.set(id, { seen, controller }); + response._chunks.set(id, new ReactPromise('fulfilled', {}, controller)); +} + +function processFullBinaryRow(response, id, tag, buffer, chunk) { + let row = ''; + const decoder = new TextDecoder(); + for (let i = 0; i < buffer.length; i++) row += decoder.decode(buffer[i], { stream: true }); + row += decoder.decode(chunk); + + switch (tag) { + case 120: + startAsyncIterable(response, id); + break; + case 67: { + const resolved = response._chunks.get(id); + if (resolved && resolved.status === 'fulfilled') resolved.reason.close(row === '' ? '"$undefined"' : row); + break; + } + default: { + const existing = response._chunks.get(id); + if (existing) { + resolveModelChunk(response, existing, row); + } else { + response._chunks.set(id, createResolvedModelChunk(response, row)); + } + break; + } + } +} + +async function parseResponseBody(body) { + const reader = body.getReader(); + const response = { + _chunks: new Map(), + controllers: new Map() + }; + + let rowState = 0; + let rowID = 0; + let rowTag = 0; + let rowLength = 0; + const buffer = []; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + let i = 0; + while (i < value.length) { + let lastIdx = -1; + switch (rowState) { + case 0: { + const ch = value[i++]; + if (ch === 58) rowState = 1; + else rowID = (rowID << 4) | (ch > 96 ? ch - 87 : ch - 48); + continue; + } + case 1: { + rowState = value[i]; + if ( + rowState === 84 || rowState === 65 || rowState === 79 || rowState === 111 || + rowState === 85 || rowState === 83 || rowState === 115 || rowState === 76 || + rowState === 108 || rowState === 71 || rowState === 103 || rowState === 77 || + rowState === 109 || rowState === 86 + ) { + rowTag = rowState; + rowState = 2; + i++; + } else if ( + (rowState > 64 && rowState < 91) || rowState === 35 || rowState === 114 || rowState === 120 + ) { + rowTag = rowState; + rowState = 3; + i++; + } else { + rowTag = 0; + rowState = 3; + } + continue; + } + case 2: { + const ch = value[i++]; + if (ch === 44) rowState = 4; + else rowLength = (rowLength << 4) | (ch > 96 ? ch - 87 : ch - 48); + continue; + } + case 3: + lastIdx = value.indexOf(10, i); + break; + case 4: + lastIdx = i + rowLength; + if (lastIdx > value.length) lastIdx = -1; + break; + } + + const offset = value.byteOffset + i; + if (lastIdx > -1) { + const rowChunk = new Uint8Array(value.buffer, offset, lastIdx - i); + processFullBinaryRow(response, rowID, rowTag, buffer, rowChunk); + i = lastIdx; + if (rowState === 3) i++; + rowLength = 0; + rowID = 0; + rowTag = 0; + rowState = 0; + buffer.length = 0; + } else { + const partial = new Uint8Array(value.buffer, offset, value.byteLength - i); + buffer.push(partial); + rowLength -= partial.byteLength; + break; + } + } + } + + return response; +} + +const server = http.createServer((_req, res) => { + res.writeHead(200, { 'content-type': 'application/octet-stream' }); + res.write('1:x\n1:"a'); + res.write('"\n1:"b"\n'); + res.end(); +}); + +server.listen(0, async () => { + const { port } = server.address(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`); + const parsed = await parseResponseBody(res.body); + const state = parsed.controllers.get(1); + console.log(`controllerType:${typeof (state && state.controller)}`); + console.log(`seen:${state ? state.seen.join(',') : 'missing'}`); + } catch (error) { + console.log(`ERR:${error && error.stack ? error.stack : String(error)}`); + } finally { + server.close(); + } +}); diff --git a/tests/react_like_http_mixed_row_parser.cjs b/tests/react_like_http_mixed_row_parser.cjs new file mode 100644 index 0000000..d514d15 --- /dev/null +++ b/tests/react_like_http_mixed_row_parser.cjs @@ -0,0 +1,193 @@ +const http = require('node:http'); + +class ReactPromise { + constructor(status, value, reason) { + this.status = status; + this.value = value; + this.reason = reason; + } +} + +function createPendingChunk() { + return new ReactPromise('pending', null, null); +} + +function createResolvedModelChunk(response, value) { + return new ReactPromise('resolved_model', value, response); +} + +function resolveModelChunk(response, chunk, value) { + if ('pending' !== chunk.status) { + chunk.reason.enqueueModel(value); + return; + } + + chunk.status = 'resolved_model'; + chunk.value = value; + chunk.reason = response; +} + +function startAsyncIterable(response, id) { + const seen = []; + const controller = { + enqueueModel(value) { + seen.push(value); + }, + close(value) { + seen.push(`close:${value}`); + }, + error(error) { + seen.push(`error:${error && error.message ? error.message : String(error)}`); + } + }; + + response.controllers.set(id, { seen, controller }); + response._chunks.set(id, new ReactPromise('fulfilled', {}, controller)); +} + +function processFullBinaryRow(response, id, tag, buffer, chunk) { + let row = ''; + const decoder = new TextDecoder(); + + for (let i = 0; i < buffer.length; i++) row += decoder.decode(buffer[i], { stream: true }); + row += decoder.decode(chunk); + + switch (tag) { + case 84: { + const existing = response._chunks.get(id); + if (existing && existing.status !== 'pending') existing.reason.enqueueValue(row); + else response._chunks.set(id, new ReactPromise('fulfilled', row, null)); + return; + } + case 120: + startAsyncIterable(response, id); + return; + case 67: { + const existing = response._chunks.get(id); + if (existing && existing.status === 'fulfilled') existing.reason.close(row === '' ? '"$undefined"' : row); + return; + } + default: { + const existing = response._chunks.get(id); + if (existing) resolveModelChunk(response, existing, row); + else response._chunks.set(id, createResolvedModelChunk(response, row)); + return; + } + } +} + +async function parseResponseBody(body) { + const reader = body.getReader(); + const response = { + _chunks: new Map(), + controllers: new Map() + }; + + let rowState = 0; + let rowID = 0; + let rowTag = 0; + let rowLength = 0; + const buffer = []; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + let i = 0; + while (i < value.length) { + let lastIdx = -1; + switch (rowState) { + case 0: { + const ch = value[i++]; + if (ch === 58) rowState = 1; + else rowID = (rowID << 4) | (ch > 96 ? ch - 87 : ch - 48); + continue; + } + case 1: { + rowState = value[i]; + if ( + rowState === 84 || rowState === 65 || rowState === 79 || rowState === 111 || + rowState === 85 || rowState === 83 || rowState === 115 || rowState === 76 || + rowState === 108 || rowState === 71 || rowState === 103 || rowState === 77 || + rowState === 109 || rowState === 86 + ) { + rowTag = rowState; + rowState = 2; + i++; + } else if ( + (rowState > 64 && rowState < 91) || rowState === 35 || rowState === 114 || rowState === 120 + ) { + rowTag = rowState; + rowState = 3; + i++; + } else { + rowTag = 0; + rowState = 3; + } + continue; + } + case 2: { + const ch = value[i++]; + if (ch === 44) rowState = 4; + else rowLength = (rowLength << 4) | (ch > 96 ? ch - 87 : ch - 48); + continue; + } + case 3: + lastIdx = value.indexOf(10, i); + break; + case 4: + lastIdx = i + rowLength; + if (lastIdx > value.length) lastIdx = -1; + break; + } + + const offset = value.byteOffset + i; + if (lastIdx > -1) { + const rowChunk = new Uint8Array(value.buffer, offset, lastIdx - i); + processFullBinaryRow(response, rowID, rowTag, buffer, rowChunk); + i = lastIdx; + if (rowState === 3) i++; + rowLength = 0; + rowID = 0; + rowTag = 0; + rowState = 0; + buffer.length = 0; + } else { + const partial = new Uint8Array(value.buffer, offset, value.byteLength - i); + buffer.push(partial); + rowLength -= partial.byteLength; + break; + } + } + } + + return response; +} + +const server = http.createServer((_req, res) => { + res.writeHead(200, { 'content-type': 'application/octet-stream' }); + res.write('2:T5,he'); + res.write('llo1:x\n1:"a'); + res.write('"\n1:"b"\n'); + res.end(); +}); + +server.listen(0, async () => { + const { port } = server.address(); + + try { + const res = await fetch(`http://127.0.0.1:${port}/`); + const parsed = await parseResponseBody(res.body); + const prelude = parsed._chunks.get(2); + const iterable = parsed.controllers.get(1); + + console.log(`preludeStatus:${prelude ? prelude.status : 'missing'}`); + console.log(`preludeValue:${prelude ? prelude.value : 'missing'}`); + console.log(`controllerType:${typeof (iterable && iterable.controller)}`); + console.log(`seen:${iterable ? iterable.seen.join(',') : 'missing'}`); + } catch (error) { + console.log(`ERR:${error && error.stack ? error.stack : String(error)}`); + } finally { + server.close(); + } +}); diff --git a/tests/react_like_stream_controller.js b/tests/react_like_stream_controller.js new file mode 100644 index 0000000..b0a6b43 --- /dev/null +++ b/tests/react_like_stream_controller.js @@ -0,0 +1,39 @@ +class ReactPromise { + constructor(status, value, reason) { + this.status = status; + this.value = value; + this.reason = reason; + } +} + +function resolveModelChunk(chunk, value) { + if ("pending" !== chunk.status) { + chunk.reason.enqueueModel(value); + return; + } + + throw new Error("unexpected pending chunk"); +} + +const seen = []; + +function makeController() { + return { + enqueueValue(value) { + seen.push(`value:${value}`); + }, + enqueueModel(value) { + seen.push(`model:${value}`); + }, + close() { + seen.push("close"); + }, + error(error) { + seen.push(`error:${error && error.message ? error.message : String(error)}`); + } + }; +} + +const chunk = new ReactPromise("fulfilled", null, makeController()); +resolveModelChunk(chunk, "ok"); +console.log(seen.join(",")); diff --git a/tests/react_server_action_encode_trace.mjs b/tests/react_server_action_encode_trace.mjs new file mode 100644 index 0000000..1f5a72c --- /dev/null +++ b/tests/react_server_action_encode_trace.mjs @@ -0,0 +1,135 @@ +const knownServerReferences = new WeakMap(); +const boundCache = new WeakMap(); +const FunctionBind = Function.prototype.bind; +const ArraySlice = Array.prototype.slice; + +function encodeFormData(reference) { + let resolve; + let reject; + const thenable = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + Promise.resolve(reference.bound).then((bound) => { + const data = new FormData(); + data.append('id', reference.id); + data.append('boundCount', String(bound.length)); + thenable.status = 'fulfilled'; + thenable.value = data; + resolve(data); + }, (error) => { + thenable.status = 'rejected'; + thenable.reason = error; + reject(error); + }); + + return thenable; +} + +function defaultEncodeFormAction(identifierPrefix) { + let referenceClosure = knownServerReferences.get(this); + let data = null; + + if (referenceClosure.bound !== null) { + data = boundCache.get(referenceClosure); + if (!data) { + data = encodeFormData({ + id: referenceClosure.id, + bound: referenceClosure.bound, + }); + boundCache.set(referenceClosure, data); + } + + if (data.status === 'rejected') throw data.reason; + if (data.status !== 'fulfilled') throw data; + + referenceClosure = data.value; + const prefixedData = new FormData(); + referenceClosure.forEach((value, key) => { + prefixedData.append(`$ACTION_${identifierPrefix}:${key}`, value); + }); + data = prefixedData; + referenceClosure = `$ACTION_REF_${identifierPrefix}`; + } else { + referenceClosure = `$ACTION_ID_${referenceClosure.id}`; + } + + return { + name: referenceClosure, + method: 'POST', + encType: 'multipart/form-data', + data, + }; +} + +function bind() { + const referenceClosure = knownServerReferences.get(this); + if (!referenceClosure) return FunctionBind.apply(this, arguments); + + const newFn = referenceClosure.originalBind.apply(this, arguments); + const args = ArraySlice.call(arguments, 1); + const boundPromise = referenceClosure.bound !== null + ? Promise.resolve(referenceClosure.bound).then((boundArgs) => boundArgs.concat(args)) + : Promise.resolve(args); + + knownServerReferences.set(newFn, { + id: referenceClosure.id, + originalBind: newFn.bind, + bound: boundPromise, + }); + + Object.defineProperties(newFn, { + $$FORM_ACTION: { value: this.$$FORM_ACTION }, + bind: { value: bind }, + }); + + return newFn; +} + +function registerBoundServerReference(reference, id, bound) { + knownServerReferences.set(reference, { + id, + originalBind: reference.bind, + bound, + }); + + Object.defineProperties(reference, { + $$FORM_ACTION: { value: defaultEncodeFormAction }, + bind: { value: bind }, + }); +} + +async function action(x) { + return x; +} + +registerBoundServerReference(action, 'demo', null); + +const bound = action.bind(null, 1); +console.log(`bound.formAction.type:${typeof bound.$$FORM_ACTION}`); + +try { + const info = bound.$$FORM_ACTION('demo'); + console.log(`first.name:${info.name}`); +} catch (error) { + console.log(`first.throw.type:${typeof error}`); + console.log(`first.throw.then:${typeof error?.then}`); + console.log(`first.throw.status:${error?.status ?? 'missing'}`); +} + +await Promise.resolve(); + +try { + const info = bound.$$FORM_ACTION('demo'); + console.log(`second.name:${info.name}`); + console.log(`second.method:${info.method}`); + console.log(`second.encType:${info.encType}`); + const pairs = []; + info.data?.forEach((value, key) => { + pairs.push(`${key}=${value}`); + }); + console.log(`second.data:${pairs.join(',')}`); +} catch (error) { + console.log(`second.throw:${error?.message ?? String(error)}`); +} diff --git a/tests/server_reference_bind_trace.mjs b/tests/server_reference_bind_trace.mjs new file mode 100644 index 0000000..70175c8 --- /dev/null +++ b/tests/server_reference_bind_trace.mjs @@ -0,0 +1,67 @@ +const knownServerReferences = new WeakMap(); +const FunctionBind = Function.prototype.bind; +const ArraySlice = Array.prototype.slice; + +function defaultEncodeFormAction() { + return { name: '$ACTION_ID_demo', method: 'POST' }; +} + +function registerBoundServerReference(reference, id) { + if (knownServerReferences.has(reference)) return; + + knownServerReferences.set(reference, { + id, + originalBind: reference.bind, + bound: null, + }); + + Object.defineProperties(reference, { + $$FORM_ACTION: { + value: defaultEncodeFormAction, + }, + bind: { + value: bind, + }, + }); +} + +function bind() { + const referenceClosure = knownServerReferences.get(this); + if (!referenceClosure) return FunctionBind.apply(this, arguments); + + const newFn = referenceClosure.originalBind.apply(this, arguments); + const args = ArraySlice.call(arguments, 1); + + knownServerReferences.set(newFn, { + id: referenceClosure.id, + originalBind: newFn.bind, + bound: Promise.resolve(args), + }); + + Object.defineProperties(newFn, { + $$FORM_ACTION: { + value: this.$$FORM_ACTION, + }, + bind: { + value: bind, + }, + }); + + return newFn; +} + +async function action(x) { + return x; +} + +registerBoundServerReference(action, 'demo'); + +console.log(`action.bind.eq.custom:${action.bind === bind}`); +console.log(`action.formAction.type:${typeof action.$$FORM_ACTION}`); + +const bound = action.bind(null, 1); + +console.log(`bound.type:${typeof bound}`); +console.log(`bound.bind.eq.custom:${bound.bind === bind}`); +console.log(`bound.formAction.type:${typeof bound.$$FORM_ACTION}`); +console.log(`bound.formAction.name:${bound.$$FORM_ACTION ? bound.$$FORM_ACTION().name : 'missing'}`); diff --git a/tests/test_map.js b/tests/test_map.js index db788d6..0b6157d 100644 --- a/tests/test_map.js +++ b/tests/test_map.js @@ -39,4 +39,22 @@ console.log('Get boolean key:', map.get(true)); // Should be 'boolean key' console.log('Get null key:', map.get(null)); // Should be 'null key' console.log('Final map size:', map.size); // Should be 3 +const obj1 = {}; +const obj2 = {}; +map.set(obj1, 'obj1'); +map.set(obj2, 'obj2'); +console.log('Object key 1:', map.get(obj1)); +console.log('Object key 2:', map.get(obj2)); +console.log('Distinct object keys:', map.get(obj1) !== map.get(obj2)); + +map.set('1', 'string one'); +map.set(1, 'number one'); +console.log('String/number split:', map.get('1'), map.get(1)); + +map.set(NaN, 'nan key'); +console.log('NaN key:', map.get(NaN)); + +map.set(-0, 'minus zero'); +console.log('Zero key normalized:', map.get(0), map.get(-0)); + console.log('Map tests completed!'); -- 2.51.2