@evolu/sqlite-wasm 2.2.4 → 3.53.4-build1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -218
  3. package/dist/src/CApi.d.ts +2108 -0
  4. package/dist/src/CApi.d.ts.map +1 -0
  5. package/dist/src/CApi.js +1919 -0
  6. package/dist/src/Constants.d.ts +868 -0
  7. package/dist/src/Constants.d.ts.map +1 -0
  8. package/dist/src/Constants.js +602 -0
  9. package/dist/src/Database.d.ts +641 -0
  10. package/dist/src/Database.d.ts.map +1 -0
  11. package/dist/src/Database.js +1177 -0
  12. package/dist/src/Memory.d.ts +119 -0
  13. package/dist/src/Memory.d.ts.map +1 -0
  14. package/dist/src/Memory.js +207 -0
  15. package/dist/src/Pointer.d.ts +100 -0
  16. package/dist/src/Pointer.d.ts.map +1 -0
  17. package/dist/src/Pointer.js +15 -0
  18. package/dist/src/SahPool.d.ts +744 -0
  19. package/dist/src/SahPool.d.ts.map +1 -0
  20. package/dist/src/SahPool.js +1985 -0
  21. package/dist/src/Wasm.d.ts +315 -0
  22. package/dist/src/Wasm.d.ts.map +1 -0
  23. package/dist/src/Wasm.js +756 -0
  24. package/dist/src/WasmUrl.d.ts +21 -0
  25. package/dist/src/WasmUrl.d.ts.map +1 -0
  26. package/dist/src/WasmUrl.js +20 -0
  27. package/dist/src/c-api/index.d.ts +9 -0
  28. package/dist/src/c-api/index.d.ts.map +1 -0
  29. package/dist/src/c-api/index.js +8 -0
  30. package/dist/src/index.d.ts +15 -0
  31. package/dist/src/index.d.ts.map +1 -0
  32. package/dist/src/index.js +13 -0
  33. package/dist/wasm/sqlite3.wasm +0 -0
  34. package/package.json +60 -60
  35. package/src/CApi.test.ts +217 -0
  36. package/src/CApi.ts +3294 -0
  37. package/src/Constants.ts +803 -0
  38. package/src/Database.ts +1714 -0
  39. package/src/Memory.test.ts +319 -0
  40. package/src/Memory.ts +237 -0
  41. package/src/Pointer.ts +121 -0
  42. package/src/SahPool.test.ts +180 -0
  43. package/src/SahPool.ts +2627 -0
  44. package/src/Wasm.test.ts +444 -0
  45. package/src/Wasm.ts +1026 -0
  46. package/src/WasmUrl.ts +26 -0
  47. package/src/c-api/index.ts +9 -0
  48. package/src/index.ts +15 -0
  49. package/bin/index.js +0 -110
  50. package/index.d.ts +0 -8118
  51. package/index.mjs +0 -7
  52. package/node.mjs +0 -3
  53. package/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +0 -13659
  54. package/sqlite-wasm/jswasm/sqlite3-node.mjs +0 -11671
  55. package/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js +0 -691
  56. package/sqlite-wasm/jswasm/sqlite3-worker1-bundler-friendly.mjs +0 -35
  57. package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.js +0 -193
  58. package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.mjs +0 -187
  59. package/sqlite-wasm/jswasm/sqlite3-worker1.js +0 -46
  60. package/sqlite-wasm/jswasm/sqlite3.js +0 -13697
  61. package/sqlite-wasm/jswasm/sqlite3.mjs +0 -13661
  62. package/sqlite-wasm/jswasm/sqlite3.wasm +0 -0
@@ -0,0 +1,1714 @@
1
+ /**
2
+ * Evolu's SQLite API: databases and statements over the CApi functions, with
3
+ * typed errors and explicit resource management.
4
+ *
5
+ * Status: {@link createSqliteDatabase} opens Memory databases, and File and
6
+ * EncryptedFile databases on a {@link SqliteVfs} or {@link SqliteEncryptingVfs},
7
+ * which a {@link SahPool} is, and {@link createEncryptedSqliteDatabase} also
8
+ * opens the encrypted databases `@evolu/web` 3 created with
9
+ * `@evolu/sqlite-wasm` 2.2.4 in a pool. The pool's own encryption is tested in
10
+ * Node.js over a fake OPFS against 2.2.4, which creates databases as
11
+ * `@evolu/web` does and leaves a hot journal the pool rolls back, and in
12
+ * Chromium, Firefox and WebKit on real OPFS against 2.2.4.
13
+ *
14
+ * ## Calls
15
+ *
16
+ * - Synchronous. Every call returns when SQLite returns. The only asynchronous
17
+ * step, deriving a legacy key with WebCrypto, runs between the two opens of
18
+ * {@link createEncryptedSqliteDatabase}.
19
+ * - Bound once per database. {@link createSqliteDatabase} binds the C functions
20
+ * and memory helpers it needs and allocates one {@link SqliteScratch}; running
21
+ * a statement then calls wasm exports directly.
22
+ * - Every fallible call returns a Result. {@link SqliteError} copies
23
+ * `sqlite3_errmsg` and `sqlite3_extended_errcode` right after the failing
24
+ * call, before any other call on the connection replaces them. It copies
25
+ * `sqlite3_error_offset` only for SQLITE_ERROR from prepare, exec or step,
26
+ * the code of the parser errors that set it, because SQLite keeps an offset
27
+ * until a later prepare succeeds or a statement resets.
28
+ * - The VFS records the first failure since the last top-level call (open,
29
+ * prepare, run, exec, export), and {@link SqliteError.cause} carries it. The
30
+ * database clears the record before each such call. A call that runs inside
31
+ * another call of a database on the same VFS, as a query a callback runs
32
+ * does, is not top-level, so it leaves the record to the outer call.
33
+ * - Every call, disposal included, enters wasm through {@link SqliteWasm.call}. An
34
+ * exception or trap that escapes wasm, such as from a call through a disposed
35
+ * function pointer, is a defect: it is rethrown, and every later call on any
36
+ * database of the instance throws without entering wasm, because the C stack
37
+ * and SQLite's state are left inconsistent. A trap after a File or
38
+ * EncryptedFile connection opened reaches the caller of the open as a
39
+ * `SuppressedError` whose `suppressed` is the trap, because the open's
40
+ * deferred close and the free of its scratch memory are refused as it
41
+ * unwinds.
42
+ *
43
+ * ## Opening
44
+ *
45
+ * - A Memory database opens `:memory:` on the default VFS.
46
+ * - A File or EncryptedFile database's path is a {@link SqliteVfsPath}, a
47
+ * canonical path, which SQLite passes to the VFS as it is, never as a URI or
48
+ * an in-memory database, followed by the name of the VFS.
49
+ * - A File or EncryptedFile database opens its path on its VFS, an EncryptedFile
50
+ * database keyed as the next section describes. Either then prepares a query
51
+ * of `sqlite_schema`, because SQLite reads a file only when a statement needs
52
+ * it. So the open rolls back a hot journal, and fails for a file that is not
53
+ * a database, with SQLITE_NOTADB, or for a hot journal that cannot be rolled
54
+ * back.
55
+ * - The database uses its VFS only through {@link SqliteVfs} or
56
+ * {@link SqliteEncryptingVfs}, so any VFS that implements them can hold it,
57
+ * and a driver can open each database on the VFS that has it, as
58
+ * {@link SqliteVfs.getPaths} tells.
59
+ * - Flags are READWRITE, CREATE and EXRESCODE, so failures during the open
60
+ * already carry extended codes.
61
+ * - When the open fails, the message is read first and then the handle SQLite
62
+ * returned anyway is closed with `sqlite3_close_v2`. Without a handle, as
63
+ * when memory runs out, the error is the open's result code.
64
+ *
65
+ * ## Encryption
66
+ *
67
+ * - The VFS encrypts an EncryptedFile database itself, and a {@link SahPool} does
68
+ * it as SQLite3 Multiple Ciphers' `sqlcipher` scheme did with its default
69
+ * parameters, those of SQLCipher 4, which `PRAGMA cipher = 'sqlcipher'`
70
+ * selected in `@evolu/web` 3 with 2.2.4. The open registers the key for the
71
+ * path with {@link SqliteEncryptingVfs.registerKey} before it enters wasm and
72
+ * disposes the registration when it returns, by when the VFS has taken the
73
+ * key for the database file; the module documentation of `SahPool.ts`
74
+ * describes the pool's format. The key never appears in SQL, a URI, an error
75
+ * or SQLite's memory, and the caller's key is never modified.
76
+ * - Then `sqlite3_file_control` with SQLITE_FCNTL_RESERVE_BYTES asks for 80 bytes
77
+ * at the end of each page, where the pool keeps the page's IV and HMAC, which
78
+ * takes effect for a new database, and `PRAGMA secure_delete = ON` makes
79
+ * deletes overwrite freed content, as SQLite3 Multiple Ciphers did for every
80
+ * encrypted connection. A {@link SahPool} refuses to turn it off for an
81
+ * encrypted database, because a freed page SQLite does not write would fail
82
+ * to authenticate, as `SahPool.ts` describes.
83
+ * - Registering validates nothing, so the read of `sqlite_schema` checks the key.
84
+ * A wrong key fails there with SQLITE_NOTADB, from page 1, or with
85
+ * SQLITE_CORRUPT when the first page of a hot journal is another page, and
86
+ * {@link SqliteError.cause} is the VFS's {@link SqlitePageAuthenticationError}.
87
+ * The rollback stops at that first page, which fails to authenticate, so it
88
+ * writes no page of the journal and keeps the journal for the right key.
89
+ * Before that page, SQLite truncates the database to the size the journal
90
+ * records, as the right key's rollback does too. It would extend a shorter
91
+ * database by one zeroed page, encrypted with the wrong key, but a commit
92
+ * shrinks the file only after it deletes the journal, and a pool refuses a
93
+ * VACUUM that would change an encrypted database's page size, so no database
94
+ * is shorter than its hot journal records.
95
+ * - `@evolu/web` with 2.2.4 ran `PRAGMA key = "x'<hex>'"`, SQLCipher's notation
96
+ * for a raw key, which SQLite3 Multiple Ciphers 2.2.4 took as a passphrase
97
+ * because of a bug
98
+ * (https://github.com/utelle/SQLite3MultipleCiphers/issues/218, fixed in
99
+ * 2.2.5), deriving the key with the `sqlcipher` scheme, so those databases
100
+ * fail as with a wrong key. {@link createEncryptedSqliteDatabase} then opens
101
+ * them with the key {@link deriveLegacySqliteKey} derives through
102
+ * {@link SubtleCryptoDep}, and {@link SqliteEncryptedDatabase.keyDerivation}
103
+ * says which key opened the database. Nothing rekeys a database.
104
+ * - `@evolu/web` 1.0.1-preview.6 to 2.4.0 also ran `PRAGMA legacy = 4`, so the
105
+ * databases it encrypted fail with SQLITE_NOTADB, as with a wrong key, since
106
+ * page 1 is encrypted from byte 16; they have not opened since `@evolu/web`
107
+ * 3.0.0 (see `SahPool.ts`).
108
+ * - Only page 1 of the database or a record of its journal that failed to
109
+ * authenticate counts as a wrong key. Another page of the database that fails
110
+ * means that page 1 proved the key and the page is damaged, so it fails with
111
+ * SQLITE_CORRUPT and that page as the cause, also with the key 2.2.4 derived.
112
+ * An I/O error fails with its own code, and the VFS's record as its cause,
113
+ * and a database whose pages authenticate but whose content is malformed
114
+ * fails with SQLite's code and no cause.
115
+ *
116
+ * ## Statements
117
+ *
118
+ * - SQL is copied into its own memory, which is freed when the call returns,
119
+ * because SQLite parses it in place and copies a statement's SQL, which a
120
+ * schema change prepares again, only after the parse. Meanwhile a callback,
121
+ * such as a collation-needed callback during the parse or a function during a
122
+ * step of {@link SqliteDatabase.exec}, can run a query on the same database,
123
+ * and that query reuses the scratch memory.
124
+ * - `sqlite3_prepare_v3` gets the SQL's byte length, counting a NUL terminator,
125
+ * which spares SQLite a copy, and, for statements
126
+ * {@link SqliteDatabase.prepare} returns, SQLITE_PREPARE_PERSISTENT. Empty SQL
127
+ * and SQL with more than one statement are errors, so nothing after the first
128
+ * statement is silently ignored. SQLite reads SQL only up to a NUL character,
129
+ * and UTF-8 cannot encode a lone surrogate, which encoding would replace with
130
+ * U+FFFD, so SQL that contains either fails with
131
+ * {@link SqliteInvalidSqlTextError} before SQLite sees it, in
132
+ * {@link SqliteDatabase.exec} too.
133
+ * - A second statement is found by scanning the rest of the SQL as SQLite
134
+ * 3.53.4's tokenizer reads it, not by preparing it, because SQLite applies
135
+ * many pragmas, such as `foreign_keys`, when it prepares them, so SQL that
136
+ * fails with {@link SqliteMultipleStatementsError} would still change the
137
+ * connection. Only semicolons, whitespace, UTF-8 byte order marks and
138
+ * comments may follow the first statement. A pragma in the first statement
139
+ * still takes effect when the SQL then fails, as SQLite documents for prepare
140
+ * (https://sqlite.org/pragma.html).
141
+ * - {@link SqliteDatabase.exec} prepares each statement where the previous one
142
+ * ended and steps it to completion, so {@link SqliteError.sqlOffset} counts
143
+ * from the start of the whole SQL.
144
+ * - Parameters are positional. Their number is read once at prepare and a run
145
+ * with a different number fails, so no binding leaks from a previous run.
146
+ * {@link SqliteDatabase.exec} binds none, so a statement with a parameter
147
+ * fails there without running, rather than run with NULL.
148
+ * - Binding: null binds NULL; a 32-bit integer binds with `sqlite3_bind_int`;
149
+ * another safe integer with `sqlite3_bind_int64`, so it stays INTEGER; any
150
+ * other number with `sqlite3_bind_double`. Text is encoded with `encodeInto`
151
+ * and bytes are copied into the scratch memory, both bound with
152
+ * SQLITE_TRANSIENT and an explicit byte length, so embedded NUL characters
153
+ * survive and an empty value stays non-NULL. A value that can take more than
154
+ * 64 KiB, counting 3 bytes per UTF-16 code unit of text, is allocated exactly
155
+ * and handed over with SQLITE_WASM_DEALLOC instead, so the scratch memory
156
+ * does not keep the size of the largest value, and SQLite does not copy it
157
+ * twice. Such text is encoded into a new array first, and when JavaScript
158
+ * cannot allocate it, the run fails with SQLITE_NOMEM and the operation
159
+ * `bind`, while the instance keeps working, because the encoding runs no
160
+ * wasm. {@link SqliteDatabase.exec} copies its SQL the same way and fails with
161
+ * the operation `exec`. SQLite keeps a bound value until its parameter is
162
+ * bound again, so a run that handed a value over clears the bindings before
163
+ * it returns, also when it fails, and a prepared statement keeps no large
164
+ * value between runs, while smaller ones keep the buffers SQLite reuses for
165
+ * them. A {@link SqliteValue} string may contain a lone surrogate, which UTF-8
166
+ * cannot encode, so text binds it as U+FFFD, as better-sqlite3 does.
167
+ * - Reading: `sqlite3_column_type` first. INTEGER and FLOAT read with
168
+ * `sqlite3_column_double`, so no BigInt is created; integers beyond 2^53 lose
169
+ * precision, as in better-sqlite3. TEXT reads the pointer, then the byte
170
+ * length, and decodes keeping a leading byte order mark. A BLOB is one copy
171
+ * into its own `ArrayBuffer`, so it can be transferred. With the type read
172
+ * first, a NULL pointer is never SQL NULL: for TEXT it is SQLITE_NOMEM, and
173
+ * for a BLOB it is an empty value unless `sqlite3_errcode`, read right after,
174
+ * returns SQLITE_NOMEM. A NULL column name is SQLITE_NOMEM too, and so is a
175
+ * TEXT or BLOB that JavaScript cannot allocate a copy of, whatever error the
176
+ * engine throws then, because the copy runs no wasm, so the instance keeps
177
+ * working. Each fails the run with the operation `step`. Rows are
178
+ * null-prototype objects, so a column named `__proto__` is an ordinary
179
+ * property, and of columns with the same name, the last one wins.
180
+ * - Column names are read after the first row and cached with the statement's
181
+ * SQLITE_STMTSTATUS_REPREPARE counter. A schema change re-prepares the
182
+ * statement, and `SELECT *` then returns other columns, so the names are read
183
+ * again when the counter changed.
184
+ * - {@link SqliteStatement.run} always resets the statement, so it is reusable
185
+ * after an error, and ignores the result: SQLITE_OK after SQLITE_DONE, and a
186
+ * failed step's error again after it. A deferred constraint or commit failure
187
+ * comes from the last step, so it is not lost. A run whose read fails is cut
188
+ * short, and its reset completes the statement, so a write with RETURNING is
189
+ * committed although the run fails.
190
+ * - `changes` compares `sqlite3_total_changes64` before and after the run: 0 when
191
+ * it did not move, `sqlite3_changes64` otherwise, as in better-sqlite3, and
192
+ * unlike it also 0 when `sqlite3_stmt_readonly` is true, as for a SELECT
193
+ * whose function writes. SQLite keeps one count per connection, of the last
194
+ * INSERT, UPDATE or DELETE that finished, so reading it alone would report a
195
+ * stale count after a SELECT or DDL. {@link SqliteRunResult.changes} says
196
+ * which writes the count can come from.
197
+ * - A callback may run queries on the database, but SQLite forbids resetting or
198
+ * finalizing a statement that is stepping and closing a connection while it
199
+ * runs. So running or disposing a statement during its own run, and disposing
200
+ * the database while one of its operations runs, throw before wasm is
201
+ * entered, and the instance keeps working. A callback installed with
202
+ * `installWasmFunctions` then fails as a defect.
203
+ *
204
+ * ## Exporting and closing
205
+ *
206
+ * - {@link SqliteDatabase.export} calls `sqlite3_serialize(db, NULL, &size, 0)`
207
+ * for the main schema, copies the pages into their own `ArrayBuffer` and
208
+ * frees SQLite's copy. The size has its own memory, because SQLite writes it
209
+ * before it finalizes its query, whose trace callback can run a query on the
210
+ * same database. `sqlite3_serialize` does not report a failed allocation to
211
+ * the connection, so the connection's error is cleared with
212
+ * `sqlite3_set_errmsg` first, and NULL then fails with the error the
213
+ * connection reports, or else SQLITE_NOMEM, as when JavaScript cannot
214
+ * allocate the copy of the pages, which still frees SQLite's copy. SQLite
215
+ * gives an empty database its first page before copying, so NULL with a size
216
+ * of 0 and no error is only a database that has no pages and cannot get one,
217
+ * such as a read-only empty one, which exports as zero bytes. Pages come
218
+ * through the pager, so an encrypted database exports as plaintext. SQLite
219
+ * zero-fills a page that fails to read and reports success, so the export of
220
+ * a File or EncryptedFile database fails when its VFS recorded a failure
221
+ * during it, caused by the VFS's record: with SQLITE_CORRUPT for a page that
222
+ * failed to authenticate, a {@link SqlitePageAuthenticationError}, and with
223
+ * SQLITE_IOERR otherwise.
224
+ * - Disposing a database finalizes its statements that are still open and then
225
+ * calls `sqlite3_close_v2`, so no deferred close keeps the VFS's files open.
226
+ *
227
+ * @module
228
+ */
229
+
230
+ import {
231
+ assert,
232
+ brand,
233
+ disposable,
234
+ err,
235
+ exhaustiveCheck,
236
+ ok,
237
+ safelyStringifyUnknownValue,
238
+ String,
239
+ trySync,
240
+ type EncryptionKey,
241
+ type NonNegativeInt,
242
+ type Result,
243
+ type SqliteRow,
244
+ type SqliteValue,
245
+ type Task,
246
+ type Typed,
247
+ type TypeError,
248
+ } from "@evolu/common";
249
+ import {
250
+ sqlite3_bind_blob,
251
+ sqlite3_bind_double,
252
+ sqlite3_bind_int,
253
+ sqlite3_bind_int64,
254
+ sqlite3_bind_null,
255
+ sqlite3_bind_parameter_count,
256
+ sqlite3_bind_text,
257
+ sqlite3_changes64,
258
+ sqlite3_clear_bindings,
259
+ sqlite3_close_v2,
260
+ sqlite3_column_blob,
261
+ sqlite3_column_bytes,
262
+ sqlite3_column_count,
263
+ sqlite3_column_double,
264
+ sqlite3_column_name,
265
+ sqlite3_column_text,
266
+ sqlite3_column_type,
267
+ sqlite3_errcode,
268
+ sqlite3_errmsg,
269
+ sqlite3_error_offset,
270
+ sqlite3_errstr,
271
+ sqlite3_extended_errcode,
272
+ sqlite3_file_control,
273
+ sqlite3_finalize,
274
+ sqlite3_free,
275
+ sqlite3_get_autocommit,
276
+ sqlite3_open_v2,
277
+ sqlite3_prepare_v3,
278
+ sqlite3_reset,
279
+ sqlite3_serialize,
280
+ sqlite3_set_errmsg,
281
+ sqlite3_step,
282
+ sqlite3_stmt_readonly,
283
+ sqlite3_stmt_status,
284
+ sqlite3_total_changes64,
285
+ } from "./CApi.ts";
286
+ import {
287
+ SQLITE_BLOB,
288
+ SQLITE_CORRUPT,
289
+ SQLITE_DONE,
290
+ SQLITE_ERROR,
291
+ SQLITE_FCNTL_RESERVE_BYTES,
292
+ SQLITE_FLOAT,
293
+ SQLITE_INTEGER,
294
+ SQLITE_IOERR,
295
+ SQLITE_NOMEM,
296
+ SQLITE_NULL,
297
+ SQLITE_OK,
298
+ SQLITE_OPEN_CREATE,
299
+ SQLITE_OPEN_EXRESCODE,
300
+ SQLITE_OPEN_READWRITE,
301
+ SQLITE_PREPARE_PERSISTENT,
302
+ SQLITE_ROW,
303
+ SQLITE_STMTSTATUS_REPREPARE,
304
+ SQLITE_TEXT,
305
+ SQLITE_TRANSIENT,
306
+ SQLITE_WASM_DEALLOC,
307
+ type SqlitePrimaryResultCode,
308
+ type SqliteResultCode,
309
+ } from "./Constants.ts";
310
+ import {
311
+ allocCString,
312
+ allocWasm,
313
+ copyWasmBytes,
314
+ createSqliteScratch,
315
+ isInt32,
316
+ readCString,
317
+ readUtf8,
318
+ writeUtf8,
319
+ writeWasmBytes,
320
+ type SqliteScratch,
321
+ } from "./Memory.ts";
322
+ import type {
323
+ CStringPtr,
324
+ SqliteDbPtr,
325
+ SqliteStmtPtr,
326
+ WasmPtr,
327
+ } from "./Pointer.ts";
328
+ import {
329
+ sahPoolMaxDatabasePathSize,
330
+ type SahPool,
331
+ type SahPoolEncryptionUnsupportedError,
332
+ type SahPoolFullError,
333
+ type SahPoolInvalidPathError,
334
+ } from "./SahPool.ts";
335
+ import type { SqliteWasm, SqliteWasmDep } from "./Wasm.ts";
336
+
337
+ /**
338
+ * An open SQLite database. Disposing it while one of its operations runs, as
339
+ * from a callback, throws.
340
+ */
341
+ export interface SqliteDatabase extends Disposable {
342
+ /** Prepares one statement for repeated runs. */
343
+ readonly prepare: (
344
+ sql: string,
345
+ ) => Result<SqliteStatement, SqlitePrepareError>;
346
+
347
+ /**
348
+ * Prepares one statement, runs it once as {@link SqliteStatement.run} does and
349
+ * finalizes it.
350
+ *
351
+ * Without SQLITE_PREPARE_PERSISTENT, SQLite prepares it from its lookaside
352
+ * memory, so a statement that runs once is faster than one from
353
+ * {@link SqliteDatabase.prepare}: about 20% for a typical query in Node.js
354
+ * 24.
355
+ */
356
+ readonly run: (
357
+ sql: string,
358
+ parameters: ReadonlyArray<SqliteValue>,
359
+ ) => Result<SqliteRunResult, SqlitePrepareError | SqliteRunError>;
360
+
361
+ /**
362
+ * Runs SQL that can hold several statements, without parameters, and discards
363
+ * any rows. Meant for schema changes and pragmas.
364
+ *
365
+ * It runs the statements in order and stops at the first that fails. A
366
+ * statement with a parameter fails with {@link SqliteParameterCountError}
367
+ * without running, so no parameter silently binds NULL.
368
+ */
369
+ readonly exec: (sql: string) => Result<void, SqliteExecError>;
370
+
371
+ /**
372
+ * Returns the whole database as bytes backed by their own `ArrayBuffer`, so
373
+ * they can be transferred to another worker.
374
+ */
375
+ readonly export: () => Result<Uint8Array<ArrayBuffer>, SqliteError>;
376
+
377
+ /**
378
+ * Whether no transaction is open.
379
+ *
380
+ * After errors such as SQLITE_FULL, SQLITE_IOERR or SQLITE_NOMEM, SQLite may
381
+ * roll back the transaction itself, and a ROLLBACK then fails. A transaction
382
+ * helper checks this before rolling back.
383
+ */
384
+ readonly isAutocommit: () => boolean;
385
+ }
386
+
387
+ /**
388
+ * A prepared statement. Running or disposing it during its own run, as from a
389
+ * callback, throws.
390
+ */
391
+ export interface SqliteStatement extends Disposable {
392
+ /** The number of SQL parameters, which each run must supply exactly. */
393
+ readonly parameterCount: number;
394
+
395
+ /**
396
+ * Binds the parameters, steps to completion, collects the rows and resets the
397
+ * statement, also when a step fails.
398
+ */
399
+ readonly run: (
400
+ parameters: ReadonlyArray<SqliteValue>,
401
+ ) => Result<SqliteRunResult, SqliteRunError>;
402
+ }
403
+
404
+ /** The result of {@link SqliteStatement.run}. */
405
+ export interface SqliteRunResult {
406
+ readonly rows: ReadonlyArray<SqliteRow>;
407
+ /**
408
+ * 0 for a read-only statement or a run that changed no rows, and otherwise
409
+ * SQLite's `sqlite3_changes64`: the rows the connection's last INSERT, UPDATE
410
+ * or DELETE that finished changed, without those of triggers, foreign key
411
+ * actions or REPLACE. For an INSERT, UPDATE or DELETE, that is its own count,
412
+ * unless a write on the same connection finished after it within the run, as
413
+ * one its profile trace callback runs. For another statement, it is the count
414
+ * of the last write that finished during the run, as one by a function it
415
+ * calls or by its trace callback.
416
+ */
417
+ readonly changes: number;
418
+ }
419
+
420
+ /** An error SQLite reported. */
421
+ export interface SqliteError extends Typed<"SqliteError"> {
422
+ readonly operation: SqliteOperation;
423
+ /**
424
+ * The extended result code, whose primary result code
425
+ * {@link sqliteResultCodeToPrimary} returns.
426
+ */
427
+ readonly extendedCode: SqliteResultCode;
428
+ /**
429
+ * From `sqlite3_errmsg`, or from `sqlite3_errstr` of the code when SQLite did
430
+ * not report the failure to the connection, as for an open without a handle,
431
+ * an allocation that failed, or an export whose VFS failed to read a page.
432
+ */
433
+ readonly message: string;
434
+ /**
435
+ * The byte offset in the UTF-8 SQL where SQLite detected the error, from
436
+ * `sqlite3_error_offset` for a parser error (SQLITE_ERROR from prepare, exec
437
+ * or step); null for any other error or when the error has no position.
438
+ */
439
+ readonly sqlOffset: number | null;
440
+ /**
441
+ * The first VFS failure since the top-level operation started, such as the
442
+ * `QuotaExceededError` behind SQLITE_FULL; null when the VFS reported none.
443
+ */
444
+ readonly cause: SqliteVfsFailure | null;
445
+ }
446
+
447
+ /** The operation a {@link SqliteError} comes from. */
448
+ export type SqliteOperation =
449
+ "open" | "prepare" | "bind" | "step" | "exec" | "export";
450
+
451
+ /**
452
+ * Returns the primary result code of a result code, its low 8 bits, such as
453
+ * SQLITE_CONSTRAINT for SQLITE_CONSTRAINT_NOTNULL. A primary result code
454
+ * returns itself.
455
+ */
456
+ export const sqliteResultCodeToPrimary = (
457
+ code: SqliteResultCode,
458
+ ): SqlitePrimaryResultCode => (code & 0xff) as SqlitePrimaryResultCode;
459
+
460
+ /** A failure a VFS method recorded before returning an error code. */
461
+ export interface SqliteVfsFailure {
462
+ readonly method: SqliteVfsMethod;
463
+ /**
464
+ * The path of the file, or null for a method without one or a name that maps
465
+ * to no path.
466
+ */
467
+ readonly path: string | null;
468
+ /**
469
+ * What the browser threw, such as a `DOMException`, or what the VFS found
470
+ * wrong, such as a {@link SqliteShortWriteError}, a
471
+ * {@link SqlitePageAuthenticationError}, a {@link SahPoolFullError}, a
472
+ * {@link SahPoolInvalidPathError}, or a
473
+ * {@link SahPoolEncryptionUnsupportedError}.
474
+ */
475
+ readonly error: unknown;
476
+ }
477
+
478
+ /** A VFS or I/O method that can record a {@link SqliteVfsFailure}. */
479
+ export type SqliteVfsMethod =
480
+ | "xOpen"
481
+ | "xDelete"
482
+ | "xAccess"
483
+ | "xClose"
484
+ | "xRead"
485
+ | "xWrite"
486
+ | "xTruncate"
487
+ | "xSync"
488
+ | "xFileSize"
489
+ | "xLock"
490
+ | "xUnlock"
491
+ | "xCheckReservedLock"
492
+ | "xFileControl";
493
+
494
+ /**
495
+ * A write that stored fewer or more bytes than requested.
496
+ *
497
+ * Firefox reports a full disk with a short count, and Chromium's off-the-record
498
+ * storage has reported impossible counts; either way the data is not all there,
499
+ * so the write fails with SQLITE_FULL.
500
+ */
501
+ export interface SqliteShortWriteError extends Typed<"SqliteShortWrite"> {
502
+ readonly requested: number;
503
+ readonly written: number;
504
+ }
505
+
506
+ /** Why {@link SqliteDatabase.prepare} failed. */
507
+ export type SqlitePrepareError =
508
+ | SqliteError
509
+ | SqliteEmptySqlError
510
+ | SqliteMultipleStatementsError
511
+ | SqliteInvalidSqlTextError;
512
+
513
+ /** The SQL contains no statement, only whitespace or comments. */
514
+ export interface SqliteEmptySqlError extends Typed<"SqliteEmptySqlError"> {}
515
+
516
+ /** The SQL contains more than one statement. */
517
+ export interface SqliteMultipleStatementsError extends Typed<"SqliteMultipleStatementsError"> {}
518
+
519
+ /**
520
+ * The SQL is text SQLite would read as other SQL, so it fails before SQLite
521
+ * sees it: it contains a NUL character, which SQLite reads SQL only up to, so
522
+ * it would silently ignore the rest, or a lone surrogate, which UTF-8 cannot
523
+ * encode, so it would run with U+FFFD in its place.
524
+ */
525
+ export interface SqliteInvalidSqlTextError extends Typed<"SqliteInvalidSqlText"> {}
526
+
527
+ /** Why {@link SqliteDatabase.exec} failed. */
528
+ export type SqliteExecError =
529
+ SqliteError | SqliteInvalidSqlTextError | SqliteParameterCountError;
530
+
531
+ /** Why {@link SqliteStatement.run} failed. */
532
+ export type SqliteRunError = SqliteError | SqliteParameterCountError;
533
+
534
+ /**
535
+ * A run supplied a different number of parameters than the SQL has, or
536
+ * {@link SqliteDatabase.exec}, which supplies none, met a statement with
537
+ * parameters.
538
+ */
539
+ export interface SqliteParameterCountError extends Typed<"SqliteParameterCountError"> {
540
+ readonly expected: number;
541
+ readonly actual: number;
542
+ }
543
+
544
+ /** How to open a database. */
545
+ export type SqliteDatabaseOptions =
546
+ | SqliteMemoryDatabaseOptions
547
+ | SqliteFileDatabaseOptions
548
+ | SqliteEncryptedFileDatabaseOptions;
549
+
550
+ /**
551
+ * An in-memory database on the default VFS, which never touches OPFS. Temporary
552
+ * tables and indexes stay in memory in every mode.
553
+ */
554
+ export interface SqliteMemoryDatabaseOptions extends Typed<"Memory"> {}
555
+
556
+ /** An unencrypted database file on a {@link SqliteVfs}, such as a {@link SahPool}. */
557
+ export interface SqliteFileDatabaseOptions extends Typed<"File"> {
558
+ readonly vfs: SqliteVfs;
559
+ /**
560
+ * The canonical path of the file on the VFS, such as `/evolu1.db`, by which
561
+ * the VFS also lists and deletes it.
562
+ */
563
+ readonly path: SqliteVfsPath;
564
+ }
565
+
566
+ /**
567
+ * A database file on a {@link SqliteEncryptingVfs}, which encrypts it, as a
568
+ * {@link SahPool} does in the format of SQLite3 Multiple Ciphers' `sqlcipher`
569
+ * scheme.
570
+ *
571
+ * Only the database file and its journal are encrypted. `VACUUM INTO` and
572
+ * `ATTACH` open their file as a database of its own, which is encrypted only
573
+ * when a key is registered for its path with
574
+ * {@link SqliteEncryptingVfs.registerKey} while they open it. Otherwise they
575
+ * write it unencrypted, because SQLite ignores the `KEY` clause of `ATTACH` and
576
+ * `PRAGMA key`, `rekey` and `cipher`, where 2.2.4 encrypted both with the
577
+ * database's key, or with the `KEY` of `ATTACH`. A file that `ATTACH` creates
578
+ * cannot be encrypted, because SQLite reserves no bytes in it, so with a key
579
+ * registered its first write fails with SQLITE_IOERR_WRITE. `VACUUM INTO` keeps
580
+ * the reserved bytes, so it can create an encrypted copy, which `ATTACH` then
581
+ * opens with its key registered.
582
+ */
583
+ export interface SqliteEncryptedFileDatabaseOptions extends Typed<"EncryptedFile"> {
584
+ readonly vfs: SqliteEncryptingVfs;
585
+ /**
586
+ * The path of the file on the VFS, as {@link SqliteFileDatabaseOptions.path}
587
+ * describes.
588
+ */
589
+ readonly path: SqliteVfsPath;
590
+ /**
591
+ * The raw key, which {@link SqliteEncryptingVfs.registerKey} gets while the
592
+ * file opens.
593
+ */
594
+ readonly key: EncryptionKey;
595
+ }
596
+
597
+ /** Error returned when a string is not a valid {@link SqliteVfsPath}. */
598
+ export interface SqliteVfsPathError extends TypeError<"SqliteVfsPath"> {
599
+ readonly value: string;
600
+ }
601
+
602
+ /**
603
+ * The canonical path of a database file on a {@link SqliteVfs}, such as
604
+ * `/evolu1.db`, one spelling per file, as {@link SqliteVfs.getPaths} lists it.
605
+ *
606
+ * A path is canonical when it is its own `new URL(path,
607
+ * "file://localhost/").pathname`, the path a {@link SahPool} maps a name to, so
608
+ * the VFS opens, lists and deletes the file by the path the database opened it
609
+ * with, and an EncryptedFile database registers its key for that file. A
610
+ * canonical path starts with `/` and is printable ASCII without `?` or `#`, so
611
+ * SQLite passes it to the VFS as it is, and the paths SQLite derives from it by
612
+ * appending a suffix, as for the journal, are canonical too. It rejects:
613
+ *
614
+ * - Another spelling of a path, such as `evolu1.db`, `/x/../evolu1.db`,
615
+ * `FILE:evolu1.db` or `/a b.db` for `/a%20b.db`, and a name with a scheme,
616
+ * such as `db:evolu1.db`, which a pool would map to the relative path
617
+ * `evolu1.db`.
618
+ * - A path SQLite reads itself: an empty path and `:memory:`, which open an
619
+ * in-memory database, `:localStorage:` and `:sessionStorage:`, which SQLite
620
+ * opens on its kvvfs, and a `file:` URI, whose query and fragment SQLite
621
+ * strips and whose escapes it decodes, so the VFS would get another name, and
622
+ * an EncryptedFile database would be written without its key.
623
+ * - A path longer than {@link sahPoolMaxDatabasePathSize}, 498 bytes, because a
624
+ * pool slot's header holds a path of up to 510 bytes, and SQLite names a
625
+ * super-journal by appending 12 bytes to the database's path.
626
+ */
627
+ export const SqliteVfsPath = /*#__PURE__*/ brand(
628
+ "SqliteVfsPath",
629
+ String,
630
+ (value) => {
631
+ const canonical = trySync(
632
+ () => new URL(value, "file://localhost/").pathname,
633
+ () => null,
634
+ );
635
+ // A canonical path is ASCII, because a URL path percent-encodes every other
636
+ // character, so its length is its length in UTF-8.
637
+ return canonical.ok &&
638
+ canonical.value === value &&
639
+ value.length <= sahPoolMaxDatabasePathSize
640
+ ? ok()
641
+ : err<SqliteVfsPathError>({ type: "SqliteVfsPath", value });
642
+ },
643
+ (error) =>
644
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a valid SqliteVfsPath.`,
645
+ );
646
+ export type SqliteVfsPath = typeof SqliteVfsPath.Output;
647
+
648
+ /**
649
+ * What a database needs from the VFS it opens a file on, and what a driver
650
+ * needs to tell which databases the VFS has and to delete them. A
651
+ * {@link SahPool} is one.
652
+ */
653
+ export interface SqliteVfs {
654
+ /** The name of the VFS, which a database opens its path with. */
655
+ readonly vfsName: string;
656
+
657
+ /**
658
+ * Returns the first failure a method of the VFS recorded since the last
659
+ * {@link SqliteVfs.clearFailure}, which a {@link SqliteError} carries as its
660
+ * cause.
661
+ */
662
+ readonly getFailure: () => SqliteVfsFailure | null;
663
+
664
+ /**
665
+ * Forgets the recorded failure; a database calls it before each operation
666
+ * that does not run inside another operation on the VFS.
667
+ */
668
+ readonly clearFailure: () => void;
669
+
670
+ /**
671
+ * Returns the canonical paths of the files the VFS has, databases and their
672
+ * journals, such as `/evolu1.db`, so a database opened at a
673
+ * {@link SqliteVfsPath} is listed by that path. A VFS may derive a file's
674
+ * canonical path from the name SQLite opened it with, as a {@link SahPool}
675
+ * lists a file SQLite's opfs-sahpool opened as `evolu1.db` as `/evolu1.db`.
676
+ */
677
+ readonly getPaths: () => ReadonlyArray<string>;
678
+
679
+ /**
680
+ * Deletes the file with a canonical path, as {@link SqliteVfs.getPaths}
681
+ * returns it, and the files SQLite keeps beside it, such as its `-journal`.
682
+ * The file goes first, so a failure never leaves a database without the
683
+ * journal that would roll it back. Returns false when the VFS has no file
684
+ * with the path, as for a path that is not canonical, and throws when a
685
+ * connection has one of the files open.
686
+ */
687
+ readonly unlink: (path: string) => Result<boolean, SqliteVfsIoError>;
688
+ }
689
+
690
+ /**
691
+ * A {@link SqliteVfs} that encrypts a database file whose path has a key
692
+ * registered while the file opens, as an EncryptedFile database needs.
693
+ *
694
+ * The database asks SQLite to leave the last 80 bytes of each page of a new
695
+ * database unused, so the VFS can store data of its own there, as a
696
+ * {@link SahPool} stores the page's IV and HMAC.
697
+ *
698
+ * A read of a page that fails to authenticate fails with SQLITE_NOTADB for page
699
+ * 1 and SQLITE_CORRUPT for any other page, and records a
700
+ * {@link SqlitePageAuthenticationError}, by which the database tells it from
701
+ * other failures, so an export that reads the page fails with SQLITE_CORRUPT
702
+ * rather than SQLITE_IOERR.
703
+ */
704
+ export interface SqliteEncryptingVfs extends SqliteVfs {
705
+ /**
706
+ * Registers a raw key for a path until the registration is disposed, as an
707
+ * EncryptedFile database does while it opens. The path is the one spelling by
708
+ * which the VFS opens the file, so it is registered as it is. When the VFS
709
+ * opens the path as a main database, it copies the key and encrypts that file
710
+ * and its journal with it until the file closes, also for the file of an
711
+ * `ATTACH` or `VACUUM INTO` that opens it meanwhile. The caller's key is
712
+ * never modified.
713
+ */
714
+ readonly registerKey: (path: SqliteVfsPath, key: EncryptionKey) => Disposable;
715
+ }
716
+
717
+ /**
718
+ * The storage of a VFS failed outside SQLite, as when {@link SqliteVfs.unlink}
719
+ * deletes a file.
720
+ */
721
+ export interface SqliteVfsIoError extends Typed<"SqliteVfsIoError"> {
722
+ /**
723
+ * What the storage threw, such as a `DOMException`, or a
724
+ * {@link SqliteShortWriteError} for a write that stored a different number of
725
+ * bytes.
726
+ */
727
+ readonly cause: unknown;
728
+ }
729
+
730
+ /**
731
+ * A page of an encrypted file that failed to authenticate, because the key is
732
+ * wrong or the page was altered or cut short, as a {@link SqliteEncryptingVfs}
733
+ * records it.
734
+ */
735
+ export interface SqlitePageAuthenticationError extends Typed<"SqlitePageAuthenticationError"> {
736
+ /**
737
+ * The number of the database page, counting from 1, also for a page in a
738
+ * journal.
739
+ */
740
+ readonly pageNumber: number;
741
+ }
742
+
743
+ /**
744
+ * Opens a database.
745
+ *
746
+ * With a wrong key or a file that is not a database, it fails with
747
+ * SQLITE_NOTADB, or with SQLITE_CORRUPT for a wrong key and a hot journal whose
748
+ * first record is not page 1. On a {@link SahPool}, an encrypted database opened
749
+ * without its key while it has a hot journal that records pages fails with
750
+ * SQLITE_CANTOPEN, and the journal stays for the key. A leftover journal that
751
+ * is not hot, such as one `journal_mode = PERSIST` or TRUNCATE keeps, gives
752
+ * SQLITE_NOTADB, as no journal does. For encrypted databases that 2.2.4 may
753
+ * have keyed, use {@link createEncryptedSqliteDatabase}.
754
+ */
755
+ export const createSqliteDatabase =
756
+ (deps: SqliteWasmDep) =>
757
+ (options: SqliteDatabaseOptions): Result<SqliteDatabase, SqliteError> => {
758
+ const { call } = deps.sqliteWasm;
759
+ const vfs = options.type === "Memory" ? null : options.vfs;
760
+
761
+ // The operations of this database that are running, as one that a
762
+ // callback of another runs, so the database is not disposed under them.
763
+ let runningOperations = 0;
764
+
765
+ // The VFS takes the key when SQLite opens the file. Registered before wasm
766
+ // is entered, so a VFS that refuses it, as a pool with a key already
767
+ // registered for the path does, throws without breaking the instance.
768
+ using _registration =
769
+ options.type === "EncryptedFile"
770
+ ? options.vfs.registerKey(options.path, options.key)
771
+ : null;
772
+
773
+ // Clears the VFS's failure record and then calls an operation through
774
+ // call, so the cause of a SqliteError is a failure of this operation. A
775
+ // VFS that throws, as a disposed pool does, then throws before wasm is
776
+ // entered, without breaking the instance. An operation inside another on
777
+ // the same VFS, as a query a callback runs, keeps the record, which the
778
+ // outer operation still reads.
779
+ const callOperation = <T>(fn: () => T): T => {
780
+ runningOperations++;
781
+ try {
782
+ if (vfs == null) return call(fn);
783
+ const running = runningOperationsByVfs.get(vfs) ?? 0;
784
+ if (running === 0) vfs.clearFailure();
785
+ runningOperationsByVfs.set(vfs, running + 1);
786
+ try {
787
+ return call(fn);
788
+ } finally {
789
+ runningOperationsByVfs.set(vfs, running);
790
+ }
791
+ } finally {
792
+ runningOperations--;
793
+ }
794
+ };
795
+ return callOperation(() => {
796
+ const bindBlob = sqlite3_bind_blob(deps);
797
+ const bindDouble = sqlite3_bind_double(deps);
798
+ const bindInt = sqlite3_bind_int(deps);
799
+ const bindInt64 = sqlite3_bind_int64(deps);
800
+ const bindNull = sqlite3_bind_null(deps);
801
+ const bindParameterCount = sqlite3_bind_parameter_count(deps);
802
+ const bindText = sqlite3_bind_text(deps);
803
+ const changes64 = sqlite3_changes64(deps);
804
+ const clearBindings = sqlite3_clear_bindings(deps);
805
+ const closeV2 = sqlite3_close_v2(deps);
806
+ const columnBlob = sqlite3_column_blob(deps);
807
+ const columnBytes = sqlite3_column_bytes(deps);
808
+ const columnCount = sqlite3_column_count(deps);
809
+ const columnDouble = sqlite3_column_double(deps);
810
+ const columnName = sqlite3_column_name(deps);
811
+ const columnText = sqlite3_column_text(deps);
812
+ const columnType = sqlite3_column_type(deps);
813
+ const errcode = sqlite3_errcode(deps);
814
+ const errmsg = sqlite3_errmsg(deps);
815
+ const errorOffset = sqlite3_error_offset(deps);
816
+ const errstr = sqlite3_errstr(deps);
817
+ const extendedErrcode = sqlite3_extended_errcode(deps);
818
+ const finalize = sqlite3_finalize(deps);
819
+ const free = sqlite3_free(deps);
820
+ const getAutocommit = sqlite3_get_autocommit(deps);
821
+ const prepareV3 = sqlite3_prepare_v3(deps);
822
+ const reset = sqlite3_reset(deps);
823
+ const serialize = sqlite3_serialize(deps);
824
+ const setErrmsg = sqlite3_set_errmsg(deps);
825
+ const step = sqlite3_step(deps);
826
+ const stmtReadonly = sqlite3_stmt_readonly(deps);
827
+ const stmtStatus = sqlite3_stmt_status(deps);
828
+ const totalChanges64 = sqlite3_total_changes64(deps);
829
+ const alloc = allocWasm(deps);
830
+ const allocString = allocCString(deps);
831
+ const copyBytes = copyWasmBytes(deps);
832
+ const readString = readCString(deps);
833
+ const readText = readUtf8(deps);
834
+ const writeBytes = writeWasmBytes(deps);
835
+ const writeText = writeUtf8(deps);
836
+
837
+ const statements = new Set<SqliteStatement>();
838
+
839
+ // Reads the connection's error right after the failing call, before any
840
+ // other call replaces it.
841
+ const toSqliteError = (
842
+ db: SqliteDbPtr,
843
+ operation: SqliteOperation,
844
+ ): SqliteError => {
845
+ const extendedCode = extendedErrcode(db);
846
+ // SQLite sets the offset only for parser errors and keeps it until a
847
+ // prepare succeeds or a statement resets or finalizes, so any other code
848
+ // or operation would read the offset of an earlier failed prepare.
849
+ const offset =
850
+ sqliteResultCodeToPrimary(extendedCode) === SQLITE_ERROR &&
851
+ (operation === "prepare" ||
852
+ operation === "exec" ||
853
+ operation === "step")
854
+ ? errorOffset(db)
855
+ : -1;
856
+ return {
857
+ type: "SqliteError",
858
+ operation,
859
+ extendedCode,
860
+ // SQLite falls back to sqlite3_errstr, so it is never NULL.
861
+ message: readString(errmsg(db) as CStringPtr),
862
+ sqlOffset: offset < 0 ? null : offset,
863
+ cause: vfs?.getFailure() ?? null,
864
+ };
865
+ };
866
+
867
+ // For a failure without a connection that could report it.
868
+ const toCodeError = (
869
+ operation: SqliteOperation,
870
+ extendedCode: SqliteResultCode,
871
+ ): SqliteError => ({
872
+ type: "SqliteError",
873
+ operation,
874
+ extendedCode,
875
+ // SQLite describes an unknown code as "unknown error", never NULL.
876
+ message: readString(errstr(extendedCode) as CStringPtr),
877
+ sqlOffset: null,
878
+ cause: null,
879
+ });
880
+
881
+ const readPtr = (ptr: WasmPtr): number =>
882
+ deps.sqliteWasm.getHeapDataView().getUint32(ptr, true);
883
+
884
+ using disposer = new DisposableStack();
885
+ const createdScratch = createSqliteScratch(deps);
886
+ if (!createdScratch.ok) return err(toCodeError("open", SQLITE_NOMEM));
887
+ const scratch = createdScratch.value;
888
+ disposer.defer(() => {
889
+ call(() => {
890
+ scratch[Symbol.dispose]();
891
+ });
892
+ });
893
+ const pzTail = (scratch.out + 4) as WasmPtr;
894
+
895
+ // A File or EncryptedFile database's path is followed by the name of its
896
+ // VFS.
897
+ const filename = allocString(
898
+ options.type === "Memory"
899
+ ? ":memory:"
900
+ : `${options.path}\0${options.vfs.vfsName}`,
901
+ );
902
+ if (!filename.ok) return err(toCodeError("open", SQLITE_NOMEM));
903
+ const zFilename = filename.value;
904
+ const pathEnd = deps.sqliteWasm.getHeapU8().indexOf(0, zFilename);
905
+ const rc = (() => {
906
+ using filenameDisposer = new DisposableStack();
907
+ filenameDisposer.defer(() => {
908
+ free(zFilename);
909
+ });
910
+ return sqlite3_open_v2(deps)(
911
+ zFilename,
912
+ scratch.out,
913
+ SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_EXRESCODE,
914
+ options.type === "Memory" ? 0 : ((pathEnd + 1) as CStringPtr),
915
+ );
916
+ })();
917
+ const db = readPtr(scratch.out) as SqliteDbPtr;
918
+ if (rc !== SQLITE_OK) {
919
+ const error =
920
+ db === 0 ? toCodeError("open", rc) : toSqliteError(db, "open");
921
+ closeV2(db);
922
+ return err(error);
923
+ }
924
+ disposer.defer(() => {
925
+ call(() => closeV2(db));
926
+ });
927
+ // Finalized before the close, so no deferred close keeps files open.
928
+ disposer.defer(() => {
929
+ for (const statement of statements) statement[Symbol.dispose]();
930
+ });
931
+
932
+ const prepareStatement = (
933
+ sql: string,
934
+ prepFlags: number,
935
+ operation: "prepare" | "open" = "prepare",
936
+ ): Result<SqliteStmtPtr, SqlitePrepareError> => {
937
+ if (!isValidSqlText(sql)) return err({ type: "SqliteInvalidSqlText" });
938
+ // SQLite parses the SQL in place, while a callback can run a query on
939
+ // this database, which reuses the scratch memory. Three bytes per
940
+ // UTF-16 code unit hold any UTF-8, and encoding into them is faster
941
+ // than allocCString's exact copy, which encodes into a temporary array
942
+ // first. A run prepares SQL every time, so it is a hot path.
943
+ const capacity = sql.length * 3;
944
+ const allocated = alloc(capacity + 1);
945
+ if (!allocated.ok) return err(toCodeError(operation, SQLITE_NOMEM));
946
+ const zSql = allocated.value;
947
+ try {
948
+ const written = writeText(sql, zSql, capacity);
949
+ deps.sqliteWasm.getHeapU8()[zSql + written] = 0;
950
+ const rc = prepareV3(
951
+ db,
952
+ zSql,
953
+ written + 1,
954
+ prepFlags,
955
+ scratch.out,
956
+ pzTail,
957
+ );
958
+ if (rc !== SQLITE_OK) return err(toSqliteError(db, operation));
959
+ const stmt = readPtr(scratch.out) as SqliteStmtPtr;
960
+ if (stmt === 0) return err({ type: "SqliteEmptySqlError" });
961
+ // The rest is scanned, not prepared, because SQLite applies some
962
+ // pragmas, such as foreign_keys, when it prepares them.
963
+ if (
964
+ !isEmptySql(
965
+ deps.sqliteWasm.getHeapU8(),
966
+ readPtr(pzTail),
967
+ zSql + written,
968
+ )
969
+ ) {
970
+ finalize(stmt);
971
+ return err({ type: "SqliteMultipleStatementsError" });
972
+ }
973
+ return ok(stmt);
974
+ } finally {
975
+ free(zSql);
976
+ }
977
+ };
978
+
979
+ // The steps after the connection opened run in a call of their own, so
980
+ // a trap in them breaks the instance before it unwinds to the deferred
981
+ // close, which then refuses to close the connection on the state the
982
+ // trap left.
983
+ const prepared = call((): Result<void, SqliteError> => {
984
+ if (options.type === "EncryptedFile") {
985
+ // The IV and the HMAC at the end of each page, for a new database. It
986
+ // returns SQLITE_OK for main, a NULL schema name, and an existing
987
+ // database keeps what its header says.
988
+ deps.sqliteWasm
989
+ .getHeapDataView()
990
+ .setInt32(scratch.out, encryptedReservedBytes, true);
991
+ sqlite3_file_control(deps)(
992
+ db,
993
+ 0,
994
+ SQLITE_FCNTL_RESERVE_BYTES,
995
+ scratch.out,
996
+ );
997
+ // As SQLite3 Multiple Ciphers did for every encrypted connection.
998
+ const secureDelete = prepareStatement(
999
+ "PRAGMA secure_delete = ON",
1000
+ 0,
1001
+ "open",
1002
+ );
1003
+ if (!secureDelete.ok) {
1004
+ assert(
1005
+ secureDelete.error.type === "SqliteError",
1006
+ "The pragma is one statement.",
1007
+ );
1008
+ return err(secureDelete.error);
1009
+ }
1010
+ const stepped = step(secureDelete.value);
1011
+ const error =
1012
+ stepped === SQLITE_ROW ? null : toSqliteError(db, "open");
1013
+ finalize(secureDelete.value);
1014
+ if (error) return err(error);
1015
+ }
1016
+
1017
+ // SQLite reads a file only when a statement needs it, so a File or
1018
+ // EncryptedFile database reads its schema now: a hot journal is rolled
1019
+ // back, and a file that is not a database fails the open rather than the
1020
+ // first statement.
1021
+ if (options.type !== "Memory") {
1022
+ const schema = prepareStatement(
1023
+ "SELECT 1 FROM sqlite_schema",
1024
+ 0,
1025
+ "open",
1026
+ );
1027
+ if (!schema.ok) {
1028
+ assert(
1029
+ schema.error.type === "SqliteError",
1030
+ "The schema query is one statement.",
1031
+ );
1032
+ return err(schema.error);
1033
+ }
1034
+ finalize(schema.value);
1035
+ }
1036
+ return ok();
1037
+ });
1038
+ if (!prepared.ok) return err(prepared.error);
1039
+
1040
+ const createRunner = (stmt: SqliteStmtPtr) => {
1041
+ const parameterCount = bindParameterCount(stmt);
1042
+ let names: ReadonlyArray<string> | null = null;
1043
+ let namesReprepares = 0;
1044
+
1045
+ const run = (
1046
+ parameters: ReadonlyArray<SqliteValue>,
1047
+ ): Result<SqliteRunResult, SqliteRunError> => {
1048
+ if (parameters.length !== parameterCount)
1049
+ return err({
1050
+ type: "SqliteParameterCountError",
1051
+ expected: parameterCount,
1052
+ actual: parameters.length,
1053
+ });
1054
+ // SQLite keeps a bound value until its parameter is bound again, so
1055
+ // a run that handed SQLite a value above 64 KiB clears the bindings
1056
+ // before it returns, also when it fails.
1057
+ let clearsBindings = false;
1058
+ let bindError: SqliteError | null = null;
1059
+ for (const [index, value] of parameters.entries()) {
1060
+ const position = index + 1;
1061
+ let bindRc: SqliteResultCode;
1062
+ if (value === null) bindRc = bindNull(stmt, position);
1063
+ else if (typeof value === "number")
1064
+ bindRc = isInt32(value)
1065
+ ? bindInt(stmt, position, value)
1066
+ : Number.isSafeInteger(value)
1067
+ ? bindInt64(stmt, position, BigInt(value))
1068
+ : bindDouble(stmt, position, value);
1069
+ else if (
1070
+ typeof value === "string" &&
1071
+ value.length * 3 <= maxScratchValue
1072
+ ) {
1073
+ const reserved = scratch.reserve(value.length * 3);
1074
+ if (!reserved.ok) {
1075
+ bindError = toCodeError("bind", SQLITE_NOMEM);
1076
+ break;
1077
+ }
1078
+ const length = writeText(value, reserved.value, value.length * 3);
1079
+ bindRc = bindText(
1080
+ stmt,
1081
+ position,
1082
+ reserved.value,
1083
+ length,
1084
+ SQLITE_TRANSIENT,
1085
+ );
1086
+ } else if (typeof value === "string") {
1087
+ // JavaScript can fail to allocate the UTF-8, which runs no wasm,
1088
+ // so the instance keeps working.
1089
+ const bytes = trySync(() => utf8Encoder.encode(value));
1090
+ if (!bytes.ok) {
1091
+ bindError = toCodeError("bind", SQLITE_NOMEM);
1092
+ break;
1093
+ }
1094
+ const allocated = alloc(bytes.value.length);
1095
+ if (!allocated.ok) {
1096
+ bindError = toCodeError("bind", SQLITE_NOMEM);
1097
+ break;
1098
+ }
1099
+ writeBytes(allocated.value, bytes.value);
1100
+ clearsBindings = true;
1101
+ bindRc = bindText(
1102
+ stmt,
1103
+ position,
1104
+ allocated.value,
1105
+ bytes.value.length,
1106
+ SQLITE_WASM_DEALLOC,
1107
+ );
1108
+ } else if (value.length <= maxScratchValue) {
1109
+ const reserved = scratch.reserve(value.length);
1110
+ if (!reserved.ok) {
1111
+ bindError = toCodeError("bind", SQLITE_NOMEM);
1112
+ break;
1113
+ }
1114
+ writeBytes(reserved.value, value);
1115
+ bindRc = bindBlob(
1116
+ stmt,
1117
+ position,
1118
+ reserved.value,
1119
+ value.length,
1120
+ SQLITE_TRANSIENT,
1121
+ );
1122
+ } else {
1123
+ const allocated = alloc(value.length);
1124
+ if (!allocated.ok) {
1125
+ bindError = toCodeError("bind", SQLITE_NOMEM);
1126
+ break;
1127
+ }
1128
+ writeBytes(allocated.value, value);
1129
+ clearsBindings = true;
1130
+ bindRc = bindBlob(
1131
+ stmt,
1132
+ position,
1133
+ allocated.value,
1134
+ value.length,
1135
+ SQLITE_WASM_DEALLOC,
1136
+ );
1137
+ }
1138
+ if (bindRc !== SQLITE_OK) {
1139
+ bindError = toSqliteError(db, "bind");
1140
+ break;
1141
+ }
1142
+ }
1143
+ if (bindError) {
1144
+ if (clearsBindings) clearBindings(stmt);
1145
+ return err(bindError);
1146
+ }
1147
+
1148
+ const totalChanges = totalChanges64(db);
1149
+ const rows: Array<SqliteRow> = [];
1150
+ let rc: SqliteResultCode;
1151
+ // A read that fails because SQLite is out of memory leaves the loop
1152
+ // with SQLITE_ROW.
1153
+ steps: while ((rc = step(stmt)) === SQLITE_ROW) {
1154
+ // A schema change re-prepares the statement, and SELECT * can then
1155
+ // return other columns, so the first row checks the counter.
1156
+ const reprepares =
1157
+ rows.length === 0
1158
+ ? stmtStatus(stmt, SQLITE_STMTSTATUS_REPREPARE, 0)
1159
+ : namesReprepares;
1160
+ if (names == null || reprepares !== namesReprepares) {
1161
+ const columnNames: Array<string> = [];
1162
+ for (let index = 0; index < columnCount(stmt); index++) {
1163
+ const name = columnName(stmt, index);
1164
+ if (name === 0) break steps;
1165
+ columnNames.push(readString(name));
1166
+ }
1167
+ names = columnNames;
1168
+ namesReprepares = reprepares;
1169
+ }
1170
+ const row = Object.create(null) as SqliteRow;
1171
+ for (const [index, name] of names.entries()) {
1172
+ // With the type read first, a NULL pointer is never SQL NULL.
1173
+ const type = columnType(stmt, index);
1174
+ switch (type) {
1175
+ case SQLITE_INTEGER:
1176
+ case SQLITE_FLOAT:
1177
+ row[name] = columnDouble(stmt, index);
1178
+ break;
1179
+ case SQLITE_TEXT: {
1180
+ const ptr = columnText(stmt, index);
1181
+ if (ptr === 0) break steps;
1182
+ const text = tryCopy(readText, ptr, columnBytes(stmt, index));
1183
+ if (text == null) break steps;
1184
+ row[name] = text;
1185
+ break;
1186
+ }
1187
+ case SQLITE_BLOB: {
1188
+ const ptr = columnBlob(stmt, index);
1189
+ // NULL for an empty BLOB too.
1190
+ if (ptr !== 0) {
1191
+ const bytes = tryCopy(
1192
+ copyBytes,
1193
+ ptr,
1194
+ columnBytes(stmt, index),
1195
+ );
1196
+ if (bytes == null) break steps;
1197
+ row[name] = bytes;
1198
+ } else if (errcode(db) === SQLITE_NOMEM) break steps;
1199
+ else row[name] = new Uint8Array();
1200
+ break;
1201
+ }
1202
+ case SQLITE_NULL:
1203
+ row[name] = null;
1204
+ break;
1205
+ default:
1206
+ exhaustiveCheck(type);
1207
+ }
1208
+ }
1209
+ rows.push(row);
1210
+ }
1211
+ const error =
1212
+ rc === SQLITE_DONE
1213
+ ? null
1214
+ : rc === SQLITE_ROW
1215
+ ? toCodeError("step", SQLITE_NOMEM)
1216
+ : toSqliteError(db, "step");
1217
+ // After SQLITE_DONE it returns SQLITE_OK, and after a failure it
1218
+ // repeats it, so its result is ignored.
1219
+ reset(stmt);
1220
+ if (clearsBindings) clearBindings(stmt);
1221
+ if (error) return err(error);
1222
+ // SQLite keeps the count of the last INSERT, UPDATE or DELETE across
1223
+ // other statements, also one that a function of this statement ran,
1224
+ // and a statement that cannot write changes no rows itself.
1225
+ const changes =
1226
+ totalChanges64(db) === totalChanges || stmtReadonly(stmt) !== 0
1227
+ ? 0
1228
+ : Number(changes64(db));
1229
+ return ok({ rows, changes });
1230
+ };
1231
+
1232
+ return { parameterCount, run };
1233
+ };
1234
+
1235
+ const database = disposable<SqliteDatabase>(
1236
+ {
1237
+ prepare: (sql) =>
1238
+ callOperation(() => {
1239
+ const prepared = prepareStatement(sql, SQLITE_PREPARE_PERSISTENT);
1240
+ if (!prepared.ok) return prepared;
1241
+ const stmt = prepared.value;
1242
+ const runner = createRunner(stmt);
1243
+ // SQLite forbids resetting or finalizing a statement that is
1244
+ // stepping, which running or disposing it from a callback of its
1245
+ // run would do. Both throw before wasm is entered, so the
1246
+ // instance keeps working.
1247
+ let running = false;
1248
+ using statementDisposer = new DisposableStack();
1249
+ statementDisposer.defer(() => {
1250
+ statements.delete(statement);
1251
+ call(() => finalize(stmt));
1252
+ });
1253
+ const statement = disposable<SqliteStatement>(
1254
+ {
1255
+ parameterCount: runner.parameterCount,
1256
+ run: (parameters) => {
1257
+ if (running) throw new Error(runningStatementMessage);
1258
+ running = true;
1259
+ try {
1260
+ return callOperation(() => runner.run(parameters));
1261
+ } finally {
1262
+ running = false;
1263
+ }
1264
+ },
1265
+ },
1266
+ statementDisposer,
1267
+ );
1268
+ const disposeStatement = statement[Symbol.dispose];
1269
+ statement[Symbol.dispose] = () => {
1270
+ if (running) throw new Error(runningStatementMessage);
1271
+ disposeStatement();
1272
+ };
1273
+ statements.add(statement);
1274
+ return ok(statement);
1275
+ }),
1276
+
1277
+ run: (sql, parameters) =>
1278
+ callOperation(() => {
1279
+ const prepared = prepareStatement(sql, 0);
1280
+ if (!prepared.ok) return prepared;
1281
+ const result = createRunner(prepared.value).run(parameters);
1282
+ finalize(prepared.value);
1283
+ return result;
1284
+ }),
1285
+
1286
+ exec: (sql) =>
1287
+ callOperation(() => {
1288
+ if (!isValidSqlText(sql))
1289
+ return err({ type: "SqliteInvalidSqlText" });
1290
+ // A step can call back into this connection, as a function that runs
1291
+ // a query does, and that call reuses the scratch memory, so the
1292
+ // statements not yet prepared need their own copy. JavaScript can
1293
+ // fail to allocate the UTF-8, which runs no wasm, so the instance
1294
+ // keeps working.
1295
+ const bytes = trySync(() => utf8Encoder.encode(sql));
1296
+ if (!bytes.ok) return err(toCodeError("exec", SQLITE_NOMEM));
1297
+ const copied = alloc(bytes.value.length + 1);
1298
+ if (!copied.ok) return err(toCodeError("exec", SQLITE_NOMEM));
1299
+ const zSql = copied.value;
1300
+ using execDisposer = new DisposableStack();
1301
+ execDisposer.defer(() => {
1302
+ free(zSql);
1303
+ });
1304
+ writeBytes(zSql, bytes.value);
1305
+ // The lengths SQLite gets count the NUL terminator, as in
1306
+ // prepare.
1307
+ const end = zSql + bytes.value.length + 1;
1308
+ deps.sqliteWasm.getHeapU8()[end - 1] = 0;
1309
+ // Each statement is prepared from where the previous one ended, so
1310
+ // an error offset is relative to it, also when a step re-prepares
1311
+ // the statement after a schema change and fails.
1312
+ const toExecError = (statementStart: WasmPtr): SqliteError => {
1313
+ const error = toSqliteError(db, "exec");
1314
+ return error.sqlOffset == null
1315
+ ? error
1316
+ : {
1317
+ ...error,
1318
+ sqlOffset: statementStart - zSql + error.sqlOffset,
1319
+ };
1320
+ };
1321
+ for (let start: WasmPtr = zSql; start < end - 1;) {
1322
+ const rc = prepareV3(
1323
+ db,
1324
+ start,
1325
+ end - start,
1326
+ 0,
1327
+ scratch.out,
1328
+ pzTail,
1329
+ );
1330
+ if (rc !== SQLITE_OK) return err(toExecError(start));
1331
+ const stmt = readPtr(scratch.out) as SqliteStmtPtr;
1332
+ const statementStart = start;
1333
+ start = readPtr(pzTail) as WasmPtr;
1334
+ // NULL for whitespace and comments.
1335
+ if (stmt === 0) continue;
1336
+ // A parameter exec cannot bind would run as NULL.
1337
+ const parameterCount = bindParameterCount(stmt);
1338
+ if (parameterCount !== 0) {
1339
+ finalize(stmt);
1340
+ return err({
1341
+ type: "SqliteParameterCountError",
1342
+ expected: parameterCount,
1343
+ actual: 0,
1344
+ });
1345
+ }
1346
+ let stepRc: SqliteResultCode;
1347
+ while ((stepRc = step(stmt)) === SQLITE_ROW);
1348
+ const error =
1349
+ stepRc === SQLITE_DONE ? null : toExecError(statementStart);
1350
+ finalize(stmt);
1351
+ if (error) return err(error);
1352
+ }
1353
+ return ok();
1354
+ }),
1355
+
1356
+ export: () =>
1357
+ callOperation(() => {
1358
+ // SQLite writes the size before it finalizes its query, which
1359
+ // can call a trace callback that runs a query on this
1360
+ // database, which reuses the scratch memory.
1361
+ const allocatedSize = alloc(8);
1362
+ if (!allocatedSize.ok)
1363
+ return err(toCodeError("export", SQLITE_NOMEM));
1364
+ const pSize = allocatedSize.value;
1365
+ using sizeDisposer = new DisposableStack();
1366
+ sizeDisposer.defer(() => {
1367
+ free(pSize);
1368
+ });
1369
+ // An error the connection reports after a NULL then comes from this
1370
+ // call, which does not report its own failed allocations.
1371
+ setErrmsg(db, SQLITE_OK, 0);
1372
+ // NULL is the main schema.
1373
+ const pages = serialize(db, 0, pSize, 0);
1374
+ const size = deps.sqliteWasm
1375
+ .getHeapDataView()
1376
+ .getBigInt64(pSize, true);
1377
+ if (pages === 0) {
1378
+ if (errcode(db) !== SQLITE_OK)
1379
+ return err(toSqliteError(db, "export"));
1380
+ // NULL with a size of 0 is a database without pages that cannot
1381
+ // get one.
1382
+ return size === 0n
1383
+ ? ok(new Uint8Array())
1384
+ : err(toCodeError("export", SQLITE_NOMEM));
1385
+ }
1386
+ const bytes = tryCopy(copyBytes, pages, Number(size));
1387
+ free(pages);
1388
+ // SQLite zero-fills a page it cannot read and reports success.
1389
+ const failure = vfs?.getFailure() ?? null;
1390
+ if (failure != null)
1391
+ return err({
1392
+ ...toCodeError(
1393
+ "export",
1394
+ isPageAuthenticationError(failure.error)
1395
+ ? SQLITE_CORRUPT
1396
+ : SQLITE_IOERR,
1397
+ ),
1398
+ cause: failure,
1399
+ });
1400
+ if (bytes == null)
1401
+ return err(toCodeError("export", SQLITE_NOMEM));
1402
+ return ok(bytes);
1403
+ }),
1404
+
1405
+ isAutocommit: () => call(() => getAutocommit(db) !== 0),
1406
+ },
1407
+ disposer,
1408
+ );
1409
+ // SQLite forbids closing a connection while it runs, which disposing the
1410
+ // database from a callback of one of its operations would do.
1411
+ const disposeDatabase = database[Symbol.dispose];
1412
+ database[Symbol.dispose] = () => {
1413
+ if (runningOperations > 0)
1414
+ throw new Error(
1415
+ "A SqliteDatabase cannot be disposed while one of its operations runs.",
1416
+ );
1417
+ disposeDatabase();
1418
+ };
1419
+ return ok(database);
1420
+ });
1421
+ };
1422
+
1423
+ /**
1424
+ * Options for {@link createEncryptedSqliteDatabase}: an EncryptedFile database
1425
+ * on a {@link SahPool}, because 2.2.4 wrote only pool files, and the fallback to
1426
+ * its key reads the salt with {@link SahPool.read}.
1427
+ */
1428
+ export interface SqliteEncryptedDatabaseOptions extends SqliteEncryptedFileDatabaseOptions {
1429
+ readonly vfs: SahPool;
1430
+ }
1431
+
1432
+ /**
1433
+ * Opens an EncryptedFile database on a pool, falling back to the key
1434
+ * `@evolu/sqlite-wasm` 2.2.4 derived.
1435
+ *
1436
+ * `@evolu/web` passed 2.2.4 the key as the SQL text `x'<hex>'`, SQLCipher's
1437
+ * notation for a raw key, which SQLite3 Multiple Ciphers 2.2.4 treated as a
1438
+ * passphrase because of a bug
1439
+ * (https://github.com/utelle/SQLite3MultipleCiphers/issues/218, fixed in
1440
+ * 2.2.5). So every database it encrypted is keyed with PBKDF2-HMAC-SHA512 of
1441
+ * that text, not with the key itself. This Task:
1442
+ *
1443
+ * 1. Opens with the raw key. A new or empty file always accepts it.
1444
+ * 2. Only when a wrong key failed it, so page 1 of the database or a record of its
1445
+ * hot journal failed to authenticate, which the pool records as a
1446
+ * {@link SqlitePageAuthenticationError}, reads the first 16 bytes of the file
1447
+ * through {@link SahPool.read}: the unencrypted cipher salt. Another page of
1448
+ * the database that fails to authenticate is damaged, because page 1 proved
1449
+ * the key, so the open fails with SQLITE_CORRUPT.
1450
+ * 3. Derives the legacy key with {@link deriveLegacySqliteKey}, opens again with it
1451
+ * as the raw key, and zeroes it.
1452
+ *
1453
+ * The result says which key opened the database. When neither key opens it, the
1454
+ * error is the raw key's, unless the second open failed otherwise. Rekeying a
1455
+ * legacy database to the raw key is a separate decision: it rewrites every
1456
+ * page, needs space, and makes the database unreadable to 2.2.4 tabs that share
1457
+ * the pool.
1458
+ */
1459
+ export const createEncryptedSqliteDatabase =
1460
+ (
1461
+ options: SqliteEncryptedDatabaseOptions,
1462
+ ): Task<
1463
+ SqliteEncryptedDatabase,
1464
+ SqliteError | SqliteVfsIoError,
1465
+ SqliteWasmDep & SubtleCryptoDep
1466
+ > =>
1467
+ async (run) => {
1468
+ const open = createSqliteDatabase(run.deps);
1469
+ const raw = open(options);
1470
+ if (raw.ok) return ok({ database: raw.value, keyDerivation: "Raw" });
1471
+ if (!isWrongKey(raw.error, options.path)) return err(raw.error);
1472
+
1473
+ const salt = options.vfs.read(
1474
+ options.path,
1475
+ 0 as NonNegativeInt,
1476
+ 16 as NonNegativeInt,
1477
+ );
1478
+ if (!salt.ok) {
1479
+ assert(
1480
+ salt.error.type === "SqliteVfsIoError",
1481
+ "The pool has the file the raw key failed to open.",
1482
+ );
1483
+ return err(salt.error);
1484
+ }
1485
+ const legacyKey = await run.ok(
1486
+ deriveLegacySqliteKey(options.key, salt.value),
1487
+ );
1488
+ let legacy: Result<SqliteDatabase, SqliteError>;
1489
+ try {
1490
+ legacy = open({ ...options, key: legacyKey });
1491
+ } finally {
1492
+ legacyKey.fill(0);
1493
+ }
1494
+ if (legacy.ok)
1495
+ return ok({ database: legacy.value, keyDerivation: "Legacy224" });
1496
+ return err(
1497
+ isWrongKey(legacy.error, options.path) ? raw.error : legacy.error,
1498
+ );
1499
+ };
1500
+
1501
+ /** The result of {@link createEncryptedSqliteDatabase}. */
1502
+ export interface SqliteEncryptedDatabase {
1503
+ readonly database: SqliteDatabase;
1504
+ readonly keyDerivation: SqliteKeyDerivation;
1505
+ }
1506
+
1507
+ /**
1508
+ * Which key opened an encrypted database: the raw key, or the key 2.2.4 derived
1509
+ * from it because of a bug in SQLite3 Multiple Ciphers, which
1510
+ * {@link deriveLegacySqliteKey} describes.
1511
+ */
1512
+ export type SqliteKeyDerivation = "Raw" | "Legacy224";
1513
+
1514
+ /**
1515
+ * Derives the key `@evolu/sqlite-wasm` 2.2.4 used: PBKDF2-HMAC-SHA512 with
1516
+ * 256000 iterations and a 32-byte output, over the UTF-8 text `x'` followed by
1517
+ * the key in lowercase hex and `'`, salted with the database's first 16 bytes.
1518
+ *
1519
+ * It exists because of a bug in SQLite3 Multiple Ciphers 2.2.4, which 2.2.4
1520
+ * packaged. `x'<hex>'` is SQLCipher's notation for a raw key, which
1521
+ * `@evolu/web` used, but that version took it as a passphrase and derived the
1522
+ * key from the text
1523
+ * (https://github.com/utelle/SQLite3MultipleCiphers/issues/218, fixed in
1524
+ * 2.2.5). So the databases it encrypted are keyed with this derivation, and
1525
+ * opening them needs it for as long as they exist, because nothing rekeys
1526
+ * them.
1527
+ *
1528
+ * The text is built byte by byte, so no string holds the key, and zeroed once
1529
+ * WebCrypto has imported it.
1530
+ */
1531
+ export const deriveLegacySqliteKey =
1532
+ (
1533
+ key: EncryptionKey,
1534
+ salt: Uint8Array,
1535
+ ): Task<EncryptionKey, never, SubtleCryptoDep> =>
1536
+ async (run) => {
1537
+ const { subtleCrypto } = run.deps;
1538
+ // x' followed by the key in lowercase hex and ', where 0x78 is x and 0x27
1539
+ // is '.
1540
+ const passphrase = new Uint8Array(2 * key.length + 3).fill(0x27);
1541
+ passphrase[0] = 0x78;
1542
+ for (const [index, byte] of key.entries()) {
1543
+ passphrase[2 + 2 * index] = lowercaseHexDigit(byte >> 4);
1544
+ passphrase[3 + 2 * index] = lowercaseHexDigit(byte & 0xf);
1545
+ }
1546
+ let passphraseKey: CryptoKey;
1547
+ try {
1548
+ passphraseKey = await subtleCrypto.importKey(
1549
+ "raw",
1550
+ passphrase,
1551
+ "PBKDF2",
1552
+ false,
1553
+ ["deriveBits"],
1554
+ );
1555
+ } finally {
1556
+ passphrase.fill(0);
1557
+ }
1558
+ const bits = await subtleCrypto.deriveBits(
1559
+ {
1560
+ name: "PBKDF2",
1561
+ hash: "SHA-512",
1562
+ salt: new Uint8Array(salt),
1563
+ iterations: 256_000,
1564
+ },
1565
+ passphraseKey,
1566
+ 256,
1567
+ );
1568
+ return ok(new Uint8Array(bits) as EncryptionKey);
1569
+ };
1570
+
1571
+ /** WebCrypto, which derives legacy keys. */
1572
+ export interface SubtleCryptoDep {
1573
+ readonly subtleCrypto: SubtleCrypto;
1574
+ }
1575
+
1576
+ // A wrong key fails to authenticate page 1 of the database, which is
1577
+ // SQLITE_NOTADB, or first a hot journal's first record, which is SQLITE_CORRUPT
1578
+ // for any other page. Without a hot journal, SQLite reads page 1 before any
1579
+ // other page, and a pool records a record of a journal that fails only until a
1580
+ // page 1 proved the key, so another page of the database that fails means that
1581
+ // the key fit and the page is damaged.
1582
+ const isWrongKey = (error: SqliteError, path: SqliteVfsPath): boolean =>
1583
+ error.cause != null &&
1584
+ isPageAuthenticationError(error.cause.error) &&
1585
+ (error.cause.path !== path || error.cause.error.pageNumber === 1);
1586
+
1587
+ const isPageAuthenticationError = (
1588
+ error: unknown,
1589
+ ): error is SqlitePageAuthenticationError =>
1590
+ typeof error === "object" &&
1591
+ error !== null &&
1592
+ "type" in error &&
1593
+ error.type === "SqlitePageAuthenticationError";
1594
+
1595
+ // The character code of a hex digit in lowercase, as 2.2.4's hex had it.
1596
+ const lowercaseHexDigit = (nibble: number): number =>
1597
+ nibble < 10 ? 0x30 + nibble : 0x61 - 10 + nibble;
1598
+
1599
+ const runningStatementMessage =
1600
+ "A SqliteStatement cannot run or be disposed while it runs.";
1601
+
1602
+ // Copies a value out of wasm memory, or returns null when JavaScript cannot
1603
+ // allocate the copy, as SQLite returns NULL when it cannot allocate. Engines
1604
+ // throw different errors then, such as V8's RangeError for an ArrayBuffer and
1605
+ // Node.js's Error for a string longer than V8's limit. The copy runs no wasm,
1606
+ // so whatever it throws leaves SQLite as it was, unlike an exception that
1607
+ // escapes wasm, which breaks the instance.
1608
+ const tryCopy = <T>(
1609
+ copy: (ptr: WasmPtr, byteLength: number) => T,
1610
+ ptr: WasmPtr,
1611
+ byteLength: number,
1612
+ ): T | null => {
1613
+ try {
1614
+ return copy(ptr, byteLength);
1615
+ } catch {
1616
+ return null;
1617
+ }
1618
+ };
1619
+
1620
+ // Whether SQLite reads SQL as the string holds it: SQLite reads SQL only up to
1621
+ // a NUL character, and UTF-8 cannot encode a lone surrogate, which encoding
1622
+ // would replace with U+FFFD.
1623
+ const isValidSqlText = (sql: string): boolean =>
1624
+ !sql.includes("\0") && sql.isWellFormed();
1625
+
1626
+ // How many operations of databases on each VFS are running, so only the
1627
+ // outermost clears the VFS's failure record. The record is one per VFS, and a
1628
+ // query a callback runs during an operation is an operation too, on the same
1629
+ // database or another one on the VFS.
1630
+ const runningOperationsByVfs = /*#__PURE__*/ new WeakMap<SqliteVfs, number>();
1631
+
1632
+ // Whether UTF-8 SQL from start to end holds no statement, only the tokens
1633
+ // SQLite 3.53.4's tokenizer skips: a semicolon; a space, which starts with a
1634
+ // space, \t, \n, \f or \r and continues over sqlite3Isspace, which adds \v,
1635
+ // while a \v that starts a token is illegal; a UTF-8 byte order mark, a space
1636
+ // of its own; a comment from "--" to a newline or the end; and a comment from
1637
+ // "/*" that a byte follows, to the first "*/" after the "/*" or the end, so
1638
+ // "/*/" stays open. An auto-extension can turn comments off with
1639
+ // SQLITE_DBCONFIG_ENABLE_COMMENTS, which makes SQLite reject them, but a tail
1640
+ // of only comments is still accepted here, and it runs nothing.
1641
+ const isEmptySql = (bytes: Uint8Array, start: number, end: number): boolean => {
1642
+ let index = start;
1643
+ while (index < end) {
1644
+ switch (bytes[index]) {
1645
+ case semicolon:
1646
+ index++;
1647
+ break;
1648
+ case space:
1649
+ case tab:
1650
+ case newline:
1651
+ case formFeed:
1652
+ case carriageReturn:
1653
+ index++;
1654
+ while (
1655
+ index < end &&
1656
+ (bytes[index] === space ||
1657
+ (bytes[index] >= tab && bytes[index] <= carriageReturn))
1658
+ )
1659
+ index++;
1660
+ break;
1661
+ // The first byte of a UTF-8 byte order mark.
1662
+ case 0xef:
1663
+ if (
1664
+ index + 2 >= end ||
1665
+ bytes[index + 1] !== 0xbb ||
1666
+ bytes[index + 2] !== 0xbf
1667
+ )
1668
+ return false;
1669
+ index += 3;
1670
+ break;
1671
+ case hyphen:
1672
+ if (index + 1 === end || bytes[index + 1] !== hyphen) return false;
1673
+ index += 2;
1674
+ while (index < end && bytes[index] !== newline) index++;
1675
+ break;
1676
+ case slash: {
1677
+ if (index + 2 >= end || bytes[index + 1] !== asterisk) return false;
1678
+ let close = index + 2;
1679
+ while (
1680
+ close + 1 < end &&
1681
+ !(bytes[close] === asterisk && bytes[close + 1] === slash)
1682
+ )
1683
+ close++;
1684
+ index = close + 1 < end ? close + 2 : end;
1685
+ break;
1686
+ }
1687
+ default:
1688
+ return false;
1689
+ }
1690
+ }
1691
+ return true;
1692
+ };
1693
+
1694
+ // The ASCII codes isEmptySql reads; \v (11) is between \n and \f.
1695
+ const tab = 0x09;
1696
+ const newline = 0x0a;
1697
+ const formFeed = 0x0c;
1698
+ const carriageReturn = 0x0d;
1699
+ const space = 0x20;
1700
+ const asterisk = 0x2a;
1701
+ const hyphen = 0x2d;
1702
+ const slash = 0x2f;
1703
+ const semicolon = 0x3b;
1704
+
1705
+ // The bytes an EncryptedFile database leaves to its VFS at the end of each
1706
+ // page, where the pool keeps the IV and the HMAC.
1707
+ const encryptedReservedBytes = 80;
1708
+
1709
+ // The largest text or blob bound from the scratch memory, which keeps the
1710
+ // memory of the largest value it held. A larger value is allocated exactly and
1711
+ // handed over, which also spares SQLite a copy.
1712
+ const maxScratchValue = 65_536;
1713
+
1714
+ const utf8Encoder = /*#__PURE__*/ new TextEncoder();