@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.
- package/LICENSE +21 -0
- package/README.md +55 -218
- package/dist/src/CApi.d.ts +2108 -0
- package/dist/src/CApi.d.ts.map +1 -0
- package/dist/src/CApi.js +1919 -0
- package/dist/src/Constants.d.ts +868 -0
- package/dist/src/Constants.d.ts.map +1 -0
- package/dist/src/Constants.js +602 -0
- package/dist/src/Database.d.ts +641 -0
- package/dist/src/Database.d.ts.map +1 -0
- package/dist/src/Database.js +1177 -0
- package/dist/src/Memory.d.ts +119 -0
- package/dist/src/Memory.d.ts.map +1 -0
- package/dist/src/Memory.js +207 -0
- package/dist/src/Pointer.d.ts +100 -0
- package/dist/src/Pointer.d.ts.map +1 -0
- package/dist/src/Pointer.js +15 -0
- package/dist/src/SahPool.d.ts +744 -0
- package/dist/src/SahPool.d.ts.map +1 -0
- package/dist/src/SahPool.js +1985 -0
- package/dist/src/Wasm.d.ts +315 -0
- package/dist/src/Wasm.d.ts.map +1 -0
- package/dist/src/Wasm.js +756 -0
- package/dist/src/WasmUrl.d.ts +21 -0
- package/dist/src/WasmUrl.d.ts.map +1 -0
- package/dist/src/WasmUrl.js +20 -0
- package/dist/src/c-api/index.d.ts +9 -0
- package/dist/src/c-api/index.d.ts.map +1 -0
- package/dist/src/c-api/index.js +8 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +13 -0
- package/dist/wasm/sqlite3.wasm +0 -0
- package/package.json +60 -60
- package/src/CApi.test.ts +217 -0
- package/src/CApi.ts +3294 -0
- package/src/Constants.ts +803 -0
- package/src/Database.ts +1714 -0
- package/src/Memory.test.ts +319 -0
- package/src/Memory.ts +237 -0
- package/src/Pointer.ts +121 -0
- package/src/SahPool.test.ts +180 -0
- package/src/SahPool.ts +2627 -0
- package/src/Wasm.test.ts +444 -0
- package/src/Wasm.ts +1026 -0
- package/src/WasmUrl.ts +26 -0
- package/src/c-api/index.ts +9 -0
- package/src/index.ts +15 -0
- package/bin/index.js +0 -110
- package/index.d.ts +0 -8118
- package/index.mjs +0 -7
- package/node.mjs +0 -3
- package/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +0 -13659
- package/sqlite-wasm/jswasm/sqlite3-node.mjs +0 -11671
- package/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js +0 -691
- package/sqlite-wasm/jswasm/sqlite3-worker1-bundler-friendly.mjs +0 -35
- package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.js +0 -193
- package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.mjs +0 -187
- package/sqlite-wasm/jswasm/sqlite3-worker1.js +0 -46
- package/sqlite-wasm/jswasm/sqlite3.js +0 -13697
- package/sqlite-wasm/jswasm/sqlite3.mjs +0 -13661
- package/sqlite-wasm/jswasm/sqlite3.wasm +0 -0
package/src/Database.ts
ADDED
|
@@ -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();
|