Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@
V(socketaddress_constructor_template, v8::FunctionTemplate) \
V(space_stats_template, v8::DictionaryTemplate) \
V(sqlite_column_template, v8::DictionaryTemplate) \
V(sqlite_database_sync_constructor_template, v8::FunctionTemplate) \
V(sqlite_limits_template, v8::ObjectTemplate) \
V(sqlite_run_result_template, v8::DictionaryTemplate) \
V(sqlite_statement_sync_constructor_template, v8::FunctionTemplate) \
Expand Down
168 changes: 92 additions & 76 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ inline MaybeLocal<String> Utf8StringMaybeOneByte(Isolate* isolate,
isolate, input.data(), NewStringType::kNormal, len);
}

static inline void SetSideEffectFreeGetter(
Isolate* isolate,
Local<FunctionTemplate> class_template,
Local<String> name,
FunctionCallback fn) {
Local<FunctionTemplate> getter =
FunctionTemplate::New(isolate,
fn,
Local<Value>(),
v8::Signature::New(isolate, class_template),
/* length */ 0,
ConstructorBehavior::kThrow,
SideEffectType::kHasNoSideEffect);
class_template->InstanceTemplate()->SetAccessorProperty(
name, getter, Local<FunctionTemplate>(), DontDelete);
}

BindingData::BindingData(Realm* realm, Local<Object> wrap)
: BaseObject(realm, wrap) {
MakeWeak();
Expand Down Expand Up @@ -1279,12 +1296,15 @@ std::optional<std::string> ValidateDatabasePath(Environment* env,
} else if (path->IsObject()) { // When is URL
auto url = path.As<Object>();
Local<Value> href;
if (url->Get(env->context(), env->href_string()).ToLocal(&href) &&
href->IsString()) {
if (!url->Get(env->context(), env->href_string()).ToLocal(&href)) {
return std::nullopt;
}
if (href->IsString()) {
Utf8Value location_value(env->isolate(), href.As<String>());
auto location = location_value.ToStringView();
if (!has_null_bytes(location)) {
CHECK(ada::can_parse(location));
// A real URL always has a parseable href, but any object with a string
// href gets this far, so the value cannot be assumed to be one.
if (!has_null_bytes(location) && ada::can_parse(location)) {
if (!location.starts_with("file:")) {
THROW_ERR_INVALID_URL_SCHEME(env->isolate());
return std::nullopt;
Expand All @@ -1303,6 +1323,63 @@ std::optional<std::string> ValidateDatabasePath(Environment* env,
return std::nullopt;
}

Local<FunctionTemplate> DatabaseSync::GetConstructorTemplate(Environment* env) {
Local<FunctionTemplate> tmpl =
env->sqlite_database_sync_constructor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, DatabaseSync::New);
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "DatabaseSync"));
tmpl->InstanceTemplate()->SetInternalFieldCount(
DatabaseSync::kInternalFieldCount);
SetProtoMethod(isolate, tmpl, "open", DatabaseSync::Open);
SetProtoMethod(isolate, tmpl, "close", DatabaseSync::Close);
SetProtoDispose(isolate, tmpl, DatabaseSync::Dispose);
SetProtoMethod(isolate, tmpl, "prepare", DatabaseSync::Prepare);
SetProtoMethod(isolate, tmpl, "exec", DatabaseSync::Exec);
SetProtoMethod(isolate, tmpl, "function", DatabaseSync::CustomFunction);
SetProtoMethod(
isolate, tmpl, "createTagStore", DatabaseSync::CreateTagStore);
SetProtoMethodNoSideEffect(
isolate, tmpl, "location", DatabaseSync::Location);
SetProtoMethod(isolate, tmpl, "aggregate", DatabaseSync::AggregateFunction);
SetProtoMethod(isolate, tmpl, "createSession", DatabaseSync::CreateSession);
SetProtoMethod(
isolate, tmpl, "applyChangeset", DatabaseSync::ApplyChangeset);
SetProtoMethod(isolate,
tmpl,
"enableLoadExtension",
DatabaseSync::EnableLoadExtension);
SetProtoMethod(
isolate, tmpl, "enableDefensive", DatabaseSync::EnableDefensive);
SetProtoMethod(isolate, tmpl, "loadExtension", DatabaseSync::LoadExtension);
SetProtoMethod(isolate, tmpl, "serialize", DatabaseSync::Serialize);
SetProtoMethod(isolate, tmpl, "deserialize", DatabaseSync::Deserialize);
SetProtoMethod(isolate, tmpl, "setAuthorizer", DatabaseSync::SetAuthorizer);
SetSideEffectFreeGetter(isolate,
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "isOpen"),
DatabaseSync::IsOpenGetter);
SetSideEffectFreeGetter(isolate,
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "isTransaction"),
DatabaseSync::IsTransactionGetter);
SetSideEffectFreeGetter(isolate,
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "limits"),
DatabaseSync::LimitsGetter);
Local<String> sqlite_type_key =
FIXED_ONE_BYTE_STRING(isolate, "sqlite-type");
Local<v8::Symbol> sqlite_type_symbol =
v8::Symbol::For(isolate, sqlite_type_key);
Local<String> database_sync_string =
FIXED_ONE_BYTE_STRING(isolate, "node:sqlite");
tmpl->InstanceTemplate()->Set(sqlite_type_symbol, database_sync_string);
env->set_sqlite_database_sync_constructor_template(tmpl);
}
return tmpl;
}

void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args.IsConstructCall()) {
Expand Down Expand Up @@ -2419,9 +2496,13 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {

void Backup(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (args.Length() < 1 || !args[0]->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"sourceDb\" argument must be an object.");
// Unlike the other unwrap sites in this file, which rely on V8's signature
// check for args.This(), this one takes an argument and must check the type
// itself before unwrapping it.
if (!DatabaseSync::GetConstructorTemplate(env)->HasInstance(args[0])) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"sourceDb\" argument must be an instance of DatabaseSync.");
return;
}

Expand Down Expand Up @@ -3811,23 +3892,6 @@ SQLTagStore::SQLTagStore(Environment* env,
MakeWeak();
}

static inline void SetSideEffectFreeGetter(
Isolate* isolate,
Local<FunctionTemplate> class_template,
Local<String> name,
FunctionCallback fn) {
Local<FunctionTemplate> getter =
FunctionTemplate::New(isolate,
fn,
Local<Value>(),
v8::Signature::New(isolate, class_template),
/* length */ 0,
ConstructorBehavior::kThrow,
SideEffectType::kHasNoSideEffect);
class_template->InstanceTemplate()->SetAccessorProperty(
name, getter, Local<FunctionTemplate>(), DontDelete);
}

SQLTagStore::~SQLTagStore() {}

Local<FunctionTemplate> SQLTagStore::GetConstructorTemplate(Environment* env) {
Expand Down Expand Up @@ -4569,62 +4633,14 @@ static void Initialize(Local<Object> target,
}
});
}
Local<FunctionTemplate> db_tmpl =
NewFunctionTemplate(isolate, DatabaseSync::New);
db_tmpl->InstanceTemplate()->SetInternalFieldCount(
DatabaseSync::kInternalFieldCount);
Local<Object> constants = Object::New(isolate);

DefineConstants(constants);

SetProtoMethod(isolate, db_tmpl, "open", DatabaseSync::Open);
SetProtoMethod(isolate, db_tmpl, "close", DatabaseSync::Close);
SetProtoDispose(isolate, db_tmpl, DatabaseSync::Dispose);
SetProtoMethod(isolate, db_tmpl, "prepare", DatabaseSync::Prepare);
SetProtoMethod(isolate, db_tmpl, "exec", DatabaseSync::Exec);
SetProtoMethod(isolate, db_tmpl, "function", DatabaseSync::CustomFunction);
SetProtoMethod(
isolate, db_tmpl, "createTagStore", DatabaseSync::CreateTagStore);
SetProtoMethodNoSideEffect(
isolate, db_tmpl, "location", DatabaseSync::Location);
SetProtoMethod(
isolate, db_tmpl, "aggregate", DatabaseSync::AggregateFunction);
SetProtoMethod(
isolate, db_tmpl, "createSession", DatabaseSync::CreateSession);
SetProtoMethod(
isolate, db_tmpl, "applyChangeset", DatabaseSync::ApplyChangeset);
SetProtoMethod(isolate,
db_tmpl,
"enableLoadExtension",
DatabaseSync::EnableLoadExtension);
SetProtoMethod(
isolate, db_tmpl, "enableDefensive", DatabaseSync::EnableDefensive);
SetProtoMethod(
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
SetProtoMethod(isolate, db_tmpl, "serialize", DatabaseSync::Serialize);
SetProtoMethod(isolate, db_tmpl, "deserialize", DatabaseSync::Deserialize);
SetProtoMethod(
isolate, db_tmpl, "setAuthorizer", DatabaseSync::SetAuthorizer);
SetSideEffectFreeGetter(isolate,
db_tmpl,
FIXED_ONE_BYTE_STRING(isolate, "isOpen"),
DatabaseSync::IsOpenGetter);
SetSideEffectFreeGetter(isolate,
db_tmpl,
FIXED_ONE_BYTE_STRING(isolate, "isTransaction"),
DatabaseSync::IsTransactionGetter);
SetSideEffectFreeGetter(isolate,
db_tmpl,
FIXED_ONE_BYTE_STRING(isolate, "limits"),
DatabaseSync::LimitsGetter);
Local<String> sqlite_type_key = FIXED_ONE_BYTE_STRING(isolate, "sqlite-type");
Local<v8::Symbol> sqlite_type_symbol =
v8::Symbol::For(isolate, sqlite_type_key);
Local<String> database_sync_string =
FIXED_ONE_BYTE_STRING(isolate, "node:sqlite");
db_tmpl->InstanceTemplate()->Set(sqlite_type_symbol, database_sync_string);

SetConstructorFunction(context, target, "DatabaseSync", db_tmpl);
SetConstructorFunction(context,
target,
"DatabaseSync",
DatabaseSync::GetConstructorTemplate(env));
SetConstructorFunction(context,
target,
"StatementSync",
Expand Down
2 changes: 2 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ class DatabaseSync : public BaseObject {
bool open,
bool allow_load_extension);
void MemoryInfo(MemoryTracker* tracker) const override;
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Open(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsOpenGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand Down
34 changes: 33 additions & 1 deletion test/parallel/test-sqlite-backup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,31 @@ describe('backup()', () => {
backup();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "sourceDb" argument must be an object.'
message: 'The "sourceDb" argument must be an instance of DatabaseSync.'
});
});

test('throws if the source database is not a DatabaseSync', (t) => {
const database = makeSourceDb();
const values = [
{},
[],
{ p0: 1, p1: 2, p2: 3, p3: 4 },
{ __proto__: DatabaseSync.prototype },
database.prepare('SELECT 1'),
database.createSession(),
];

for (const value of values) {
t.assert.throws(() => {
backup(value, nextDb());
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "sourceDb" argument must be an instance of DatabaseSync.'
});
}
});

test('throws if path is not a string, URL, or Buffer', (t) => {
const database = makeSourceDb();

Expand Down Expand Up @@ -88,6 +109,17 @@ describe('backup()', () => {
});
});

test('throws if the database path is an object with an unparseable href', (t) => {
const database = makeSourceDb();

t.assert.throws(() => {
backup(database, { href: 'zzz' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
});
});

test('throws if options is not an object', (t) => {
const database = makeSourceDb();

Expand Down
18 changes: 18 additions & 0 deletions test/parallel/test-sqlite-database-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ suite('DatabaseSync() constructor', () => {
});
});

test('throws if the database location is an object with an unparseable href', (t) => {
t.assert.throws(() => {
new DatabaseSync({ href: 'zzz' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.',
});
});

test('propagates an exception thrown by the href getter', (t) => {
t.assert.throws(() => {
new DatabaseSync({ get href() { throw new RangeError('boom'); } });
}, {
name: 'RangeError',
message: 'boom',
});
});

test('throws if options is provided but is not an object', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', null);
Expand Down
Loading