From bf6f95e6c30f85548bacd644b523dcde2da7ed0c Mon Sep 17 00:00:00 2001 From: theMackabu Date: Mon, 22 Dec 2025 22:40:05 -0800 Subject: [PATCH] add local storage module with file persistence --- examples/spec/localstorage.js | 94 ++++++++++ include/ant.h | 1 + include/modules/atomics.h | 1 - include/modules/localstorage.h | 6 + include/runtime.h | 14 +- meson.build | 2 +- src/ant.c | 9 +- src/main.c | 7 +- src/modules/localstorage.c | 321 +++++++++++++++++++++++++++++++++ src/modules/sessionstorage.c | 2 +- src/runtime.c | 3 +- 11 files changed, 442 insertions(+), 18 deletions(-) create mode 100644 examples/spec/localstorage.js create mode 100644 include/modules/localstorage.h create mode 100644 src/modules/localstorage.c diff --git a/examples/spec/localstorage.js b/examples/spec/localstorage.js new file mode 100644 index 0000000..08a6f30 --- /dev/null +++ b/examples/spec/localstorage.js @@ -0,0 +1,94 @@ +import { test, testThrows, summary } from './helpers.js'; + +console.log('localStorage Tests\n'); + +test('localStorage exists', typeof localStorage, 'object'); + +test('setFile method exists', typeof localStorage.setFile, 'function'); + +testThrows('setItem throws without file', () => localStorage.setItem('x', 'y')); +testThrows('getItem throws without file', () => localStorage.getItem('x')); +testThrows('removeItem throws without file', () => localStorage.removeItem('x')); +testThrows('clear throws without file', () => localStorage.clear()); +testThrows('key throws without file', () => localStorage.key(0)); + +localStorage.setFile('storage.json'); + +localStorage.setItem('key1', 'value1'); +test('setItem/getItem basic', localStorage.getItem('key1'), 'value1'); + +localStorage.setItem('key1', 'newValue'); +test('setItem overwrites', localStorage.getItem('key1'), 'newValue'); + +test('getItem non-existent', localStorage.getItem('nonExistent'), null); + +localStorage.clear(); +test('length after clear', localStorage.length, 0); + +localStorage.setItem('a', '1'); +test('length after 1 item', localStorage.length, 1); + +localStorage.setItem('b', '2'); +test('length after 2 items', localStorage.length, 2); + +localStorage.setItem('c', '3'); +test('length after 3 items', localStorage.length, 3); + +let keys = []; +for (let i = 0; i < localStorage.length; i++) { + keys.push(localStorage.key(i)); +} +test('key() returns keys', keys.length, 3); +test('key() includes a', keys.includes('a'), true); +test('key() includes b', keys.includes('b'), true); +test('key() includes c', keys.includes('c'), true); + +test('key() out of bounds', localStorage.key(100), null); +test('key() negative index', localStorage.key(-1), null); + +localStorage.removeItem('b'); +test('removeItem decreases length', localStorage.length, 2); +test('removeItem removes key', localStorage.getItem('b'), null); +test('removeItem keeps others', localStorage.getItem('a'), '1'); + +localStorage.removeItem('nonExistent'); +test('removeItem non-existent key', localStorage.length, 2); + +localStorage.clear(); +test('clear removes all', localStorage.length, 0); +test('clear removes a', localStorage.getItem('a'), null); +test('clear removes c', localStorage.getItem('c'), null); + +localStorage.setItem('number', '42'); +test('store number string', localStorage.getItem('number'), '42'); + +localStorage.setItem('bool', 'true'); +test('store bool string', localStorage.getItem('bool'), 'true'); + +localStorage.setItem('empty', ''); +test('store empty string', localStorage.getItem('empty'), ''); + +localStorage.setItem('key-with-dash', 'dash'); +test('key with dash', localStorage.getItem('key-with-dash'), 'dash'); + +localStorage.setItem('key_with_underscore', 'underscore'); +test('key with underscore', localStorage.getItem('key_with_underscore'), 'underscore'); + +localStorage.setItem('key.with.dot', 'dot'); +test('key with dot', localStorage.getItem('key.with.dot'), 'dot'); + +localStorage.setItem('special', 'hello\nworld'); +test('value with newline', localStorage.getItem('special'), 'hello\nworld'); + +localStorage.setItem('unicode', 'Hello'); +test('value with unicode', localStorage.getItem('unicode'), 'Hello'); + +const obj = { name: 'John', age: 30 }; +localStorage.setItem('user', JSON.stringify(obj)); +const retrieved = JSON.parse(localStorage.getItem('user')); +test('JSON storage name', retrieved.name, 'John'); +test('JSON storage age', retrieved.age, 30); + +localStorage.clear(); + +summary(); diff --git a/include/ant.h b/include/ant.h index c8406e9..c8d2b17 100644 --- a/include/ant.h +++ b/include/ant.h @@ -81,6 +81,7 @@ void js_set_proto(struct js *, jsval_t obj, jsval_t proto); jsval_t js_get_proto(struct js *, jsval_t obj); jsval_t js_get_ctor_proto(struct js *, const char *name, size_t len); +jsval_t js_tostring_val(struct js *js, jsval_t value); int js_type(jsval_t val); int js_type_ex(struct js *js, jsval_t val); diff --git a/include/modules/atomics.h b/include/modules/atomics.h index bd46ef4..4d1cf6e 100644 --- a/include/modules/atomics.h +++ b/include/modules/atomics.h @@ -2,7 +2,6 @@ #define ATOMICS_H #include -#include #include void init_atomics_module(void); diff --git a/include/modules/localstorage.h b/include/modules/localstorage.h new file mode 100644 index 0000000..e0b17c0 --- /dev/null +++ b/include/modules/localstorage.h @@ -0,0 +1,6 @@ +#ifndef LOCALSTORAGE_H +#define LOCALSTORAGE_H + +void init_localstorage_module(void); + +#endif diff --git a/include/runtime.h b/include/runtime.h index bc3fa12..8d35254 100644 --- a/include/runtime.h +++ b/include/runtime.h @@ -2,19 +2,21 @@ #define RUNTIME_H #include "ant.h" +#include #define ANT_RUNTIME_CRYPTO_INIT (1u << 0) #define ANT_RUNTIME_EXT_EVENT_LOOP (1u << 1) struct ant_runtime { - struct js *js; - char **argv; - jsval_t ant_obj; - int argc; - unsigned int flags; + struct js *js; // offset 0 + char **argv; // offset 8 + jsval_t ant_obj; // offset 16 + int argc; // offset 24 + unsigned int flags; // offset 28 + const char *ls_fp; // offset 32 }; extern struct ant_runtime *const rt; -struct ant_runtime *ant_runtime_init(struct js *js, int argc, char **argv); +struct ant_runtime *ant_runtime_init(struct js *js, int argc, char **argv, struct arg_file *ls_p); #endif \ No newline at end of file diff --git a/meson.build b/meson.build index 11cac7c..63cbefd 100644 --- a/meson.build +++ b/meson.build @@ -75,7 +75,7 @@ endif build_date = run_command('date', '+%Y-%m-%d', check: true).stdout().strip() version_conf = configuration_data() -version_conf.set('ANT_VERSION', '0.2.0.10') +version_conf.set('ANT_VERSION', '0.2.0.11') version_conf.set('ANT_GIT_HASH', git_hash) version_conf.set('ANT_BUILD_DATE', build_date) diff --git a/src/ant.c b/src/ant.c index 1a6e614..02ef9c8 100644 --- a/src/ant.c +++ b/src/ant.c @@ -511,6 +511,7 @@ static inline jsoff_t esize(jsoff_t w); static bool parse_func_params(struct js *js, uint8_t *flags, int *out_count); static double js_to_number(struct js *js, jsval_t arg); +static jsval_t js_call_toString(struct js *js, jsval_t value); static jsval_t js_expr(struct js *js); static jsval_t js_eval_slice(struct js *js, jsoff_t off, jsoff_t len); @@ -559,7 +560,7 @@ static jsval_t call_js(struct js *js, const char *fn, jsoff_t fnlen, jsval_t clo static jsval_t call_js_internal(struct js *js, const char *fn, jsoff_t fnlen, jsval_t closure_scope, jsval_t *bound_args, int bound_argc); static jsval_t call_js_internal_nfe(struct js *js, const char *fn, jsoff_t fnlen, jsval_t closure_scope, jsval_t *bound_args, int bound_argc, jsval_t func_name, jsval_t func_val); -static jsval_t call_js_with_args(struct js *js, jsval_t func, jsval_t *args, int nargs); +static jsval_t call_js_with_args(struct js *js, jsval_t fn, jsval_t *args, int nargs); static jsval_t call_js_code_with_args(struct js *js, const char *fn, jsoff_t fnlen, jsval_t closure_scope, jsval_t *args, int nargs); static jsval_t call_js_code_with_args_nfe(struct js *js, const char *fn, jsoff_t fnlen, jsval_t closure_scope, jsval_t *args, int nargs, jsval_t func_name, jsval_t func_val); @@ -589,8 +590,6 @@ static jsval_t unwrap_primitive(struct js *js, jsval_t val) { return resolveprop(js, mkval(T_PROP, prim_off)); } -static jsval_t call_js_with_args(struct js *js, jsval_t fn, jsval_t *args, int nargs); - static jsval_t to_string_val(struct js *js, jsval_t val) { uint8_t t = vtype(val); if (t == T_STR) return val; @@ -2017,9 +2016,7 @@ static size_t tostr(struct js *js, jsval_t value, char *buf, size_t len) { } } -static jsval_t js_call_toString(struct js *js, jsval_t value); - -static jsval_t js_tostring_val(struct js *js, jsval_t value) { +jsval_t js_tostring_val(struct js *js, jsval_t value) { uint8_t t = vtype(value); char buf[256]; size_t len; diff --git a/src/main.c b/src/main.c index 96397ab..c6b578a 100644 --- a/src/main.c +++ b/src/main.c @@ -37,6 +37,7 @@ #include "modules/symbol.h" #include "modules/textcodec.h" #include "modules/sessionstorage.h" +#include "modules/localstorage.h" int js_result = EXIT_SUCCESS; @@ -140,10 +141,11 @@ int main(int argc, char *argv[]) { struct arg_int *gct = arg_int0(NULL, "gct", "", "set garbage collection threshold"); struct arg_int *initial_mem = arg_int0(NULL, "initial-mem", "", "initial memory size in MB (default: 4)"); struct arg_int *max_mem = arg_int0(NULL, "max-mem", "", "maximum memory size in MB (default: 512)"); + struct arg_file *localstorage_file = arg_file0(NULL, "localstorage-file", "", "file path for localStorage persistence"); struct arg_file *file = arg_file0(NULL, NULL, "", "JavaScript module file to execute"); struct arg_end *end = arg_end(20); - void *argtable[] = {help, version, debug, eval, print, gct, initial_mem, max_mem, file, end}; + void *argtable[] = {help, version, debug, eval, print, gct, initial_mem, max_mem, localstorage_file, file, end}; int nerrors = arg_parse(argc, argv, argtable); if (help->count > 0) { @@ -188,7 +190,7 @@ int main(int argc, char *argv[]) { } if (gct->count > 0) js_setgct(js, gct->ival[0]); - ant_runtime_init(js, argc, argv); + ant_runtime_init(js, argc, argv, localstorage_file); init_symbol_module(); init_builtin_module(); @@ -208,6 +210,7 @@ int main(int argc, char *argv[]) { init_reflect_module(); init_textcodec_module(); init_sessionstorage_module(); + init_localstorage_module(); ant_register_library(shell_library, "ant:shell", NULL); ant_register_library(ffi_library, "ant:ffi", NULL); diff --git a/src/modules/localstorage.c b/src/modules/localstorage.c new file mode 100644 index 0000000..6b8f1f8 --- /dev/null +++ b/src/modules/localstorage.c @@ -0,0 +1,321 @@ +#include +#include +#include +#include +#include + +#include "runtime.h" +#include "modules/localstorage.h" +#include "modules/symbol.h" + +typedef struct storage_entry { + char *key; + char *value; + UT_hash_handle hh; +} storage_entry_t; + +static storage_entry_t *local_storage = NULL; +static char *storage_file_path = NULL; + +static void storage_save(void) { + if (!storage_file_path) return; + + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + if (!doc) return; + + yyjson_mut_val *root = yyjson_mut_obj(doc); + yyjson_mut_doc_set_root(doc, root); + + storage_entry_t *entry, *tmp; + HASH_ITER(hh, local_storage, entry, tmp) { + yyjson_mut_obj_add_str(doc, root, entry->key, entry->value); + } + + yyjson_write_err err; + yyjson_mut_write_file(storage_file_path, doc, YYJSON_WRITE_PRETTY, NULL, &err); + yyjson_mut_doc_free(doc); +} + +static void storage_load(void) { + if (!storage_file_path) return; + + yyjson_read_err err; + yyjson_doc *doc = yyjson_read_file(storage_file_path, 0, NULL, &err); + if (!doc) return; + + yyjson_val *root = yyjson_doc_get_root(doc); + if (!yyjson_is_obj(root)) { + yyjson_doc_free(doc); + return; + } + + yyjson_obj_iter iter; + yyjson_obj_iter_init(root, &iter); + yyjson_val *key, *val; + + while ((key = yyjson_obj_iter_next(&iter))) { + val = yyjson_obj_iter_get_val(key); + if (yyjson_is_str(key) && yyjson_is_str(val)) { + const char *k = yyjson_get_str(key); + const char *v = yyjson_get_str(val); + size_t klen = yyjson_get_len(key); + size_t vlen = yyjson_get_len(val); + + storage_entry_t *entry = malloc(sizeof(storage_entry_t)); + if (!entry) continue; + + entry->key = malloc(klen + 1); + entry->value = malloc(vlen + 1); + + if (!entry->key || !entry->value) { + free(entry->key); + free(entry->value); + free(entry); + continue; + } + + memcpy(entry->key, k, klen); + entry->key[klen] = '\0'; + memcpy(entry->value, v, vlen); + entry->value[vlen] = '\0'; + + HASH_ADD_KEYPTR(hh, local_storage, entry->key, klen, entry); + } + } + + yyjson_doc_free(doc); +} + +static void storage_clear(void) { + storage_entry_t *entry, *tmp; + HASH_ITER(hh, local_storage, entry, tmp) { + HASH_DEL(local_storage, entry); + free(entry->key); + free(entry->value); + free(entry); + } +} + +static void storage_set_item(const char *key, size_t key_len, const char *value, size_t value_len) { + storage_entry_t *entry = NULL; + + HASH_FIND(hh, local_storage, key, key_len, entry); + + if (entry) { + free(entry->value); + entry->value = malloc(value_len + 1); + if (entry->value) { + memcpy(entry->value, value, value_len); + entry->value[value_len] = '\0'; + } + } else { + entry = malloc(sizeof(storage_entry_t)); + if (!entry) return; + + entry->key = malloc(key_len + 1); + entry->value = malloc(value_len + 1); + + if (!entry->key || !entry->value) { + free(entry->key); + free(entry->value); + free(entry); + return; + } + + memcpy(entry->key, key, key_len); + entry->key[key_len] = '\0'; + memcpy(entry->value, value, value_len); + entry->value[value_len] = '\0'; + + HASH_ADD_KEYPTR(hh, local_storage, entry->key, key_len, entry); + } + + storage_save(); +} + +static char *storage_get_item(const char *key, size_t key_len) { + storage_entry_t *entry = NULL; + HASH_FIND(hh, local_storage, key, key_len, entry); + return entry ? entry->value : NULL; +} + +static void storage_remove_item(const char *key, size_t key_len) { + storage_entry_t *entry = NULL; + HASH_FIND(hh, local_storage, key, key_len, entry); + + if (entry) { + HASH_DEL(local_storage, entry); + free(entry->key); + free(entry->value); + free(entry); + storage_save(); + } +} + +static size_t storage_length(void) { + return HASH_COUNT(local_storage); +} + +static char *storage_key(size_t index) { + storage_entry_t *entry; + size_t i = 0; + + for (entry = local_storage; entry != NULL; entry = entry->hh.next) { + if (i == index) return entry->key; + i++; + } + + return NULL; +} + +#define CHECK_FILE_SET(js) \ + if (!storage_file_path) { \ + return js_mkerr(js, "Warning: `--localstorage-file` or `localStorage.setFile` were not provided with valid paths."); \ + } + +// localStorage.setItem(key, value) +static jsval_t js_localstorage_setItem(struct js *js, jsval_t *args, int nargs) { + CHECK_FILE_SET(js); + + if (nargs < 2) { + return js_mkerr(js, "Failed to execute 'setItem' on 'Storage': 2 arguments required"); + } + + size_t key_len, value_len; + char *key = js_getstr(js, args[0], &key_len); + char *value = js_getstr(js, js_tostring_val(js, args[1]), &value_len); + + storage_set_item(key, key_len, value, value_len); + + return js_mkundef(); +} + +// localStorage.getItem(key) +static jsval_t js_localstorage_getItem(struct js *js, jsval_t *args, int nargs) { + CHECK_FILE_SET(js); + + if (nargs < 1) { + return js_mkerr(js, "Failed to execute 'getItem' on 'Storage': 1 argument required"); + } + + size_t key_len; + char *key = js_getstr(js, args[0], &key_len); + char *value = storage_get_item(key, key_len); + + if (!value) return js_mknull(); + + return js_mkstr(js, value, strlen(value)); +} + +// localStorage.removeItem(key) +static jsval_t js_localstorage_removeItem(struct js *js, jsval_t *args, int nargs) { + CHECK_FILE_SET(js); + + if (nargs < 1) { + return js_mkerr(js, "Failed to execute 'removeItem' on 'Storage': 1 argument required"); + } + + size_t key_len; + char *key = js_getstr(js, args[0], &key_len); + storage_remove_item(key, key_len); + + return js_mkundef(); +} + +// localStorage.clear() +static jsval_t js_localstorage_clear(struct js *js, jsval_t *args, int nargs) { + CHECK_FILE_SET(js); + (void)args; (void)nargs; + storage_clear(); + storage_save(); + return js_mkundef(); +} + +// localStorage.key(index) +static jsval_t js_localstorage_key(struct js *js, jsval_t *args, int nargs) { + CHECK_FILE_SET(js); + + if (nargs < 1) { + return js_mkerr(js, "Failed to execute 'key' on 'Storage': 1 argument required"); + } + + if (js_type(args[0]) != JS_NUM) { + return js_mknull(); + } + + double idx = js_getnum(args[0]); + if (idx < 0) return js_mknull(); + + size_t index = (size_t)idx; + char *key = storage_key(index); + + if (!key) return js_mknull(); + + return js_mkstr(js, key, strlen(key)); +} + +// localStorage.length +static jsval_t js_localstorage_length(struct js *js, jsval_t *args, int nargs) { + (void)args; (void)nargs; + CHECK_FILE_SET(js) + return js_mknum((double)storage_length()); +} + +// localStorage.setFile(path) +static jsval_t js_localstorage_setFile(struct js *js, jsval_t *args, int nargs) { + if (nargs < 1) { + return js_mkerr(js, "Failed to execute 'setFile' on 'Storage': 1 argument required"); + } + + size_t path_len; + char *path = js_getstr(js, args[0], &path_len); + + if (storage_file_path) { + free(storage_file_path); + } + + storage_file_path = malloc(path_len + 1); + if (!storage_file_path) { + return js_mkerr(js, "Failed to allocate memory for file path"); + } + + memcpy(storage_file_path, path, path_len); + storage_file_path[path_len] = '\0'; + + // Load existing data from the new file + storage_load(); + + return js_mkundef(); +} + +void init_localstorage_module() { + struct js *js = rt->js; + + jsval_t glob = js_glob(js); + const char *file_path = rt->ls_fp; + + if (file_path) { + storage_file_path = strdup(file_path); + storage_load(); + } + + jsval_t storage_obj = js_mkobj(js); + + js_set(js, storage_obj, "setItem", js_mkfun(js_localstorage_setItem)); + js_set(js, storage_obj, "getItem", js_mkfun(js_localstorage_getItem)); + js_set(js, storage_obj, "removeItem", js_mkfun(js_localstorage_removeItem)); + js_set(js, storage_obj, "clear", js_mkfun(js_localstorage_clear)); + js_set(js, storage_obj, "key", js_mkfun(js_localstorage_key)); + js_set(js, storage_obj, "setFile", js_mkfun(js_localstorage_setFile)); + + jsval_t length_getter = js_mkfun(js_localstorage_length); + jsval_t desc = js_mkobj(js); + js_set(js, desc, "get", length_getter); + js_set(js, desc, "enumerable", js_mktrue()); + js_set(js, desc, "configurable", js_mkfalse()); + js_set(js, storage_obj, "__desc_length", desc); + js_set(js, storage_obj, "__getter", js_mktrue()); + + js_set(js, storage_obj, get_toStringTag_sym_key(), js_mkstr(js, "Storage", 7)); + js_set(js, glob, "localStorage", storage_obj); +} diff --git a/src/modules/sessionstorage.c b/src/modules/sessionstorage.c index 6426629..fdbbbcc 100644 --- a/src/modules/sessionstorage.c +++ b/src/modules/sessionstorage.c @@ -101,7 +101,7 @@ static jsval_t js_sessionstorage_setItem(struct js *js, jsval_t *args, int nargs size_t key_len, value_len; char *key = js_getstr(js, args[0], &key_len); - char *value = js_getstr(js, args[1], &value_len); + char *value = js_getstr(js, js_tostring_val(js, args[1]), &value_len); storage_set_item(key, key_len, value, value_len); diff --git a/src/runtime.c b/src/runtime.c index 14620ae..a938c7e 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -6,13 +6,14 @@ static struct ant_runtime runtime = {0}; struct ant_runtime *const rt = &runtime; -struct ant_runtime *ant_runtime_init(struct js *js, int argc, char **argv) { +struct ant_runtime *ant_runtime_init(struct js *js, int argc, char **argv, struct arg_file *ls_p) { runtime.js = js; runtime.ant_obj = js_mkobj(js); runtime.flags = 0; runtime.argc = argc; runtime.argv = argv; + runtime.ls_fp = ls_p->count > 0 ? ls_p->filename[0] : NULL; js_set(js, js_glob(js), "Ant", runtime.ant_obj); js_set(js, js_glob(js), "global", js_glob(js)); -- 2.51.2