Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
794 lines
26 KiB
Plaintext
794 lines
26 KiB
Plaintext
// Incremental BLOB I/O (Deliverable 08). See src/blob.h for the
|
|
// threading model and lifetime answers.
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include <sqlite3.h>
|
|
#include <napi.h>
|
|
|
|
#include "macros.h"
|
|
#include "database.h"
|
|
#include "blob.h"
|
|
|
|
using namespace node_sqlite3;
|
|
|
|
namespace {
|
|
|
|
#define BLOB_BEGIN(type) \
|
|
assert(baton); \
|
|
assert(baton->blob); \
|
|
assert(!baton->blob->locked); \
|
|
assert(!baton->blob->closed); \
|
|
assert(baton->blob->inited); \
|
|
assert(baton->blob->_handle != NULL); \
|
|
baton->blob->locked = true; \
|
|
baton->blob->db->pending++; \
|
|
auto env = baton->blob->Env(); \
|
|
CREATE_WORK("sqlite3.Blob." #type, Work_##type, Work_After##type);
|
|
|
|
// A view of a JS binary value with its base pointer and available byte
|
|
// count, honouring byteOffset.
|
|
struct TargetView {
|
|
void* data = NULL;
|
|
size_t length = 0;
|
|
};
|
|
|
|
bool GetMutableTarget(napi_env env, const Napi::Value& value,
|
|
const std::string& what, TargetView* out) {
|
|
if (value.IsDataView()) {
|
|
size_t bytes = 0;
|
|
void* data = NULL;
|
|
napi_get_dataview_info(env, value, &bytes, &data, NULL, NULL);
|
|
out->data = data;
|
|
out->length = bytes;
|
|
return true;
|
|
}
|
|
if (value.IsBuffer()) {
|
|
Napi::Buffer<char> buf = value.As<Napi::Buffer<char>>();
|
|
out->data = buf.Data();
|
|
out->length = buf.Length();
|
|
return true;
|
|
}
|
|
if (value.IsTypedArray()) {
|
|
napi_typedarray_type type;
|
|
size_t elements = 0;
|
|
void* data = NULL;
|
|
napi_get_typedarray_info(env, value, &type, &elements, &data,
|
|
NULL, NULL);
|
|
switch (type) {
|
|
case napi_int8_array:
|
|
case napi_uint8_array:
|
|
case napi_uint8_clamped_array:
|
|
out->data = data;
|
|
out->length = elements;
|
|
return true;
|
|
default:
|
|
Napi::TypeError::New(env, "Cannot " + what +
|
|
": only byte-sized typed arrays are accepted"
|
|
).ThrowAsJavaScriptException();
|
|
return false;
|
|
}
|
|
}
|
|
Napi::TypeError::New(env, "Cannot " + what +
|
|
": a Buffer or Uint8Array is required"
|
|
).ThrowAsJavaScriptException();
|
|
return false;
|
|
}
|
|
|
|
// The message accompanying SQLITE_ABORT from blob read/write: the row
|
|
// was written after the handle was opened, invalidating it.
|
|
std::string BlobAbortMessage() {
|
|
return "the row this blob handle was opened on has been modified, "
|
|
"so the handle is no longer valid; reopen it (openBlob/reopen) "
|
|
"to continue";
|
|
}
|
|
|
|
// Carries a raw sqlite3_blob* whose wrapper is gone (~Blob).
|
|
struct BlobHandleBaton : Database::Baton {
|
|
sqlite3_blob* handle;
|
|
BlobHandleBaton(Database* db_, sqlite3_blob* handle_) :
|
|
Baton(db_, Napi::Function()), handle(handle_) {}
|
|
virtual ~BlobHandleBaton() override = default;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
Napi::Object Blob::Init(Napi::Env env, Napi::Object exports) {
|
|
Napi::HandleScope scope(env);
|
|
|
|
auto napi_default_method = static_cast<napi_property_attributes>(
|
|
napi_writable | napi_configurable);
|
|
|
|
auto t = DefineClass(env, "Blob", {
|
|
InstanceMethod("read", &Blob::Read, napi_default_method),
|
|
InstanceMethod("write", &Blob::Write, napi_default_method),
|
|
InstanceMethod("reopen", &Blob::Reopen, napi_default_method),
|
|
InstanceMethod("close", &Blob::Close, napi_default_method),
|
|
InstanceAccessor("size", &Blob::SizeGetter, nullptr),
|
|
InstanceAccessor("closed", &Blob::ClosedGetter, nullptr),
|
|
});
|
|
|
|
// Per-env (see Database::AddonData).
|
|
env.GetInstanceData<Database::AddonData>()->blob_ctor =
|
|
Napi::Persistent(t);
|
|
exports.Set("Blob", t);
|
|
return exports;
|
|
}
|
|
|
|
void Blob::Process() {
|
|
if ((closed || !inited) && !queue.empty()) {
|
|
return CleanQueue();
|
|
}
|
|
|
|
while (inited && !locked && !closed && !queue.empty()) {
|
|
auto call = std::unique_ptr<Call>(queue.front());
|
|
queue.pop();
|
|
|
|
call->callback(call->baton);
|
|
}
|
|
}
|
|
|
|
void Blob::Schedule(Work_Callback callback, Baton* baton) {
|
|
if (closed) {
|
|
queue.emplace(new Call(callback, baton));
|
|
CleanQueue();
|
|
}
|
|
else if (!inited || locked || !queue.empty()) {
|
|
queue.emplace(new Call(callback, baton));
|
|
}
|
|
else {
|
|
callback(baton);
|
|
}
|
|
}
|
|
|
|
template <class T> void Blob::Error(T* baton) {
|
|
auto env = baton->blob->Env();
|
|
Napi::HandleScope scope(env);
|
|
|
|
Blob* blob = baton->blob;
|
|
assert(blob->status != 0);
|
|
EXCEPTION(blob->message, blob->status, exception);
|
|
|
|
Napi::Function cb = baton->callback.Value();
|
|
|
|
if (IS_FUNCTION(cb)) {
|
|
Napi::Value argv[] = { exception };
|
|
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
|
|
}
|
|
else {
|
|
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
|
|
EMIT_EVENT(blob->Value(), 2, info);
|
|
}
|
|
}
|
|
|
|
void Blob::CleanQueue() {
|
|
auto env = this->Env();
|
|
Napi::HandleScope scope(env);
|
|
|
|
if (queue.empty()) return;
|
|
|
|
// Environment teardown (worker termination): failing the queued
|
|
// calls constructs JS on a dying environment, which is fatal. Drop
|
|
// them instead — reference cleanup still works there, so the
|
|
// batons are destroyed normally.
|
|
if (Database::EnvCannotRunJs(env)) {
|
|
while (!queue.empty()) {
|
|
auto call = std::unique_ptr<Call>(queue.front());
|
|
queue.pop();
|
|
delete call->baton;
|
|
}
|
|
return;
|
|
}
|
|
|
|
EXCEPTION("Blob is already closed", SQLITE_MISUSE, exception);
|
|
Napi::Value argv[] = { exception };
|
|
bool called = false;
|
|
|
|
while (!queue.empty()) {
|
|
auto call = std::unique_ptr<Call>(queue.front());
|
|
queue.pop();
|
|
|
|
std::unique_ptr<Baton> baton(call->baton);
|
|
Napi::Function cb = baton->callback.Value();
|
|
if (IS_FUNCTION(cb)) {
|
|
TRY_CATCH_CALL(Value(), cb, 1, argv);
|
|
called = true;
|
|
}
|
|
}
|
|
|
|
if (!called) {
|
|
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
|
|
EMIT_EVENT(Value(), 2, info);
|
|
}
|
|
}
|
|
|
|
// { Database db, String dbName, String table, String column,
|
|
// Number rowid, Boolean readOnly, [Function callback] }
|
|
Blob::Blob(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Blob>(info) {
|
|
auto env = info.Env();
|
|
if (!info.IsConstructCall()) {
|
|
Napi::TypeError::New(env, "Use the new operator to create new Blob objects").ThrowAsJavaScriptException();
|
|
return;
|
|
}
|
|
|
|
if (info.Length() <= 0 || !Database::HasInstance(info[0])) {
|
|
Napi::TypeError::New(env, "Database object expected").ThrowAsJavaScriptException();
|
|
return;
|
|
}
|
|
else if (info.Length() <= 1 || !info[1].IsString()
|
|
|| info.Length() <= 2 || !info[2].IsString()
|
|
|| info.Length() <= 3 || !info[3].IsString()) {
|
|
Napi::TypeError::New(env, "Database, table and column names expected").ThrowAsJavaScriptException();
|
|
return;
|
|
}
|
|
else if (info.Length() <= 4 || !info[4].IsNumber()) {
|
|
Napi::TypeError::New(env, "Rowid expected").ThrowAsJavaScriptException();
|
|
return;
|
|
}
|
|
else if (info.Length() <= 5 || !info[5].IsBoolean()) {
|
|
Napi::TypeError::New(env, "Read-only flag expected").ThrowAsJavaScriptException();
|
|
return;
|
|
}
|
|
else if (info.Length() > 6 && !info[6].IsUndefined() && !info[6].IsFunction()) {
|
|
Napi::TypeError::New(env, "Callback expected").ThrowAsJavaScriptException();
|
|
return;
|
|
}
|
|
|
|
this->db = Napi::ObjectWrap<Database>::Unwrap(info[0].As<Napi::Object>());
|
|
this->db->Ref();
|
|
|
|
auto dbName = info[1].As<Napi::String>().Utf8Value();
|
|
auto table = info[2].As<Napi::String>().Utf8Value();
|
|
auto column = info[3].As<Napi::String>().Utf8Value();
|
|
sqlite3_int64 rowid = info[4].As<Napi::Number>().Int64Value();
|
|
bool readOnly = info[5].As<Napi::Boolean>().Value();
|
|
|
|
info.This().As<Napi::Object>().DefineProperty(
|
|
Napi::PropertyDescriptor::Value("db", info[1]));
|
|
info.This().As<Napi::Object>().DefineProperty(
|
|
Napi::PropertyDescriptor::Value("table", info[2]));
|
|
info.This().As<Napi::Object>().DefineProperty(
|
|
Napi::PropertyDescriptor::Value("column", info[3]));
|
|
info.This().As<Napi::Object>().DefineProperty(
|
|
Napi::PropertyDescriptor::Value("rowid", info[4]));
|
|
info.This().As<Napi::Object>().DefineProperty(
|
|
Napi::PropertyDescriptor::Value("readOnly", info[5]));
|
|
|
|
// Tracked from construction so CloseLiveBlobs cannot miss a blob
|
|
// whose open is still queued or in flight.
|
|
this->db->live_blobs.push_back(this);
|
|
|
|
auto* baton = new OpenBaton(this->db, info[6].As<Napi::Function>(), this);
|
|
baton->dbName = dbName;
|
|
baton->table = table;
|
|
baton->column = column;
|
|
baton->rowid = rowid;
|
|
baton->readOnly = readOnly;
|
|
|
|
this->db->Schedule(Work_BeginOpen, baton);
|
|
}
|
|
|
|
void Blob::Work_BeginOpen(Database::Baton* baton) {
|
|
assert(baton->db->IsOpen());
|
|
assert(baton->db->_handle);
|
|
baton->db->pending++;
|
|
|
|
auto env = baton->db->Env();
|
|
CREATE_WORK("sqlite3.Blob.Open", Work_Open, Work_AfterOpen);
|
|
}
|
|
|
|
void Blob::Work_Open(napi_env e, void* data) {
|
|
auto* baton = static_cast<OpenBaton*>(data);
|
|
Blob* blob = baton->blob;
|
|
|
|
// sqlite3_blob_open takes the connection mutex internally (and a
|
|
// read/write lock on the row); running here on the worker pool is
|
|
// what serializes it against other work.
|
|
blob->_handle = NULL;
|
|
int rc = sqlite3_blob_open(baton->db->_handle,
|
|
baton->dbName.c_str(), baton->table.c_str(),
|
|
baton->column.c_str(), baton->rowid,
|
|
baton->readOnly ? 0 : 1, &blob->_handle);
|
|
blob->status = rc;
|
|
if (rc != SQLITE_OK) {
|
|
blob->message = std::string(sqlite3_errmsg(baton->db->_handle));
|
|
}
|
|
}
|
|
|
|
void Blob::Work_AfterOpen(napi_env e, napi_status status, void* data) {
|
|
std::unique_ptr<OpenBaton> baton(static_cast<OpenBaton*>(data));
|
|
AFTER_WORK_TEARDOWN_GUARD(baton);
|
|
auto* blob = baton->blob;
|
|
auto* db = baton->db;
|
|
|
|
auto env = blob->Env();
|
|
Napi::HandleScope scope(env);
|
|
|
|
db->pending--;
|
|
db->Process();
|
|
|
|
if (blob->status != SQLITE_OK) {
|
|
blob->DetachFromDatabase();
|
|
Error(baton.get());
|
|
}
|
|
else {
|
|
blob->inited = true;
|
|
Napi::Function cb = baton->callback.Value();
|
|
if (IS_FUNCTION(cb)) {
|
|
Napi::Value argv[] = { env.Null() };
|
|
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
|
|
}
|
|
}
|
|
|
|
blob->Process();
|
|
}
|
|
|
|
void Blob::EndCall() {
|
|
assert(locked);
|
|
assert(db->pending);
|
|
locked = false;
|
|
db->pending--;
|
|
Process();
|
|
db->Process();
|
|
}
|
|
|
|
Blob::~Blob() {
|
|
// GC safety net, mirroring ~Statement: every queued Baton holds a Ref
|
|
// on the blob, so the queue is provably empty and CleanQueue cannot
|
|
// fire JS from this finalizer. sqlite3_blob_close takes the
|
|
// connection mutex internally; defer through the exclusive queue when
|
|
// a worker could be mid-round-trip holding that mutex while waiting
|
|
// for this very thread.
|
|
CleanQueue();
|
|
if (_handle != NULL) {
|
|
if (db != NULL && db->MayBlockOnWorkerRoundTrip()) {
|
|
db->Schedule(Work_DeferredBlobClose,
|
|
new BlobHandleBaton(db, _handle), true);
|
|
_handle = NULL;
|
|
}
|
|
else {
|
|
sqlite3_blob_close(_handle);
|
|
_handle = NULL;
|
|
}
|
|
}
|
|
// Detached blobs have already released the database (and nulled the
|
|
// pointer); this covers the never-detached case only.
|
|
if (db != NULL) {
|
|
UntrackFromDatabase();
|
|
db->Unref();
|
|
}
|
|
}
|
|
|
|
void Blob::Work_DeferredBlobClose(Database::Baton* b) {
|
|
auto baton = std::unique_ptr<BlobHandleBaton>(
|
|
static_cast<BlobHandleBaton*>(b));
|
|
auto* db = baton->db;
|
|
|
|
sqlite3_blob_close(baton->handle);
|
|
|
|
db->exclusiveHeld = false;
|
|
db->Process();
|
|
}
|
|
|
|
void Blob::UntrackFromDatabase() {
|
|
if (db == NULL) return;
|
|
auto& v = db->live_blobs;
|
|
for (auto it = v.begin(); it != v.end(); ++it) {
|
|
if (*it == this) {
|
|
v.erase(it);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// JS thread, nothing in flight (Work_BeginClose / ~Database) or at a
|
|
// completion point where the handle is already gone. Idempotent.
|
|
// Severs the link to the Database, and is the single place the blob's
|
|
// reference on it is released. Nulling `db` is the point: at environment
|
|
// teardown napi finalizes wrappers in an unspecified order, so ~Blob can
|
|
// run after ~Database, and a back-pointer left dangling there means
|
|
// UntrackFromDatabase walks a destroyed std::vector. (That is a
|
|
// segfault, not a stale read — it was reproducible on musl.)
|
|
//
|
|
// owner_dying is set when ~Database is the caller: the Database is
|
|
// already being destroyed, so its refcount is moot and calling Unref on
|
|
// a half-destroyed wrapper is exactly the dereference being avoided.
|
|
void Blob::DetachFromDatabase(bool owner_dying) {
|
|
if (db == NULL) return;
|
|
if (_handle != NULL) {
|
|
sqlite3_blob_close(_handle);
|
|
_handle = NULL;
|
|
}
|
|
UntrackFromDatabase();
|
|
closed = true;
|
|
if (owner_dying) {
|
|
// ~Database is running. napi finalizes wrappers in an
|
|
// unspecified order at environment teardown, so ~Blob can run
|
|
// afterwards — and UntrackFromDatabase would then walk a
|
|
// destroyed std::vector, which faults rather than reading stale
|
|
// data (reproducible on musl). Drop the back-pointer so the
|
|
// destructor skips it. The reference is deliberately not
|
|
// released: the Database is already being destroyed, and
|
|
// Unref'ing a half-destroyed wrapper is the same dereference.
|
|
//
|
|
// Only on this path: the ordinary close still needs `db` for
|
|
// the in-flight bookkeeping that runs after this returns
|
|
// (Work_AfterClose's CallGuard decrements db->pending).
|
|
db = NULL;
|
|
}
|
|
}
|
|
|
|
void Database::CloseLiveBlobs(bool owner_dying) {
|
|
// Main thread, nothing in flight (Work_BeginClose / ~Database).
|
|
std::vector<Blob*> blobs = live_blobs;
|
|
live_blobs.clear();
|
|
for (Blob* blob : blobs) {
|
|
blob->DetachFromDatabase(owner_dying);
|
|
}
|
|
}
|
|
|
|
// --- read ---------------------------------------------------------------------
|
|
|
|
// read(target[, offset[, callback]]): copies target.length bytes of the
|
|
// blob, starting at `offset` within the blob, into the target. The
|
|
// callback may sit in the offset's slot (the promise layer appends it
|
|
// there when no offset was given), so each slot accepts either its own
|
|
// type or the trailing function.
|
|
Napi::Value Blob::Read(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
Blob* blob = this;
|
|
|
|
if (info.Length() < 1) {
|
|
Napi::TypeError::New(env, "Expected at least 1 argument").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
TargetView target;
|
|
if (!GetMutableTarget(env, info[0], "read into a blob target", &target)) {
|
|
return env.Null();
|
|
}
|
|
|
|
sqlite3_int64 offset = 0;
|
|
Napi::Function callback;
|
|
if (info.Length() > 1 && info[1].IsNumber()) {
|
|
offset = info[1].As<Napi::Number>().Int64Value();
|
|
if (offset < 0) {
|
|
Napi::RangeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
if (info.Length() > 2 && !info[2].IsUndefined()) {
|
|
if (!info[2].IsFunction()) {
|
|
Napi::TypeError::New(env, "Argument 2 must be a function").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
callback = info[2].As<Napi::Function>();
|
|
}
|
|
}
|
|
else if (info.Length() > 1 && info[1].IsFunction()) {
|
|
callback = info[1].As<Napi::Function>();
|
|
}
|
|
else if (info.Length() > 1 && !info[1].IsUndefined()) {
|
|
Napi::TypeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
|
|
auto* baton = new IoBaton(blob, callback);
|
|
baton->target = Napi::Persistent(info[0]);
|
|
baton->data = target.data;
|
|
baton->length = target.length;
|
|
baton->offset = offset;
|
|
blob->Schedule(Work_BeginRead, baton);
|
|
|
|
return info.This();
|
|
}
|
|
|
|
void Blob::Work_BeginRead(Baton* baton) {
|
|
BLOB_BEGIN(Read);
|
|
}
|
|
|
|
void Blob::Work_Read(napi_env e, void* data) {
|
|
auto* baton = static_cast<IoBaton*>(data);
|
|
Blob* blob = baton->blob;
|
|
|
|
int rc = sqlite3_blob_read(blob->_handle, baton->data,
|
|
static_cast<int>(baton->length), static_cast<int>(baton->offset));
|
|
blob->status = rc;
|
|
if (rc == SQLITE_ABORT) {
|
|
blob->message = BlobAbortMessage();
|
|
}
|
|
else if (rc != SQLITE_OK) {
|
|
blob->message = std::string(sqlite3_errmsg(blob->db->_handle));
|
|
}
|
|
}
|
|
|
|
void Blob::Work_AfterRead(napi_env e, napi_status status, void* data) {
|
|
std::unique_ptr<IoBaton> baton(static_cast<IoBaton*>(data));
|
|
AFTER_WORK_TEARDOWN_GUARD(baton);
|
|
auto* blob = baton->blob;
|
|
|
|
auto env = blob->Env();
|
|
Napi::HandleScope scope(env);
|
|
|
|
// Runs the end-of-call bookkeeping on every exit path, including
|
|
// TRY_CATCH_CALL's early return when a JS callback throws.
|
|
Blob::CallGuard blob_call_guard__(blob);
|
|
|
|
if (blob->status != SQLITE_OK) {
|
|
Error(baton.get());
|
|
return;
|
|
}
|
|
|
|
Napi::Function cb = baton->callback.Value();
|
|
if (IS_FUNCTION(cb)) {
|
|
// The number of bytes actually copied is the full target length:
|
|
// sqlite3_blob_read fails (rather than short-reading) when
|
|
// offset+length exceeds the blob size.
|
|
Napi::Value argv[] = { env.Null(),
|
|
Napi::Number::New(env, baton->length) };
|
|
TRY_CATCH_CALL(blob->Value(), cb, 2, argv);
|
|
}
|
|
}
|
|
|
|
// --- write --------------------------------------------------------------------
|
|
|
|
// write(source[, offset[, callback]]): copies source.length bytes into
|
|
// the blob starting at `offset` within the blob. The callback may sit in
|
|
// the offset's slot; see Read.
|
|
Napi::Value Blob::Write(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
Blob* blob = this;
|
|
|
|
if (info.Length() < 1) {
|
|
Napi::TypeError::New(env, "Expected at least 1 argument").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
TargetView target;
|
|
if (!GetMutableTarget(env, info[0], "write from a blob source", &target)) {
|
|
return env.Null();
|
|
}
|
|
|
|
sqlite3_int64 offset = 0;
|
|
Napi::Function callback;
|
|
if (info.Length() > 1 && info[1].IsNumber()) {
|
|
offset = info[1].As<Napi::Number>().Int64Value();
|
|
if (offset < 0) {
|
|
Napi::RangeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
if (info.Length() > 2 && !info[2].IsUndefined()) {
|
|
if (!info[2].IsFunction()) {
|
|
Napi::TypeError::New(env, "Argument 2 must be a function").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
callback = info[2].As<Napi::Function>();
|
|
}
|
|
}
|
|
else if (info.Length() > 1 && info[1].IsFunction()) {
|
|
callback = info[1].As<Napi::Function>();
|
|
}
|
|
else if (info.Length() > 1 && !info[1].IsUndefined()) {
|
|
Napi::TypeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
|
|
auto* baton = new IoBaton(blob, callback);
|
|
baton->target = Napi::Persistent(info[0]);
|
|
baton->data = target.data;
|
|
baton->length = target.length;
|
|
baton->offset = offset;
|
|
blob->Schedule(Work_BeginWrite, baton);
|
|
|
|
return info.This();
|
|
}
|
|
|
|
void Blob::Work_BeginWrite(Baton* baton) {
|
|
BLOB_BEGIN(Write);
|
|
}
|
|
|
|
void Blob::Work_Write(napi_env e, void* data) {
|
|
auto* baton = static_cast<IoBaton*>(data);
|
|
Blob* blob = baton->blob;
|
|
|
|
int rc = sqlite3_blob_write(blob->_handle, baton->data,
|
|
static_cast<int>(baton->length), static_cast<int>(baton->offset));
|
|
blob->status = rc;
|
|
if (rc == SQLITE_ABORT) {
|
|
blob->message = BlobAbortMessage();
|
|
}
|
|
else if (rc != SQLITE_OK) {
|
|
blob->message = std::string(sqlite3_errmsg(blob->db->_handle));
|
|
}
|
|
}
|
|
|
|
void Blob::Work_AfterWrite(napi_env e, napi_status status, void* data) {
|
|
std::unique_ptr<IoBaton> baton(static_cast<IoBaton*>(data));
|
|
AFTER_WORK_TEARDOWN_GUARD(baton);
|
|
auto* blob = baton->blob;
|
|
|
|
auto env = blob->Env();
|
|
Napi::HandleScope scope(env);
|
|
|
|
Blob::CallGuard blob_call_guard__(blob);
|
|
|
|
if (blob->status != SQLITE_OK) {
|
|
Error(baton.get());
|
|
return;
|
|
}
|
|
|
|
Napi::Function cb = baton->callback.Value();
|
|
if (IS_FUNCTION(cb)) {
|
|
Napi::Value argv[] = { env.Null(),
|
|
Napi::Number::New(env, baton->length) };
|
|
TRY_CATCH_CALL(blob->Value(), cb, 2, argv);
|
|
}
|
|
}
|
|
|
|
// --- reopen --------------------------------------------------------------------
|
|
|
|
Napi::Value Blob::Reopen(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
Blob* blob = this;
|
|
|
|
REQUIRE_ARGUMENT_INTEGER(0, rowid);
|
|
OPTIONAL_ARGUMENT_FUNCTION(1, callback);
|
|
|
|
auto* baton = new Baton(blob, callback);
|
|
baton->rowid = rowid;
|
|
blob->Schedule(Work_BeginReopen, baton);
|
|
|
|
return info.This();
|
|
}
|
|
|
|
void Blob::Work_BeginReopen(Baton* baton) {
|
|
BLOB_BEGIN(Reopen);
|
|
}
|
|
|
|
void Blob::Work_Reopen(napi_env e, void* data) {
|
|
auto* baton = static_cast<Baton*>(data);
|
|
Blob* blob = baton->blob;
|
|
|
|
int rc = sqlite3_blob_reopen(blob->_handle, baton->rowid);
|
|
blob->status = rc;
|
|
if (rc != SQLITE_OK) {
|
|
blob->message = std::string(sqlite3_errmsg(blob->db->_handle));
|
|
}
|
|
}
|
|
|
|
void Blob::Work_AfterReopen(napi_env e, napi_status status, void* data) {
|
|
std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
|
|
AFTER_WORK_TEARDOWN_GUARD(baton);
|
|
auto* blob = baton->blob;
|
|
|
|
auto env = blob->Env();
|
|
Napi::HandleScope scope(env);
|
|
|
|
Blob::CallGuard blob_call_guard__(blob);
|
|
|
|
if (blob->status != SQLITE_OK) {
|
|
Error(baton.get());
|
|
return;
|
|
}
|
|
|
|
Napi::Function cb = baton->callback.Value();
|
|
if (IS_FUNCTION(cb)) {
|
|
Napi::Value argv[] = { env.Null() };
|
|
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
|
|
}
|
|
}
|
|
|
|
// --- close ----------------------------------------------------------------------
|
|
|
|
Napi::Value Blob::Close(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
Blob* blob = this;
|
|
|
|
OPTIONAL_ARGUMENT_FUNCTION(0, callback);
|
|
|
|
if (blob->closed) {
|
|
// Dispose contract: a second close is a benign no-op, and one
|
|
// after the database closed finds the handle already gone.
|
|
if (IS_FUNCTION(callback)) {
|
|
Napi::Value argv[] = { env.Null() };
|
|
TRY_CATCH_CALL(blob->Value(), callback, 1, argv, info.This());
|
|
}
|
|
return info.This();
|
|
}
|
|
|
|
auto* baton = new Baton(blob, callback);
|
|
blob->Schedule(Work_BeginClose, baton);
|
|
|
|
return info.This();
|
|
}
|
|
|
|
void Blob::Work_BeginClose(Baton* baton) {
|
|
BLOB_BEGIN(Close);
|
|
}
|
|
|
|
void Blob::Work_Close(napi_env e, void* data) {
|
|
auto* baton = static_cast<Baton*>(data);
|
|
Blob* blob = baton->blob;
|
|
|
|
sqlite3_blob_close(blob->_handle);
|
|
blob->_handle = NULL;
|
|
}
|
|
|
|
void Blob::Work_AfterClose(napi_env e, napi_status status, void* data) {
|
|
std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
|
|
AFTER_WORK_TEARDOWN_GUARD(baton);
|
|
auto* blob = baton->blob;
|
|
|
|
auto env = blob->Env();
|
|
Napi::HandleScope scope(env);
|
|
|
|
Blob::CallGuard blob_call_guard__(blob);
|
|
|
|
blob->DetachFromDatabase();
|
|
|
|
Napi::Function cb = baton->callback.Value();
|
|
if (IS_FUNCTION(cb)) {
|
|
Napi::Value argv[] = { env.Null() };
|
|
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
|
|
}
|
|
}
|
|
|
|
// --- accessors -------------------------------------------------------------------
|
|
|
|
Napi::Value Blob::SizeGetter(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
Blob* blob = this;
|
|
|
|
// read()/write() queue behind the open, but a synchronous getter
|
|
// cannot; say which of the two states the caller is actually in,
|
|
// because "not open" reads as "closed" when it usually means "not
|
|
// open yet".
|
|
//
|
|
// Decision (D08 finding 5, settled in D09): keep throwing rather than
|
|
// join Statement's read-`undefined`-until-ready convention. The
|
|
// statement accessors expose derived metadata snapshots; `size` is a
|
|
// dimension of a live native handle, and `undefined` would flow into
|
|
// arithmetic (`size + offset` → NaN) and buffer sizing silently — the
|
|
// silent-coercion bug class this package exists to close. The error
|
|
// messages above are state-specific, so the loud path is precise.
|
|
// Order matters: _handle is NULL before the open completes as well as
|
|
// after a close, so the not-yet-open case has to be tested first.
|
|
if (!blob->inited && !blob->closed) {
|
|
Napi::Error::New(env,
|
|
"blob.size is not available until the handle has finished "
|
|
"opening; read it from the openBlob() callback, or after an "
|
|
"awaited read/write (those queue behind the open)"
|
|
).ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
if (blob->closed || blob->_handle == NULL) {
|
|
Napi::Error::New(env, "Blob is already closed")
|
|
.ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
if (blob->locked) {
|
|
// An op is in flight on this handle; reading the size now could
|
|
// observe a torn state.
|
|
Napi::Error::New(env,
|
|
"cannot read blob.size while an operation on this blob is "
|
|
"in flight; read it from a callback or after the operation"
|
|
).ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
// The handle's own queue is idle (locked == false). Same gate as the
|
|
// other main-thread sqlite reads: refuse while a worker could be
|
|
// blocked mid-round-trip on this thread holding the connection mutex.
|
|
if (blob->db->MayBlockOnWorkerRoundTrip()) {
|
|
Napi::Error::New(env,
|
|
"cannot read blob.size while a JavaScript function, "
|
|
"collation or progress callback is mid-call on this "
|
|
"connection; read it from a callback or after the query"
|
|
).ThrowAsJavaScriptException();
|
|
return env.Null();
|
|
}
|
|
return Napi::Number::New(env,
|
|
static_cast<double>(sqlite3_blob_bytes(blob->_handle)));
|
|
}
|
|
|
|
Napi::Value Blob::ClosedGetter(const Napi::CallbackInfo& info) {
|
|
return Napi::Boolean::New(info.Env(), closed);
|
|
}
|