diff --git a/examples/results.txt b/examples/results.txt index 9ddbb67..46e484c 100644 --- a/examples/results.txt +++ b/examples/results.txt @@ -464,7 +464,7 @@ compat-table/es6/Map.constructor-arguments.js: failed 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 -compat-table/es6/Map.iterator-prototype-chain.js: TypeError: Cannot read properties of null (reading 'hasOwnProperty') +compat-table/es6/Map.iterator-prototype-chain.js: OK compat-table/es6/Map.js: OK compat-table/es6/Map.prototype-not-instance.js: failed compat-table/es6/Map.prototype.Symbol.iterator.js: OK @@ -581,7 +581,7 @@ compat-table/es6/Set.constructor-arguments.js: OK compat-table/es6/Set.constructor-invokes-add.js: failed compat-table/es6/Set.constructor-requires-new.js: OK compat-table/es6/Set.iterator-closing.js: failed -compat-table/es6/Set.iterator-prototype-chain.js: TypeError: Cannot read properties of null (reading 'hasOwnProperty') +compat-table/es6/Set.iterator-prototype-chain.js: OK compat-table/es6/Set.js: OK compat-table/es6/Set.prototype-not-instance.js: failed compat-table/es6/Set.prototype.Symbol.iterator.js: OK @@ -1343,14 +1343,14 @@ compat-table/es2025/Iterator.from.iterator.js: ReferenceError: 'Iterator' is not compat-table/es2025/Iterator.instanceof.js: ReferenceError: 'Iterator' is not defined compat-table/es2025/Iterator.prototype.Symbol.toStringTag.js: ReferenceError: 'Iterator' is not defined compat-table/es2025/Iterator.prototype.drop.js: TypeError: undefined is not a function -compat-table/es2025/Iterator.prototype.every.js: OK -compat-table/es2025/Iterator.prototype.filter.js: OK -compat-table/es2025/Iterator.prototype.find.js: OK -compat-table/es2025/Iterator.prototype.flatMap.js: OK -compat-table/es2025/Iterator.prototype.forEach.js: OK -compat-table/es2025/Iterator.prototype.map.js: OK -compat-table/es2025/Iterator.prototype.reduce.js: OK -compat-table/es2025/Iterator.prototype.some.js: OK +compat-table/es2025/Iterator.prototype.every.js: TypeError: undefined is not a function +compat-table/es2025/Iterator.prototype.filter.js: TypeError: undefined is not a function +compat-table/es2025/Iterator.prototype.find.js: TypeError: undefined is not a function +compat-table/es2025/Iterator.prototype.flatMap.js: TypeError: undefined is not a function +compat-table/es2025/Iterator.prototype.forEach.js: TypeError: undefined is not a function +compat-table/es2025/Iterator.prototype.map.js: TypeError: undefined is not a function +compat-table/es2025/Iterator.prototype.reduce.js: TypeError: undefined is not a function +compat-table/es2025/Iterator.prototype.some.js: TypeError: undefined is not a function compat-table/es2025/Iterator.prototype.take.js: TypeError: undefined is not a function compat-table/es2025/Iterator.prototype.toArray.js: TypeError: undefined is not a function compat-table/es2025/Promise.try.js: OK diff --git a/examples/spec/blob.js b/examples/spec/blob.js new file mode 100644 index 0000000..c94939d --- /dev/null +++ b/examples/spec/blob.js @@ -0,0 +1,113 @@ +import { test, testThrows, summary } from './helpers.js'; + +console.log('Blob constructor\n'); + +const empty = new Blob(); +test('empty Blob size', empty.size, 0); +test('empty Blob type', empty.type, ''); + +const b1 = new Blob(['hello']); +test('string part size', b1.size, 5); + +const b2 = new Blob(['hello', ' world']); +test('two string parts size', b2.size, 11); + +const b3 = new Blob(['hello'], { type: 'text/plain' }); +test('type set from options', b3.type, 'text/plain'); + +const b4 = new Blob(['hello'], { type: 'TEXT/PLAIN' }); +test('type lowercased', b4.type, 'text/plain'); + +const b5 = new Blob(['x'], { type: 'te\x01xt' }); +test('invalid type byte → empty string', b5.type, ''); + +testThrows('requires new', () => Blob(['x'])); + +console.log('\nBlob from typed parts\n'); + +const ab = new Uint8Array([1, 2, 3]).buffer; +const b6 = new Blob([ab]); +test('ArrayBuffer part size', b6.size, 3); + +const ta = new Uint8Array([4, 5, 6]); +const b7 = new Blob([ta]); +test('Uint8Array part size', b7.size, 3); + +const b8 = new Blob([b1, ' there']); +test('Blob part size', b8.size, 11); + +console.log('\nBlob.text()\n'); + +const bt = await new Blob(['hello world']).text(); +test('text() content', bt, 'hello world'); + +const be = await new Blob().text(); +test('empty blob text()', be, ''); + +console.log('\nBlob.arrayBuffer()\n'); + +const bab = await new Blob([new Uint8Array([10, 20, 30])]).arrayBuffer(); +const view = new Uint8Array(bab); +test('arrayBuffer() byte 0', view[0], 10); +test('arrayBuffer() byte 1', view[1], 20); +test('arrayBuffer() byte 2', view[2], 30); + +console.log('\nBlob.bytes()\n'); + +const bytes = await new Blob([new Uint8Array([7, 8, 9])]).bytes(); +test('bytes() is Uint8Array', bytes instanceof Uint8Array, true); +test('bytes() byte 0', bytes[0], 7); +test('bytes() byte 2', bytes[2], 9); + +console.log('\nBlob.slice()\n'); + +const src = new Blob(['hello world']); +const s1 = src.slice(0, 5); +test('slice(0,5) size', s1.size, 5); +test('slice content', await s1.text(), 'hello'); + +const s2 = src.slice(6); +test('slice(6) size', s2.size, 5); +test('slice(6) content', await s2.text(), 'world'); + +const s3 = src.slice(-5); +test('slice(-5) size', s3.size, 5); +test('slice(-5) content', await s3.text(), 'world'); + +const s4 = src.slice(0, 5, 'text/plain'); +test('slice with type', s4.type, 'text/plain'); + +const s5 = src.slice(5, 3); +test('slice end < start → empty', s5.size, 0); + +console.log('\nFile constructor\n'); + +const f1 = new File(['content'], 'test.txt'); +test('File name', f1.name, 'test.txt'); +test('File size', f1.size, 7); +test('File type default', f1.type, ''); +test('File lastModified is number', typeof f1.lastModified, 'number'); +test('File lastModified > 0', f1.lastModified > 0, true); + +const f2 = new File(['data'], 'file.txt', { type: 'text/plain', lastModified: 1000 }); +test('File type from options', f2.type, 'text/plain'); +test('File lastModified from options', f2.lastModified, 1000); + +testThrows('File requires new', () => File(['x'], 'f.txt')); +testThrows('File requires 2 args', () => new File(['x'])); + +console.log('\nFile instanceof Blob\n'); + +test('File instanceof Blob', f1 instanceof Blob, true); +test('File instanceof File', f1 instanceof File, true); +test('Blob not instanceof File', b1 instanceof File, false); + +const ft = await f1.text(); +test('File.text() works', ft, 'content'); + +console.log('\nSymbol.toStringTag\n'); + +test('Blob toStringTag', Object.prototype.toString.call(b1), '[object Blob]'); +test('File toStringTag', Object.prototype.toString.call(f1), '[object File]'); + +summary(); diff --git a/examples/spec/headers.js b/examples/spec/headers.js new file mode 100644 index 0000000..33cb895 --- /dev/null +++ b/examples/spec/headers.js @@ -0,0 +1,114 @@ +import { test, testThrows, testDeep, summary } from './helpers.js'; + +console.log('Headers constructor\n'); + +const h0 = new Headers(); +test('empty constructor', h0.has('x-foo'), false); + +const h1 = new Headers({ 'Content-Type': 'text/html', 'x-custom': 'val' }); +test('init from record', h1.get('content-type'), 'text/html'); +test('record key case-folded', h1.get('Content-Type'), 'text/html'); + +const h2 = new Headers([['x-a', '1'], ['x-b', '2']]); +test('init from sequence', h2.get('x-a'), '1'); +test('init from sequence second', h2.get('x-b'), '2'); + +const h3 = new Headers(new Headers({ 'x-copy': 'yes' })); +test('init from another Headers', h3.get('x-copy'), 'yes'); + +testThrows('requires new', () => Headers()); +testThrows('null init throws', () => new Headers(null)); +testThrows('string init throws', () => new Headers('bad')); +testThrows('number init throws', () => new Headers(42)); + +console.log('\nappend / get / has / delete / set\n'); + +const h = new Headers(); + +h.append('x-foo', 'one'); +test('append then get', h.get('x-foo'), 'one'); + +h.append('x-foo', 'two'); +test('append combines values', h.get('x-foo'), 'one, two'); + +test('has returns true', h.has('x-foo'), true); +test('has case-insensitive', h.has('X-FOO'), true); +test('has missing returns false', h.has('x-bar'), false); + +h.set('x-foo', 'replaced'); +test('set replaces combined value', h.get('x-foo'), 'replaced'); + +h.delete('x-foo'); +test('delete removes header', h.has('x-foo'), false); +test('get deleted returns null', h.get('x-foo'), null); + +console.log('\nset-cookie special case\n'); + +const hc = new Headers(); +hc.append('set-cookie', 'a=1'); +hc.append('set-cookie', 'b=2'); +test('set-cookie not combined', hc.get('set-cookie'), 'a=1'); + +const cookies = hc.getSetCookie ? hc.getSetCookie() : null; +if (cookies) { + test('getSetCookie length', cookies.length, 2); + test('getSetCookie first', cookies[0], 'a=1'); + test('getSetCookie second', cookies[1], 'b=2'); +} + +console.log('\nvalue normalization\n'); + +const hn = new Headers(); +hn.set('x-trim', ' hello '); +test('leading/trailing whitespace stripped', hn.get('x-trim'), 'hello'); + +hn.set('x-tab', '\thello\t'); +test('leading/trailing tab stripped', hn.get('x-tab'), 'hello'); + +console.log('\ninvalid name / value\n'); + +testThrows('empty name throws', () => h.set('', 'val')); +testThrows('name with space throws', () => h.set('x bad', 'val')); +testThrows('name with colon throws', () => h.set('x:bad', 'val')); + +console.log('\nforEach\n'); + +const hf = new Headers({ 'b-key': 'bval', 'a-key': 'aval' }); +const seen = []; +hf.forEach((val, name) => seen.push(`${name}:${val}`)); +test('forEach visits all entries', seen.length, 2); +test('forEach sorted order', seen[0], 'a-key:aval'); +test('forEach sorted order 2', seen[1], 'b-key:bval'); + +console.log('\nentries / keys / values iteration\n'); + +const hi = new Headers({ 'b-hdr': '2', 'a-hdr': '1' }); + +const entries = [...hi.entries()]; +test('entries sorted', entries[0][0], 'a-hdr'); +test('entries value', entries[0][1], '1'); +test('entries length', entries.length, 2); + +const keys = [...hi.keys()]; +test('keys sorted', keys[0], 'a-hdr'); +test('keys length', keys.length, 2); + +const vals = [...hi.values()]; +test('values order matches keys', vals[0], '1'); + +console.log('\nlive iteration\n'); + +const hl = new Headers({ 'x-a': '1' }); +const iter = hl.entries(); +hl.set('x-b', '2'); +const all = []; +for (const e of { [Symbol.iterator]: () => iter }) all.push(e[0]); +test('live iterator sees added key', all.includes('x-b'), true); + +console.log('\niterator prototype chain\n'); + +const it = new Headers().entries(); +const iterProto = Object.getPrototypeOf(Object.getPrototypeOf(it)); +test('iterator proto chain has Symbol.iterator', typeof iterProto[Symbol.iterator], 'function'); + +summary(); diff --git a/include/ant.h b/include/ant.h index 6b7c2b8..bb4540a 100644 --- a/include/ant.h +++ b/include/ant.h @@ -160,14 +160,17 @@ void js_process_promise_handlers(ant_t *js, ant_value_t promise); void js_setup_import_meta(ant_t *js, const char *filename); typedef ant_value_t (*js_getter_fn)(ant_t *js, ant_value_t obj, const char *key, size_t key_len); +typedef ant_value_t (*js_keys_fn)(ant_t *js, ant_value_t obj); + typedef bool (*js_setter_fn)(ant_t *js, ant_value_t obj, const char *key, size_t key_len, ant_value_t value); typedef bool (*js_deleter_fn)(ant_t *js, ant_value_t obj, const char *key, size_t key_len); -typedef ant_value_t (*js_keys_fn)(ant_t *js, ant_value_t obj); +typedef void (*js_finalizer_fn)(ant_t *js, ant_object_t *obj); void js_set_getter(ant_value_t obj, js_getter_fn getter); void js_set_setter(ant_value_t obj, js_setter_fn setter); void js_set_deleter(ant_value_t obj, js_deleter_fn deleter); void js_set_keys(ant_value_t obj, js_keys_fn keys); +void js_set_finalizer(ant_value_t obj, js_finalizer_fn fn); ant_value_t js_get_slot(ant_value_t obj, internal_slot_t slot); void js_set_slot(ant_value_t obj, internal_slot_t slot, ant_value_t value); diff --git a/include/modules/blob.h b/include/modules/blob.h new file mode 100644 index 0000000..49d9d56 --- /dev/null +++ b/include/modules/blob.h @@ -0,0 +1,24 @@ +#ifndef BLOB_H +#define BLOB_H + +#include +#include +#include "types.h" + +typedef struct { + uint8_t *data; + size_t size; + char *type; + char *name; + int64_t last_modified; +} blob_data_t; + +void init_blob_module(void); + +blob_data_t *blob_get_data(ant_value_t obj); +ant_value_t blob_create(ant_t *js, const uint8_t *data, size_t size, const char *type); + +extern ant_value_t g_blob_proto; +extern ant_value_t g_file_proto; + +#endif diff --git a/include/modules/collections.h b/include/modules/collections.h index 76d296f..e99f8c3 100644 --- a/include/modules/collections.h +++ b/include/modules/collections.h @@ -52,4 +52,7 @@ void init_collections_module(void); map_entry_t **get_map_from_obj(ant_t *js, ant_value_t obj); set_entry_t **get_set_from_obj(ant_t *js, ant_value_t obj); +extern ant_value_t g_map_iter_proto; +extern ant_value_t g_set_iter_proto; + #endif diff --git a/include/modules/headers.h b/include/modules/headers.h new file mode 100644 index 0000000..7aa7a53 --- /dev/null +++ b/include/modules/headers.h @@ -0,0 +1,6 @@ +#ifndef HEADERS_H +#define HEADERS_H + +void init_headers_module(void); + +#endif diff --git a/include/object.h b/include/object.h index 5b6e59e..c0e2d4f 100644 --- a/include/object.h +++ b/include/object.h @@ -47,6 +47,7 @@ typedef struct ant_object { ant_value_t *extra_slots; struct ant_object *gc_pending_next; + void (*finalizer)(ant_t *, struct ant_object *); ant_value_t inobj[ANT_INOBJ_MAX_SLOTS]; union { diff --git a/src/ant.c b/src/ant.c index 63f99b7..05035a8 100644 --- a/src/ant.c +++ b/src/ant.c @@ -57,6 +57,7 @@ #include "modules/ffi.h" #include "modules/date.h" #include "modules/buffer.h" +#include "modules/blob.h" #include "modules/collections.h" #include "modules/lmdb.h" #include "modules/regex.h" @@ -1341,6 +1342,29 @@ static size_t strobj(ant_t *js, ant_value_t obj, char *buf, size_t len) { return n; } + if (tag_str) { + bool is_blob = (tlen == 4 && memcmp(tag_str, "Blob", 4) == 0); + bool is_file = (tlen == 4 && memcmp(tag_str, "File", 4) == 0); + if (is_blob || is_file) { + blob_data_t *bd = blob_get_data(obj); + n += cpy(buf + n, REMAIN(n, len), is_file ? "File" : "Blob", 4); + n += cpy(buf + n, REMAIN(n, len), " { size: ", 9); + n += (size_t) snprintf(buf + n, REMAIN(n, len), "%zu", bd ? bd->size : 0); + n += cpy(buf + n, REMAIN(n, len), ", type: '", 9); + if (bd && bd->type) n += cpy(buf + n, REMAIN(n, len), bd->type, strlen(bd->type)); + n += cpy(buf + n, REMAIN(n, len), "'", 1); + if (is_file) { + n += cpy(buf + n, REMAIN(n, len), ", name: '", 9); + if (bd && bd->name) n += cpy(buf + n, REMAIN(n, len), bd->name, strlen(bd->name)); + n += cpy(buf + n, REMAIN(n, len), "'", 1); + n += cpy(buf + n, REMAIN(n, len), ", lastModified: ", 16); + n += (size_t) snprintf(buf + n, REMAIN(n, len), "%" PRId64, bd ? bd->last_modified : 0); + } + n += cpy(buf + n, REMAIN(n, len), " }", 2); + pop_stringify(); + return n; + }} + print_tagged_object: n += cpy(buf + n, REMAIN(n, len), "Object [", 8); n += cpy(buf + n, REMAIN(n, len), (const char *)(uintptr_t)(toff), tlen); @@ -7771,72 +7795,117 @@ static ant_value_t builtin_array_with(ant_t *js, ant_value_t *args, int nargs) { return mkval(T_ARR, vdata(result)); } -static ant_value_t builtin_array_keys(ant_t *js, ant_value_t *args, int nargs) { - (void) args; - (void) nargs; - ant_value_t arr = js->this_val; - if (vtype(arr) != T_ARR && vtype(arr) != T_OBJ) { - return js_mkerr(js, "keys called on non-array"); - } - - ant_offset_t len = get_array_length(js, arr); - - ant_value_t result = mkarr(js); - if (is_err(result)) return result; +static ant_value_t arr_values_iter_next(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t iter = js->this_val; + ant_value_t array = js_get_slot(iter, SLOT_DATA); + ant_value_t idx_v = js_get_slot(iter, SLOT_ITER_STATE); + ant_offset_t idx = (vtype(idx_v) == T_NUM) ? (ant_offset_t)js_getnum(idx_v) : 0; + ant_value_t result = js_mkobj(js); + ant_offset_t len = js_arr_len(js, array); - for (ant_offset_t i = 0; i < len; i++) { - arr_set(js, result, i, tov((double) i)); + if (idx >= len) { + js_set(js, result, "done", js_true); + js_set(js, result, "value", js_mkundef()); + return result; } - return mkval(T_ARR, vdata(result)); + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)(idx + 1))); + js_set(js, result, "done", js_false); + js_set(js, result, "value", js_arr_get(js, array, idx)); + + return result; } -static ant_value_t builtin_array_values(ant_t *js, ant_value_t *args, int nargs) { - (void) args; - (void) nargs; - ant_value_t arr = js->this_val; - if (vtype(arr) != T_ARR && vtype(arr) != T_OBJ) { - return js_mkerr(js, "values called on non-array"); +static ant_value_t arr_keys_iter_next(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t iter = js->this_val; + ant_value_t array = js_get_slot(iter, SLOT_DATA); + ant_value_t idx_v = js_get_slot(iter, SLOT_ITER_STATE); + ant_offset_t idx = (vtype(idx_v) == T_NUM) ? (ant_offset_t)js_getnum(idx_v) : 0; + ant_value_t result = js_mkobj(js); + ant_offset_t len = js_arr_len(js, array); + + if (idx >= len) { + js_set(js, result, "done", js_true); + js_set(js, result, "value", js_mkundef()); + return result; } - ant_offset_t len = get_array_length(js, arr); + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)(idx + 1))); + js_set(js, result, "done", js_false); + js_set(js, result, "value", js_mknum((double)idx)); - ant_value_t result = mkarr(js); - if (is_err(result)) return result; + return result; +} + +static ant_value_t arr_entries_iter_next(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t iter = js->this_val; + ant_value_t array = js_get_slot(iter, SLOT_DATA); + ant_value_t idx_v = js_get_slot(iter, SLOT_ITER_STATE); + ant_offset_t idx = (vtype(idx_v) == T_NUM) ? (ant_offset_t)js_getnum(idx_v) : 0; + ant_value_t result = js_mkobj(js); + ant_offset_t len = js_arr_len(js, array); - for (ant_offset_t i = 0; i < len; i++) { - arr_set(js, result, i, arr_get(js, arr, i)); + if (idx >= len) { + js_set(js, result, "done", js_true); + js_set(js, result, "value", js_mkundef()); + return result; } - return mkval(T_ARR, vdata(result)); + ant_value_t pair = js_mkarr(js); + js_arr_push(js, pair, js_mknum((double)idx)); + js_arr_push(js, pair, js_arr_get(js, array, idx)); + js_set_slot(iter, SLOT_ITER_STATE, js_mknum((double)(idx + 1))); + js_set(js, result, "done", js_false); + js_set(js, result, "value", pair); + + return result; +} + +static ant_value_t g_arr_values_iter_proto = 0; +static ant_value_t g_arr_keys_iter_proto = 0; +static ant_value_t g_arr_entries_iter_proto = 0; + +static void ensure_array_iter_protos(ant_t *js) { + if (g_arr_values_iter_proto) return; + + g_arr_values_iter_proto = js_mkobj(js); + js_set_proto_init(g_arr_values_iter_proto, js->sym.iterator_proto); + js_set(js, g_arr_values_iter_proto, "next", js_mkfun(arr_values_iter_next)); + + g_arr_keys_iter_proto = js_mkobj(js); + js_set_proto_init(g_arr_keys_iter_proto, js->sym.iterator_proto); + js_set(js, g_arr_keys_iter_proto, "next", js_mkfun(arr_keys_iter_next)); + + g_arr_entries_iter_proto = js_mkobj(js); + js_set_proto_init(g_arr_entries_iter_proto, js->sym.iterator_proto); + js_set(js, g_arr_entries_iter_proto, "next", js_mkfun(arr_entries_iter_next)); +} + +static ant_value_t make_array_iterator(ant_t *js, ant_value_t iter_proto) { + ensure_array_iter_protos(js); + ant_value_t iter = js_mkobj(js); + js_set_slot_wb(js, iter, SLOT_DATA, js->this_val); + js_set_slot(iter, SLOT_ITER_STATE, js_mknum(0)); + js_set_proto_init(iter, iter_proto); + return iter; +} + +static ant_value_t builtin_array_keys(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->this_val) != T_ARR && vtype(js->this_val) != T_OBJ) + return js_mkerr(js, "keys called on non-array"); + return make_array_iterator(js, g_arr_keys_iter_proto); +} + +static ant_value_t builtin_array_values(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->this_val) != T_ARR && vtype(js->this_val) != T_OBJ) + return js_mkerr(js, "values called on non-array"); + return make_array_iterator(js, g_arr_values_iter_proto); } static ant_value_t builtin_array_entries(ant_t *js, ant_value_t *args, int nargs) { - (void) args; - (void) nargs; - ant_value_t arr = js->this_val; - if (vtype(arr) != T_ARR && vtype(arr) != T_OBJ) { + if (vtype(js->this_val) != T_ARR && vtype(js->this_val) != T_OBJ) return js_mkerr(js, "entries called on non-array"); - } - - ant_offset_t len = get_array_length(js, arr); - - ant_value_t result = mkarr(js); - if (is_err(result)) return result; - - for (ant_offset_t i = 0; i < len; i++) { - ant_value_t entry = mkarr(js); - if (is_err(entry)) return entry; - - ant_value_t elem = arr_get(js, arr, i); - - arr_set(js, entry, 0, tov((double) i)); - arr_set(js, entry, 1, elem); - - arr_set(js, result, i, mkval(T_ARR, vdata(entry))); - } - - return mkval(T_ARR, vdata(result)); + return make_array_iterator(js, g_arr_entries_iter_proto); } static ant_value_t builtin_array_toString(ant_t *js, ant_value_t *args, int nargs) { @@ -12624,6 +12693,14 @@ void js_set_deleter(ant_value_t obj, js_deleter_fn deleter) { ops->deleter = deleter; } +void js_set_finalizer(ant_value_t obj, js_finalizer_fn fn) { + if (!is_object_type(obj)) return; + if (vtype(obj) != T_OBJ) obj = js_as_obj(obj); + ant_object_t *ptr = js_obj_ptr(obj); + if (!ptr) return; + ptr->finalizer = fn; +} + void js_set_keys(ant_value_t obj, js_keys_fn keys) { if (!is_object_type(obj)) return; if (vtype(obj) != T_OBJ) obj = js_as_obj(obj); diff --git a/src/gc/objects.c b/src/gc/objects.c index 9d2b1ec..7f6811b 100644 --- a/src/gc/objects.c +++ b/src/gc/objects.c @@ -473,6 +473,8 @@ static void gc_mark_roots(ant_t *js) { void gc_object_free(ant_t *js, ant_object_t *obj) { if (!obj) return; + if (obj->finalizer) obj->finalizer(js, obj); + obj->mark_epoch = ANT_GC_DEAD; if (obj->shape) { diff --git a/src/main.c b/src/main.c index 5624aeb..76d4218 100644 --- a/src/main.c +++ b/src/main.c @@ -74,6 +74,8 @@ #include "modules/globals.h" #include "modules/v8.h" #include "modules/worker_threads.h" +#include "modules/headers.h" +#include "modules/blob.h" int js_result = EXIT_SUCCESS; typedef int (*cmd_fn)(int argc, char **argv); @@ -580,6 +582,8 @@ int main(int argc, char *argv[]) { init_domexception_module(); init_globals_module(); init_abort_module(); + init_headers_module(); + init_blob_module(); init_math_module(); init_bigint_module(); init_date_module(); diff --git a/src/modules/blob.c b/src/modules/blob.c new file mode 100644 index 0000000..6df26a1 --- /dev/null +++ b/src/modules/blob.c @@ -0,0 +1,464 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "ant.h" +#include "errors.h" +#include "runtime.h" +#include "internal.h" +#include "descriptors.h" + +#include "modules/blob.h" +#include "modules/buffer.h" +#include "modules/symbol.h" + +ant_value_t g_blob_proto = 0; +ant_value_t g_file_proto = 0; + +blob_data_t *blob_get_data(ant_value_t obj) { + ant_value_t slot = js_get_slot(obj, SLOT_DATA); + if (vtype(slot) != T_NUM) return NULL; + return (blob_data_t *)(uintptr_t)(size_t)js_getnum(slot); +} + +static blob_data_t *blob_data_new(const uint8_t *data, size_t size, const char *type) { + blob_data_t *bd = calloc(1, sizeof(blob_data_t)); + + if (!bd) return NULL; + if (size > 0 && data) { + bd->data = malloc(size); + if (!bd->data) { free(bd); return NULL; } + memcpy(bd->data, data, size); + } + + bd->size = size; + bd->type = type ? strdup(type) : strdup(""); + + return bd; +} + +static char *normalize_mime_type(const char *s) { + if (!s) return strdup(""); + for (const unsigned char *p = (const unsigned char *)s; *p; p++) { + if (*p < 0x20 || *p > 0x7E) return strdup(""); + } + + size_t len = strlen(s); + char *out = malloc(len + 1); + + if (!out) return strdup(""); + for (size_t i = 0; i <= len; i++) out[i] = (char)tolower((unsigned char)s[i]); + + return out; +} + +typedef struct { + uint8_t *buf; + size_t size; + size_t cap; +} byte_buf_t; + +static bool byte_buf_grow(byte_buf_t *b, size_t extra) { + size_t needed = b->size + extra; + if (needed <= b->cap) return true; + size_t new_cap = b->cap ? b->cap * 2 : 64; + while (new_cap < needed) new_cap *= 2; + uint8_t *p = realloc(b->buf, new_cap); + if (!p) return false; + b->buf = p; + b->cap = new_cap; + return true; +} + +static bool byte_buf_append(byte_buf_t *b, const uint8_t *data, size_t len) { + if (!byte_buf_grow(b, len)) return false; + memcpy(b->buf + b->size, data, len); + b->size += len; + return true; +} + +static ant_value_t process_blob_part(ant_t *js, byte_buf_t *buf, ant_value_t part) { + uint8_t t = vtype(part); + + if (t == T_TYPEDARRAY) { + TypedArrayData *ta = (TypedArrayData *)js_gettypedarray(part); + if (!ta || !ta->buffer) return js_mkundef(); + if (!byte_buf_append(buf, ta->buffer->data + ta->byte_offset, ta->byte_length)) + return js_mkerr(js, "out of memory"); + return js_mkundef(); + } + + if (t == T_OBJ) { + ant_value_t buf_slot = js_get_slot(part, SLOT_BUFFER); + if (vtype(buf_slot) == T_TYPEDARRAY) { + TypedArrayData *ta = (TypedArrayData *)js_gettypedarray(buf_slot); + if (ta && ta->buffer && !ta->buffer->is_detached) { + if (!byte_buf_append(buf, ta->buffer->data + ta->byte_offset, ta->byte_length)) + return js_mkerr(js, "out of memory"); + } + return js_mkundef(); + } + if (vtype(buf_slot) == T_NUM) { + ArrayBufferData *abd = (ArrayBufferData *)(uintptr_t)(size_t)js_getnum(buf_slot); + if (abd && !abd->is_detached) { + if (!byte_buf_append(buf, abd->data, abd->length)) + return js_mkerr(js, "out of memory"); + return js_mkundef(); + } + } + blob_data_t *bd = blob_get_data(part); + if (bd && bd->size > 0) { + if (!byte_buf_append(buf, bd->data, bd->size)) + return js_mkerr(js, "out of memory"); + return js_mkundef(); + } + } + + ant_value_t str = (t == T_STR) ? part : js_tostring_val(js, part); + if (is_err(str)) return str; + + size_t len; + char *s = js_getstr(js, str, &len); + if (s && len > 0) { + if (!byte_buf_append(buf, (const uint8_t *)s, len)) + return js_mkerr(js, "out of memory"); + } + return js_mkundef(); +} + +static ant_value_t process_blob_parts(ant_t *js, byte_buf_t *buf, ant_value_t parts) { + uint8_t t = vtype(parts); + if (t == T_UNDEF || t == T_NULL) return js_mkundef(); + + if (t != T_OBJ && t != T_ARR && t != T_FUNC) + return js_mkerr_typed(js, JS_ERR_TYPE, + "Failed to construct 'Blob': The provided value cannot be converted to a sequence."); + + ant_value_t iter_fn = js_get_sym(js, parts, get_iterator_sym()); + if (vtype(iter_fn) != T_FUNC && vtype(iter_fn) != T_CFUNC) + return js_mkerr_typed(js, JS_ERR_TYPE, + "Failed to construct 'Blob': The provided value is not of type 'BlobPart'"); + + ant_value_t iter = sv_call_native(js, iter_fn, parts, NULL, 0); + if (is_err(iter)) return iter; + + ant_value_t next_fn = js_getprop_fallback(js, iter, "next"); + + for (;;) { + ant_value_t result = sv_call_native(js, next_fn, iter, NULL, 0); + if (is_err(result)) return result; + if (js_truthy(js, js_get(js, result, "done"))) break; + + ant_value_t r = process_blob_part(js, buf, js_get(js, result, "value")); + if (is_err(r)) return r; + } + return js_mkundef(); +} + +static void blob_finalize(ant_t *js, ant_object_t *obj) { + (void)js; + if (!obj->extra_slots) return; + ant_extra_slot_t *entries = (ant_extra_slot_t *)obj->extra_slots; + for (uint8_t i = 0; i < obj->extra_count; i++) { + if (entries[i].slot == SLOT_DATA && vtype(entries[i].value) == T_NUM) { + blob_data_t *bd = (blob_data_t *)(uintptr_t)(size_t)js_getnum(entries[i].value); + if (bd) { free(bd->data); free(bd->type); free(bd->name); free(bd); } + return; + } + } +} + +ant_value_t blob_create(ant_t *js, const uint8_t *data, size_t size, const char *type) { + blob_data_t *bd = blob_data_new(data, size, type); + if (!bd) return js_mkerr(js, "out of memory"); + ant_value_t obj = js_mkobj(js); + js_set_proto_init(obj, g_blob_proto); + js_set_slot(obj, SLOT_DATA, ANT_PTR(bd)); + js_set_finalizer(obj, blob_finalize); + return obj; +} + +static ant_value_t blob_get_size(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + blob_data_t *bd = blob_get_data(js->this_val); + return js_mknum(bd ? (double)bd->size : 0); +} + +static ant_value_t blob_get_type(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + blob_data_t *bd = blob_get_data(js->this_val); + if (!bd || !bd->type) return js_mkstr(js, "", 0); + return js_mkstr(js, bd->type, strlen(bd->type)); +} + +static ant_value_t file_get_name(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + blob_data_t *bd = blob_get_data(js->this_val); + if (!bd || !bd->name) return js_mkstr(js, "", 0); + return js_mkstr(js, bd->name, strlen(bd->name)); +} + +static ant_value_t file_get_last_modified(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + blob_data_t *bd = blob_get_data(js->this_val); + return js_mknum(bd ? (double)bd->last_modified : 0); +} + +static ant_value_t js_blob_text(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + blob_data_t *bd = blob_get_data(js->this_val); + ant_value_t promise = js_mkpromise(js); + ant_value_t str = (!bd || bd->size == 0) + ? js_mkstr(js, "", 0) + : js_mkstr(js, (const char *)bd->data, bd->size); + js_resolve_promise(js, promise, str); + return promise; +} + +static ant_value_t js_blob_array_buffer(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + blob_data_t *bd = blob_get_data(js->this_val); + ant_value_t promise = js_mkpromise(js); + + size_t sz = (bd && bd->data) ? bd->size : 0; + ArrayBufferData *abd = create_array_buffer_data(sz); + if (!abd) { js_reject_promise(js, promise, js_mkerr(js, "out of memory")); return promise; } + if (sz > 0 && bd) memcpy(abd->data, bd->data, sz); + + js_resolve_promise(js, promise, create_arraybuffer_obj(js, abd)); + return promise; +} + +static ant_value_t js_blob_bytes(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + blob_data_t *bd = blob_get_data(js->this_val); + ant_value_t promise = js_mkpromise(js); + + size_t sz = (bd && bd->data) ? bd->size : 0; + ArrayBufferData *abd = create_array_buffer_data(sz); + if (!abd) { js_reject_promise(js, promise, js_mkerr(js, "out of memory")); return promise; } + if (sz > 0 && bd) memcpy(abd->data, bd->data, sz); + + js_resolve_promise(js, promise, + create_typed_array(js, TYPED_ARRAY_UINT8, abd, 0, sz, "Uint8Array")); + return promise; +} + +static ant_value_t js_blob_slice(ant_t *js, ant_value_t *args, int nargs) { + blob_data_t *bd = blob_get_data(js->this_val); + size_t blob_size = bd ? bd->size : 0; + + ssize_t start = 0; + if (nargs >= 1 && vtype(args[0]) != T_UNDEF) { + double d = js_to_number(js, args[0]); + start = (ssize_t)d; + if (start < 0) start = (ssize_t)blob_size + start; + if (start < 0) start = 0; + if ((size_t)start > blob_size) start = (ssize_t)blob_size; + } + + ssize_t end = (ssize_t)blob_size; + if (nargs >= 2 && vtype(args[1]) != T_UNDEF) { + double d = js_to_number(js, args[1]); + end = (ssize_t)d; + if (end < 0) end = (ssize_t)blob_size + end; + if (end < 0) end = 0; + if ((size_t)end > blob_size) end = (ssize_t)blob_size; + } + + if (end < start) end = start; + + size_t new_size = (size_t)(end - start); + const uint8_t *src = (bd && bd->data && new_size > 0) ? (bd->data + start) : NULL; + + const char *new_type = (bd && bd->type) ? bd->type : ""; + char *type_owned = NULL; + if (nargs >= 3 && vtype(args[2]) != T_UNDEF) { + ant_value_t tv = args[2]; + if (vtype(tv) != T_STR) { tv = js_tostring_val(js, tv); if (is_err(tv)) return tv; } + type_owned = normalize_mime_type(js_getstr(js, tv, NULL)); + new_type = type_owned; + } + + ant_value_t result = blob_create(js, src, new_size, new_type); + free(type_owned); + return result; +} + +static ant_value_t js_blob_stream(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + return js_mkerr_typed(js, JS_ERR_TYPE, "Blob.stream() is not yet implemented"); +} + +static ant_value_t js_blob_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "Blob constructor requires 'new'"); + + byte_buf_t buf = {NULL, 0, 0}; + + if (nargs >= 1 && vtype(args[0]) != T_UNDEF) { + uint8_t pt = vtype(args[0]); + if (pt != T_OBJ && pt != T_ARR && pt != T_FUNC) + return js_mkerr_typed(js, JS_ERR_TYPE, + "Failed to construct 'Blob': The provided value cannot be converted to a sequence."); + ant_value_t r = process_blob_parts(js, &buf, args[0]); + if (is_err(r)) { free(buf.buf); return r; } + } + + const char *type_str = ""; + char *type_owned = NULL; + + if (nargs >= 2 && vtype(args[1]) != T_UNDEF && vtype(args[1]) != T_NULL) { + uint8_t ot = vtype(args[1]); + if (ot != T_OBJ && ot != T_ARR && ot != T_FUNC && ot != T_CFUNC) { + free(buf.buf); + return js_mkerr_typed(js, JS_ERR_TYPE, + "Failed to construct 'Blob': The 'options' argument is not an object."); + } + // access "endings" before "type" per lexicographic order (WPT requirement) + (void)js_get(js, args[1], "endings"); + ant_value_t type_v = js_get(js, args[1], "type"); + if (vtype(type_v) != T_UNDEF) { + if (vtype(type_v) != T_STR) { + type_v = js_tostring_val(js, type_v); + if (is_err(type_v)) { free(buf.buf); return type_v; } + } + type_owned = normalize_mime_type(js_getstr(js, type_v, NULL)); + type_str = type_owned; + } + } + + blob_data_t *bd = blob_data_new(buf.buf, buf.size, type_str); + free(buf.buf); free(type_owned); + if (!bd) return js_mkerr(js, "out of memory"); + + ant_value_t obj = js_mkobj(js); + ant_value_t proto = js_instance_proto_from_new_target(js, g_blob_proto); + if (is_object_type(proto)) js_set_proto_init(obj, proto); + + js_set_slot(obj, SLOT_DATA, ANT_PTR(bd)); + js_set_finalizer(obj, blob_finalize); + return obj; +} + +static ant_value_t js_file_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "File constructor requires 'new'"); + if (nargs < 2) + return js_mkerr_typed(js, JS_ERR_TYPE, "File constructor requires at least 2 arguments"); + + byte_buf_t buf = {NULL, 0, 0}; + + if (vtype(args[0]) != T_UNDEF) { + uint8_t pt = vtype(args[0]); + if (pt != T_OBJ && pt != T_ARR && pt != T_FUNC) { + return js_mkerr_typed(js, JS_ERR_TYPE, + "Failed to construct 'File': The provided value cannot be converted to a sequence."); + } + ant_value_t r = process_blob_parts(js, &buf, args[0]); + if (is_err(r)) { free(buf.buf); return r; } + } + + ant_value_t name_v = args[1]; + if (vtype(name_v) != T_STR) { + name_v = js_tostring_val(js, name_v); + if (is_err(name_v)) { free(buf.buf); return name_v; } + } + const char *name_str = js_getstr(js, name_v, NULL); + + const char *type_str = ""; + char *type_owned = NULL; + + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + int64_t last_modified = (int64_t)ts.tv_sec * 1000LL + (int64_t)(ts.tv_nsec / 1000000); + + if (nargs >= 3 && vtype(args[2]) != T_UNDEF && vtype(args[2]) != T_NULL) { + ant_value_t opts = args[2]; + uint8_t ot = vtype(opts); + if (ot == T_OBJ || ot == T_ARR) { + ant_value_t type_v = js_get(js, opts, "type"); + if (vtype(type_v) != T_UNDEF) { + if (vtype(type_v) != T_STR) { + type_v = js_tostring_val(js, type_v); + if (is_err(type_v)) { free(buf.buf); return type_v; } + } + type_owned = normalize_mime_type(js_getstr(js, type_v, NULL)); + type_str = type_owned; + } + ant_value_t lm_v = js_get(js, opts, "lastModified"); + if (vtype(lm_v) != T_UNDEF) { + double d = js_to_number(js, lm_v); + if (d == d) last_modified = (int64_t)d; + } + } + } + + blob_data_t *bd = blob_data_new(buf.buf, buf.size, type_str); + free(buf.buf); free(type_owned); + if (!bd) return js_mkerr(js, "out of memory"); + + bd->name = strdup(name_str ? name_str : ""); + bd->last_modified = last_modified; + + ant_value_t obj = js_mkobj(js); + ant_value_t proto = js_instance_proto_from_new_target(js, g_file_proto); + if (is_object_type(proto)) js_set_proto_init(obj, proto); + + js_set_slot(obj, SLOT_DATA, ANT_PTR(bd)); + js_set_finalizer(obj, blob_finalize); + + return obj; +} + +static ant_value_t make_ctor(ant_t *js, ant_cfunc_t fn, ant_value_t proto, const char *name, size_t nlen) { + ant_value_t obj = js_mkobj(js); + js_set_slot(obj, SLOT_CFUNC, js_mkfun(fn)); + js_mkprop_fast(js, obj, "prototype", 9, proto); + js_mkprop_fast(js, obj, "name", 4, js_mkstr(js, name, nlen)); + js_set_descriptor(js, obj, "name", 4, 0); + + ant_value_t fn_val = js_obj_to_func(obj); + js_set(js, proto, "constructor", fn_val); + js_set_descriptor(js, proto, "constructor", 11, JS_DESC_W | JS_DESC_C); + + return fn_val; +} + +void init_blob_module(void) { + ant_t *js = rt->js; + ant_value_t g = js_glob(js); + + g_blob_proto = js_mkobj(js); + + js_set_getter_desc(js, g_blob_proto, "size", 4, js_mkfun(blob_get_size), JS_DESC_C); + js_set_getter_desc(js, g_blob_proto, "type", 4, js_mkfun(blob_get_type), JS_DESC_C); + + js_set(js, g_blob_proto, "text", js_mkfun(js_blob_text)); + js_set(js, g_blob_proto, "arrayBuffer", js_mkfun(js_blob_array_buffer)); + js_set(js, g_blob_proto, "bytes", js_mkfun(js_blob_bytes)); + js_set(js, g_blob_proto, "slice", js_mkfun(js_blob_slice)); + js_set(js, g_blob_proto, "stream", js_mkfun(js_blob_stream)); + + js_set_sym(js, g_blob_proto, get_toStringTag_sym(), js_mkstr(js, "Blob", 4)); + ant_value_t blob_ctor = make_ctor(js, js_blob_ctor, g_blob_proto, "Blob", 4); + + js_set(js, g, "Blob", blob_ctor); + js_set_descriptor(js, g, "Blob", 4, JS_DESC_W | JS_DESC_C); + + g_file_proto = js_mkobj(js); + js_set_proto_init(g_file_proto, g_blob_proto); + + js_set_getter_desc(js, g_file_proto, "name", 4, js_mkfun(file_get_name), JS_DESC_C); + js_set_getter_desc(js, g_file_proto, "lastModified", 12, js_mkfun(file_get_last_modified), JS_DESC_C); + + js_set_sym(js, g_file_proto, get_toStringTag_sym(), js_mkstr(js, "File", 4)); + ant_value_t file_ctor = make_ctor(js, js_file_ctor, g_file_proto, "File", 4); + + js_set(js, g, "File", file_ctor); + js_set_descriptor(js, g, "File", 4, JS_DESC_W | JS_DESC_C); +} diff --git a/src/modules/collections.c b/src/modules/collections.c index 0e87672..d99a3a4 100644 --- a/src/modules/collections.c +++ b/src/modules/collections.c @@ -12,6 +12,9 @@ #include "modules/collections.h" #include "modules/symbol.h" +ant_value_t g_map_iter_proto = 0; +ant_value_t g_set_iter_proto = 0; + static const char *ant_value_to_key(ant_t *js, ant_value_t val) { if (vtype(val) == T_STR) { ant_offset_t len; @@ -232,10 +235,8 @@ static ant_value_t create_map_iterator(ant_t *js, ant_value_t map_obj, iter_type state->type = type; ant_value_t iter = js_mkobj(js); + js_set_proto_init(iter, g_map_iter_proto); js_set_slot(iter, SLOT_ITER_STATE, ANT_PTR(state)); - js_set(js, iter, "next", js_mkfun(map_iter_next)); - - js_set_sym(js, iter, get_iterator_sym(), js_mkfun(sym_this_cb)); return iter; } @@ -305,10 +306,8 @@ static ant_value_t create_set_iterator(ant_t *js, ant_value_t set_obj, iter_type state->type = type; ant_value_t iter = js_mkobj(js); + js_set_proto_init(iter, g_set_iter_proto); js_set_slot(iter, SLOT_ITER_STATE, ANT_PTR(state)); - js_set(js, iter, "next", js_mkfun(set_iter_next)); - - js_set_sym(js, iter, get_iterator_sym(), js_mkfun(sym_this_cb)); return iter; } @@ -943,6 +942,14 @@ void init_collections_module(void) { ant_value_t iter_sym = get_iterator_sym(); ant_value_t tag_sym = get_toStringTag_sym(); + g_map_iter_proto = js_mkobj(js); + js_set_proto_init(g_map_iter_proto, js->sym.iterator_proto); + js_set(js, g_map_iter_proto, "next", js_mkfun(map_iter_next)); + + g_set_iter_proto = js_mkobj(js); + js_set_proto_init(g_set_iter_proto, js->sym.iterator_proto); + js_set(js, g_set_iter_proto, "next", js_mkfun(set_iter_next)); + ant_value_t map_proto = js_mkobj(js); js_set_proto_init(map_proto, object_proto); js_set(js, map_proto, "set", js_mkfun(map_set)); diff --git a/src/modules/headers.c b/src/modules/headers.c new file mode 100644 index 0000000..b4a21a1 --- /dev/null +++ b/src/modules/headers.c @@ -0,0 +1,611 @@ +#include +#include +#include +#include + +#include "ant.h" +#include "errors.h" +#include "runtime.h" +#include "internal.h" +#include "descriptors.h" +#include "silver/engine.h" + +#include "modules/headers.h" +#include "modules/symbol.h" + +typedef struct hdr_entry { + char *name; + char *value; + struct hdr_entry *next; +} hdr_entry_t; + +typedef struct { + hdr_entry_t *head; + hdr_entry_t **tail; + size_t count; +} hdr_list_t; + +typedef struct { + char *name; + char *value; +} sorted_pair_t; + +typedef struct { + hdr_list_t *list; + size_t index; + int kind; +} hdr_iter_t; + +enum { + ITER_ENTRIES = 0, + ITER_KEYS = 1, + ITER_VALUES = 2 +}; + +static ant_value_t g_headers_proto = 0; +static ant_value_t g_headers_iter_proto = 0; + +static hdr_list_t *list_new(void) { + hdr_list_t *l = ant_calloc(sizeof(hdr_list_t)); + if (!l) return NULL; + l->head = NULL; + l->tail = &l->head; + return l; +} + +static void list_free(hdr_list_t *l) { + if (!l) return; + for (hdr_entry_t *e = l->head; e; ) { + hdr_entry_t *n = e->next; + free(e->name); free(e->value); free(e); + e = n; + } + free(l); +} + +static hdr_list_t *get_list(ant_value_t obj) { + ant_value_t slot = js_get_slot(obj, SLOT_DATA); + if (vtype(slot) != T_NUM) return NULL; + return (hdr_list_t *)(uintptr_t)(size_t)js_getnum(slot); +} + +static bool is_token_char(unsigned char c) { + if (c > 127) return false; + static const char ok[] = + "!#$%&'*+-.^_`|~" + "0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + return strchr(ok, (char)c) != NULL; +} + +static bool is_valid_name(const char *s) { + if (!s || !*s) return false; + for (const unsigned char *p = (const unsigned char *)s; *p; p++) + if (!is_token_char(*p)) return false; + return true; +} + +static bool is_valid_value(const char *s) { + if (!s) return false; + for (const unsigned char *p = (const unsigned char *)s; *p; p++) { + unsigned char c = *p; + if (c == 0 || c == '\r' || c == '\n' || c > 127) return false; + } + return true; +} + +static char *normalize_value(const char *s) { + if (!s) return strdup(""); + while (*s == ' ' || *s == '\t') s++; + + size_t len = strlen(s); + while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t')) len--; + + char *out = malloc(len + 1); + if (!out) return NULL; + + memcpy(out, s, len); + out[len] = '\0'; + + return out; +} + +static char *lowercase_dup(const char *s) { + if (!s) return strdup(""); + size_t len = strlen(s); + char *out = malloc(len + 1); + if (!out) return NULL; + for (size_t i = 0; i <= len; i++) + out[i] = (char)tolower((unsigned char)s[i]); + return out; +} + +static void list_append_raw(hdr_list_t *l, const char *lower_name, const char *value) { + hdr_entry_t *e = ant_calloc(sizeof(hdr_entry_t)); + if (!e) return; + e->name = strdup(lower_name); + e->value = strdup(value); + *l->tail = e; + l->tail = &e->next; + l->count++; +} + +static void list_delete_name(hdr_list_t *l, const char *lower_name) { + hdr_entry_t **pp = &l->head; + l->tail = &l->head; + while (*pp) { + if (strcmp((*pp)->name, lower_name) == 0) { + hdr_entry_t *dead = *pp; + *pp = dead->next; + free(dead->name); free(dead->value); free(dead); + l->count--; + } else { + l->tail = &(*pp)->next; + pp = &(*pp)->next; + }} +} + +static int cmp_pairs(const void *a, const void *b) { + return strcmp(((const sorted_pair_t *)a)->name, ((const sorted_pair_t *)b)->name); +} + +static sorted_pair_t *build_sorted_view(hdr_list_t *l, size_t *out) { + *out = 0; + if (!l || l->count == 0) return NULL; + + sorted_pair_t *raw = malloc(l->count * sizeof(sorted_pair_t)); + if (!raw) return NULL; + + size_t n = 0; + for (hdr_entry_t *e = l->head; e; e = e->next) { + raw[n].name = e->name; + raw[n].value = e->value; + n++; + } + + qsort(raw, n, sizeof(sorted_pair_t), cmp_pairs); + sorted_pair_t *res = malloc(n * sizeof(sorted_pair_t)); + if (!res) { free(raw); return NULL; } + + size_t ri = 0; + for (size_t i = 0; i < n; ) { + if (strcmp(raw[i].name, "set-cookie") == 0) { + res[ri].name = strdup(raw[i].name); + res[ri].value = strdup(raw[i].value); + ri++; i++; + } else { + size_t j = i + 1; + size_t total = strlen(raw[i].value); + while (j < n && strcmp(raw[j].name, raw[i].name) == 0) { + total += 2 + strlen(raw[j].value); + j++; + } + char *combined = malloc(total + 1); + if (!combined) combined = strdup(""); + size_t pos = 0; + for (size_t k = i; k < j; k++) { + if (k > i) { combined[pos++] = ','; combined[pos++] = ' '; } + size_t vl = strlen(raw[k].value); + memcpy(combined + pos, raw[k].value, vl); + pos += vl; + } + combined[pos] = '\0'; + res[ri].name = strdup(raw[i].name); + res[ri].value = combined; + ri++; i = j; + }} + + free(raw); + *out = ri; + + return res; +} + +static void free_sorted_view(sorted_pair_t *v, size_t n) { + if (!v) return; + for (size_t i = 0; i < n; i++) { free(v[i].name); free(v[i].value); } + free(v); +} + +static ant_value_t headers_append_pair(ant_t *js, hdr_list_t *l, ant_value_t name_v, ant_value_t value_v) { + if (vtype(name_v) != T_STR) { + name_v = js_tostring_val(js, name_v); + if (is_err(name_v)) return name_v; + } + + if (vtype(value_v) != T_STR) { + value_v = js_tostring_val(js, value_v); + if (is_err(value_v)) return value_v; + } + + const char *name = js_getstr(js, name_v, NULL); + const char *value = js_getstr(js, value_v, NULL); + + if (!is_valid_name(name)) + return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid header name: %s", name ? name : ""); + + char *norm = normalize_value(value); + if (!norm) return js_mkerr(js, "out of memory"); + if (!is_valid_value(norm)) { + free(norm); + return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid header value"); + } + + char *lower = lowercase_dup(name); + if (!lower) { free(norm); return js_mkerr(js, "out of memory"); } + + list_append_raw(l, lower, norm); + free(lower); free(norm); + return js_mkundef(); +} + +static ant_value_t init_from_sequence(ant_t *js, hdr_list_t *l, ant_value_t seq) { + ant_value_t iter_fn = js_get_sym(js, seq, get_iterator_sym()); + if (vtype(iter_fn) != T_FUNC && vtype(iter_fn) != T_CFUNC) + return js_mkerr_typed(js, JS_ERR_TYPE, "Headers init is not iterable"); + + ant_value_t iter = sv_call_native(js, iter_fn, seq, NULL, 0); + if (is_err(iter)) return iter; + + ant_value_t next_fn = js_getprop_fallback(js, iter, "next"); + + for (;;) { + ant_value_t result = sv_call_native(js, next_fn, iter, NULL, 0); + if (is_err(result)) return result; + if (js_truthy(js, js_get(js, result, "done"))) break; + + ant_value_t pair = js_get(js, result, "value"); + uint8_t pt = vtype(pair); + if (pt != T_ARR && pt != T_OBJ) + return js_mkerr_typed(js, JS_ERR_TYPE, "Each header init pair must be a sequence"); + + if (js_arr_len(js, pair) != 2) + return js_mkerr_typed(js, JS_ERR_TYPE, + "Each header init pair must have exactly 2 elements"); + + ant_value_t r = headers_append_pair(js, l, js_arr_get(js, pair, 0), + js_arr_get(js, pair, 1)); + if (is_err(r)) return r; + } + return js_mkundef(); +} + +// iterate own enumerable properties as a record +static ant_value_t init_from_record(ant_t *js, hdr_list_t *l, ant_value_t obj) { + ant_iter_t it = js_prop_iter_begin(js, obj); + const char *key; + size_t key_len; + ant_value_t val; + + while (js_prop_iter_next(&it, &key, &key_len, &val)) { + ant_value_t r = headers_append_pair(js, l, js_mkstr(js, key, key_len), val); + if (is_err(r)) { js_prop_iter_end(&it); return r; } + } + js_prop_iter_end(&it); + return js_mkundef(); +} + +static ant_value_t headers_iter_next(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_val = js_get_slot(js->this_val, SLOT_ITER_STATE); + if (vtype(state_val) == T_UNDEF) return js_mkerr(js, "Invalid iterator"); + hdr_iter_t *st = (hdr_iter_t *)(uintptr_t)(size_t)js_getnum(state_val); + + size_t count = 0; + sorted_pair_t *view = build_sorted_view(st->list, &count); + + ant_value_t result = js_mkobj(js); + + if (st->index >= count) { + free_sorted_view(view, count); + js_set(js, result, "done", js_true); + js_set(js, result, "value", js_mkundef()); + return result; + } + + sorted_pair_t *e = &view[st->index]; + ant_value_t value; + + switch (st->kind) { + case ITER_KEYS: + value = js_mkstr(js, e->name, strlen(e->name)); + break; + case ITER_VALUES: + value = js_mkstr(js, e->value, strlen(e->value)); + break; + default: { + value = js_mkarr(js); + js_arr_push(js, value, js_mkstr(js, e->name, strlen(e->name))); + js_arr_push(js, value, js_mkstr(js, e->value, strlen(e->value))); + break; + }} + + free_sorted_view(view, count); + st->index++; + + js_set(js, result, "done", js_false); + js_set(js, result, "value", value); + return result; +} + +static ant_value_t make_headers_iter(ant_t *js, ant_value_t headers_obj, int kind) { + hdr_list_t *l = get_list(headers_obj); + + hdr_iter_t *st = ant_calloc(sizeof(hdr_iter_t)); + if (!st) return js_mkerr(js, "out of memory"); + st->list = l ? l : list_new(); + st->kind = kind; + + ant_value_t iter = js_mkobj(js); + js_set_proto_init(iter, g_headers_iter_proto); + js_set_slot(iter, SLOT_ITER_STATE, ANT_PTR(st)); + return iter; +} + +static ant_value_t js_headers_append(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkerr_typed(js, JS_ERR_TYPE, "Headers.append requires 2 arguments"); + hdr_list_t *l = get_list(js->this_val); + if (!l) return js_mkerr(js, "Invalid Headers object"); + return headers_append_pair(js, l, args[0], args[1]); +} + +static ant_value_t js_headers_set(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2) return js_mkerr_typed(js, JS_ERR_TYPE, "Headers.set requires 2 arguments"); + hdr_list_t *l = get_list(js->this_val); + if (!l) return js_mkerr(js, "Invalid Headers object"); + + ant_value_t name_v = args[0]; + ant_value_t value_v = args[1]; + if (vtype(name_v) != T_STR) { name_v = js_tostring_val(js, name_v); if (is_err(name_v)) return name_v; } + if (vtype(value_v) != T_STR) { value_v = js_tostring_val(js, value_v); if (is_err(value_v)) return value_v; } + + const char *name = js_getstr(js, name_v, NULL); + const char *value = js_getstr(js, value_v, NULL); + + if (!is_valid_name(name)) + return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid header name: %s", name ? name : ""); + + char *norm = normalize_value(value); + if (!norm) return js_mkerr(js, "out of memory"); + if (!is_valid_value(norm)) { free(norm); return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid header value"); } + + char *lower = lowercase_dup(name); + if (!lower) { free(norm); return js_mkerr(js, "out of memory"); } + + list_delete_name(l, lower); + list_append_raw(l, lower, norm); + free(lower); free(norm); + return js_mkundef(); +} + +static ant_value_t js_headers_get(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "Headers.get requires 1 argument"); + hdr_list_t *l = get_list(js->this_val); + if (!l) return js_mknull(); + + ant_value_t name_v = args[0]; + if (vtype(name_v) != T_STR) { name_v = js_tostring_val(js, name_v); if (is_err(name_v)) return name_v; } + const char *name = js_getstr(js, name_v, NULL); + if (!is_valid_name(name)) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid header name"); + + char *lower = lowercase_dup(name); + if (!lower) return js_mkerr(js, "out of memory"); + + // set-cookie is never combined per Fetch spec + if (strcmp(lower, "set-cookie") == 0) { + for (hdr_entry_t *e = l->head; e; e = e->next) { + if (strcmp(e->name, lower) == 0) { + ant_value_t ret = js_mkstr(js, e->value, strlen(e->value)); + free(lower); + return ret; + } + } + free(lower); + return js_mknull(); + } + + size_t total = 0; + int count = 0; + for (hdr_entry_t *e = l->head; e; e = e->next) { + if (strcmp(e->name, lower) == 0) { + if (count > 0) total += 2; + total += strlen(e->value); + count++; + } + } + + if (count == 0) { free(lower); return js_mknull(); } + + char *combined = malloc(total + 1); + if (!combined) { free(lower); return js_mkerr(js, "out of memory"); } + + size_t pos = 0; + int seen = 0; + for (hdr_entry_t *e = l->head; e; e = e->next) { + if (strcmp(e->name, lower) == 0) { + if (seen > 0) { combined[pos++] = ','; combined[pos++] = ' '; } + size_t vl = strlen(e->value); + memcpy(combined + pos, e->value, vl); + pos += vl; + seen++; + } + } + combined[pos] = '\0'; + free(lower); + + ant_value_t ret = js_mkstr(js, combined, pos); + free(combined); + return ret; +} + +static ant_value_t js_headers_has(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "Headers.has requires 1 argument"); + hdr_list_t *l = get_list(js->this_val); + if (!l) return js_false; + + ant_value_t name_v = args[0]; + if (vtype(name_v) != T_STR) { name_v = js_tostring_val(js, name_v); if (is_err(name_v)) return name_v; } + const char *name = js_getstr(js, name_v, NULL); + if (!is_valid_name(name)) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid header name"); + + char *lower = lowercase_dup(name); + if (!lower) return js_mkerr(js, "out of memory"); + + bool found = false; + for (hdr_entry_t *e = l->head; e; e = e->next) { + if (strcmp(e->name, lower) == 0) { found = true; break; } + } + free(lower); + return js_bool(found); +} + +static ant_value_t js_headers_delete(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "Headers.delete requires 1 argument"); + hdr_list_t *l = get_list(js->this_val); + if (!l) return js_mkundef(); + + ant_value_t name_v = args[0]; + if (vtype(name_v) != T_STR) { name_v = js_tostring_val(js, name_v); if (is_err(name_v)) return name_v; } + const char *name = js_getstr(js, name_v, NULL); + if (!is_valid_name(name)) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid header name"); + + char *lower = lowercase_dup(name); + if (!lower) return js_mkerr(js, "out of memory"); + list_delete_name(l, lower); + free(lower); + return js_mkundef(); +} + +static ant_value_t js_headers_get_set_cookie(ant_t *js, ant_value_t *args, int nargs) { + (void)args; (void)nargs; + hdr_list_t *l = get_list(js->this_val); + ant_value_t arr = js_mkarr(js); + if (!l) return arr; + for (hdr_entry_t *e = l->head; e; e = e->next) { + if (strcmp(e->name, "set-cookie") == 0) + js_arr_push(js, arr, js_mkstr(js, e->value, strlen(e->value))); + } + return arr; +} + +static ant_value_t js_headers_for_each(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "Headers.forEach requires 1 argument"); + + ant_value_t cb = args[0]; + uint8_t cbt = vtype(cb); + if (cbt != T_FUNC && cbt != T_CFUNC) + return js_mkerr_typed(js, JS_ERR_TYPE, "Headers.forEach callback must be callable"); + + ant_value_t this_obj = js->this_val; + hdr_list_t *l = get_list(this_obj); + if (!l) return js_mkundef(); + + ant_value_t this_arg = (nargs >= 2) ? args[1] : js_mkundef(); + + size_t count = 0; + sorted_pair_t *view = build_sorted_view(l, &count); + + for (size_t i = 0; i < count; i++) { + ant_value_t call_args[3] = { + js_mkstr(js, view[i].value, strlen(view[i].value)), + js_mkstr(js, view[i].name, strlen(view[i].name)), + this_obj + }; + ant_value_t r = sv_vm_call(js->vm, js, cb, this_arg, call_args, 3, NULL, false); + if (is_err(r)) { free_sorted_view(view, count); return r; } + } + + free_sorted_view(view, count); + return js_mkundef(); +} + +static ant_value_t js_headers_keys(ant_t *js, ant_value_t *args, int nargs) { + return make_headers_iter(js, js->this_val, ITER_KEYS); +} + +static ant_value_t js_headers_values(ant_t *js, ant_value_t *args, int nargs) { + return make_headers_iter(js, js->this_val, ITER_VALUES); +} + +static ant_value_t js_headers_entries(ant_t *js, ant_value_t *args, int nargs) { + return make_headers_iter(js, js->this_val, ITER_ENTRIES); +} + +static ant_value_t js_headers_symbol_iterator(ant_t *js, ant_value_t *args, int nargs) { + return make_headers_iter(js, js->this_val, ITER_ENTRIES); +} + +static ant_value_t js_headers_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "Headers constructor requires 'new'"); + + hdr_list_t *l = list_new(); + if (!l) return js_mkerr(js, "out of memory"); + + ant_value_t init = (nargs >= 1) ? args[0] : js_mkundef(); + + if (vtype(init) != T_UNDEF) { + uint8_t t = vtype(init); + + if (t == T_NULL || (t != T_OBJ && t != T_ARR && t != T_FUNC && t != T_CFUNC)) { + list_free(l); + return js_mkerr_typed(js, JS_ERR_TYPE, + "Failed to construct 'Headers': The provided value is not of type 'HeadersInit'"); + } + + ant_value_t iter_fn = js_get_sym(js, init, get_iterator_sym()); + bool has_iter = (vtype(iter_fn) == T_FUNC || vtype(iter_fn) == T_CFUNC); + + ant_value_t r; + if (t == T_ARR || has_iter) r = init_from_sequence(js, l, init); + else r = init_from_record(js, l, init); + if (is_err(r)) { list_free(l); return r; } + } + + ant_value_t obj = js_mkobj(js); + ant_value_t proto = js_instance_proto_from_new_target(js, g_headers_proto); + if (is_object_type(proto)) js_set_proto_init(obj, proto); + + js_set_slot(obj, SLOT_DATA, ANT_PTR(l)); + return obj; +} + +void init_headers_module(void) { + ant_t *js = rt->js; + ant_value_t g = js_glob(js); + + g_headers_iter_proto = js_mkobj(js); + js_set_proto_init(g_headers_iter_proto, js->sym.iterator_proto); + js_set(js, g_headers_iter_proto, "next", js_mkfun(headers_iter_next)); + js_set_descriptor(js, g_headers_iter_proto, "next", 4, JS_DESC_W | JS_DESC_E | JS_DESC_C); + js_set_sym(js, g_headers_iter_proto, get_iterator_sym(), js_mkfun(sym_this_cb)); + + g_headers_proto = js_mkobj(js); + + js_set(js, g_headers_proto, "append", js_mkfun(js_headers_append)); + js_set(js, g_headers_proto, "set", js_mkfun(js_headers_set)); + js_set(js, g_headers_proto, "get", js_mkfun(js_headers_get)); + js_set(js, g_headers_proto, "has", js_mkfun(js_headers_has)); + js_set(js, g_headers_proto, "delete", js_mkfun(js_headers_delete)); + js_set(js, g_headers_proto, "forEach", js_mkfun(js_headers_for_each)); + js_set(js, g_headers_proto, "keys", js_mkfun(js_headers_keys)); + js_set(js, g_headers_proto, "values", js_mkfun(js_headers_values)); + js_set(js, g_headers_proto, "entries", js_mkfun(js_headers_entries)); + js_set(js, g_headers_proto, "getSetCookie", js_mkfun(js_headers_get_set_cookie)); + + js_set_sym(js, g_headers_proto, get_iterator_sym(), js_mkfun(js_headers_symbol_iterator)); + js_set_sym_getter_desc(js, g_headers_proto, get_toStringTag_sym(), js_mkfun(sym_this_cb), 0); + + ant_value_t ctor_obj = js_mkobj(js); + js_set_slot(ctor_obj, SLOT_CFUNC, js_mkfun(js_headers_ctor)); + js_mkprop_fast(js, ctor_obj, "prototype", 9, g_headers_proto); + js_mkprop_fast(js, ctor_obj, "name", 4, js_mkstr(js, "Headers", 7)); + js_set_descriptor(js, ctor_obj, "name", 4, 0); + + ant_value_t ctor = js_obj_to_func(ctor_obj); + js_set(js, g_headers_proto, "constructor", ctor); + js_set_descriptor(js, g_headers_proto, "constructor", 11, JS_DESC_W | JS_DESC_C); + + js_set(js, g, "Headers", ctor); + js_set_descriptor(js, g, "Headers", 7, JS_DESC_W | JS_DESC_C); +} diff --git a/src/modules/observable.c b/src/modules/observable.c index 3c5d979..07e79e9 100644 --- a/src/modules/observable.c +++ b/src/modules/observable.c @@ -447,16 +447,7 @@ static ant_value_t js_observable_from(ant_t *js, ant_value_t *args, int nargs) { } ant_value_t iteratorMethod = js_get_sym(js, x, get_iterator_sym()); - - if (!is_callable(iteratorMethod) && vtype(x) == T_ARR) { - ant_value_t array_ctor = js_get(js, js_glob(js), "Array"); - ant_value_t array_proto = js_get(js, array_ctor, "prototype"); - iteratorMethod = js_get_sym(js, array_proto, get_iterator_sym()); - } - - if (!is_callable(iteratorMethod)) { - return js_mkerr_typed(js, JS_ERR_TYPE, "Object is not observable or iterable"); - } + if (!is_callable(iteratorMethod)) return js_mkerr_typed(js, JS_ERR_TYPE, "Object is not observable or iterable"); ant_value_t data = js_mkobj(js); js_set(js, data, "iterable", x); diff --git a/src/silver/ops/iteration.h b/src/silver/ops/iteration.h index 2af4e99..89237e0 100644 --- a/src/silver/ops/iteration.h +++ b/src/silver/ops/iteration.h @@ -28,12 +28,13 @@ static inline bool sv_is_map_iter( iter_type_t *out_type ) { if (vtype(obj) != T_OBJ) return false; + if (!g_map_iter_proto || js_get_proto(js, obj) != g_map_iter_proto) return false; + ant_value_t state_val = js_get_slot(obj, SLOT_ITER_STATE); if (vtype(state_val) == T_UNDEF) return false; map_iterator_state_t *st = (map_iterator_state_t *)(uintptr_t)js_getnum(state_val); if (!st) return false; - if (st->type > ITER_TYPE_MAP_ENTRIES) return false; *out_state = st; *out_type = st->type; @@ -47,15 +48,14 @@ static inline bool sv_is_set_iter( iter_type_t *out_type ) { if (vtype(obj) != T_OBJ) return false; + if (!g_set_iter_proto || js_get_proto(js, obj) != g_set_iter_proto) return false; + ant_value_t state_val = js_get_slot(obj, SLOT_ITER_STATE); if (vtype(state_val) == T_UNDEF) return false; set_iterator_state_t *st = (set_iterator_state_t *)(uintptr_t)js_getnum(state_val); if (!st) return false; - if (st->type != ITER_TYPE_SET_VALUES && st->type != ITER_TYPE_SET_ENTRIES) - return false; - *out_state = st; *out_type = st->type;