diff --git a/.changeset/fuzzy-spoons-smile.md b/.changeset/fuzzy-spoons-smile.md new file mode 100644 index 00000000..4f4b54a5 --- /dev/null +++ b/.changeset/fuzzy-spoons-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/wa-sqlite": patch +--- + +Preserve embedded NUL bytes when binding and reading TEXT values. diff --git a/src/sqlite-api.js b/src/sqlite-api.js index b4a517e3..fa19c558 100644 --- a/src/sqlite-api.js +++ b/src/sqlite-api.js @@ -221,8 +221,16 @@ export function Factory(Module) { const f = Module.cwrap(fname, ...decl("nnnnn:n")) return function (stmt, i, value) { verifyStatement(stmt) - const ptr = createUTF8(value) - const result = f(stmt, i, ptr, -1, sqliteFreeAddress) + let ptr = 0 + let nBytes = -1 + if (typeof value === "string") { + const utf8 = textEncoder.encode(value) + ptr = Module._sqlite3_malloc(utf8.byteLength + 1) + Module.HEAPU8.set(utf8, ptr) + Module.HEAPU8[ptr + utf8.byteLength] = 0 + nBytes = utf8.byteLength + } + const result = f(stmt, i, ptr, nBytes, sqliteFreeAddress) return check(fname, result, mapStmtToDB.get(stmt)) } })() @@ -402,11 +410,26 @@ export function Factory(Module) { sqlite3.column_text = (function () { const fname = "sqlite3_column_text" - const f = Module.cwrap(fname, ...decl("nn:s")) + const f = Module.cwrap(fname, ...decl("nn:n")) return function (stmt, iCol) { verifyStatement(stmt) - const result = f(stmt, iCol) - return result + const address = f(stmt, iCol) + const nBytes = sqlite3.column_bytes(stmt, iCol) + const parts = [] + let start = 0 + for (let end = 0; end < nBytes; ++end) { + if (Module.HEAPU8[address + end] === 0) { + if (start < end) { + parts.push(Module.UTF8ToString(address + start, end - start)) + } + parts.push("\0") + start = end + 1 + } + } + if (start < nBytes) { + parts.push(Module.UTF8ToString(address + start, nBytes - start)) + } + return parts.join("") } })() diff --git a/test/api_statements.js b/test/api_statements.js index 830ae1ac..b1e4fa71 100644 --- a/test/api_statements.js +++ b/test/api_statements.js @@ -238,6 +238,39 @@ export function api_statements(context) { } }); + it('should bind text with NUL bytes', async function() { + let rc; + const sql = 'SELECT hex(?)'; + const value = 'Before\0After'; + + for await (const stmt of i(sqlite3.statements(db, sql))) { + rc = await sqlite3.bind_text(stmt, 1, value); + expect(rc).toEqual(SQLite.SQLITE_OK); + + while ((rc = await sqlite3.step(stmt)) !== SQLite.SQLITE_DONE) { + expect(rc).toEqual(SQLite.SQLITE_ROW); + expect(await sqlite3.column_text(stmt, 0)).toEqual( + '4265666F7265004166746572' + ); + } + } + }); + + it('should read text with NUL bytes', async function() { + let rc; + const sql = "SELECT char(65279) || 'Before' || char(0) || 'After'"; + const value = '\uFEFFBefore\0After'; + + for await (const stmt of i(sqlite3.statements(db, sql))) { + while ((rc = await sqlite3.step(stmt)) !== SQLite.SQLITE_DONE) { + expect(rc).toEqual(SQLite.SQLITE_ROW); + expect(await sqlite3.column_bytes(stmt, 0)).toEqual(15); + expect(await sqlite3.column_text(stmt, 0)).toEqual(value); + expect(await sqlite3.column(stmt, 0)).toEqual(value); + } + } + }); + it('should bind boolean', async function() { let rc; const sql = 'SELECT ?'; @@ -444,4 +477,4 @@ async function* i(p) { } finally { await x.return(); } -} \ No newline at end of file +}