mirror of
https://github.com/aphrody-labs/deno.git
synced 2026-10-14 06:19:28 +00:00
3367 lines
99 KiB
Rust
3367 lines
99 KiB
Rust
// Copyright 2018-2026 the Deno authors. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::cell::Cell;
|
|
use std::cell::RefCell;
|
|
use std::ffi::CStr;
|
|
use std::ffi::CString;
|
|
use std::ffi::c_char;
|
|
use std::ffi::c_void;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::ptr::NonNull;
|
|
use std::ptr::null;
|
|
use std::rc::Rc;
|
|
|
|
use deno_core::FromV8;
|
|
use deno_core::GarbageCollected;
|
|
use deno_core::OpState;
|
|
use deno_core::convert::OptionUndefined;
|
|
use deno_core::cppgc;
|
|
use deno_core::op2;
|
|
use deno_core::v8;
|
|
use deno_core::v8_static_strings;
|
|
use deno_permissions::OpenAccessKind;
|
|
use deno_permissions::PermissionsContainer;
|
|
use rusqlite::ffi as libsqlite3_sys;
|
|
use rusqlite::ffi::SQLITE_DBCONFIG_DEFENSIVE;
|
|
use rusqlite::ffi::SQLITE_DBCONFIG_DQS_DDL;
|
|
use rusqlite::ffi::SQLITE_DBCONFIG_DQS_DML;
|
|
use rusqlite::ffi::sqlite3_create_window_function;
|
|
use rusqlite::ffi::sqlite3_db_filename;
|
|
use rusqlite::limits::Limit;
|
|
|
|
use super::Session;
|
|
use super::SqliteError;
|
|
use super::StatementSync;
|
|
use super::session::InnerSessionPtr;
|
|
use super::session::SessionOptions;
|
|
use super::sql_tag_store::SQLTagStore;
|
|
use super::statement::InnerStatementPtr;
|
|
use super::statement::check_error_code;
|
|
use super::statement::check_error_code2;
|
|
use super::validators;
|
|
|
|
const SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: i32 = 1005;
|
|
const SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE: i32 = 1021;
|
|
const MAX_SAFE_JS_INTEGER: i64 = 9_007_199_254_740_991;
|
|
|
|
const NUM_LIMITS: usize = 11;
|
|
|
|
fn resolve_sqlite_system_path_alias(path: &Path) -> PathBuf {
|
|
// SQLITE_OPEN_NOFOLLOW rejects symlinks in every path component. macOS
|
|
// exposes its temporary directories through root-owned aliases, so resolve
|
|
// only those fixed system prefixes and leave every user-controlled path
|
|
// component visible to SQLite.
|
|
#[cfg(target_os = "macos")]
|
|
if let Some(path) = path.to_str() {
|
|
for (uri_prefix, prefix) in [
|
|
("file:///var", Path::new("/var")),
|
|
("file:///tmp", Path::new("/tmp")),
|
|
] {
|
|
let Some(suffix) = path.strip_prefix(uri_prefix) else {
|
|
continue;
|
|
};
|
|
if !matches!(
|
|
suffix.as_bytes().first(),
|
|
None | Some(b'/') | Some(b'?') | Some(b'#')
|
|
) {
|
|
continue;
|
|
}
|
|
#[allow(
|
|
clippy::disallowed_methods,
|
|
reason = "node:sqlite operates on the real file system"
|
|
)]
|
|
if let Ok(prefix) = std::fs::canonicalize(prefix) {
|
|
return PathBuf::from(format!("file://{}{}", prefix.display(), suffix));
|
|
}
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
for prefix in [Path::new("/var"), Path::new("/tmp")] {
|
|
if let Ok(suffix) = path.strip_prefix(prefix) {
|
|
#[allow(
|
|
clippy::disallowed_methods,
|
|
reason = "node:sqlite operates on the real file system"
|
|
)]
|
|
if let Ok(prefix) = std::fs::canonicalize(prefix) {
|
|
return prefix.join(suffix);
|
|
}
|
|
}
|
|
}
|
|
path.to_path_buf()
|
|
}
|
|
|
|
/// SQLite does not enforce `SQLITE_OPEN_NOFOLLOW` on Windows (its
|
|
/// `winFullPathname` never resolves reparse points), so reject symlinks and
|
|
/// junctions in every path component manually before opening.
|
|
#[cfg(windows)]
|
|
fn refuse_reparse_point_components(path: &Path) -> Result<(), rusqlite::Error> {
|
|
let mut current = PathBuf::new();
|
|
for component in path.components() {
|
|
current.push(component);
|
|
#[allow(
|
|
clippy::disallowed_methods,
|
|
reason = "node:sqlite operates on the real file system"
|
|
)]
|
|
match std::fs::symlink_metadata(¤t) {
|
|
Ok(metadata) if metadata.file_type().is_symlink() => {
|
|
return Err(rusqlite::Error::SqliteFailure(
|
|
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
|
|
Some(format!(
|
|
"unable to open database file: \"{}\" is a symlink",
|
|
current.display()
|
|
)),
|
|
));
|
|
}
|
|
Ok(_) => {}
|
|
// Missing components are created (or rejected) by SQLite itself.
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Static mapping of JavaScript property names to SQLite limits.
|
|
/// Order matches SQLite limit constant values (0-10).
|
|
/// Keep in sync with LIMIT_NAMES in ext/node/polyfills/sqlite.ts.
|
|
const LIMIT_MAPPING: [(&str, Limit); NUM_LIMITS] = [
|
|
("length", Limit::SQLITE_LIMIT_LENGTH),
|
|
("sqlLength", Limit::SQLITE_LIMIT_SQL_LENGTH),
|
|
("column", Limit::SQLITE_LIMIT_COLUMN),
|
|
("exprDepth", Limit::SQLITE_LIMIT_EXPR_DEPTH),
|
|
("compoundSelect", Limit::SQLITE_LIMIT_COMPOUND_SELECT),
|
|
("vdbeOp", Limit::SQLITE_LIMIT_VDBE_OP),
|
|
("functionArg", Limit::SQLITE_LIMIT_FUNCTION_ARG),
|
|
("attach", Limit::SQLITE_LIMIT_ATTACHED),
|
|
("likePatternLength", Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH),
|
|
("variableNumber", Limit::SQLITE_LIMIT_VARIABLE_NUMBER),
|
|
("triggerDepth", Limit::SQLITE_LIMIT_TRIGGER_DEPTH),
|
|
];
|
|
|
|
struct DatabaseSyncOptions {
|
|
open: bool,
|
|
enable_foreign_key_constraints: bool,
|
|
read_only: bool,
|
|
allow_extension: bool,
|
|
enable_double_quoted_string_literals: bool,
|
|
use_big_int_arguments: bool,
|
|
allow_bare_named_params: bool,
|
|
return_arrays: bool,
|
|
allow_unknown_named_params: bool,
|
|
is_defensive_mode: bool,
|
|
timeout: u64,
|
|
initial_limits: [Option<i32>; NUM_LIMITS],
|
|
}
|
|
|
|
impl<'a> FromV8<'a> for DatabaseSyncOptions {
|
|
type Error = validators::Error;
|
|
|
|
fn from_v8(
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
value: v8::Local<'a, v8::Value>,
|
|
) -> Result<Self, Self::Error> {
|
|
use validators::Error;
|
|
|
|
if value.is_undefined() {
|
|
return Ok(Self::default());
|
|
}
|
|
|
|
let Ok(obj) = v8::Local::<v8::Object>::try_from(value) else {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
));
|
|
};
|
|
|
|
let mut options = Self::default();
|
|
|
|
v8_static_strings! {
|
|
OPEN_STRING = "open",
|
|
ENABLE_FOREIGN_KEY_CONSTRAINTS_STRING = "enableForeignKeyConstraints",
|
|
READ_ONLY_STRING = "readOnly",
|
|
ALLOW_EXTENSION_STRING = "allowExtension",
|
|
ENABLE_DOUBLE_QUOTED_STRING_LITERALS_STRING = "enableDoubleQuotedStringLiterals",
|
|
TIMEOUT_STRING = "timeout",
|
|
READ_BIG_INTS = "readBigInts",
|
|
RETURN_ARRAYS = "returnArrays",
|
|
ALLOW_BARE_NAMED_PARAMS = "allowBareNamedParameters",
|
|
ALLOW_UNKNOWN_NAMED_PARAMS = "allowUnknownNamedParameters",
|
|
DEFENSIVE_STRING = "defensive",
|
|
LIMITS_STRING = "limits",
|
|
}
|
|
|
|
let open_string = OPEN_STRING.v8_string(scope).unwrap();
|
|
if let Some(open) = obj.get(scope, open_string.into())
|
|
&& !open.is_undefined()
|
|
{
|
|
options.open = v8::Local::<v8::Boolean>::try_from(open)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.open\" argument must be a boolean.".into(),
|
|
)
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let read_only_string = READ_ONLY_STRING.v8_string(scope).unwrap();
|
|
if let Some(read_only) = obj.get(scope, read_only_string.into())
|
|
&& !read_only.is_undefined()
|
|
{
|
|
options.read_only = v8::Local::<v8::Boolean>::try_from(read_only)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.readOnly\" argument must be a boolean.".into(),
|
|
)
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let enable_foreign_key_constraints_string =
|
|
ENABLE_FOREIGN_KEY_CONSTRAINTS_STRING
|
|
.v8_string(scope)
|
|
.unwrap();
|
|
if let Some(enable_foreign_key_constraints) =
|
|
obj.get(scope, enable_foreign_key_constraints_string.into())
|
|
&& !enable_foreign_key_constraints.is_undefined()
|
|
{
|
|
options.enable_foreign_key_constraints =
|
|
v8::Local::<v8::Boolean>::try_from(enable_foreign_key_constraints)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType("The \"options.enableForeignKeyConstraints\" argument must be a boolean.".into())
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let allow_extension_string =
|
|
ALLOW_EXTENSION_STRING.v8_string(scope).unwrap();
|
|
if let Some(allow_extension) = obj.get(scope, allow_extension_string.into())
|
|
&& !allow_extension.is_undefined()
|
|
{
|
|
options.allow_extension =
|
|
v8::Local::<v8::Boolean>::try_from(allow_extension)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.allowExtension\" argument must be a boolean."
|
|
.into(),
|
|
)
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let enable_double_quoted_string_literals_string =
|
|
ENABLE_DOUBLE_QUOTED_STRING_LITERALS_STRING
|
|
.v8_string(scope)
|
|
.unwrap();
|
|
if let Some(enable_double_quoted_string_literals) =
|
|
obj.get(scope, enable_double_quoted_string_literals_string.into())
|
|
&& !enable_double_quoted_string_literals.is_undefined()
|
|
{
|
|
options.enable_double_quoted_string_literals =
|
|
v8::Local::<v8::Boolean>::try_from(enable_double_quoted_string_literals)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType("The \"options.enableDoubleQuotedStringLiterals\" argument must be a boolean.".into())
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let timeout_string = TIMEOUT_STRING.v8_string(scope).unwrap();
|
|
if let Some(timeout) = obj.get(scope, timeout_string.into())
|
|
&& !timeout.is_undefined()
|
|
{
|
|
let timeout = v8::Local::<v8::Integer>::try_from(timeout)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.timeout\" argument must be an integer.".into(),
|
|
)
|
|
})?
|
|
.value();
|
|
|
|
if timeout > 0 {
|
|
options.timeout = timeout as u64;
|
|
}
|
|
}
|
|
|
|
let read_big_ints_string = READ_BIG_INTS.v8_string(scope).unwrap();
|
|
if let Some(read_big_ints) = obj.get(scope, read_big_ints_string.into())
|
|
&& !read_big_ints.is_undefined()
|
|
{
|
|
options.use_big_int_arguments =
|
|
v8::Local::<v8::Boolean>::try_from(read_big_ints)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.readBigInts\" argument must be a boolean.".into(),
|
|
)
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let return_arrays_string = RETURN_ARRAYS.v8_string(scope).unwrap();
|
|
if let Some(return_arrays) = obj.get(scope, return_arrays_string.into())
|
|
&& !return_arrays.is_undefined()
|
|
{
|
|
options.return_arrays = v8::Local::<v8::Boolean>::try_from(return_arrays)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.returnArrays\" argument must be a boolean.".into(),
|
|
)
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let allow_bare_named_params_string =
|
|
ALLOW_BARE_NAMED_PARAMS.v8_string(scope).unwrap();
|
|
if let Some(allow_bare_named_params) =
|
|
obj.get(scope, allow_bare_named_params_string.into())
|
|
&& !allow_bare_named_params.is_undefined()
|
|
{
|
|
options.allow_bare_named_params =
|
|
v8::Local::<v8::Boolean>::try_from(allow_bare_named_params)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType("The \"options.allowBareNamedParameters\" argument must be a boolean.".into())
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let allow_unknown_named_params_string =
|
|
ALLOW_UNKNOWN_NAMED_PARAMS.v8_string(scope).unwrap();
|
|
if let Some(allow_unknown_named_params) =
|
|
obj.get(scope, allow_unknown_named_params_string.into())
|
|
&& !allow_unknown_named_params.is_undefined()
|
|
{
|
|
options.allow_unknown_named_params =
|
|
v8::Local::<v8::Boolean>::try_from(allow_unknown_named_params)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType("The \"options.allowUnknownNamedParameters\" argument must be a boolean.".into())
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let defensive_string = DEFENSIVE_STRING.v8_string(scope).unwrap();
|
|
if let Some(is_defensive_mode) = obj.get(scope, defensive_string.into())
|
|
&& !is_defensive_mode.is_undefined()
|
|
{
|
|
options.is_defensive_mode =
|
|
v8::Local::<v8::Boolean>::try_from(is_defensive_mode)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.defensive\" argument must be a boolean.".into(),
|
|
)
|
|
})?
|
|
.is_true();
|
|
}
|
|
|
|
let limits_string = LIMITS_STRING.v8_string(scope).unwrap();
|
|
if let Some(limits_value) = obj.get(scope, limits_string.into())
|
|
&& !limits_value.is_undefined()
|
|
{
|
|
let limits_obj = v8::Local::<v8::Object>::try_from(limits_value)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.limits\" argument must be an object.".into(),
|
|
)
|
|
})?;
|
|
|
|
for (idx, &(js_name, _limit)) in LIMIT_MAPPING.iter().enumerate() {
|
|
let key = v8::String::new(scope, js_name).ok_or({
|
|
Error::InvalidArgType("Failed to create limit key string.".into())
|
|
})?;
|
|
|
|
if let Some(val) = limits_obj.get(scope, key.into())
|
|
&& !val.is_undefined()
|
|
{
|
|
let int_val =
|
|
v8::Local::<v8::Int32>::try_from(val).map_err(|_| {
|
|
Error::InvalidArgType(
|
|
format!(
|
|
"The \"options.limits.{}\" argument must be an integer.",
|
|
js_name
|
|
)
|
|
.into(),
|
|
)
|
|
})?;
|
|
let limit_val = int_val.value();
|
|
if limit_val < 0 {
|
|
return Err(Error::OutOfRange(
|
|
format!(
|
|
"The \"options.limits.{}\" argument must be non-negative.",
|
|
js_name
|
|
)
|
|
.into(),
|
|
));
|
|
}
|
|
|
|
options.initial_limits[idx] = Some(limit_val);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(options)
|
|
}
|
|
}
|
|
|
|
impl Default for DatabaseSyncOptions {
|
|
fn default() -> Self {
|
|
DatabaseSyncOptions {
|
|
open: true,
|
|
enable_foreign_key_constraints: true,
|
|
read_only: false,
|
|
allow_extension: false,
|
|
enable_double_quoted_string_literals: false,
|
|
use_big_int_arguments: false,
|
|
return_arrays: false,
|
|
allow_bare_named_params: true,
|
|
allow_unknown_named_params: false,
|
|
is_defensive_mode: true,
|
|
timeout: 0,
|
|
initial_limits: [None; NUM_LIMITS],
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AggregateFunctionOption<'a> {
|
|
deterministic: bool,
|
|
direct_only: bool,
|
|
use_big_int_arguments: bool,
|
|
varargs: bool,
|
|
start: v8::Local<'a, v8::Value>,
|
|
step: v8::Local<'a, v8::Function>,
|
|
result: Option<v8::Local<'a, v8::Function>>,
|
|
inverse: Option<v8::Local<'a, v8::Function>>,
|
|
}
|
|
|
|
impl<'a> AggregateFunctionOption<'a> {
|
|
fn from_value(
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
value: v8::Local<'a, v8::Value>,
|
|
) -> Result<Self, validators::Error> {
|
|
use validators::Error;
|
|
|
|
if !value.is_object() {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
));
|
|
}
|
|
|
|
v8_static_strings! {
|
|
DETERMINISTIC_STRING = "deterministic",
|
|
DIRECT_ONLY_STRING = "directOnly",
|
|
USE_BIG_INT_ARGUMENTS_STRING = "useBigIntArguments",
|
|
VARARGS_STRING = "varargs",
|
|
START_STRING = "start",
|
|
STEP_STRING = "step",
|
|
RESULT_STRING = "result",
|
|
INVERSE_STRING = "inverse",
|
|
}
|
|
|
|
let object = v8::Local::<v8::Object>::try_from(value).map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
)
|
|
})?;
|
|
|
|
let start_key = START_STRING.v8_string(scope).unwrap();
|
|
let start_value = object
|
|
.get(scope, start_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
if start_value.is_undefined() {
|
|
return Err(Error::InvalidArgType("The \"options.start\" argument must be a function or a primitive value.".into()));
|
|
}
|
|
|
|
let step_key = STEP_STRING.v8_string(scope).unwrap();
|
|
let step_value = object
|
|
.get(scope, step_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
let step_function = v8::Local::<v8::Function>::try_from(step_value)
|
|
.map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.step\" argument must be a function.".into(),
|
|
)
|
|
})?;
|
|
|
|
let result_key = RESULT_STRING.v8_string(scope).unwrap();
|
|
let result_value = object
|
|
.get(scope, result_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
let result_function = if result_value.is_undefined() {
|
|
None
|
|
} else {
|
|
let func =
|
|
v8::Local::<v8::Function>::try_from(result_value).map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.result\" argument must be a function.".into(),
|
|
)
|
|
})?;
|
|
Some(func)
|
|
};
|
|
|
|
let mut deterministic = false;
|
|
let mut use_big_int_arguments = false;
|
|
let mut varargs = false;
|
|
let mut direct_only = false;
|
|
|
|
let deterministic_key = DETERMINISTIC_STRING.v8_string(scope).unwrap();
|
|
let deterministic_value = object
|
|
.get(scope, deterministic_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
if !deterministic_value.is_undefined() {
|
|
if !deterministic_value.is_boolean() {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options.deterministic\" argument must be a boolean.".into(),
|
|
));
|
|
}
|
|
deterministic = deterministic_value.boolean_value(scope);
|
|
}
|
|
|
|
let use_bigint_key = USE_BIG_INT_ARGUMENTS_STRING.v8_string(scope).unwrap();
|
|
let bigint_value = object
|
|
.get(scope, use_bigint_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
if !bigint_value.is_undefined() {
|
|
if !bigint_value.is_boolean() {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options.useBigIntArguments\" argument must be a boolean."
|
|
.into(),
|
|
));
|
|
}
|
|
use_big_int_arguments = bigint_value.boolean_value(scope);
|
|
}
|
|
|
|
let varargs_key = VARARGS_STRING.v8_string(scope).unwrap();
|
|
let varargs_value = object
|
|
.get(scope, varargs_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
if !varargs_value.is_undefined() {
|
|
if !varargs_value.is_boolean() {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options.varargs\" argument must be a boolean.".into(),
|
|
));
|
|
}
|
|
varargs = varargs_value.boolean_value(scope);
|
|
}
|
|
|
|
let direct_only_key = DIRECT_ONLY_STRING.v8_string(scope).unwrap();
|
|
let direct_only_value = object
|
|
.get(scope, direct_only_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
if !direct_only_value.is_undefined() {
|
|
if !direct_only_value.is_boolean() {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options.directOnly\" argument must be a boolean.".into(),
|
|
));
|
|
}
|
|
direct_only = direct_only_value.boolean_value(scope);
|
|
}
|
|
|
|
let inverse_key = INVERSE_STRING.v8_string(scope).unwrap();
|
|
let inverse_value = object
|
|
.get(scope, inverse_key.into())
|
|
.ok_or(Error::V8Exception)?;
|
|
let inverse_function = if inverse_value.is_undefined() {
|
|
None
|
|
} else {
|
|
let func =
|
|
v8::Local::<v8::Function>::try_from(inverse_value).map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options.inverse\" argument must be a function.".into(),
|
|
)
|
|
})?;
|
|
Some(func)
|
|
};
|
|
|
|
Ok(AggregateFunctionOption {
|
|
deterministic,
|
|
direct_only,
|
|
use_big_int_arguments,
|
|
varargs,
|
|
start: start_value,
|
|
step: step_function,
|
|
result: result_function,
|
|
inverse: inverse_function,
|
|
})
|
|
}
|
|
}
|
|
|
|
struct ApplyChangesetOptions<'a> {
|
|
filter: Option<v8::Local<'a, v8::Value>>,
|
|
on_conflict: Option<v8::Local<'a, v8::Value>>,
|
|
}
|
|
|
|
// Note: Can't use `FromV8` here because of lifetime issues with holding
|
|
// Local references.
|
|
impl<'a> ApplyChangesetOptions<'a> {
|
|
fn from_value(
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
value: v8::Local<'a, v8::Value>,
|
|
) -> Result<Option<Self>, validators::Error> {
|
|
use validators::Error;
|
|
|
|
if value.is_undefined() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let obj = v8::Local::<v8::Object>::try_from(value).map_err(|_| {
|
|
Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
)
|
|
})?;
|
|
|
|
let mut options = Self {
|
|
filter: None,
|
|
on_conflict: None,
|
|
};
|
|
|
|
v8_static_strings! {
|
|
FILTER_STRING = "filter",
|
|
ON_CONFLICT_STRING = "onConflict",
|
|
}
|
|
|
|
let filter_string = FILTER_STRING.v8_string(scope).unwrap();
|
|
if let Some(filter) = obj.get(scope, filter_string.into())
|
|
&& !filter.is_undefined()
|
|
{
|
|
if !filter.is_function() {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options.filter\" argument must be a function.".into(),
|
|
));
|
|
}
|
|
|
|
options.filter = Some(filter);
|
|
}
|
|
|
|
let on_conflict_string = ON_CONFLICT_STRING.v8_string(scope).unwrap();
|
|
if let Some(on_conflict) = obj.get(scope, on_conflict_string.into())
|
|
&& !on_conflict.is_undefined()
|
|
{
|
|
if !on_conflict.is_function() {
|
|
return Err(Error::InvalidArgType(
|
|
"The \"options.onConflict\" argument must be a function.".into(),
|
|
));
|
|
}
|
|
|
|
options.on_conflict = Some(on_conflict);
|
|
}
|
|
|
|
Ok(Some(options))
|
|
}
|
|
}
|
|
|
|
pub struct DatabaseSync {
|
|
pub conn: Rc<RefCell<Option<rusqlite::Connection>>>,
|
|
statements: Rc<RefCell<Vec<InnerStatementPtr>>>,
|
|
sessions: Rc<RefCell<Vec<InnerSessionPtr>>>,
|
|
options: DatabaseSyncOptions,
|
|
location: String,
|
|
ignore_next_sqlite_error: Rc<Cell<bool>>,
|
|
authorizer_data: Rc<RefCell<Option<*mut AuthorizerData>>>,
|
|
// Non-zero while a user-defined callback is on the stack; close() refuses
|
|
// to free the connection in that state to avoid a SQLite VDBE use-after-free.
|
|
callback_depth: Rc<Cell<usize>>,
|
|
// Whether ATTACH DATABASE is held disabled because the process lacks full
|
|
// permissions for the database path. Used to stop the `limits.attach` setter
|
|
// from raising the cap and bypassing the boundary.
|
|
disable_attach: Rc<Cell<bool>>,
|
|
}
|
|
|
|
struct CallbackDepthGuard {
|
|
depth: Rc<Cell<usize>>,
|
|
}
|
|
|
|
impl CallbackDepthGuard {
|
|
fn new(depth: &Rc<Cell<usize>>) -> Self {
|
|
depth.set(depth.get().saturating_add(1));
|
|
Self {
|
|
depth: Rc::clone(depth),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for CallbackDepthGuard {
|
|
fn drop(&mut self) {
|
|
self.depth.set(self.depth.get().saturating_sub(1));
|
|
}
|
|
}
|
|
|
|
// SAFETY: we're sure this can be GCed
|
|
unsafe impl GarbageCollected for DatabaseSync {
|
|
fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}
|
|
|
|
fn get_name(&self) -> &'static std::ffi::CStr {
|
|
c"DatabaseSync"
|
|
}
|
|
}
|
|
|
|
fn set_db_config(
|
|
conn: &rusqlite::Connection,
|
|
config: i32,
|
|
value: bool,
|
|
) -> bool {
|
|
// SAFETY: call to sqlite3_db_config is safe because the connection
|
|
// handle is valid and the parameters are correct.
|
|
unsafe {
|
|
let mut set = 0;
|
|
let r = libsqlite3_sys::sqlite3_db_config(
|
|
conn.handle(),
|
|
config,
|
|
value as i32,
|
|
&mut set,
|
|
);
|
|
|
|
if r != libsqlite3_sys::SQLITE_OK {
|
|
panic!("Failed to set db config");
|
|
}
|
|
|
|
set == value as i32
|
|
}
|
|
}
|
|
|
|
enum LimitCoercionError {
|
|
NotANumber,
|
|
NotAnInteger,
|
|
Negative,
|
|
}
|
|
|
|
/// Coerce a v8 value to a valid SQLite limit integer.
|
|
/// Accepts non-negative integers and positive Infinity (mapped to i32::MAX).
|
|
fn coerce_limit_value(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<i32, LimitCoercionError> {
|
|
if !value.is_number() {
|
|
return Err(LimitCoercionError::NotANumber);
|
|
}
|
|
let num_value = value.number_value(scope).unwrap_or(f64::NAN);
|
|
if num_value.is_infinite() && num_value > 0.0 {
|
|
return Ok(i32::MAX);
|
|
}
|
|
if !value.is_int32() {
|
|
return Err(LimitCoercionError::NotAnInteger);
|
|
}
|
|
let int_val = value.int32_value(scope).unwrap_or(-1);
|
|
if int_val < 0 {
|
|
return Err(LimitCoercionError::Negative);
|
|
}
|
|
Ok(int_val)
|
|
}
|
|
|
|
/// Apply initial limits to a connection based on options
|
|
fn apply_initial_limits(
|
|
conn: &rusqlite::Connection,
|
|
options: &DatabaseSyncOptions,
|
|
disable_attach: bool,
|
|
) -> Result<(), SqliteError> {
|
|
for (idx, &(_js_name, limit)) in LIMIT_MAPPING.iter().enumerate() {
|
|
if let Some(value) = options.initial_limits[idx] {
|
|
// When the process lacks full permissions for the database path, the
|
|
// attach limit is a security cap held at 0: ATTACH DATABASE can reach
|
|
// files outside the database path, so it must not be raised through the
|
|
// user-controlled `limits.attach` option. Reject raising it above 0,
|
|
// mirroring the `db.limits.attach` setter. A value of 0 (keeping it
|
|
// disabled) is still accepted.
|
|
if disable_attach
|
|
&& matches!(limit, Limit::SQLITE_LIMIT_ATTACHED)
|
|
&& value > 0
|
|
{
|
|
return Err(SqliteError::AttachLimitDenied);
|
|
}
|
|
conn.set_limit(limit, value)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Returns the open connection together with `disable_attach`: whether the
|
|
// process lacks full permissions for the database path and so ATTACH DATABASE
|
|
// is held disabled. Callers store this so the `limits.attach` setter cannot
|
|
// later raise the cap and bypass the boundary.
|
|
fn open_db(
|
|
state: &mut OpState,
|
|
location: &str,
|
|
options: &DatabaseSyncOptions,
|
|
) -> Result<(rusqlite::Connection, bool), SqliteError> {
|
|
let perms = state.borrow::<PermissionsContainer>();
|
|
let disable_attach = perms
|
|
.check_has_all_permissions(Path::new(location))
|
|
.is_err();
|
|
|
|
if location == ":memory:" {
|
|
let conn = rusqlite::Connection::open_in_memory()?;
|
|
if disable_attach {
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE,
|
|
false
|
|
));
|
|
conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?;
|
|
}
|
|
|
|
if options.allow_extension {
|
|
perms.check_ffi_all()?;
|
|
} else {
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
|
|
false
|
|
));
|
|
}
|
|
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_DEFENSIVE,
|
|
options.is_defensive_mode,
|
|
));
|
|
|
|
apply_initial_limits(&conn, options, disable_attach)?;
|
|
|
|
return Ok((conn, disable_attach));
|
|
}
|
|
|
|
let location = perms
|
|
.check_open(
|
|
Cow::Borrowed(Path::new(location)),
|
|
match options.read_only {
|
|
true => OpenAccessKind::ReadNoFollow,
|
|
false => OpenAccessKind::ReadWriteNoFollow,
|
|
},
|
|
Some("node:sqlite"),
|
|
)?
|
|
.into_path();
|
|
let location = resolve_sqlite_system_path_alias(&location);
|
|
#[cfg(windows)]
|
|
refuse_reparse_point_components(&location)?;
|
|
|
|
if options.read_only {
|
|
let conn = rusqlite::Connection::open_with_flags(
|
|
&location,
|
|
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
|
|
| rusqlite::OpenFlags::SQLITE_OPEN_NOFOLLOW,
|
|
)?;
|
|
if disable_attach {
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE,
|
|
false
|
|
));
|
|
conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?;
|
|
}
|
|
|
|
if options.allow_extension {
|
|
perms.check_ffi_all()?;
|
|
} else {
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
|
|
false
|
|
));
|
|
}
|
|
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_DEFENSIVE,
|
|
options.is_defensive_mode,
|
|
));
|
|
|
|
apply_initial_limits(&conn, options, disable_attach)?;
|
|
|
|
return Ok((conn, disable_attach));
|
|
}
|
|
|
|
let conn = rusqlite::Connection::open_with_flags(
|
|
&location,
|
|
rusqlite::OpenFlags::default() | rusqlite::OpenFlags::SQLITE_OPEN_NOFOLLOW,
|
|
)?;
|
|
conn.busy_timeout(std::time::Duration::from_millis(options.timeout))?;
|
|
|
|
if options.allow_extension {
|
|
perms.check_ffi_all()?;
|
|
} else {
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
|
|
false
|
|
));
|
|
}
|
|
|
|
if disable_attach {
|
|
conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?;
|
|
}
|
|
|
|
assert!(set_db_config(
|
|
&conn,
|
|
SQLITE_DBCONFIG_DEFENSIVE,
|
|
options.is_defensive_mode,
|
|
));
|
|
|
|
apply_initial_limits(&conn, options, disable_attach)?;
|
|
|
|
Ok((conn, disable_attach))
|
|
}
|
|
|
|
fn is_open(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
args: &v8::FunctionCallbackArguments,
|
|
) -> Result<(), SqliteError> {
|
|
let this_ = args.this();
|
|
let db = cppgc::try_unwrap_cppgc_object::<DatabaseSync>(scope, this_.into())
|
|
.ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
db.conn
|
|
.borrow()
|
|
.as_ref()
|
|
.ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Represents a single connection to a SQLite database.
|
|
#[op2]
|
|
impl DatabaseSync {
|
|
// Constructs a new `DatabaseSync` instance.
|
|
//
|
|
// A SQLite database can be stored in a file or in memory. To
|
|
// use a file-backed database, the `location` should be a path.
|
|
// To use an in-memory database, the `location` should be special
|
|
// name ":memory:".
|
|
#[constructor]
|
|
#[cppgc]
|
|
fn new(
|
|
state: &mut OpState,
|
|
#[string] location: String,
|
|
#[scoped] options: DatabaseSyncOptions,
|
|
) -> Result<DatabaseSync, SqliteError> {
|
|
let mut disable_attach = false;
|
|
let db = if options.open {
|
|
let (db, da) = open_db(state, &location, &options)?;
|
|
disable_attach = da;
|
|
|
|
if options.enable_foreign_key_constraints {
|
|
db.execute("PRAGMA foreign_keys = ON", [])?;
|
|
} else {
|
|
db.execute("PRAGMA foreign_keys = OFF", [])?;
|
|
}
|
|
|
|
set_db_config(
|
|
&db,
|
|
SQLITE_DBCONFIG_DQS_DDL,
|
|
options.enable_double_quoted_string_literals,
|
|
);
|
|
set_db_config(
|
|
&db,
|
|
SQLITE_DBCONFIG_DQS_DML,
|
|
options.enable_double_quoted_string_literals,
|
|
);
|
|
Some(db)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(DatabaseSync {
|
|
conn: Rc::new(RefCell::new(db)),
|
|
statements: Rc::new(RefCell::new(Vec::new())),
|
|
sessions: Rc::new(RefCell::new(Vec::new())),
|
|
location,
|
|
options,
|
|
ignore_next_sqlite_error: Rc::new(Cell::new(false)),
|
|
authorizer_data: Rc::new(RefCell::new(None)),
|
|
callback_depth: Rc::new(Cell::new(0)),
|
|
disable_attach: Rc::new(Cell::new(disable_attach)),
|
|
})
|
|
}
|
|
|
|
// Opens the database specified by `location` of this instance.
|
|
//
|
|
// This method should only be used when the database is not opened
|
|
// via the constructor. An exception is thrown if the database is
|
|
// already opened.
|
|
#[fast]
|
|
#[undefined]
|
|
fn open(&self, state: &mut OpState) -> Result<(), SqliteError> {
|
|
if self.conn.borrow().is_some() {
|
|
return Err(SqliteError::AlreadyOpen);
|
|
}
|
|
|
|
let (db, disable_attach) = open_db(state, &self.location, &self.options)?;
|
|
if self.options.enable_foreign_key_constraints {
|
|
db.execute("PRAGMA foreign_keys = ON", [])?;
|
|
} else {
|
|
db.execute("PRAGMA foreign_keys = OFF", [])?;
|
|
}
|
|
|
|
set_db_config(
|
|
&db,
|
|
SQLITE_DBCONFIG_DQS_DDL,
|
|
self.options.enable_double_quoted_string_literals,
|
|
);
|
|
set_db_config(
|
|
&db,
|
|
SQLITE_DBCONFIG_DQS_DML,
|
|
self.options.enable_double_quoted_string_literals,
|
|
);
|
|
|
|
*self.conn.borrow_mut() = Some(db);
|
|
self.disable_attach.set(disable_attach);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Closes the database connection. An exception is thrown if the
|
|
// database is not open.
|
|
#[fast]
|
|
#[undefined]
|
|
fn close(&self) -> Result<(), SqliteError> {
|
|
if self.conn.borrow().is_none() {
|
|
return Err(SqliteError::AlreadyClosed);
|
|
}
|
|
|
|
// Refuse to close while a user-defined callback is on the SQLite stack —
|
|
// freeing the in-flight statement would be a VDBE use-after-free.
|
|
if self.callback_depth.get() > 0 {
|
|
return Err(SqliteError::ActiveCallback);
|
|
}
|
|
|
|
// Finalize all prepared statements
|
|
for stmt in self.statements.borrow_mut().drain(..) {
|
|
match stmt.get() {
|
|
None => continue,
|
|
Some(ptr) => {
|
|
// SAFETY: `ptr` is a valid statement handle.
|
|
unsafe {
|
|
libsqlite3_sys::sqlite3_finalize(ptr);
|
|
};
|
|
stmt.set(None);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Delete all sessions while their database connection is still valid.
|
|
for session in self.sessions.borrow_mut().drain(..) {
|
|
if let Some(ptr) = session.take() {
|
|
// SAFETY: `ptr` is a valid session handle associated with the open
|
|
// connection. Taking it first prevents stale Session objects from
|
|
// deleting or using it after the connection is reopened.
|
|
unsafe {
|
|
libsqlite3_sys::sqlite3session_delete(ptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
let mut authorizer_data = self.authorizer_data.borrow_mut();
|
|
if let Some(data_ptr) = authorizer_data.take() {
|
|
// SAFETY: data_ptr was allocated in authorizer setup.
|
|
unsafe {
|
|
release_authorizer_data(data_ptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = self.conn.borrow_mut().take();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// This method allows one or more SQL statements to be executed
|
|
// without returning any results.
|
|
//
|
|
// This method is a wrapper around sqlite3_exec().
|
|
#[fast]
|
|
#[reentrant]
|
|
#[validate(is_open)]
|
|
#[undefined]
|
|
fn exec(
|
|
&self,
|
|
#[validate(validators::sql_str)]
|
|
#[string]
|
|
sql: &str,
|
|
) -> Result<(), SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::InUse)?;
|
|
|
|
if let Err(err) = db.execute_batch(sql) {
|
|
if self.consume_ignore_next_sqlite_error() {
|
|
return Ok(());
|
|
}
|
|
return Err(err.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Compiles an SQL statement into a prepared statement.
|
|
//
|
|
// This method is a wrapper around `sqlite3_prepare_v2()`.
|
|
#[reentrant]
|
|
#[validate(is_open)]
|
|
#[cppgc]
|
|
fn prepare(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
#[validate(validators::sql_str)]
|
|
#[string]
|
|
sql: &str,
|
|
#[varargs] args: Option<&v8::FunctionCallbackArguments>,
|
|
) -> Result<StatementSync, SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::InUse)?;
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference
|
|
// counting.
|
|
let raw_handle = unsafe { db.handle() };
|
|
|
|
let mut raw_stmt = std::ptr::null_mut();
|
|
|
|
// SAFETY: `sql` points to a valid memory location and its length
|
|
// is correct.
|
|
let r = unsafe {
|
|
libsqlite3_sys::sqlite3_prepare_v2(
|
|
raw_handle,
|
|
sql.as_ptr() as *const _,
|
|
sql.len() as i32,
|
|
&mut raw_stmt,
|
|
std::ptr::null_mut(),
|
|
)
|
|
};
|
|
check_error_code(r, raw_handle)?;
|
|
|
|
let mut allow_unknown_named_params =
|
|
self.options.allow_unknown_named_params;
|
|
let mut use_big_ints = self.options.use_big_int_arguments;
|
|
let mut return_arrays = self.options.return_arrays;
|
|
let mut allow_bare_named_params = self.options.allow_bare_named_params;
|
|
|
|
// Parse options object (second argument)
|
|
if let Some(args) = args {
|
|
// args[0] is already consumed as `sql`, so options is args[1]
|
|
if args.length() > 1 {
|
|
let options = args.get(1);
|
|
if !options.is_undefined() && !options.is_null() {
|
|
let options =
|
|
v8::Local::<v8::Object>::try_from(options).map_err(|_| {
|
|
SqliteError::Validation(validators::Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
))
|
|
})?;
|
|
|
|
v8_static_strings! {
|
|
ALLOW_UNKNOWN = "allowUnknownNamedParameters",
|
|
READ_BIG_INTS = "readBigInts",
|
|
RETURN_ARRAYS = "returnArrays",
|
|
ALLOW_BARE = "allowBareNamedParameters",
|
|
}
|
|
|
|
macro_rules! parse_bool_opt {
|
|
($key:expr, $name:expr, $target:ident) => {
|
|
let key = $key.v8_string(scope).unwrap().into();
|
|
if let Some(val) = options.get(scope, key) {
|
|
if !val.is_undefined() {
|
|
$target = v8::Local::<v8::Boolean>::try_from(val)
|
|
.map_err(|_| {
|
|
SqliteError::Validation(
|
|
validators::Error::InvalidArgType(
|
|
concat!(
|
|
"The \"",
|
|
$name,
|
|
"\" argument must be a boolean."
|
|
)
|
|
.into(),
|
|
),
|
|
)
|
|
})?
|
|
.is_true();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
parse_bool_opt!(
|
|
ALLOW_UNKNOWN,
|
|
"options.allowUnknownNamedParameters",
|
|
allow_unknown_named_params
|
|
);
|
|
parse_bool_opt!(READ_BIG_INTS, "options.readBigInts", use_big_ints);
|
|
parse_bool_opt!(RETURN_ARRAYS, "options.returnArrays", return_arrays);
|
|
parse_bool_opt!(
|
|
ALLOW_BARE,
|
|
"options.allowBareNamedParameters",
|
|
allow_bare_named_params
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let stmt_cell = Rc::new(Cell::new(Some(raw_stmt)));
|
|
self.statements.borrow_mut().push(stmt_cell.clone());
|
|
|
|
Ok(StatementSync {
|
|
inner: stmt_cell,
|
|
db: self.conn.clone(),
|
|
statements: Rc::clone(&self.statements),
|
|
ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
|
|
return_arrays: Cell::new(return_arrays),
|
|
use_big_ints: Cell::new(use_big_ints),
|
|
allow_bare_named_params: Cell::new(allow_bare_named_params),
|
|
allow_unknown_named_params: Cell::new(allow_unknown_named_params),
|
|
is_iter_finished: Cell::new(false),
|
|
iter_generation: Cell::new(0),
|
|
iter_contexts: RefCell::new(Vec::new()),
|
|
})
|
|
}
|
|
|
|
#[fast]
|
|
#[validate(is_open)]
|
|
#[undefined]
|
|
fn function<'a>(
|
|
&self,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
#[varargs] args: Option<&v8::FunctionCallbackArguments>,
|
|
) -> Result<(), SqliteError> {
|
|
let Some(args) = args.filter(|args| args.length() > 0) else {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"name\" argument must be a string.".into(),
|
|
)
|
|
.into(),
|
|
);
|
|
};
|
|
|
|
if !args.get(0).is_string() {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"name\" argument must be a string.".into(),
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
let name = args.get(0).to_rust_string_lossy(scope);
|
|
|
|
let (options_value, function_value) = if args.length() < 3 {
|
|
(None, args.get(1))
|
|
} else {
|
|
(Some(args.get(1)), args.get(2))
|
|
};
|
|
|
|
let Ok(function) = v8::Local::<v8::Function>::try_from(function_value)
|
|
else {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"function\" argument must be a function.".into(),
|
|
)
|
|
.into(),
|
|
);
|
|
};
|
|
|
|
let mut use_big_int_arguments = false;
|
|
let mut varargs = false;
|
|
let mut deterministic = false;
|
|
let mut direct_only = false;
|
|
|
|
if let Some(value) = options_value
|
|
&& !value.is_undefined()
|
|
{
|
|
if value.is_null() || !value.is_object() {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
let options = v8::Local::<v8::Object>::try_from(value).unwrap();
|
|
|
|
v8_static_strings! {
|
|
USE_BIG_INT_ARGUMENTS = "useBigIntArguments",
|
|
VARARGS = "varargs",
|
|
DETERMINISTIC = "deterministic",
|
|
DIRECT_ONLY = "directOnly",
|
|
}
|
|
|
|
let use_bigint_key = USE_BIG_INT_ARGUMENTS.v8_string(scope).unwrap();
|
|
let bigint_value = options
|
|
.get(scope, use_bigint_key.into())
|
|
.ok_or(validators::Error::V8Exception)?;
|
|
if !bigint_value.is_undefined() {
|
|
if !bigint_value.is_boolean() {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.useBigIntArguments\" argument must be a boolean."
|
|
.into(),
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
use_big_int_arguments = bigint_value.boolean_value(scope);
|
|
}
|
|
|
|
let varargs_key = VARARGS.v8_string(scope).unwrap();
|
|
let varargs_value = options
|
|
.get(scope, varargs_key.into())
|
|
.ok_or(validators::Error::V8Exception)?;
|
|
if !varargs_value.is_undefined() {
|
|
if !varargs_value.is_boolean() {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.varargs\" argument must be a boolean.".into(),
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
varargs = varargs_value.boolean_value(scope);
|
|
}
|
|
|
|
let deterministic_key = DETERMINISTIC.v8_string(scope).unwrap();
|
|
let deterministic_value = options
|
|
.get(scope, deterministic_key.into())
|
|
.ok_or(validators::Error::V8Exception)?;
|
|
if !deterministic_value.is_undefined() {
|
|
if !deterministic_value.is_boolean() {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.deterministic\" argument must be a boolean."
|
|
.into(),
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
deterministic = deterministic_value.boolean_value(scope);
|
|
}
|
|
|
|
let direct_only_key = DIRECT_ONLY.v8_string(scope).unwrap();
|
|
let direct_only_value = options
|
|
.get(scope, direct_only_key.into())
|
|
.ok_or(validators::Error::V8Exception)?;
|
|
if !direct_only_value.is_undefined() {
|
|
if !direct_only_value.is_boolean() {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.directOnly\" argument must be a boolean.".into(),
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
direct_only = direct_only_value.boolean_value(scope);
|
|
}
|
|
}
|
|
|
|
v8_static_strings! {
|
|
LENGTH = "length",
|
|
}
|
|
|
|
let argc = if varargs {
|
|
-1
|
|
} else {
|
|
let length_key = LENGTH.v8_string(scope).unwrap();
|
|
let length = function
|
|
.get(scope, length_key.into())
|
|
.ok_or(validators::Error::V8Exception)?;
|
|
length
|
|
.int32_value(scope)
|
|
.ok_or(validators::Error::V8Exception)?
|
|
};
|
|
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::InUse)?;
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let raw_handle = unsafe { db.handle() };
|
|
let name_cstring = CString::new(name)?;
|
|
|
|
let callback = v8::Global::new(scope, function).into_raw();
|
|
let context =
|
|
v8::Global::new(scope, scope.get_current_context()).into_raw();
|
|
|
|
let data = Box::new(CustomFunctionData {
|
|
callback,
|
|
context,
|
|
use_big_int_arguments,
|
|
ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
|
|
callback_depth: Rc::clone(&self.callback_depth),
|
|
});
|
|
let data_ptr = Box::into_raw(data);
|
|
|
|
let mut text_rep = libsqlite3_sys::SQLITE_UTF8;
|
|
if deterministic {
|
|
text_rep |= libsqlite3_sys::SQLITE_DETERMINISTIC;
|
|
}
|
|
if direct_only {
|
|
text_rep |= libsqlite3_sys::SQLITE_DIRECTONLY;
|
|
}
|
|
|
|
// SAFETY: `raw_handle` is a valid database handle.
|
|
// `data_ptr` points to a valid memory location.
|
|
// The v8 handles that are held in `CustomFunctionData` will be
|
|
// dropped when the data is destroyed via `custom_function_destroy`.
|
|
let result = unsafe {
|
|
libsqlite3_sys::sqlite3_create_function_v2(
|
|
raw_handle,
|
|
name_cstring.as_ptr(),
|
|
argc,
|
|
text_rep,
|
|
data_ptr as *mut c_void,
|
|
Some(custom_function_handler),
|
|
None,
|
|
None,
|
|
Some(custom_function_destroy),
|
|
)
|
|
};
|
|
check_error_code(result, raw_handle)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Applies a changeset to the database.
|
|
//
|
|
// This method is a wrapper around `sqlite3changeset_apply()`.
|
|
#[fast]
|
|
#[reentrant]
|
|
fn apply_changeset<'a>(
|
|
&self,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
#[validate(validators::changeset_buffer)]
|
|
#[buffer]
|
|
changeset: &[u8],
|
|
options: v8::Local<'a, v8::Value>,
|
|
) -> Result<bool, SqliteError> {
|
|
let options = ApplyChangesetOptions::from_value(scope, options)?;
|
|
|
|
struct HandlerCtx<'a, 'b, 'c> {
|
|
scope: &'a mut v8::PinScope<'b, 'c>,
|
|
conflict: Option<v8::Local<'b, v8::Function>>,
|
|
filter: Option<v8::Local<'b, v8::Function>>,
|
|
has_caught: bool,
|
|
}
|
|
|
|
// Conflict handler callback for `sqlite3changeset_apply()`.
|
|
unsafe extern "C" fn conflict_handler(
|
|
p_ctx: *mut c_void,
|
|
e_conflict: i32,
|
|
_: *mut libsqlite3_sys::sqlite3_changeset_iter,
|
|
) -> i32 {
|
|
#[allow(
|
|
clippy::undocumented_unsafe_blocks,
|
|
reason = "safety comment on the containing block"
|
|
)]
|
|
unsafe {
|
|
let ctx = &mut *(p_ctx as *mut HandlerCtx);
|
|
|
|
if let Some(conflict) = &mut ctx.conflict {
|
|
let recv = v8::undefined(ctx.scope).into();
|
|
let args = [v8::Integer::new(ctx.scope, e_conflict).into()];
|
|
|
|
v8::tc_scope!(tc_scope, ctx.scope);
|
|
|
|
let ret = conflict
|
|
.call(tc_scope, recv, &args)
|
|
.unwrap_or_else(|| v8::undefined(tc_scope).into());
|
|
if tc_scope.has_caught() {
|
|
tc_scope.rethrow();
|
|
return libsqlite3_sys::SQLITE_CHANGESET_ABORT;
|
|
}
|
|
|
|
const INVALID_VALUE: i32 = -1;
|
|
if !ret.is_int32() {
|
|
return INVALID_VALUE;
|
|
}
|
|
|
|
let value = ret
|
|
.int32_value(tc_scope)
|
|
.unwrap_or(libsqlite3_sys::SQLITE_CHANGESET_ABORT);
|
|
|
|
return value;
|
|
}
|
|
|
|
libsqlite3_sys::SQLITE_CHANGESET_ABORT
|
|
}
|
|
}
|
|
|
|
// Filter handler callback for `sqlite3changeset_apply()`.
|
|
unsafe extern "C" fn filter_handler(
|
|
p_ctx: *mut c_void,
|
|
z_tab: *const c_char,
|
|
) -> i32 {
|
|
#[allow(
|
|
clippy::undocumented_unsafe_blocks,
|
|
reason = "safety comment on the containing block"
|
|
)]
|
|
unsafe {
|
|
let ctx = &mut *(p_ctx as *mut HandlerCtx);
|
|
|
|
// If we've already caught an exception, don't call the filter again
|
|
// to avoid overwriting the original exception.
|
|
if ctx.has_caught {
|
|
return 0;
|
|
}
|
|
|
|
if let Some(filter) = &mut ctx.filter {
|
|
let tab = CStr::from_ptr(z_tab).to_str().unwrap();
|
|
|
|
let recv = v8::undefined(ctx.scope).into();
|
|
let args = [v8::String::new(ctx.scope, tab).unwrap().into()];
|
|
|
|
v8::tc_scope!(tc_scope, ctx.scope);
|
|
|
|
let ret = filter
|
|
.call(tc_scope, recv, &args)
|
|
.unwrap_or_else(|| v8::undefined(tc_scope).into());
|
|
if tc_scope.has_caught() {
|
|
ctx.has_caught = true;
|
|
tc_scope.rethrow();
|
|
return 0;
|
|
}
|
|
|
|
return ret.boolean_value(tc_scope) as i32;
|
|
}
|
|
|
|
1
|
|
}
|
|
}
|
|
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
// It is safe to use scope in the handlers because they are never
|
|
// called after the call to `sqlite3changeset_apply()`.
|
|
let mut ctx = HandlerCtx {
|
|
scope,
|
|
conflict: None,
|
|
filter: None,
|
|
has_caught: false,
|
|
};
|
|
|
|
if let Some(options) = options {
|
|
if let Some(filter) = options.filter {
|
|
let filter_cb: v8::Local<v8::Function> = filter
|
|
.try_into()
|
|
.map_err(|_| SqliteError::InvalidCallback("filter"))?;
|
|
ctx.filter = Some(filter_cb);
|
|
}
|
|
|
|
if let Some(on_conflict) = options.on_conflict {
|
|
let on_conflict_cb: v8::Local<v8::Function> = on_conflict
|
|
.try_into()
|
|
.map_err(|_| SqliteError::InvalidCallback("onConflict"))?;
|
|
ctx.conflict = Some(on_conflict_cb);
|
|
}
|
|
}
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference
|
|
// counting.
|
|
let raw_handle = unsafe { db.handle() };
|
|
|
|
// Block close() while filter/onConflict callbacks may be on the stack.
|
|
let _apply_depth_guard = CallbackDepthGuard::new(&self.callback_depth);
|
|
|
|
// SAFETY: `changeset` points to a valid memory location and its
|
|
// length is correct. `ctx` is stack allocated and its lifetime is
|
|
// longer than the call to `sqlite3changeset_apply()`.
|
|
unsafe {
|
|
let r = libsqlite3_sys::sqlite3changeset_apply(
|
|
raw_handle,
|
|
changeset.len() as i32,
|
|
changeset.as_ptr() as *mut _,
|
|
Some(filter_handler),
|
|
Some(conflict_handler),
|
|
&mut ctx as *mut _ as *mut c_void,
|
|
);
|
|
|
|
if r == libsqlite3_sys::SQLITE_OK {
|
|
return Ok(true);
|
|
} else if r == libsqlite3_sys::SQLITE_ABORT {
|
|
return Ok(false);
|
|
}
|
|
|
|
check_error_code2(r)?;
|
|
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
// Loads a SQLite extension.
|
|
//
|
|
// This is a wrapper around `sqlite3_load_extension`. It requires FFI permission
|
|
// to be granted and allowExtension must be set to true when opening the database.
|
|
fn load_extension(
|
|
&self,
|
|
state: &mut OpState,
|
|
#[validate(validators::path_str)]
|
|
#[string]
|
|
path: &str,
|
|
#[string] entry_point: Option<String>,
|
|
) -> Result<(), SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
if !self.options.allow_extension {
|
|
return Err(SqliteError::LoadExensionFailed(
|
|
"Cannot load SQLite extensions when allowExtension is not enabled"
|
|
.to_string(),
|
|
));
|
|
}
|
|
|
|
state.borrow::<PermissionsContainer>().check_ffi_all()?;
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let raw_handle = unsafe { db.handle() };
|
|
|
|
let path_cstring = std::ffi::CString::new(path.as_bytes())?;
|
|
let entry_point_cstring =
|
|
entry_point.map(|ep| std::ffi::CString::new(ep).unwrap_or_default());
|
|
|
|
let entry_point_ptr = match &entry_point_cstring {
|
|
Some(cstr) => cstr.as_ptr(),
|
|
None => std::ptr::null(),
|
|
};
|
|
|
|
let mut err_msg: *mut c_char = std::ptr::null_mut();
|
|
|
|
// SAFETY: Using sqlite3_load_extension with proper error handling
|
|
let result = unsafe {
|
|
let res = libsqlite3_sys::sqlite3_load_extension(
|
|
raw_handle,
|
|
path_cstring.as_ptr(),
|
|
entry_point_ptr,
|
|
&mut err_msg,
|
|
);
|
|
|
|
if res != libsqlite3_sys::SQLITE_OK {
|
|
let error_message = if !err_msg.is_null() {
|
|
let c_str = std::ffi::CStr::from_ptr(err_msg);
|
|
let message = c_str.to_string_lossy().into_owned();
|
|
libsqlite3_sys::sqlite3_free(err_msg as *mut _);
|
|
message
|
|
} else {
|
|
format!("Failed to load extension with error code: {}", res)
|
|
};
|
|
|
|
return Err(SqliteError::LoadExensionFailed(error_message));
|
|
}
|
|
|
|
res
|
|
};
|
|
|
|
if result == libsqlite3_sys::SQLITE_OK {
|
|
Ok(())
|
|
} else {
|
|
Err(SqliteError::LoadExensionFailed(
|
|
"Unknown error loading SQLite extension".to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
// Creates and attaches a session to the database.
|
|
//
|
|
// This method is a wrapper around `sqlite3session_create()` and
|
|
// `sqlite3session_attach()`.
|
|
#[cppgc]
|
|
fn create_session(
|
|
&self,
|
|
#[scoped] options: OptionUndefined<SessionOptions>,
|
|
) -> Result<Session, SqliteError> {
|
|
let options = options.0;
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference
|
|
// counting.
|
|
let raw_handle = unsafe { db.handle() };
|
|
|
|
let mut options = options;
|
|
|
|
let z_db = options
|
|
.as_mut()
|
|
.and_then(|options| options.db.take())
|
|
.map(CString::new)
|
|
.transpose()?
|
|
.unwrap_or_else(|| c"main".to_owned());
|
|
let table = options
|
|
.as_mut()
|
|
.and_then(|options| options.table.take())
|
|
.map(CString::new)
|
|
.transpose()?;
|
|
|
|
let mut raw_session = std::ptr::null_mut();
|
|
// SAFETY: `z_db` points to a valid c-string.
|
|
let r = unsafe {
|
|
libsqlite3_sys::sqlite3session_create(
|
|
raw_handle,
|
|
z_db.as_ptr() as *const _,
|
|
&mut raw_session,
|
|
)
|
|
};
|
|
|
|
if r != libsqlite3_sys::SQLITE_OK {
|
|
return Err(SqliteError::SessionCreateFailed);
|
|
}
|
|
|
|
let z_table = table.as_ref().map(|table| table.as_ptr()).unwrap_or(null());
|
|
let r =
|
|
// SAFETY: `z_table` points to a valid c-string and `raw_session`
|
|
// is a valid session handle.
|
|
unsafe { libsqlite3_sys::sqlite3session_attach(raw_session, z_table) };
|
|
|
|
if r != libsqlite3_sys::SQLITE_OK {
|
|
return Err(SqliteError::SessionCreateFailed);
|
|
}
|
|
|
|
let session = Rc::new(Cell::new(Some(raw_session)));
|
|
self.sessions.borrow_mut().push(Rc::clone(&session));
|
|
|
|
Ok(Session {
|
|
inner: session,
|
|
db: self.conn.clone(),
|
|
sessions: Rc::clone(&self.sessions),
|
|
})
|
|
}
|
|
|
|
fn location<'a>(
|
|
&self,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
name_value: v8::Local<'a, v8::Value>,
|
|
) -> Result<v8::Local<'a, v8::Value>, SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
let name = if !name_value.is_undefined() {
|
|
if !name_value.is_string() {
|
|
return Err(SqliteError::Validation(
|
|
validators::Error::InvalidArgType(
|
|
"The \"dbName\" argument must be a string.".into(),
|
|
),
|
|
));
|
|
}
|
|
name_value.to_rust_string_lossy(scope)
|
|
} else {
|
|
"main".to_string()
|
|
};
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let raw_handle = unsafe { db.handle() };
|
|
let name_cstring = CString::new(name)?;
|
|
|
|
// SAFETY: `raw_handle` is a valid sqlite3 pointer.
|
|
let db_filename =
|
|
unsafe { sqlite3_db_filename(raw_handle, name_cstring.as_ptr()) };
|
|
if db_filename.is_null() {
|
|
return Ok(v8::null(scope).into());
|
|
}
|
|
|
|
// SAFETY: `db_filename` is a valid C string pointer.
|
|
let filename_cstr = unsafe { CStr::from_ptr(db_filename).to_bytes() };
|
|
if filename_cstr.is_empty() {
|
|
return Ok(v8::null(scope).into());
|
|
}
|
|
|
|
let filename = v8::String::new_from_utf8(
|
|
scope,
|
|
filename_cstr,
|
|
v8::NewStringType::Normal,
|
|
)
|
|
.unwrap();
|
|
|
|
Ok(filename.into())
|
|
}
|
|
|
|
#[fast]
|
|
#[validate(is_open)]
|
|
fn enable_defensive(
|
|
&self,
|
|
#[validate(validators::active_bool)] is_enabled: bool,
|
|
) -> Result<(), SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
assert!(set_db_config(conn, SQLITE_DBCONFIG_DEFENSIVE, is_enabled));
|
|
Ok(())
|
|
}
|
|
|
|
// Sets an authorizer callback that SQLite will invoke whenever it attempts
|
|
// to access data or modify the database schema through prepared statements.
|
|
// This can be used to implement security policies, audit access, or restrict
|
|
// certain operations.
|
|
#[fast]
|
|
#[validate(is_open)]
|
|
#[undefined]
|
|
fn set_authorizer<'a>(
|
|
&self,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
callback: v8::Local<'a, v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let raw_handle = unsafe { conn.handle() };
|
|
|
|
if callback.is_null() {
|
|
// SAFETY: `raw_handle` is a valid database handle.
|
|
let result = unsafe {
|
|
libsqlite3_sys::sqlite3_set_authorizer(
|
|
raw_handle,
|
|
None,
|
|
std::ptr::null_mut(),
|
|
)
|
|
};
|
|
check_error_code(result, raw_handle)?;
|
|
|
|
let mut authorizer_data = self.authorizer_data.borrow_mut();
|
|
if let Some(old_data_ptr) = authorizer_data.take() {
|
|
// SAFETY: data_ptr was allocated in authorizer setup.
|
|
unsafe {
|
|
release_authorizer_data(old_data_ptr);
|
|
}
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let Ok(function) = v8::Local::<v8::Function>::try_from(callback) else {
|
|
return Err(
|
|
validators::Error::InvalidArgType(
|
|
"The \"callback\" argument must be a function or null.".into(),
|
|
)
|
|
.into(),
|
|
);
|
|
};
|
|
|
|
let callback_global = v8::Global::new(scope, function).into_raw();
|
|
let context_global =
|
|
v8::Global::new(scope, scope.get_current_context()).into_raw();
|
|
|
|
let data = Box::new(AuthorizerData {
|
|
callback: callback_global,
|
|
context: context_global,
|
|
ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
|
|
callback_depth: Rc::clone(&self.callback_depth),
|
|
active_callbacks: Cell::new(0),
|
|
free_after_callback: Cell::new(false),
|
|
});
|
|
let data_ptr = Box::into_raw(data);
|
|
|
|
// SAFETY: `raw_handle` is a valid database handle.
|
|
// `data_ptr` points to a valid memory location.
|
|
let result = unsafe {
|
|
libsqlite3_sys::sqlite3_set_authorizer(
|
|
raw_handle,
|
|
Some(authorizer_callback),
|
|
data_ptr as *mut c_void,
|
|
)
|
|
};
|
|
|
|
if result != libsqlite3_sys::SQLITE_OK {
|
|
// Clean up the data we allocated if setting the authorizer failed
|
|
// SAFETY: we just allocated this data and it failed to be registered
|
|
unsafe {
|
|
free_authorizer_data(data_ptr);
|
|
}
|
|
check_error_code(result, raw_handle)?;
|
|
}
|
|
|
|
let mut authorizer_data = self.authorizer_data.borrow_mut();
|
|
if let Some(old_data_ptr) = authorizer_data.replace(data_ptr) {
|
|
// SAFETY: data_ptr was allocated in authorizer setup.
|
|
unsafe {
|
|
release_authorizer_data(old_data_ptr);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[getter]
|
|
fn is_open(&self) -> bool {
|
|
self.conn.borrow().is_some()
|
|
}
|
|
|
|
#[getter]
|
|
fn is_transaction(&self) -> Result<bool, SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let res = unsafe { libsqlite3_sys::sqlite3_get_autocommit(db.handle()) };
|
|
Ok(res == 0)
|
|
}
|
|
|
|
#[getter]
|
|
#[cppgc]
|
|
fn limits(&self) -> Result<DatabaseSyncLimits, SqliteError> {
|
|
if self.conn.borrow().is_none() {
|
|
return Err(SqliteError::AlreadyClosed);
|
|
}
|
|
Ok(DatabaseSyncLimits::create(
|
|
Rc::clone(&self.conn),
|
|
self.disable_attach.get(),
|
|
))
|
|
}
|
|
|
|
#[fast]
|
|
#[validate(is_open)]
|
|
fn aggregate<'a>(
|
|
&self,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
#[validate(validators::name_str)]
|
|
#[string]
|
|
name: &str,
|
|
options: v8::Local<'a, v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
use std::cmp;
|
|
|
|
let options = AggregateFunctionOption::from_value(scope, options)?;
|
|
|
|
let mut argc = -1;
|
|
if !options.varargs {
|
|
v8_static_strings! {
|
|
LENGTH = "length",
|
|
}
|
|
let length_key = LENGTH.v8_string(scope).unwrap();
|
|
let step_fn_length = options
|
|
.step
|
|
.get(scope, length_key.into())
|
|
.ok_or(validators::Error::V8Exception)?
|
|
.int32_value(scope)
|
|
.ok_or(validators::Error::V8Exception)?;
|
|
|
|
// Subtract 1 because the first argument is the aggregate value.
|
|
argc = step_fn_length - 1;
|
|
|
|
let inverse_fn_length = if let Some(inverse_fn) = &options.inverse {
|
|
inverse_fn
|
|
.get(scope, length_key.into())
|
|
.ok_or(validators::Error::V8Exception)?
|
|
.int32_value(scope)
|
|
.ok_or(validators::Error::V8Exception)?
|
|
} else {
|
|
0
|
|
};
|
|
|
|
argc = cmp::max(argc, cmp::max(inverse_fn_length - 1, 0));
|
|
}
|
|
|
|
let mut text_rep = libsqlite3_sys::SQLITE_UTF8;
|
|
if options.deterministic {
|
|
text_rep |= libsqlite3_sys::SQLITE_DETERMINISTIC;
|
|
}
|
|
if options.direct_only {
|
|
text_rep |= libsqlite3_sys::SQLITE_DIRECTONLY;
|
|
}
|
|
|
|
let db = self.conn.borrow();
|
|
let db = db.as_ref().ok_or(SqliteError::InUse)?;
|
|
|
|
let context =
|
|
v8::Global::new(scope, scope.get_current_context()).into_raw();
|
|
|
|
let start = v8::Global::new(scope, options.start).into_raw();
|
|
|
|
let step_fn = v8::Global::new(scope, options.step).into_raw();
|
|
|
|
let inverse_fn = options
|
|
.inverse
|
|
.map(|inv| v8::Global::new(scope, inv).into_raw());
|
|
|
|
let final_fn = options
|
|
.result
|
|
.map(|res| v8::Global::new(scope, res).into_raw());
|
|
|
|
let custom_aggregate = Box::new(CustomAggregate {
|
|
context,
|
|
use_big_int_arguments: options.use_big_int_arguments,
|
|
start,
|
|
step_fn,
|
|
inverse_fn,
|
|
final_fn,
|
|
ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
|
|
callback_depth: Rc::clone(&self.callback_depth),
|
|
});
|
|
|
|
let custom_aggregate_ptr = Box::into_raw(custom_aggregate);
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let raw_handle = unsafe { db.handle() };
|
|
let name_cstring = CString::new(name)?;
|
|
|
|
let (value_callback, inverse_callback) = if options.inverse.is_some() {
|
|
(
|
|
Some(
|
|
custom_aggregate_xvalue
|
|
as unsafe extern "C" fn(*mut libsqlite3_sys::sqlite3_context),
|
|
),
|
|
Some(
|
|
custom_aggregate_xinverse
|
|
as unsafe extern "C" fn(
|
|
*mut libsqlite3_sys::sqlite3_context,
|
|
i32,
|
|
*mut *mut libsqlite3_sys::sqlite3_value,
|
|
),
|
|
),
|
|
)
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
// SAFETY: `raw_handle` is a valid database handle.
|
|
// The v8 handles stored in `CustomAggregate` are released in `custom_aggregate_xdestroy`.
|
|
let r = unsafe {
|
|
sqlite3_create_window_function(
|
|
raw_handle,
|
|
name_cstring.as_ptr(),
|
|
argc,
|
|
text_rep,
|
|
custom_aggregate_ptr as *mut c_void,
|
|
Some(custom_aggregate_xstep),
|
|
Some(custom_aggregate_xfinal),
|
|
value_callback,
|
|
inverse_callback,
|
|
Some(custom_aggregate_xdestroy),
|
|
)
|
|
};
|
|
check_error_code(r, raw_handle)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[validate(is_open)]
|
|
#[cppgc]
|
|
fn create_tag_store(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
#[varargs] args: Option<&v8::FunctionCallbackArguments>,
|
|
) -> Result<SQLTagStore, SqliteError> {
|
|
let capacity = if let Some(args) = args
|
|
&& args.length() > 0
|
|
{
|
|
let val = args.get(0);
|
|
if val.is_number() {
|
|
val.uint32_value(scope).unwrap_or(1000)
|
|
} else {
|
|
1000
|
|
}
|
|
} else {
|
|
1000
|
|
};
|
|
|
|
let db_object = args.map(|a| a.this()).ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
Ok(SQLTagStore::create(
|
|
self.conn.clone(),
|
|
self.statements.clone(),
|
|
capacity,
|
|
self.options.return_arrays,
|
|
self.options.use_big_int_arguments,
|
|
v8::Global::new(scope, db_object),
|
|
))
|
|
}
|
|
|
|
// Serializes the contents of the database into a Uint8Array. The database
|
|
// can be either an in-memory database or a database opened from a file.
|
|
//
|
|
// This method is a wrapper around `sqlite3_serialize()`.
|
|
#[validate(is_open)]
|
|
fn serialize<'a>(
|
|
&self,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
db_name_value: v8::Local<'a, v8::Value>,
|
|
) -> Result<v8::Local<'a, v8::Value>, SqliteError> {
|
|
let db = self.conn.borrow();
|
|
let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
let db_name = if db_name_value.is_undefined() {
|
|
"main".to_string()
|
|
} else {
|
|
let Ok(s) = v8::Local::<v8::String>::try_from(db_name_value) else {
|
|
return Err(SqliteError::Validation(
|
|
validators::Error::InvalidArgType(
|
|
"The \"dbName\" argument must be a string.".into(),
|
|
),
|
|
));
|
|
};
|
|
s.to_rust_string_lossy(scope)
|
|
};
|
|
|
|
let name_cstring = CString::new(db_name)?;
|
|
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let raw_handle = unsafe { conn.handle() };
|
|
|
|
let mut size: libsqlite3_sys::sqlite3_int64 = 0;
|
|
// SAFETY: `raw_handle` is a valid sqlite3 pointer; `name_cstring` is a
|
|
// valid C string for the lifetime of this call; `size` is a valid out
|
|
// pointer.
|
|
let data = unsafe {
|
|
libsqlite3_sys::sqlite3_serialize(
|
|
raw_handle,
|
|
name_cstring.as_ptr(),
|
|
&mut size,
|
|
0,
|
|
)
|
|
};
|
|
|
|
if data.is_null() {
|
|
return Err(SqliteError::SqliteSysError {
|
|
message: "unable to serialize database".to_string(),
|
|
errstr: "unable to serialize database".to_string(),
|
|
errcode: libsqlite3_sys::SQLITE_ERROR as _,
|
|
});
|
|
}
|
|
|
|
let len = size as usize;
|
|
// SAFETY: `data` points to `len` bytes owned by SQLite and is valid for
|
|
// reads of that length.
|
|
let bytes = unsafe { std::slice::from_raw_parts(data, len) }.to_vec();
|
|
// SAFETY: `data` was allocated by `sqlite3_serialize` and must be freed
|
|
// with `sqlite3_free`.
|
|
unsafe {
|
|
libsqlite3_sys::sqlite3_free(data as *mut c_void);
|
|
}
|
|
|
|
let backing =
|
|
v8::ArrayBuffer::new_backing_store_from_vec(bytes).make_shared();
|
|
let ab = v8::ArrayBuffer::with_backing_store(scope, &backing);
|
|
let view = v8::Uint8Array::new(scope, ab, 0, len).unwrap();
|
|
Ok(view.into())
|
|
}
|
|
|
|
// Deserializes the given buffer into the database. Replaces the contents
|
|
// of the named database (default `"main"`) with the contents of the
|
|
// serialized buffer.
|
|
//
|
|
// This method is a wrapper around `sqlite3_deserialize()`.
|
|
#[fast]
|
|
#[validate(is_open)]
|
|
#[undefined]
|
|
fn deserialize<'a>(
|
|
&self,
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
buffer_value: v8::Local<'a, v8::Value>,
|
|
options_value: v8::Local<'a, v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
// Refuse to replace the database while SQLite is executing a
|
|
// user-defined callback. deserialize() finalizes existing statements,
|
|
// which would invalidate the currently executing statement.
|
|
if self.callback_depth.get() > 0 {
|
|
return Err(SqliteError::ActiveCallback);
|
|
}
|
|
|
|
if !buffer_value.is_uint8_array() {
|
|
return Err(SqliteError::Validation(validators::Error::InvalidArgType(
|
|
"The \"buffer\" argument must be a Uint8Array.".into(),
|
|
)));
|
|
}
|
|
|
|
let view: v8::Local<v8::ArrayBufferView> = buffer_value.try_into().unwrap();
|
|
let byte_length = view.byte_length();
|
|
if byte_length == 0 {
|
|
return Err(SqliteError::Validation(validators::Error::InvalidArgValue(
|
|
"The \"buffer\" argument must not be empty.".into(),
|
|
)));
|
|
}
|
|
|
|
let mut db_name = "main".to_string();
|
|
let mut read_only = false;
|
|
|
|
if !options_value.is_undefined() {
|
|
let Ok(options_obj) = v8::Local::<v8::Object>::try_from(options_value)
|
|
else {
|
|
return Err(SqliteError::Validation(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options\" argument must be an object.".into(),
|
|
),
|
|
));
|
|
};
|
|
|
|
v8_static_strings! {
|
|
DB_NAME_STRING = "dbName",
|
|
READ_ONLY_STRING = "readOnly",
|
|
}
|
|
|
|
let db_name_string = DB_NAME_STRING.v8_string(scope).unwrap();
|
|
if let Some(name_val) = options_obj.get(scope, db_name_string.into())
|
|
&& !name_val.is_undefined()
|
|
{
|
|
let Ok(name_str) = v8::Local::<v8::String>::try_from(name_val) else {
|
|
return Err(SqliteError::Validation(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.dbName\" argument must be a string.".into(),
|
|
),
|
|
));
|
|
};
|
|
db_name = name_str.to_rust_string_lossy(scope);
|
|
}
|
|
|
|
let read_only_string = READ_ONLY_STRING.v8_string(scope).unwrap();
|
|
if let Some(read_only_val) =
|
|
options_obj.get(scope, read_only_string.into())
|
|
&& !read_only_val.is_undefined()
|
|
{
|
|
let Ok(b) = v8::Local::<v8::Boolean>::try_from(read_only_val) else {
|
|
return Err(SqliteError::Validation(
|
|
validators::Error::InvalidArgType(
|
|
"The \"options.readOnly\" argument must be a boolean.".into(),
|
|
),
|
|
));
|
|
};
|
|
read_only = b.is_true();
|
|
}
|
|
}
|
|
|
|
let name_cstring = CString::new(db_name)?;
|
|
|
|
// Per Node's contract, existing prepared statements are finalized before
|
|
// deserialization is attempted, even if the operation subsequently fails.
|
|
for stmt in self.statements.borrow_mut().drain(..) {
|
|
if let Some(ptr) = stmt.get() {
|
|
// SAFETY: `ptr` is a valid statement handle.
|
|
unsafe {
|
|
libsqlite3_sys::sqlite3_finalize(ptr);
|
|
}
|
|
stmt.set(None);
|
|
}
|
|
}
|
|
|
|
let db = self.conn.borrow();
|
|
let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
// SAFETY: lifetime of the connection is guaranteed by reference counting.
|
|
let raw_handle = unsafe { conn.handle() };
|
|
|
|
// Allocate memory owned by SQLite, copy the input bytes in, and hand
|
|
// ownership over via the FREEONCLOSE flag so SQLite frees it when the
|
|
// database is closed or another deserialize replaces it.
|
|
// sqlite3_malloc64(0) is allowed to return null; only treat null as an
|
|
// error when we actually need a non-empty allocation.
|
|
// SAFETY: ffi call with no preconditions other than a valid size.
|
|
let buf = unsafe {
|
|
libsqlite3_sys::sqlite3_malloc64(byte_length.max(1) as u64) as *mut u8
|
|
};
|
|
if buf.is_null() {
|
|
return Err(SqliteError::SqliteSysError {
|
|
message: "out of memory".to_string(),
|
|
errstr: "out of memory".to_string(),
|
|
errcode: libsqlite3_sys::SQLITE_NOMEM as _,
|
|
});
|
|
}
|
|
|
|
if byte_length > 0 {
|
|
let data = view.data() as *const u8;
|
|
// SAFETY: `data` points to `byte_length` bytes; `buf` is a freshly
|
|
// allocated buffer with the same capacity; the two regions do not
|
|
// overlap.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(data, buf, byte_length);
|
|
}
|
|
}
|
|
|
|
let mut flags: u32 = libsqlite3_sys::SQLITE_DESERIALIZE_FREEONCLOSE;
|
|
if read_only {
|
|
flags |= libsqlite3_sys::SQLITE_DESERIALIZE_READONLY;
|
|
} else {
|
|
flags |= libsqlite3_sys::SQLITE_DESERIALIZE_RESIZEABLE;
|
|
}
|
|
|
|
// SAFETY: `raw_handle` is a valid sqlite3 pointer; `name_cstring` is a
|
|
// valid C string for the lifetime of this call; `buf` was allocated by
|
|
// SQLite and ownership transfers via FREEONCLOSE.
|
|
let r = unsafe {
|
|
libsqlite3_sys::sqlite3_deserialize(
|
|
raw_handle,
|
|
name_cstring.as_ptr(),
|
|
buf,
|
|
byte_length as i64,
|
|
byte_length as i64,
|
|
flags,
|
|
)
|
|
};
|
|
|
|
if r != libsqlite3_sys::SQLITE_OK {
|
|
// sqlite3_deserialize frees `buf` itself on failure when FREEONCLOSE
|
|
// is set.
|
|
check_error_code(r, raw_handle)?;
|
|
check_error_code2(r)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct AggregateData {
|
|
value: Option<NonNull<v8::Value>>,
|
|
is_window: bool,
|
|
initialized: bool,
|
|
}
|
|
|
|
struct CustomAggregate {
|
|
context: NonNull<v8::Context>,
|
|
use_big_int_arguments: bool,
|
|
start: NonNull<v8::Value>,
|
|
step_fn: NonNull<v8::Function>,
|
|
inverse_fn: Option<NonNull<v8::Function>>,
|
|
final_fn: Option<NonNull<v8::Function>>,
|
|
ignore_next_sqlite_error: Rc<Cell<bool>>,
|
|
callback_depth: Rc<Cell<usize>>,
|
|
}
|
|
|
|
enum AggregateStepKind {
|
|
Step,
|
|
Inverse,
|
|
}
|
|
|
|
unsafe fn get_aggregate_data(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
custom_aggregate: &CustomAggregate,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
) -> Option<*mut AggregateData> {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
let agg_ptr = libsqlite3_sys::sqlite3_aggregate_context(
|
|
ctx,
|
|
std::mem::size_of::<AggregateData>() as i32,
|
|
) as *mut AggregateData;
|
|
|
|
if agg_ptr.is_null() {
|
|
sqlite_result_error(ctx, "Failed to allocate aggregate context");
|
|
return None;
|
|
}
|
|
|
|
let agg = &mut *agg_ptr;
|
|
if !agg.initialized {
|
|
let start_local: v8::Local<v8::Value> =
|
|
std::mem::transmute(custom_aggregate.start.as_ptr());
|
|
|
|
let start_value = if start_local.is_function() {
|
|
let start_fn: v8::Local<v8::Function> = start_local.try_into().unwrap();
|
|
let recv = v8::null(scope).into();
|
|
match start_fn.call(scope, recv, &[]) {
|
|
Some(val) => val,
|
|
None => {
|
|
custom_aggregate.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
return None;
|
|
}
|
|
}
|
|
} else {
|
|
start_local
|
|
};
|
|
|
|
let global = v8::Global::new(scope, start_value);
|
|
agg.value = Some(global.into_raw());
|
|
agg.initialized = true;
|
|
agg.is_window = false;
|
|
}
|
|
|
|
Some(agg_ptr)
|
|
}
|
|
}
|
|
|
|
unsafe fn custom_aggregate_step_base(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
argc: i32,
|
|
argv: *mut *mut libsqlite3_sys::sqlite3_value,
|
|
kind: AggregateStepKind,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
let data_ptr =
|
|
libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomAggregate;
|
|
|
|
if data_ptr.is_null() {
|
|
sqlite_result_error(
|
|
ctx,
|
|
"Internal error: missing custom aggregate context",
|
|
);
|
|
return;
|
|
}
|
|
|
|
let data = &*data_ptr;
|
|
// Block close() from freeing the in-flight statement under the VDBE.
|
|
let _depth_guard = CallbackDepthGuard::new(&data.callback_depth);
|
|
let context_local: v8::Local<v8::Context> =
|
|
std::mem::transmute(data.context.as_ptr());
|
|
|
|
v8::callback_scope!(unsafe cb_scope, context_local);
|
|
v8::scope!(scope, cb_scope);
|
|
v8::tc_scope!(tc_scope, scope);
|
|
|
|
let Some(agg_ptr) = get_aggregate_data(ctx, data, tc_scope) else {
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
}
|
|
return;
|
|
};
|
|
let agg = &mut *agg_ptr;
|
|
|
|
let step_fn = match kind {
|
|
AggregateStepKind::Step => {
|
|
let step_fn: v8::Local<v8::Function> =
|
|
std::mem::transmute(data.step_fn.as_ptr());
|
|
step_fn
|
|
}
|
|
AggregateStepKind::Inverse => {
|
|
if let Some(inverse_ptr) = data.inverse_fn {
|
|
let inverse_fn: v8::Local<v8::Function> =
|
|
std::mem::transmute(inverse_ptr.as_ptr());
|
|
inverse_fn
|
|
} else {
|
|
sqlite_result_error(ctx, "Internal error: inverse function not set");
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
let argc_len = usize::try_from(argc).unwrap_or(0);
|
|
let args_slice = if argc_len == 0 {
|
|
&[]
|
|
} else {
|
|
std::slice::from_raw_parts(argv, argc_len)
|
|
};
|
|
|
|
let current_value = if let Some(value_ptr) = agg.value {
|
|
let global = v8::Global::from_raw(tc_scope, value_ptr);
|
|
let local = v8::Local::new(tc_scope, &global);
|
|
std::mem::forget(global);
|
|
local
|
|
} else {
|
|
v8::undefined(tc_scope).into()
|
|
};
|
|
|
|
let mut js_args = Vec::with_capacity(args_slice.len() + 1);
|
|
js_args.push(current_value);
|
|
|
|
for &value_ptr in args_slice {
|
|
if let Some(arg) =
|
|
sqlite_value_to_v8(tc_scope, value_ptr, data.use_big_int_arguments)
|
|
{
|
|
js_args.push(arg);
|
|
} else {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
return;
|
|
}
|
|
}
|
|
|
|
let recv = v8::undefined(tc_scope).into();
|
|
let result = step_fn
|
|
.call(tc_scope, recv, &js_args)
|
|
.unwrap_or_else(|| v8::undefined(tc_scope).into());
|
|
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
return;
|
|
}
|
|
|
|
if let Some(old_ptr) = agg.value {
|
|
let _ = v8::Global::from_raw(tc_scope, old_ptr);
|
|
}
|
|
let global = v8::Global::new(tc_scope, result);
|
|
agg.value = Some(global.into_raw());
|
|
}
|
|
}
|
|
|
|
unsafe fn custom_aggregate_value_base(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
is_final: bool,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
let data_ptr =
|
|
libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomAggregate;
|
|
if data_ptr.is_null() {
|
|
sqlite_result_error(
|
|
ctx,
|
|
"Internal error: missing custom aggregate context",
|
|
);
|
|
return;
|
|
}
|
|
|
|
let data = &*data_ptr;
|
|
// Block close() from freeing the in-flight statement under the VDBE.
|
|
let _depth_guard = CallbackDepthGuard::new(&data.callback_depth);
|
|
let context_local: v8::Local<v8::Context> =
|
|
std::mem::transmute(data.context.as_ptr());
|
|
|
|
v8::callback_scope!(unsafe cb_scope, context_local);
|
|
v8::scope!(scope, cb_scope);
|
|
v8::tc_scope!(tc_scope, scope);
|
|
|
|
let Some(agg_ptr) = get_aggregate_data(ctx, data, tc_scope) else {
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
}
|
|
return;
|
|
};
|
|
let agg = &mut *agg_ptr;
|
|
|
|
if !is_final {
|
|
agg.is_window = true;
|
|
} else if agg.is_window {
|
|
if let Some(value_ptr) = agg.value.take() {
|
|
let _ = v8::Global::from_raw(tc_scope, value_ptr);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let current_value = if let Some(value_ptr) = agg.value {
|
|
let global = v8::Global::from_raw(tc_scope, value_ptr);
|
|
let local = v8::Local::new(tc_scope, &global);
|
|
if is_final {
|
|
agg.value = None;
|
|
} else {
|
|
std::mem::forget(global);
|
|
}
|
|
local
|
|
} else {
|
|
v8::undefined(tc_scope).into()
|
|
};
|
|
|
|
let result = if let Some(result_fn_ptr) = data.final_fn {
|
|
let result_fn: v8::Local<v8::Function> =
|
|
std::mem::transmute(result_fn_ptr.as_ptr());
|
|
let ret = result_fn
|
|
.call(tc_scope, v8::null(tc_scope).into(), &[current_value])
|
|
.unwrap_or_else(|| v8::undefined(tc_scope).into());
|
|
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
return;
|
|
}
|
|
ret
|
|
} else {
|
|
current_value
|
|
};
|
|
|
|
js_value_to_sqlite(tc_scope, ctx, result);
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn custom_aggregate_xstep(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
argc: i32,
|
|
argv: *mut *mut libsqlite3_sys::sqlite3_value,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
custom_aggregate_step_base(ctx, argc, argv, AggregateStepKind::Step);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn custom_aggregate_xinverse(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
argc: i32,
|
|
argv: *mut *mut libsqlite3_sys::sqlite3_value,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
custom_aggregate_step_base(ctx, argc, argv, AggregateStepKind::Inverse);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn custom_aggregate_xfinal(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
custom_aggregate_value_base(ctx, true);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn custom_aggregate_xvalue(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
custom_aggregate_value_base(ctx, false);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn custom_aggregate_xdestroy(data: *mut c_void) {
|
|
// SAFETY: `data` is a valid pointer to CustomAggregate.
|
|
// We intentionally do not re-enter V8 here. SQLite may invoke this
|
|
// destructor while the isolate is already tearing down, so creating a
|
|
// callback scope can panic. The raw V8 handles are allowed to leak in that
|
|
// case, matching the custom function destroy path.
|
|
unsafe {
|
|
let _ = Box::from_raw(data as *mut CustomAggregate);
|
|
}
|
|
}
|
|
|
|
impl DatabaseSync {
|
|
fn consume_ignore_next_sqlite_error(&self) -> bool {
|
|
self.ignore_next_sqlite_error.replace(false)
|
|
}
|
|
}
|
|
|
|
struct CustomFunctionData {
|
|
callback: NonNull<v8::Function>,
|
|
context: NonNull<v8::Context>,
|
|
use_big_int_arguments: bool,
|
|
ignore_next_sqlite_error: Rc<Cell<bool>>,
|
|
callback_depth: Rc<Cell<usize>>,
|
|
}
|
|
|
|
struct AuthorizerData {
|
|
callback: NonNull<v8::Function>,
|
|
context: NonNull<v8::Context>,
|
|
ignore_next_sqlite_error: Rc<Cell<bool>>,
|
|
callback_depth: Rc<Cell<usize>>,
|
|
// authorizer_callback holds shared references across reentrant JS calls.
|
|
// Mutable callback-lifetime state must use interior mutability so reentrant
|
|
// release/drop paths never create an aliasing &mut AuthorizerData.
|
|
active_callbacks: Cell<usize>,
|
|
free_after_callback: Cell<bool>,
|
|
}
|
|
|
|
struct AuthorizerCallbackGuard {
|
|
data_ptr: *mut AuthorizerData,
|
|
callback_depth: Rc<Cell<usize>>,
|
|
}
|
|
|
|
impl AuthorizerCallbackGuard {
|
|
unsafe fn new(data_ptr: *mut AuthorizerData) -> Self {
|
|
// SAFETY: caller guarantees `data_ptr` points to valid AuthorizerData.
|
|
let data = unsafe { &*data_ptr };
|
|
data
|
|
.active_callbacks
|
|
.set(data.active_callbacks.get().saturating_add(1));
|
|
data
|
|
.callback_depth
|
|
.set(data.callback_depth.get().saturating_add(1));
|
|
Self {
|
|
data_ptr,
|
|
callback_depth: Rc::clone(&data.callback_depth),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for AuthorizerCallbackGuard {
|
|
fn drop(&mut self) {
|
|
// SAFETY: AuthorizerData cannot be freed while active_callbacks is non-zero.
|
|
unsafe {
|
|
let data = &*self.data_ptr;
|
|
let active_callbacks = data.active_callbacks.get().saturating_sub(1);
|
|
data.active_callbacks.set(active_callbacks);
|
|
self
|
|
.callback_depth
|
|
.set(self.callback_depth.get().saturating_sub(1));
|
|
if active_callbacks == 0 && data.free_after_callback.get() {
|
|
free_authorizer_data(self.data_ptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn custom_function_handler(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
argc: i32,
|
|
argv: *mut *mut libsqlite3_sys::sqlite3_value,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
let data_ptr =
|
|
libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomFunctionData;
|
|
if data_ptr.is_null() {
|
|
sqlite_result_error(
|
|
ctx,
|
|
"Internal error: missing custom function context",
|
|
);
|
|
return;
|
|
}
|
|
|
|
let data = &*data_ptr;
|
|
// Block close() from freeing the in-flight statement under the VDBE.
|
|
let _depth_guard = CallbackDepthGuard::new(&data.callback_depth);
|
|
let context_local: v8::Local<v8::Context> =
|
|
std::mem::transmute(data.context.as_ptr());
|
|
|
|
v8::callback_scope!(unsafe cb_scope, context_local);
|
|
v8::scope!(scope, cb_scope);
|
|
v8::tc_scope!(tc_scope, scope);
|
|
|
|
let function_local: v8::Local<v8::Function> =
|
|
std::mem::transmute(data.callback.as_ptr());
|
|
|
|
let argc_len = usize::try_from(argc).unwrap_or(0);
|
|
let args_slice = if argc_len == 0 {
|
|
&[]
|
|
} else {
|
|
std::slice::from_raw_parts(argv, argc_len)
|
|
};
|
|
|
|
let mut js_args = Vec::with_capacity(args_slice.len());
|
|
for &value_ptr in args_slice {
|
|
if let Some(arg) =
|
|
sqlite_value_to_v8(tc_scope, value_ptr, data.use_big_int_arguments)
|
|
{
|
|
js_args.push(arg);
|
|
} else {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
return;
|
|
}
|
|
}
|
|
|
|
let recv = v8::undefined(tc_scope).into();
|
|
let result = function_local.call(tc_scope, recv, &js_args);
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
return;
|
|
}
|
|
|
|
if let Some(value) = result {
|
|
js_value_to_sqlite(tc_scope, ctx, value);
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
sqlite_result_error(ctx, "");
|
|
tc_scope.rethrow();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn custom_function_destroy(data: *mut c_void) {
|
|
// SAFETY: `data` is a valid pointer to CustomFunctionData.
|
|
// Drop the Box to free the raw pointers stored in it.
|
|
// We intentionally do NOT convert the raw v8::Global handles back
|
|
// via `Global::from_raw` because this callback can fire during
|
|
// isolate teardown (e.g. sqlite3_close during GC) when the
|
|
// isolate annex is already disposed, making scope creation unsafe.
|
|
// The leaked Global pointers are harmless: the isolate is being
|
|
// destroyed and will reclaim all V8 heap memory anyway.
|
|
unsafe {
|
|
let _ = Box::from_raw(data as *mut CustomFunctionData);
|
|
}
|
|
}
|
|
|
|
fn sqlite_value_to_v8<'a>(
|
|
scope: &mut v8::PinScope<'a, '_>,
|
|
value: *mut libsqlite3_sys::sqlite3_value,
|
|
use_big_int_arguments: bool,
|
|
) -> Option<v8::Local<'a, v8::Value>> {
|
|
// SAFETY: `value` is a valid sqlite3_value pointer.
|
|
unsafe {
|
|
match libsqlite3_sys::sqlite3_value_type(value) {
|
|
libsqlite3_sys::SQLITE_INTEGER => {
|
|
let val = libsqlite3_sys::sqlite3_value_int64(value);
|
|
if use_big_int_arguments {
|
|
Some(v8::BigInt::new_from_i64(scope, val).into())
|
|
} else if (-MAX_SAFE_JS_INTEGER..=MAX_SAFE_JS_INTEGER).contains(&val) {
|
|
Some(v8::Number::new(scope, val as f64).into())
|
|
} else {
|
|
let msg = format!(
|
|
"Value is too large to be represented as a JavaScript number: {}",
|
|
val
|
|
);
|
|
throw_range_error(scope, &msg);
|
|
None
|
|
}
|
|
}
|
|
libsqlite3_sys::SQLITE_FLOAT => {
|
|
let val = libsqlite3_sys::sqlite3_value_double(value);
|
|
Some(v8::Number::new(scope, val).into())
|
|
}
|
|
libsqlite3_sys::SQLITE_TEXT => {
|
|
let len = libsqlite3_sys::sqlite3_value_bytes(value) as usize;
|
|
if len == 0 {
|
|
let text =
|
|
v8::String::new_from_utf8(scope, b"", v8::NewStringType::Normal)
|
|
.unwrap();
|
|
Some(text.into())
|
|
} else {
|
|
let ptr = libsqlite3_sys::sqlite3_value_text(value);
|
|
let slice = std::slice::from_raw_parts(ptr, len);
|
|
let text =
|
|
v8::String::new_from_utf8(scope, slice, v8::NewStringType::Normal)
|
|
.unwrap();
|
|
Some(text.into())
|
|
}
|
|
}
|
|
libsqlite3_sys::SQLITE_BLOB => {
|
|
let len = libsqlite3_sys::sqlite3_value_bytes(value);
|
|
if len == 0 {
|
|
let ab = v8::ArrayBuffer::new(scope, 0);
|
|
let view = v8::Uint8Array::new(scope, ab, 0, 0).unwrap();
|
|
Some(view.into())
|
|
} else {
|
|
let ptr = libsqlite3_sys::sqlite3_value_blob(value) as *const u8;
|
|
let slice = std::slice::from_raw_parts(ptr, len as usize);
|
|
let backing =
|
|
v8::ArrayBuffer::new_backing_store_from_vec(slice.to_vec())
|
|
.make_shared();
|
|
let ab = v8::ArrayBuffer::with_backing_store(scope, &backing);
|
|
let view = v8::Uint8Array::new(scope, ab, 0, len as usize).unwrap();
|
|
Some(view.into())
|
|
}
|
|
}
|
|
libsqlite3_sys::SQLITE_NULL => Some(v8::null(scope).into()),
|
|
_ => Some(v8::undefined(scope).into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn js_value_to_sqlite(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
value: v8::Local<v8::Value>,
|
|
) {
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
if value.is_null_or_undefined() {
|
|
libsqlite3_sys::sqlite3_result_null(ctx);
|
|
return;
|
|
}
|
|
|
|
if value.is_number() {
|
|
let number = value.number_value(scope).unwrap_or(0f64);
|
|
libsqlite3_sys::sqlite3_result_double(ctx, number);
|
|
return;
|
|
}
|
|
|
|
if value.is_string() {
|
|
let text = value.to_rust_string_lossy(scope);
|
|
libsqlite3_sys::sqlite3_result_text(
|
|
ctx,
|
|
text.as_ptr() as *const _,
|
|
text.len() as i32,
|
|
libsqlite3_sys::SQLITE_TRANSIENT(),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if value.is_array_buffer_view() {
|
|
let view: v8::Local<v8::ArrayBufferView> = value.try_into().unwrap();
|
|
let mut data = view.data();
|
|
let mut size = view.byte_length();
|
|
if data.is_null() {
|
|
static EMPTY: [u8; 0] = [];
|
|
data = EMPTY.as_ptr() as *mut _;
|
|
size = 0;
|
|
}
|
|
libsqlite3_sys::sqlite3_result_blob(
|
|
ctx,
|
|
data,
|
|
size as i32,
|
|
libsqlite3_sys::SQLITE_TRANSIENT(),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if value.is_big_int() {
|
|
let bigint: v8::Local<v8::BigInt> = value.try_into().unwrap();
|
|
let (int_value, lossless) = bigint.i64_value();
|
|
if !lossless {
|
|
throw_range_error(scope, "BigInt value is too large for SQLite");
|
|
sqlite_result_error(ctx, "");
|
|
return;
|
|
}
|
|
libsqlite3_sys::sqlite3_result_int64(ctx, int_value);
|
|
return;
|
|
}
|
|
|
|
if value.is_promise() {
|
|
sqlite_result_error(
|
|
ctx,
|
|
"Asynchronous user-defined functions are not supported",
|
|
);
|
|
return;
|
|
}
|
|
|
|
sqlite_result_error(
|
|
ctx,
|
|
"Returned JavaScript value cannot be converted to a SQLite value",
|
|
);
|
|
}
|
|
}
|
|
|
|
fn sqlite_result_error(
|
|
ctx: *mut libsqlite3_sys::sqlite3_context,
|
|
message: &str,
|
|
) {
|
|
let msg = CString::new(message).unwrap();
|
|
// SAFETY: `ctx` is a valid sqlite3_context pointer.
|
|
unsafe {
|
|
libsqlite3_sys::sqlite3_result_error(
|
|
ctx,
|
|
msg.as_ptr(),
|
|
msg.as_bytes().len() as i32,
|
|
);
|
|
}
|
|
}
|
|
|
|
macro_rules! nullable_cstring_to_v8_value {
|
|
($scope:expr, $ptr:expr) => {{
|
|
if ($ptr).is_null() {
|
|
v8::null($scope).into()
|
|
} else {
|
|
let cstr = CStr::from_ptr($ptr);
|
|
v8::String::new_from_utf8(
|
|
$scope,
|
|
cstr.to_bytes(),
|
|
v8::NewStringType::Normal,
|
|
)
|
|
.map(|s| s.into())
|
|
.unwrap_or_else(|| v8::null($scope).into())
|
|
}
|
|
}};
|
|
}
|
|
|
|
unsafe extern "C" fn authorizer_callback(
|
|
user_data: *mut c_void,
|
|
action_code: i32,
|
|
param1: *const c_char,
|
|
param2: *const c_char,
|
|
param3: *const c_char,
|
|
param4: *const c_char,
|
|
) -> i32 {
|
|
// SAFETY: `user_data` is a valid pointer to AuthorizerData.
|
|
unsafe {
|
|
let data_ptr = user_data as *mut AuthorizerData;
|
|
if data_ptr.is_null() {
|
|
return libsqlite3_sys::SQLITE_DENY;
|
|
}
|
|
|
|
let _callback_guard = AuthorizerCallbackGuard::new(data_ptr);
|
|
let data = &*data_ptr;
|
|
let context_local: v8::Local<v8::Context> =
|
|
std::mem::transmute(data.context.as_ptr());
|
|
|
|
v8::callback_scope!(unsafe cb_scope, context_local);
|
|
v8::scope!(scope, cb_scope);
|
|
|
|
let result: Option<v8::Local<'_, v8::Value>>;
|
|
{
|
|
v8::tc_scope!(tc_scope, scope);
|
|
|
|
let function_local: v8::Local<v8::Function> =
|
|
std::mem::transmute(data.callback.as_ptr());
|
|
|
|
let action_code_js = v8::Integer::new(tc_scope, action_code).into();
|
|
|
|
let param1_js: v8::Local<v8::Value> =
|
|
nullable_cstring_to_v8_value!(tc_scope, param1);
|
|
let param2_js: v8::Local<v8::Value> =
|
|
nullable_cstring_to_v8_value!(tc_scope, param2);
|
|
let param3_js: v8::Local<v8::Value> =
|
|
nullable_cstring_to_v8_value!(tc_scope, param3);
|
|
let param4_js: v8::Local<v8::Value> =
|
|
nullable_cstring_to_v8_value!(tc_scope, param4);
|
|
|
|
let js_args =
|
|
[action_code_js, param1_js, param2_js, param3_js, param4_js];
|
|
let recv = v8::undefined(tc_scope).into();
|
|
result = function_local.call(tc_scope, recv, &js_args);
|
|
|
|
if tc_scope.has_caught() {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
tc_scope.rethrow();
|
|
return libsqlite3_sys::SQLITE_DENY;
|
|
}
|
|
}
|
|
|
|
let Some(value) = result else {
|
|
data.ignore_next_sqlite_error.set(true);
|
|
return libsqlite3_sys::SQLITE_DENY;
|
|
};
|
|
|
|
if !value.is_int32() {
|
|
let message = v8::String::new(
|
|
scope,
|
|
"Authorizer callback must return an integer authorization code",
|
|
)
|
|
.unwrap();
|
|
let err = v8::Exception::type_error(scope, message);
|
|
data.ignore_next_sqlite_error.set(true);
|
|
scope.throw_exception(err);
|
|
return libsqlite3_sys::SQLITE_DENY;
|
|
}
|
|
|
|
let int_result = value
|
|
.int32_value(scope)
|
|
.unwrap_or(libsqlite3_sys::SQLITE_DENY);
|
|
|
|
if int_result != libsqlite3_sys::SQLITE_OK
|
|
&& int_result != libsqlite3_sys::SQLITE_DENY
|
|
&& int_result != libsqlite3_sys::SQLITE_IGNORE
|
|
{
|
|
let message = v8::String::new(
|
|
scope,
|
|
"Authorizer callback returned a invalid authorization code",
|
|
)
|
|
.unwrap();
|
|
let err = v8::Exception::range_error(scope, message);
|
|
data.ignore_next_sqlite_error.set(true);
|
|
scope.throw_exception(err);
|
|
return libsqlite3_sys::SQLITE_DENY;
|
|
}
|
|
|
|
int_result
|
|
}
|
|
}
|
|
|
|
unsafe fn free_authorizer_data(data_ptr: *mut AuthorizerData) {
|
|
if data_ptr.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: `data_ptr` is a valid pointer to AuthorizerData.
|
|
// The v8 handles are properly dropped here.
|
|
unsafe {
|
|
let data = Box::from_raw(data_ptr);
|
|
let context_local: v8::Local<v8::Context> =
|
|
std::mem::transmute(data.context.as_ptr());
|
|
|
|
v8::callback_scope!(unsafe cb_scope, context_local);
|
|
v8::scope!(scope, cb_scope);
|
|
|
|
let _ = v8::Global::from_raw(scope, data.callback);
|
|
let _ = v8::Global::from_raw(scope, data.context);
|
|
}
|
|
}
|
|
|
|
unsafe fn release_authorizer_data(data_ptr: *mut AuthorizerData) {
|
|
if data_ptr.is_null() {
|
|
return;
|
|
}
|
|
|
|
// SAFETY: `data_ptr` is a valid pointer to AuthorizerData.
|
|
unsafe {
|
|
let data = &*data_ptr;
|
|
if data.active_callbacks.get() > 0 {
|
|
data.free_after_callback.set(true);
|
|
} else {
|
|
free_authorizer_data(data_ptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn throw_range_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
|
|
let msg = v8::String::new(scope, message).unwrap();
|
|
let error = v8::Exception::range_error(scope, msg);
|
|
|
|
v8_static_strings!(CODE = "code", ERR_OUT_OF_RANGE = "ERR_OUT_OF_RANGE");
|
|
let code_key = CODE.v8_string(scope).unwrap();
|
|
let code_value = ERR_OUT_OF_RANGE.v8_string(scope).unwrap();
|
|
let error_obj: v8::Local<v8::Object> = error.try_into().unwrap();
|
|
error_obj
|
|
.create_data_property(scope, code_key.into(), code_value.into())
|
|
.unwrap();
|
|
|
|
scope.throw_exception(error);
|
|
}
|
|
|
|
fn throw_type_error_with_code(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
message: &str,
|
|
code: &str,
|
|
) {
|
|
let msg = v8::String::new(scope, message).unwrap();
|
|
let error = v8::Exception::type_error(scope, msg);
|
|
|
|
v8_static_strings!(CODE = "code");
|
|
let code_key = CODE.v8_string(scope).unwrap();
|
|
let code_value = v8::String::new(scope, code).unwrap();
|
|
let error_obj: v8::Local<v8::Object> = error.try_into().unwrap();
|
|
error_obj
|
|
.create_data_property(scope, code_key.into(), code_value.into())
|
|
.unwrap();
|
|
|
|
scope.throw_exception(error);
|
|
}
|
|
|
|
// Throws a plain `Error` carrying `code`. Used for conditions that are not
|
|
// `TypeError`s or `RangeError`s, such as the ATTACH_DATABASE access boundary.
|
|
fn throw_error_with_code(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
message: &str,
|
|
code: &str,
|
|
) {
|
|
let msg = v8::String::new(scope, message).unwrap();
|
|
let error = v8::Exception::error(scope, msg);
|
|
|
|
v8_static_strings!(CODE = "code");
|
|
let code_key = CODE.v8_string(scope).unwrap();
|
|
let code_value = v8::String::new(scope, code).unwrap();
|
|
let error_obj: v8::Local<v8::Object> = error.try_into().unwrap();
|
|
error_obj
|
|
.create_data_property(scope, code_key.into(), code_value.into())
|
|
.unwrap();
|
|
|
|
scope.throw_exception(error);
|
|
}
|
|
|
|
/// Object representing SQLite database limits.
|
|
/// This is returned by DatabaseSync.limits getter.
|
|
pub struct DatabaseSyncLimits {
|
|
conn: Rc<RefCell<Option<rusqlite::Connection>>>,
|
|
// When true, ATTACH DATABASE is held disabled for this connection because the
|
|
// process lacks full permissions for the database path. The `attach` setter
|
|
// refuses to raise the cap above 0 in that state.
|
|
disable_attach: bool,
|
|
}
|
|
|
|
// SAFETY: we're sure this can be GCed
|
|
unsafe impl GarbageCollected for DatabaseSyncLimits {
|
|
fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}
|
|
|
|
fn get_name(&self) -> &'static std::ffi::CStr {
|
|
c"DatabaseSyncLimits"
|
|
}
|
|
}
|
|
|
|
impl DatabaseSyncLimits {
|
|
fn create(
|
|
conn: Rc<RefCell<Option<rusqlite::Connection>>>,
|
|
disable_attach: bool,
|
|
) -> Self {
|
|
Self {
|
|
conn,
|
|
disable_attach,
|
|
}
|
|
}
|
|
|
|
fn get_limit(&self, limit: Limit) -> Result<i32, SqliteError> {
|
|
let conn = self.conn.borrow();
|
|
let conn = conn.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
conn.limit(limit).map_err(SqliteError::from)
|
|
}
|
|
|
|
// Uses manual v8 exception throwing instead of returning Err because
|
|
// returning Err from a cppgc setter would produce a double-throw.
|
|
fn set_limit_value(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
limit: Limit,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
let conn = self.conn.borrow();
|
|
let conn = conn.as_ref().ok_or(SqliteError::AlreadyClosed)?;
|
|
|
|
let new_value = match coerce_limit_value(scope, value) {
|
|
Ok(v) => v,
|
|
Err(
|
|
LimitCoercionError::NotANumber | LimitCoercionError::NotAnInteger,
|
|
) => {
|
|
throw_type_error_with_code(
|
|
scope,
|
|
"Limit value must be a non-negative integer or Infinity.",
|
|
"ERR_INVALID_ARG_TYPE",
|
|
);
|
|
return Ok(());
|
|
}
|
|
Err(LimitCoercionError::Negative) => {
|
|
throw_range_error(scope, "Limit value must be non-negative.");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
conn.set_limit(limit, new_value)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[op2]
|
|
impl DatabaseSyncLimits {
|
|
#[constructor]
|
|
#[cppgc]
|
|
fn new(_: bool) -> Result<DatabaseSyncLimits, SqliteError> {
|
|
Err(SqliteError::InvalidConstructor)
|
|
}
|
|
|
|
#[getter]
|
|
fn length(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_LENGTH)
|
|
}
|
|
|
|
#[setter]
|
|
fn length(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_LENGTH, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("sqlLength")]
|
|
fn sql_length(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_SQL_LENGTH)
|
|
}
|
|
|
|
#[rename("sqlLength")]
|
|
#[setter]
|
|
fn sql_length(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_SQL_LENGTH, value)
|
|
}
|
|
|
|
#[getter]
|
|
fn column(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_COLUMN)
|
|
}
|
|
|
|
#[setter]
|
|
fn column(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_COLUMN, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("exprDepth")]
|
|
fn expr_depth(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_EXPR_DEPTH)
|
|
}
|
|
|
|
#[rename("exprDepth")]
|
|
#[setter]
|
|
fn expr_depth(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_EXPR_DEPTH, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("compoundSelect")]
|
|
fn compound_select(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_COMPOUND_SELECT)
|
|
}
|
|
|
|
#[rename("compoundSelect")]
|
|
#[setter]
|
|
fn compound_select(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_COMPOUND_SELECT, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("vdbeOp")]
|
|
fn vdbe_op(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_VDBE_OP)
|
|
}
|
|
|
|
#[rename("vdbeOp")]
|
|
#[setter]
|
|
fn vdbe_op(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_VDBE_OP, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("functionArg")]
|
|
fn function_arg(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_FUNCTION_ARG)
|
|
}
|
|
|
|
#[rename("functionArg")]
|
|
#[setter]
|
|
fn function_arg(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_FUNCTION_ARG, value)
|
|
}
|
|
|
|
#[getter]
|
|
fn attach(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_ATTACHED)
|
|
}
|
|
|
|
#[setter]
|
|
fn attach(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
// ATTACH DATABASE is held disabled for processes without full permissions
|
|
// for the database path. Refuse to raise the cap above 0 so the limit
|
|
// setter cannot re-enable attach and bypass that boundary. A value of 0
|
|
// (keeping it disabled) is allowed and falls through to the normal setter.
|
|
// Malformed values also fall through so set_limit_value reports the right
|
|
// ERR_INVALID_ARG_* error.
|
|
if self.disable_attach
|
|
&& matches!(coerce_limit_value(scope, value), Ok(v) if v > 0)
|
|
{
|
|
throw_error_with_code(
|
|
scope,
|
|
"Cannot raise the \"attach\" limit: ATTACH DATABASE is disabled \
|
|
without full permissions for the database path.",
|
|
"ERR_ACCESS_DENIED",
|
|
);
|
|
return Ok(());
|
|
}
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_ATTACHED, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("likePatternLength")]
|
|
fn like_pattern_length(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH)
|
|
}
|
|
|
|
#[rename("likePatternLength")]
|
|
#[setter]
|
|
fn like_pattern_length(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("variableNumber")]
|
|
fn variable_number(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER)
|
|
}
|
|
|
|
#[rename("variableNumber")]
|
|
#[setter]
|
|
fn variable_number(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_VARIABLE_NUMBER, value)
|
|
}
|
|
|
|
#[getter]
|
|
#[rename("triggerDepth")]
|
|
fn trigger_depth(&self) -> Result<i32, SqliteError> {
|
|
self.get_limit(Limit::SQLITE_LIMIT_TRIGGER_DEPTH)
|
|
}
|
|
|
|
#[rename("triggerDepth")]
|
|
#[setter]
|
|
fn trigger_depth(
|
|
&self,
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
value: v8::Local<v8::Value>,
|
|
) -> Result<(), SqliteError> {
|
|
self.set_limit_value(scope, Limit::SQLITE_LIMIT_TRIGGER_DEPTH, value)
|
|
}
|
|
}
|