#include #include #include #include #include "convert.h" namespace node_sqlite3 { namespace { // Element size in bytes for each typed-array kind. inline size_t TypedArrayElementSize(napi_typedarray_type type) { switch (type) { case napi_int8_array: case napi_uint8_array: case napi_uint8_clamped_array: return 1; case napi_int16_array: case napi_uint16_array: return 2; case napi_int32_array: case napi_uint32_array: case napi_float32_array: return 4; case napi_float64_array: case napi_bigint64_array: case napi_biguint64_array: return 8; default: return 0; } } // Human-readable type name for "unsupported type" errors. Never throws; // falls back to a generic name when the constructor is inaccessible // (e.g. a Proxy whose get trap throws). std::string BindTypeName(const Napi::Value& source) { auto env = source.Env(); switch (source.Type()) { case napi_symbol: return "Symbol"; case napi_function: return "Function"; case napi_external: return "External"; default: break; } if (source.IsObject()) { Napi::Object obj = source.As(); Napi::Value ctor = obj.Get("constructor"); if (!env.IsExceptionPending() && ctor.IsObject()) { Napi::Value name = ctor.As().Get("name"); if (!env.IsExceptionPending() && name.IsString()) { std::string s = name.As().Utf8Value(); if (!s.empty()) return s; } } return "Object"; } return "value"; } void ThrowUnsupportedBindType(const Napi::Value& source, const std::string& subject) { auto env = source.Env(); std::string msg = "Cannot bind " + subject + ": unsupported type " + BindTypeName(source) + ". Serialize it explicitly (e.g. JSON.stringify) before binding."; Napi::TypeError::New(env, msg).ThrowAsJavaScriptException(); } // Range of doubles that convert to int64 without undefined behaviour. // The upper bound is inclusive: JS cannot express 2^63-1, so the double // 2^63 is the rounded form of it and clamps to INT64_MAX. const double kInt64MinAsDouble = -9223372036854775808.0; // -(2^63) const double kInt64MaxAsDouble = 9223372036854775808.0; // 2^63 } // namespace // Resolves one global constructor, caching it per environment. // // The uncached form read `env.Global().Get("RegExp")` on every call: a // napi_get_global, a JS string built from the C literal, and a property // get — to answer a question whose answer is fixed for the lifetime of // the environment. The bind paths ask it for every named-parameter call, // where it was one of the largest single costs. // // The reference holds the current environment's constructor, which is // precisely what the uncached lookup returned; caching changes nothing // about which objects match. static Napi::Function GlobalCtor(Napi::Env env, napi_ref* slot, const char* name) { if (*slot != NULL) { napi_value cached = NULL; if (napi_get_reference_value(env, *slot, &cached) == napi_ok && cached != NULL) { return Napi::Function(env, cached); } } Napi::Value ctor = env.Global().Get(name); if (!ctor.IsFunction()) return Napi::Function(); // A weak reference would let the constructor be collected out from // under us; these live as long as the environment does. napi_create_reference(env, ctor, 1, slot); return ctor.As(); } bool IsNamedParameterMap(Napi::Value source) { if (!source.IsObject() || source.IsArray()) return false; auto env = source.Env(); auto* addon = env.GetInstanceData(); // Fast path: a plain object literal — which is what a named-parameter // map almost always is. Its prototype is Object.prototype, and none // of the shapes rejected below share that prototype, so one // comparison settles all six of them. napi_value proto = NULL; if (napi_get_prototype(env, source, &proto) == napi_ok && proto != NULL) { napi_value object_proto = NULL; if (addon->object_prototype == NULL) { Napi::Value base = env.Global().Get("Object"); if (base.IsFunction()) { Napi::Value p = base.As().Get("prototype"); if (p.IsObject()) { napi_create_reference(env, p, 1, &addon->object_prototype); } } } if (addon->object_prototype != NULL && napi_get_reference_value(env, addon->object_prototype, &object_proto) == napi_ok) { bool same = false; if (napi_strict_equals(env, proto, object_proto, &same) == napi_ok && same) { return true; } } } // Anything else: the explicit checks. Cheap predicates first; IsDate // matches across realms, and the RegExp lookup only runs once the // value is known to be an object. Binary views bind positionally, // like the other non-map shapes. return !source.IsBuffer() && !source.IsTypedArray() && !source.IsDataView() && !source.IsArrayBuffer() && !source.IsDate() && !OtherInstanceOf(source.As(), "RegExp"); } bool ResolveNamedKey(Napi::Env env, Napi::Value key, NamedKey* out, std::string* storage) { // Long enough that no realistic parameter name reaches the fallback, // so the whole classification is a single napi call. char stack[128]; size_t written = 0; const size_t capacity = sizeof(stack); bool have_text = false; if (key.IsString() && napi_get_value_string_utf8(env, key, stack, capacity, &written) == napi_ok && written + 1 < capacity) { storage->assign(stack, written); have_text = true; } if (have_text) { const char c = storage->empty() ? '\0' : (*storage)[0]; const bool could_be_numeric = c == '\0' || (c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.' || c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; if (!could_be_numeric) { out->positional = false; out->name = storage->c_str(); return true; } } // Either an unusual key, or one that really might be a number: fall // back to the coercion, which is the definitive test. Napi::Number num = key.ToNumber(); if (env.IsExceptionPending()) return false; // Bind positions are 1-based, so a key reading as zero or a negative // number selects nothing. Such a key is treated as a name, which is // what it literally is, and reported as an unknown one — the async // path cannot represent a zero position either, so this is also what // keeps the two implementations wording it the same way. if (num.Int32Value() == num.DoubleValue() && num.Int32Value() > 0) { out->positional = true; out->index = num.Int32Value(); return true; } if (!have_text) { *storage = key.As().Utf8Value(); if (env.IsExceptionPending()) return false; } out->positional = false; out->name = storage->c_str(); return true; } // True for a plain-object instance of the named global constructor // ("Date", "RegExp"), matching JS instanceof across realms. bool OtherInstanceOf(Napi::Object source, const char* object_type) { auto env = source.Env(); auto* addon = env.GetInstanceData(); Napi::Function ctor; if (strncmp(object_type, "Date", 4) == 0) { ctor = GlobalCtor(env, &addon->date_ctor, "Date"); } else if (strncmp(object_type, "RegExp", 6) == 0) { ctor = GlobalCtor(env, &addon->regexp_ctor, "RegExp"); } if (ctor.IsEmpty()) return false; return source.InstanceOf(ctor); } std::unique_ptr ConvertToField(const Napi::Value source, const std::string& subject, const FieldPos& pos) { // Exhaustive dispatch. Order matters for the hot path: cheap primitive // checks run before the object checks (InstanceOf lookups hit the // global object). Every JS type either maps to a field or throws — // returning nullptr therefore always implies a pending exception, so // nothing can silently skip a value. The constructors carry the bind // position (index or name) through to Bind(Parameters&&, bool). #define MAKE_FIELD(kind, ...) (pos.index > 0 ? std::make_unique(pos.index, __VA_ARGS__) : std::make_unique(pos.name, __VA_ARGS__)) // One napi_typeof up front, then compare against it. Each of // node-addon-api's IsNumber()/IsString()/... calls napi_typeof itself, // so an if-else chain over them pays that ABI call once per arm it // tries — six times over for a value that turns out to be a BigInt. napi_valuetype vtype = napi_undefined; if (napi_typeof(source.Env(), source, &vtype) != napi_ok) { return nullptr; } if (vtype == napi_number) { double val = source.As().DoubleValue(); // Number.isInteger within the int64 range binds as INTEGER (64-bit, // not the old Int32 round-trip). NaN and ±Infinity fail the // trunc/finiteness test and bind as REAL (NaN becomes NULL, per // sqlite's bind_double semantics). if (std::isfinite(val) && val == std::trunc(val) && val >= kInt64MinAsDouble && val < kInt64MaxAsDouble) { return MAKE_FIELD(Integer, static_cast(val)); } if (val == kInt64MaxAsDouble) { // 2^63 as a double is the rounded form of 2^63-1: clamp so the // top of the int64 range stays reachable from JS numbers. return MAKE_FIELD(Integer, INT64_MAX); } return MAKE_FIELD(Float, val); } else if (vtype == napi_string) { std::string val = source.As().Utf8Value(); return MAKE_FIELD(Text, val.length(), val.c_str()); } else if (vtype == napi_boolean) { return MAKE_FIELD(Integer, source.As().Value() ? 1 : 0); } else if (vtype == napi_null) { return pos.index > 0 ? std::make_unique(pos.index) : std::make_unique(pos.name); } else if (vtype == napi_undefined) { // Binds as NULL, matching null: object shorthand // { $x: obj.maybeMissing } is a common call shape. Typo'd property // names are caught by the named-parameter and arity checks in // Bind(Parameters&&, bool), not by rejecting undefined. auto field = pos.index > 0 ? std::make_unique(pos.index) : std::make_unique(pos.name); field->from_undefined = true; return field; } else if (vtype == napi_bigint) { bool lossless = false; int64_t val = source.As().Int64Value(&lossless); if (!lossless) { std::string digits = source.ToString().Utf8Value(); Napi::RangeError::New(source.Env(), "Cannot bind " + subject + ": BigInt " + digits + " is outside the signed 64-bit integer range" ).ThrowAsJavaScriptException(); return nullptr; } return MAKE_FIELD(Integer, val); } else if (vtype != napi_object) { // Symbols, functions, external values. ThrowUnsupportedBindType(source, subject); return nullptr; } else if (source.IsDataView()) { // Must be tested before IsBuffer(): napi_is_buffer() also answers // true for DataViews, and routing one through Napi::Buffer fails // ("Invalid argument"). data arrives already offset by // byte_offset, like the typed-array call. size_t bytes = 0; void* data = NULL; napi_get_dataview_info(source.Env(), source, &bytes, &data, NULL, NULL); if (bytes > static_cast(std::numeric_limits::max())) { Napi::RangeError::New(source.Env(), "Cannot bind " + subject + ": DataView of " + std::to_string(bytes) + " bytes exceeds the bind size limit" ).ThrowAsJavaScriptException(); return nullptr; } return MAKE_FIELD(Blob, bytes, data); } else if (source.IsBuffer()) { // Node Buffers and plain Uint8Arrays: Data() and Length() honour // byteOffset for both. Napi::Buffer buffer = source.As>(); if (buffer.Length() > static_cast(std::numeric_limits::max())) { // Buffers can exceed 2 GB on 64-bit Node; sqlite3_bind_blob takes // an int, so without this the length narrows to a negative number. Napi::RangeError::New(source.Env(), "Cannot bind " + subject + ": Buffer of " + std::to_string(buffer.Length()) + " bytes exceeds the bind size limit" ).ThrowAsJavaScriptException(); return nullptr; } return MAKE_FIELD(Blob, buffer.Length(), buffer.Data()); } else if (source.IsTypedArray()) { // Any other typed array (Uint16Array, Float64Array, ...): raw // element count times the element size, with the data pointer // already offset by byteOffset per napi_get_typedarray_info. napi_typedarray_type type; size_t elements = 0; void* data = NULL; napi_get_typedarray_info(source.Env(), source, &type, &elements, &data, NULL, NULL); size_t bytes = elements * TypedArrayElementSize(type); if (bytes > static_cast(std::numeric_limits::max())) { Napi::RangeError::New(source.Env(), "Cannot bind " + subject + ": typed array of " + std::to_string(bytes) + " bytes exceeds the bind size limit" ).ThrowAsJavaScriptException(); return nullptr; } return MAKE_FIELD(Blob, bytes, data); } else if (source.IsArrayBuffer()) { Napi::ArrayBuffer buffer = source.As(); if (buffer.ByteLength() > static_cast(std::numeric_limits::max())) { Napi::RangeError::New(source.Env(), "Cannot bind " + subject + ": ArrayBuffer exceeds the bind size limit" ).ThrowAsJavaScriptException(); return nullptr; } return MAKE_FIELD(Blob, buffer.ByteLength(), buffer.Data()); } else if (source.IsDate()) { // Documented v8/v9 behaviour: epoch milliseconds as REAL. Opt-in // TEXT binding is deliberately out of scope for D02. return MAKE_FIELD(Float, source.As().ValueOf()); } else if (source.IsObject()) { if (OtherInstanceOf(source.As(), "RegExp")) { std::string val = source.ToString().Utf8Value(); return MAKE_FIELD(Text, val.length(), val.c_str()); } // Plain objects, arrays, Maps, class instances: refused. The old // behaviour bound the literal string "[object Object]". ThrowUnsupportedBindType(source, subject); return nullptr; } // Symbols, functions, anything else. ThrowUnsupportedBindType(source, subject); return nullptr; #undef MAKE_FIELD } std::string BindSubject::Describe() const { if (name != nullptr) return std::string("parameter ") + name; return "parameter " + std::to_string(index); } bool BindValueDirect(sqlite3_stmt* stmt, int pos, const Napi::Value source, const BindSubject& subject, int* rc, bool* from_undefined) { auto env = source.Env(); // One napi_typeof, then a switch. node-addon-api's IsNumber()/ // IsString()/... each call napi_typeof, so an if-else chain over them // pays that ABI call once per arm it tries. napi_valuetype type = napi_undefined; if (napi_typeof(env, source, &type) != napi_ok) return false; switch (type) { case napi_number: { const double val = source.As().DoubleValue(); // Number.isInteger within the int64 range binds as INTEGER; // NaN and +/-Infinity fail the finiteness test and bind REAL. if (std::isfinite(val) && val == std::trunc(val) && val >= kInt64MinAsDouble && val < kInt64MaxAsDouble) { *rc = sqlite3_bind_int64(stmt, pos, static_cast(val)); return true; } if (val == kInt64MaxAsDouble) { // 2^63 as a double is the rounded form of 2^63-1: clamp so // the top of the range stays reachable from a JS number. *rc = sqlite3_bind_int64(stmt, pos, INT64_MAX); return true; } *rc = sqlite3_bind_double(stmt, pos, val); return true; } case napi_string: { // Two napi calls (length, then write) is the documented way to // read a string of unknown length; the payload is malloc'd // once and handed to SQLite rather than copied into a // std::string and then copied again. size_t length = 0; if (napi_get_value_string_utf8(env, source, NULL, 0, &length) != napi_ok) { return false; } char* buffer = static_cast(malloc(length + 1)); if (buffer == NULL) { Napi::Error::New(env, "Cannot bind " + subject.Describe() + ": out of memory").ThrowAsJavaScriptException(); return false; } size_t written = 0; if (napi_get_value_string_utf8(env, source, buffer, length + 1, &written) != napi_ok) { free(buffer); return false; } *rc = sqlite3_bind_text64(stmt, pos, buffer, static_cast(written), free, SQLITE_UTF8); return true; } case napi_boolean: { *rc = sqlite3_bind_int64(stmt, pos, source.As().Value() ? 1 : 0); return true; } case napi_null: { *rc = sqlite3_bind_null(stmt, pos); return true; } case napi_undefined: { // Binds as NULL, like the Field path; the caller uses // `from_undefined` for the zero-parameter call shape. if (from_undefined != NULL) *from_undefined = true; *rc = sqlite3_bind_null(stmt, pos); return true; } case napi_bigint: { bool lossless = false; const int64_t val = source.As().Int64Value(&lossless); if (!lossless) { const std::string digits = source.ToString().Utf8Value(); Napi::RangeError::New(env, "Cannot bind " + subject.Describe() + ": BigInt " + digits + " is outside the signed 64-bit integer range" ).ThrowAsJavaScriptException(); return false; } *rc = sqlite3_bind_int64(stmt, pos, val); return true; } case napi_object: break; // handled below default: // Symbols, functions, external values. ThrowUnsupportedBindType(source, subject.Describe()); return false; } // Binary views. SQLITE_TRANSIENT: SQLite copies before returning, so // the JS-owned bytes need not outlive this call. const void* data = NULL; size_t bytes = 0; const char* kind = NULL; if (source.IsDataView()) { // Before IsBuffer(): napi_is_buffer() also answers true for a // DataView, and routing one through Napi::Buffer fails. napi_get_dataview_info(env, source, &bytes, const_cast(&data), NULL, NULL); kind = "DataView"; } else if (source.IsBuffer()) { // Node Buffers and plain Uint8Arrays: Data() and Length() honour // byteOffset for both. Napi::Buffer buffer = source.As>(); data = buffer.Data(); bytes = buffer.Length(); kind = "Buffer"; } else if (source.IsTypedArray()) { napi_typedarray_type ta_type; size_t elements = 0; napi_get_typedarray_info(env, source, &ta_type, &elements, const_cast(&data), NULL, NULL); bytes = elements * TypedArrayElementSize(ta_type); kind = "typed array"; } else if (source.IsArrayBuffer()) { Napi::ArrayBuffer buffer = source.As(); data = buffer.Data(); bytes = buffer.ByteLength(); kind = "ArrayBuffer"; } if (kind != NULL) { if (bytes > static_cast(std::numeric_limits::max())) { // Views can exceed 2 GB on 64-bit Node; the wording matches // the Field path per shape. const std::string what = std::strcmp(kind, "ArrayBuffer") == 0 ? "ArrayBuffer exceeds the bind size limit" : std::string(kind) + " of " + std::to_string(bytes) + " bytes exceeds the bind size limit"; Napi::RangeError::New(env, "Cannot bind " + subject.Describe() + ": " + what ).ThrowAsJavaScriptException(); return false; } // A zero-length view can carry a NULL data pointer, and // sqlite3_bind_blob64(NULL, 0) binds SQL NULL rather than an empty // blob. The Field path always had a real allocation behind it, so // an empty Buffer round-tripped as an empty blob; keep that by // handing SQLite a non-NULL pointer it will read zero bytes from. *rc = sqlite3_bind_blob64(stmt, pos, data != NULL ? data : "", static_cast(bytes), SQLITE_TRANSIENT); return true; } if (source.IsDate()) { // Documented v8/v9 behaviour: epoch milliseconds as REAL. *rc = sqlite3_bind_double(stmt, pos, source.As().ValueOf()); return true; } if (OtherInstanceOf(source.As(), "RegExp")) { const std::string val = source.ToString().Utf8Value(); *rc = sqlite3_bind_text64(stmt, pos, val.c_str(), static_cast(val.length()), SQLITE_TRANSIENT, SQLITE_UTF8); return true; } // Plain objects, arrays, Maps, class instances: refused. ThrowUnsupportedBindType(source, subject.Describe()); return false; } void ValueToCell(Cell* cell, sqlite3_value* value) { cell->str.clear(); cell->integer = 0; cell->real = 0.; switch (sqlite3_value_type(value)) { case SQLITE_INTEGER: { cell->type = SQLITE_INTEGER; cell->integer = sqlite3_value_int64(value); } break; case SQLITE_FLOAT: { cell->type = SQLITE_FLOAT; cell->real = sqlite3_value_double(value); } break; case SQLITE_TEXT: { cell->type = SQLITE_TEXT; // sqlite3_value_text first, then sqlite3_value_bytes: the docs // require that order so the UTF-8 conversion happens before the // length is read. const char* text = reinterpret_cast(sqlite3_value_text(value)); int length = sqlite3_value_bytes(value); if (length > 0 && text != NULL) { cell->str.assign(text, length); } } break; case SQLITE_BLOB: { cell->type = SQLITE_BLOB; const void* blob = sqlite3_value_blob(value); int length = sqlite3_value_bytes(value); if (length > 0 && blob != NULL) { cell->str.assign(static_cast(blob), length); } } break; default: { cell->type = SQLITE_NULL; } } } std::string ValueOrigin::Describe() const { if (literal != nullptr) return *literal; if (literal_cstr != nullptr) return std::string(literal_cstr); if (column_names != nullptr && index < column_names->size()) { return "column '" + (*column_names)[index] + "'"; } return "result column " + std::to_string(index); } Napi::Value ConvertInt64ToJS(Napi::Env env, sqlite3_int64 value, int integer_mode, const ValueOrigin& origin) { // A single range compare on the int64 — deliberately not a call into // JS: this runs per integer cell. const bool safe = value >= -(1LL << 53) + 1 && value < (1LL << 53); switch (integer_mode) { case Database::INTEGER_BIGINT: return Napi::BigInt::New(env, static_cast(value)); case Database::INTEGER_MIXED: if (!safe) return Napi::BigInt::New(env, static_cast(value)); return Napi::Number::New(env, static_cast(value)); default: if (safe) return Napi::Number::New(env, static_cast(value)); // The default throws instead of truncating: a silent double // conversion is exactly the corruption this mode guards // against. The callback-free sync paths surface this directly; // async completions deliver it to the user callback. Napi::RangeError::New(env, "Integer " + std::to_string(value) + " in " + origin.Describe() + " is outside the safe integer range (-(2^53-1) .. 2^53-1); " "configure('integerMode', 'bigint' | 'mixed') to read it " "exactly" ).ThrowAsJavaScriptException(); return env.Null(); } } Napi::Value ColumnToJS(Napi::Env env, sqlite3_stmt* stmt, int column, int integer_mode, const ValueOrigin& origin, bool* raised) { switch (sqlite3_column_type(stmt, column)) { case SQLITE_INTEGER: { // The one branch that can raise (the 'number'-mode RangeError // for an unsafe int64): report it through `raised` so the row // loop stays free of napi_is_exception_pending calls. const sqlite3_int64 value = sqlite3_column_int64(stmt, column); const bool safe = value >= -(1LL << 53) + 1 && value < (1LL << 53); if (integer_mode == Database::INTEGER_NUMBER && !safe) { if (raised != NULL) *raised = true; } return ConvertInt64ToJS(env, value, integer_mode, origin); } case SQLITE_FLOAT: { return Napi::Number::New(env, sqlite3_column_double(stmt, column)); } case SQLITE_TEXT: { const char* text = reinterpret_cast( sqlite3_column_text(stmt, column)); const int length = sqlite3_column_bytes(stmt, column); if (text == NULL || length <= 0) { return Napi::String::New(env, "", 0); } return Napi::String::New(env, text, static_cast(length)); } case SQLITE_BLOB: { const char* blob = reinterpret_cast( sqlite3_column_blob(stmt, column)); const int length = sqlite3_column_bytes(stmt, column); if (blob == NULL || length <= 0) { return Napi::Buffer::Copy(env, "", 0); } return Napi::Buffer::Copy(env, blob, static_cast(length)); } default: { // SQLITE_NULL, and anything unexpected, as the Cell path does. return env.Null(); } } } Napi::Value CellToJS(Napi::Env env, Cell& cell, int integer_mode, const ValueOrigin& origin, bool move_payload, bool* raised) { switch (cell.type) { case SQLITE_INTEGER: { // The one branch that can raise; see ColumnToJS for why the // failure travels in a bool rather than through the env. const bool safe = cell.integer >= -(1LL << 53) + 1 && cell.integer < (1LL << 53); if (integer_mode == Database::INTEGER_NUMBER && !safe) { if (raised != NULL) *raised = true; } return ConvertInt64ToJS(env, cell.integer, integer_mode, origin); } case SQLITE_FLOAT: { return Napi::Number::New(env, cell.real); } case SQLITE_TEXT: { return Napi::String::New(env, cell.str.data(), cell.str.size()); } case SQLITE_BLOB: { // Zero-copy for large blobs: transfer ownership of the bytes // to the Buffer finalizer. Small blobs are cheaper to copy // (external-buffer bookkeeping outweighs the memcpy), and the // copy fallback also covers environments without external // buffer support (e.g. sandboxed renderers). if (move_payload && cell.str.size() >= 4096) { auto* payload = new std::string(std::move(cell.str)); napi_value buf = NULL; napi_status st = napi_create_external_buffer(env, payload->size(), &(*payload)[0], [](napi_env, void*, void* hint) { delete static_cast(hint); }, payload, &buf); if (st == napi_ok) { return Napi::Buffer(env, buf); } // No external-buffer support (e.g. sandboxed renderers): // fall back to a copy from the payload we still hold. Napi::Buffer copy = Napi::Buffer::Copy(env, payload->data(), payload->size()); delete payload; return copy; } return Napi::Buffer::Copy(env, cell.str.data(), cell.str.size()); } case SQLITE_NULL: { return env.Null(); } default: return env.Null(); } } } // namespace node_sqlite3