diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 038af9812f9e..4430e7bf7f59 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -23,6 +23,7 @@ namespace sqlite { using v8::Array; using v8::ArrayBuffer; +using v8::BackingStore; using v8::BackingStoreInitializationMode; using v8::BackingStoreOnFailureMode; using v8::BigInt; @@ -2404,16 +2405,34 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& args) { BaseObjectPtr guard(db); ArrayBufferViewContents buf(args[0]); + if (buf.length() > std::numeric_limits::max()) { + THROW_ERR_OUT_OF_RANGE(env, "The changeset is too large."); + return; + } + + std::unique_ptr changeset; + if (buf.length() > 0) { + changeset = ArrayBuffer::NewBackingStore( + env->isolate(), + buf.length(), + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!changeset) { + THROW_ERR_MEMORY_ALLOCATION_FAILED(env); + return; + } + std::memcpy(changeset->Data(), buf.data(), buf.length()); + } + int r; { CallbackDepthGuard guard(db); - r = sqlite3changeset_apply( - db->connection_, - buf.length(), - const_cast(static_cast(buf.data())), - context.filterCallback ? xFilter : nullptr, - xConflict, - static_cast(&context)); + r = sqlite3changeset_apply(db->connection_, + static_cast(buf.length()), + changeset ? changeset->Data() : nullptr, + context.filterCallback ? xFilter : nullptr, + xConflict, + static_cast(&context)); } if (r == SQLITE_OK) { args.GetReturnValue().Set(true); diff --git a/test/parallel/test-sqlite-session.js b/test/parallel/test-sqlite-session.js index c36b4352a341..9f5161da5501 100644 --- a/test/parallel/test-sqlite-session.js +++ b/test/parallel/test-sqlite-session.js @@ -282,6 +282,25 @@ suite('conflict resolution', () => { [{ value: 'world' }]); // unchanged }); + test('database.applyChangeset() - changeset detached by onConflict', (t) => { + const { database2, changeset } = prepareConflict(); + const result = database2.applyChangeset(changeset, { + onConflict: () => { + const transferred = structuredClone(changeset.buffer, { + transfer: [changeset.buffer], + }); + new Uint8Array(transferred).fill(0); + return constants.SQLITE_CHANGESET_REPLACE; + } + }); + + t.assert.strictEqual(result, true); + t.assert.strictEqual(changeset.byteLength, 0); + deepStrictEqual(t)( + database2.prepare('SELECT * FROM data ORDER BY key').all(), + [{ key: 1, value: 'hello' }, { key: 2, value: 'foo' }]); + }); + test('database.applyChangeset() - SQLITE_CHANGESET_DATA conflict handled with SQLITE_CHANGESET_REPLACE', (t) => { const { database2, changeset } = prepareDataConflict(); let conflictType = null; @@ -406,6 +425,35 @@ test('filter handler throws', (t) => { }); }); +test('database.applyChangeset() - changeset detached by filter', (t) => { + const database1 = new DatabaseSync(':memory:'); + const database2 = new DatabaseSync(':memory:'); + database1.exec('CREATE TABLE data(key INTEGER PRIMARY KEY)'); + database2.exec('CREATE TABLE data(key INTEGER PRIMARY KEY)'); + + const session = database1.createSession(); + database1.exec('INSERT INTO data VALUES (1), (2), (3)'); + const changeset = session.changeset(); + + const result = database2.applyChangeset(changeset, { + filter: () => { + const transferred = structuredClone(changeset.buffer, { + transfer: [changeset.buffer], + }); + new Uint8Array(transferred).fill(0); + return true; + } + }); + + t.assert.strictEqual(result, true); + t.assert.strictEqual(changeset.byteLength, 0); + deepStrictEqual(t)(database2.prepare('SELECT * FROM data').all(), [ + { key: 1 }, + { key: 2 }, + { key: 3 }, + ]); +}); + test('database.createSession() - filter changes', (t) => { const database1 = new DatabaseSync(':memory:'); const database2 = new DatabaseSync(':memory:');