#include #include #include #include #include static napi_value ffi_open(napi_env env, napi_callback_info info); static napi_value ffi_exec(napi_env env, napi_callback_info info); static napi_value ffi_all (napi_env env, napi_callback_info info); static napi_value ffi_run (napi_env env, napi_callback_info info); struct NAPIContext { napi_env env; napi_value value; uint32_t index; }; struct Fn { const char *const label; napi_value(*const handle)(napi_env env, napi_callback_info info); }; static const struct Fn fns[] = { { .label = "open", .handle = ffi_open, }, { NULL, NULL }, }; struct Fn methods[] = { { .label = "exec", .handle = ffi_exec, }, { .label = "all", .handle = ffi_all, }, { .label = "run", .handle = ffi_run, }, { NULL, NULL }, }; static const napi_type_tag SQLITE_DB_TYPE_TAG = { 0x0e9614d459f746cc, 0x88b814a5dc5c4cf7 }; static const int SQLITE_OPEN_FLAGS = /* From https://www.sqlite.org/c3ref/open.html: > The database is opened for reading and writing, and is created if > it does not already exist. This is the behavior that is always > used for sqlite3_open() and sqlite3_open16(). */ SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | /* From https://www.sqlite.org/c3ref/open.html: > The new database connection will use the "serialized" threading > mode. This means the multiple threads can safely attempt to use > the same database connection at the same time. (Mutexes will block > any actual concurrency, but in this mode there is no harm in > trying.) */ SQLITE_OPEN_FULLMUTEX | /* From https://www.sqlite.org/c3ref/open.html: > The database connection comes up in "extended result code mode". > In other words, the database behaves has if > sqlite3_extended_result_codes(db,1) where called on the database > connection as soon as the connection is created. In addition to > setting the extended result code mode, this flag also causes > sqlite3_open_v2() to return an extended result code. From https://www.sqlite.org/c3ref/extended_result_codes.html: > The sqlite3_extended_result_codes() routine enables or disables the > extended result codes feature of SQLite. The extended result codes > are disabled by default for historical compatibility. */ SQLITE_OPEN_EXRESCODE; static const char *const FIX_SQLITE_PRAGMAS = /* From https://research.cs.wisc.edu/adsl/Publications/alice-osdi14.pdf: > Similarly, SQLite does not provide durability under the default > journal-mode (we became aware of this only after interacting with > developers), but its documentation seems misleading. > > (...) > > The developers suggest the SQLite vulnerability is actually not a > behavior guaranteed by SQLite (specifically, that durability cannot > be achieved under rollback journaling); we believe the > documentation is misleading. > > (...) > > Unclear documentation of application guarantees contributes to the > confusion about crash vulnerabilities. During discussions with > developers about durability vulnerabilities, we found that SQLite, > which proclaims itself as fully ACID-complaint, does not provide > durability (even optionally) with the default storage engine, > though the documentation suggests it does. */ "PRAGMA journal_mode = WAL;\n" "PRAGMA synchronous = EXTRA;\n" /* From https://www.sqlite.org/foreignkeys.html: > Foreign key constraints are disabled by default (for backwards > compatibility), so must be enabled separately for each database > connection. */ "PRAGMA foreign_keys = ON;\n" ; // FIXME: make this async static napi_value ffi_exec(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value argv[1]; int sqlite_rc; napi_status napi_rc; char *sql = NULL; size_t sql_size1; size_t sql_size2; sqlite3 *db_handle = NULL; char *error_msg = NULL; napi_rc = napi_get_cb_info( env, info, &argc, argv, NULL, (void *)&db_handle ); if (napi_rc != napi_ok) { napi_throw_error( env, "111TODO ERRCODE", "Failed to parse arguments TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8(env, argv[0], NULL, 0, &sql_size1); if (napi_rc != napi_ok) { napi_throw_error( env, "222TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } if (sql_size1 == SIZE_MAX) { napi_throw_error( env, "SQLITE_EOVERFLOW", "TODO " ); goto out; } sql_size1++; // include the NULL-terminator in size measurement sql = malloc(sql_size1); if (!sql) { napi_throw_error( env, "SQLITE_ENOMEM", "TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8( env, argv[0], sql, sql_size1, &sql_size2 ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } assert(sql_size1 == sql_size2 + 1 && "Unstable behaviour from Node-API"); assert(sql); sqlite_rc = sqlite3_exec( db_handle, sql, NULL, NULL, &error_msg ); if (sqlite_rc != SQLITE_OK) { napi_throw_error( env, "3iii33TODO ERRCODE", sqlite3_errstr(sqlite_rc) ); goto out; } out: if (error_msg) { sqlite3_free(error_msg); } if (sql) { free(sql); } return NULL; } static int accumulate_all_results( void *ctxptr, int argc, char **argv, char **column_names ) { int rc = 0; napi_status napi_rc; napi_value row = NULL; struct NAPIContext *ctx = ctxptr; napi_env env = ctx->env; napi_value results = ctx->value; uint32_t index = ctx->index; napi_rc = napi_create_object(env, &row); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "TODO i18n" ); rc = -1; goto out; } for (int i = 0; i < argc; i++) { const char *const column_name = column_names[i]; const char *const column_value = argv[i]; // printf("column_name: %s\n", column_name); // printf("column_value: %s\n", column_value); napi_value str; // FIXME: dispatch on the type napi_rc = napi_create_string_utf8( env, column_value, NAPI_AUTO_LENGTH, &str ); if (napi_rc != napi_ok) { napi_throw_error( env, "SQLITE_MiiiETHOD_CREATE", "TODO i18n" ); rc = -1; goto out; } napi_rc = napi_set_named_property( env, row, column_name, str ); if (napi_rc != napi_ok) { napi_throw_error( env, "iSQLITE_MiETHOD_CREATE", "TODO i18n" ); rc = -1; goto out; } } napi_rc = napi_set_element( env, results, index, row ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "TODO i18n" ); rc = -1; goto out; } ctx->index++; out: return rc; } static napi_value ffi_all(napi_env env, napi_callback_info info) { napi_value ret = NULL; size_t argc = 1; napi_value argv[1]; int sqlite_rc; napi_status napi_rc; char *sql = NULL; size_t sql_size1; size_t sql_size2; sqlite3 *db_handle = NULL; char *error_msg = NULL; napi_value results_array = NULL; napi_rc = napi_get_cb_info( env, info, &argc, argv, NULL, (void *)&db_handle ); if (napi_rc != napi_ok) { napi_throw_error( env, "111TODO ERRCODE", "Failed to parse arguments TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8(env, argv[0], NULL, 0, &sql_size1); if (napi_rc != napi_ok) { napi_throw_error( env, "222TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } if (sql_size1 == SIZE_MAX) { napi_throw_error( env, "SQLITE_EOVERFLOW", "TODO " ); goto out; } sql_size1++; // include the NULL-terminator in size measurement sql = malloc(sql_size1); if (!sql) { napi_throw_error( env, "SQLITE_ENOMEM", "TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8( env, argv[0], sql, sql_size1, &sql_size2 ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } assert(sql_size1 == sql_size2 + 1 && "Unstable behaviour from Node-API"); assert(sql); napi_rc = napi_create_array(env, &results_array); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } struct NAPIContext ctx = { .env = env, .value = results_array, .index = 0, }; sqlite_rc = sqlite3_exec( db_handle, sql, accumulate_all_results, &ctx, &error_msg ); if (sqlite_rc != SQLITE_OK) { napi_throw_error( env, "i3iiii33TODO ERRCODE", sqlite3_errstr(sqlite_rc) ); goto out; } ret = results_array; out: if (error_msg) { sqlite3_free(error_msg); } if (sql) { free(sql); } return ret; } static napi_value ffi_run(napi_env env, napi_callback_info info) { napi_value ret = NULL; size_t argc = 1; napi_value argv[1]; int sqlite_rc; napi_status napi_rc; char *sql = NULL; size_t sql_size1; size_t sql_size2; sqlite3 *db_handle = NULL; char *error_msg = NULL; napi_value results_array = NULL; napi_rc = napi_get_cb_info( env, info, &argc, argv, NULL, (void *)&db_handle ); if (napi_rc != napi_ok) { napi_throw_error( env, "111TODO ERRCODE", "Failed to parse arguments TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8(env, argv[0], NULL, 0, &sql_size1); if (napi_rc != napi_ok) { napi_throw_error( env, "222TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } if (sql_size1 == SIZE_MAX) { napi_throw_error( env, "SQLITE_EOVERFLOW", "TODO " ); goto out; } sql_size1++; // include the NULL-terminator in size measurement sql = malloc(sql_size1); if (!sql) { napi_throw_error( env, "SQLITE_ENOMEM", "TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8( env, argv[0], sql, sql_size1, &sql_size2 ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } assert(sql_size1 == sql_size2 + 1 && "Unstable behaviour from Node-API"); assert(sql); napi_rc = napi_create_array(env, &results_array); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } struct NAPIContext ctx = { .env = env, .value = results_array, .index = 0, }; sqlite_rc = sqlite3_exec( db_handle, sql, accumulate_all_results, &ctx, &error_msg ); if (sqlite_rc != SQLITE_OK) { napi_throw_error( env, "i3iiii33TODO ERRCODE", sqlite3_errstr(sqlite_rc) ); goto out; } ret = results_array; out: if (error_msg) { sqlite3_free(error_msg); } if (sql) { free(sql); } return ret; } static void finalize_db_handle(napi_env env, void *finalize_data, void *finalize_hint) { (void)finalize_hint; sqlite3 *db_handle = finalize_data; int sqlite_rc; sqlite_rc = sqlite3_close(db_handle); if (sqlite_rc != SQLITE_OK) { napi_throw_error( env, "TODO ERRCODE", sqlite3_errstr(sqlite_rc) ); goto out; } out: return; } static napi_value ffi_open(napi_env env, napi_callback_info info) { napi_value ret = NULL; size_t argc = 1; napi_value argv[1]; int sqlite_rc; napi_status napi_rc; char *filename = NULL; size_t filename_size1; size_t filename_size2; sqlite3 *db_handle = NULL; napi_value wrapped_db_handle = NULL; char *error_msg = NULL; napi_rc = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Failed to parse arguments TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8( env, argv[0], NULL, // FIXME: what is this? 0, // FIXME: what is this? &filename_size1 ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } if (filename_size1 == SIZE_MAX) { napi_throw_error( env, "SQLITE_EOVERFLOW", "TODO " ); goto out; } filename_size1++; // include the NULL-terminator in size measurement filename = malloc(filename_size1); if (!filename) { napi_throw_error( env, "SQLITE_ENOMEM", "TODO i18n" ); goto out; } napi_rc = napi_get_value_string_utf8( env, argv[0], filename, filename_size1, &filename_size2 ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "Invalid number was passed as argument TODO i18n" ); goto out; } assert(filename_size1 == filename_size2 + 1 && "Unstable behaviour from Node-API"); assert(filename); sqlite_rc = sqlite3_open_v2( filename, &db_handle, SQLITE_OPEN_FLAGS, NULL ); if (sqlite_rc != SQLITE_OK) { napi_throw_error( env, "3i33TODO ERRCODE", sqlite3_errstr(sqlite_rc) ); goto out; } sqlite_rc = sqlite3_exec( db_handle, FIX_SQLITE_PRAGMAS, NULL, NULL, &error_msg ); if (sqlite_rc != SQLITE_OK) { napi_throw_error( env, "3ii33TODO ERRCODE", sqlite3_errstr(sqlite_rc) ); goto out; } // FIXME: setup the global error log: // https://www.sqlite.org/errlog.html napi_rc = napi_create_object(env, &wrapped_db_handle); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "TODO i18n" ); goto out; } napi_rc = napi_type_tag_object( env, wrapped_db_handle, &SQLITE_DB_TYPE_TAG ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "TODO i18n" ); goto out; } napi_rc = napi_wrap( env, wrapped_db_handle, db_handle, finalize_db_handle, NULL, NULL ); if (napi_rc != napi_ok) { napi_throw_error( env, "TODO ERRCODE", "TODO i18n" ); goto out; } for (size_t i = 0; methods[i].label && methods[i].handle; i++) { napi_value fn; napi_rc = napi_create_function( env, methods[i].label, NAPI_AUTO_LENGTH, methods[i].handle, db_handle, &fn ); if (napi_rc != napi_ok) { napi_throw_error( env, "SQLITE_MiiETHOD_CREATE", "TODO i18n" ); goto out; } napi_rc = napi_set_named_property( env, wrapped_db_handle, methods[i].label, fn ); if (napi_rc != napi_ok) { napi_throw_error( env, "SQLITE_METHOD_SETNAME", "TODO i18n" ); goto out; } } ret = wrapped_db_handle; out: if (error_msg) { sqlite3_free(error_msg); } if (filename) { free(filename); } if (!ret) { if (db_handle) { if (sqlite3_close(db_handle)) { // logerr(); } } } return ret; } static napi_value ffi_init(napi_env env, napi_value exports) { napi_value ret = exports; napi_status status; for (size_t i = 0; fns[i].label && fns[i].handle; i++) { napi_value fn; status = napi_create_function( env, fns[i].label, NAPI_AUTO_LENGTH, fns[i].handle, NULL, &fn ); if (status != napi_ok) { ret = NULL; napi_throw_error( env, "SQLITE_FN_CREATE", "Unable to wrap native function TODO i18n" ); goto out; } status = napi_set_named_property( env, exports, fns[i].label, fn ); if (status != napi_ok) { ret = NULL; napi_throw_error( env, "SQLITE_FN_SETNAME", "Unable to populate exports TODO i18n" ); goto out; } } out: return ret; } NAPI_MODULE_INIT() { return ffi_init(env, exports); }