@amritk/nish-x86_64-linux 0.20.0 → 0.22.1
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/INSTALL.md +52 -10
- package/bin/nish +0 -0
- package/docs/AI.md +2777 -0
- package/package.json +2 -1
- package/runtime/browser.d.ts +6 -0
- package/runtime/browser.mjs +77 -0
- package/runtime/core/arrays.d.ts +36 -0
- package/runtime/core/arrays.mjs +153 -0
- package/runtime/core/checks.mjs +20 -0
- package/runtime/core/host.d.ts +80 -0
- package/runtime/core/host.mjs +325 -0
- package/runtime/core/numbers.d.ts +66 -0
- package/runtime/core/numbers.mjs +343 -0
- package/runtime/core/result.d.ts +45 -0
- package/runtime/core/result.mjs +140 -0
- package/runtime/core/strings.d.ts +14 -0
- package/runtime/core/strings.mjs +169 -0
- package/runtime/core.d.ts +9 -0
- package/runtime/core.mjs +55 -0
- package/runtime/host.d.ts +6 -0
- package/runtime/nish.d.ts +46 -2
- package/runtime/nish.h +75 -2
- package/runtime/nish.mjs +23 -3
- package/runtime/node.mjs +386 -0
- package/runtime/runtime-bytes.c +401 -0
- package/runtime/runtime-cross.c +1 -1
- package/runtime/runtime-host.c +1 -1
- package/runtime/runtime-net.c +12 -1
- package/runtime/runtime-os.c +1 -1
- package/runtime/runtime-parallel.c +1 -1
- package/runtime/runtime-simd.c +231 -7
- package/runtime/runtime-term.c +504 -0
- package/runtime/runtime-wasm.c +0 -11
- package/runtime/runtime.c +190 -6
- package/runtime/shim.mjs +6 -985
- package/runtime/sqlite/LICENSE.md +85 -0
- package/runtime/sqlite/README.md +45 -0
- package/runtime/sqlite/nish-sqlite.c +359 -0
- package/runtime/sqlite/sqlite3.c +271131 -0
- package/runtime/sqlite/sqlite3.h +14447 -0
- package/scripts/build.sh +311 -59
- package/std/LICENSE-fast-float +23 -0
- package/std/LICENSE-fdlibm +8 -0
- package/std/README.md +32 -12
- package/std/base64.ts +116 -0
- package/std/bytes.ts +256 -0
- package/std/cli.ts +344 -0
- package/std/collections.ts +6 -5
- package/std/crypto/LICENSE-crypt_blowfish +26 -0
- package/std/crypto/base64url.ts +125 -0
- package/std/crypto/bcrypt.ts +1557 -0
- package/std/crypto/hmac.ts +138 -2
- package/std/crypto/x509.ts +11 -122
- package/std/json-tokenizer.ts +2998 -57
- package/std/json-value.ts +22 -3
- package/std/json.ts +87 -8
- package/std/math.ts +608 -0
- package/std/net/http1-server.ts +282 -40
- package/std/net/http1.ts +40 -21
- package/std/net/http3-server.ts +50 -12
- package/std/net/postgres.ts +2294 -0
- package/std/net/prefork.ts +23 -3
- package/std/net/quic-listener.ts +20 -4
- package/std/random.ts +66 -4
- package/std/regex.ts +4274 -0
- package/std/sqlite.ts +391 -0
- package/std/term-io.ts +978 -0
- package/std/term-pixels.ts +874 -0
- package/std/term-probe.ts +459 -0
- package/std/term-rate.ts +993 -0
- package/std/term.ts +2453 -0
- package/std/text.ts +90 -7
- package/std/yaml-lines.ts +141 -0
- package/std/yaml.ts +4675 -0
|
@@ -0,0 +1,2294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/postgres` — the client side of PostgreSQL's frontend/backend
|
|
3
|
+
* protocol, version 3.0: a sans-IO connection that writes the frontend's
|
|
4
|
+
* messages into an output buffer and reads the backend's from an input
|
|
5
|
+
* buffer, for a program that owns its socket (`nish:net`) and its loop.
|
|
6
|
+
*
|
|
7
|
+
* const config = new PgConfig();
|
|
8
|
+
* config.user = "app";
|
|
9
|
+
* config.database = "app";
|
|
10
|
+
* const nonce = new Array<u8>(18);
|
|
11
|
+
* crypto.getRandomValues(nonce);
|
|
12
|
+
* const conn = new PgConnection(config, password, nonce);
|
|
13
|
+
* // write conn.outputBuffer()[conn.outputStart() .. conn.outputEnd()), then conn.sent(n)
|
|
14
|
+
* // read into conn.inputBuffer() at conn.inputEnd(), up to conn.inputRoom() bytes, then conn.received(n)
|
|
15
|
+
* let event: i32 = conn.next();
|
|
16
|
+
* while (event !== PG_NEED_MORE && event !== PG_FAILED) {
|
|
17
|
+
* // PG_READY: authenticated; prepare statements and send queries
|
|
18
|
+
* // conn.prepare("by_id", "select id, name from users where id = $1", [23]); conn.sync()
|
|
19
|
+
* // conn.bindStart("by_id"); conn.paramInt(7); conn.bindEnd(); conn.sync()
|
|
20
|
+
* // PG_ROW: conn.int(0), conn.colStart(1) .. + conn.colLength(1) in conn.inputBuffer()
|
|
21
|
+
* // PG_COMPLETE: conn.tagRows(); PG_ERROR: conn.lastError.code
|
|
22
|
+
* // PG_GROUP_DONE: one sync()'s group has been answered
|
|
23
|
+
* event = conn.next();
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* **Events.** `next()` answers `PG_READY` once, when the server has
|
|
27
|
+
* authenticated the connection and is ready for queries; `PG_DESCRIBED` when a
|
|
28
|
+
* prepared statement has been described (`columns` and `columnType(c)` say
|
|
29
|
+
* what its rows hold, `columns` is 0 when it returns none); `PG_ROW` for each
|
|
30
|
+
* row, its columns as windows into the input buffer (`colStart(c)`,
|
|
31
|
+
* `colLength(c)`, -1 for NULL); `PG_COMPLETE` when an execution has finished
|
|
32
|
+
* (`tagIs("UPDATE")`, `tagRows()`); `PG_ERROR` when a statement in the current
|
|
33
|
+
* group failed (`lastError`); `PG_NOTICE` for a notice (`lastNotice`);
|
|
34
|
+
* `PG_GROUP_DONE` when a group has been answered to its end (`groupFailed`,
|
|
35
|
+
* `txStatus`); and `PG_FAILED`, final: the connection broke a rule or the
|
|
36
|
+
* server ended it, `failure` says which and `lastError` holds the server's
|
|
37
|
+
* reason when it gave one. `PG_NEED_MORE` asks for more input.
|
|
38
|
+
*
|
|
39
|
+
* **Windows and buffers.** A column's window, the tag `tagIs` and `tagRows`
|
|
40
|
+
* read are valid until the next `next()`,
|
|
41
|
+
* which may move the unread input to the start of the buffer or replace the
|
|
42
|
+
* buffer with a larger one. Fetch `inputBuffer()` again after every `next()`
|
|
43
|
+
* rather than keep it; the same holds for `outputBuffer()` after a builder
|
|
44
|
+
* call.
|
|
45
|
+
*
|
|
46
|
+
* **Groups and pipelining.** Every statement is sent with the extended query
|
|
47
|
+
* protocol: `prepare` (Parse and Describe), `bindStart`, the `param...` calls
|
|
48
|
+
* and `bindEnd` (Bind and Execute), `closeStatement` (Close), then `sync()`,
|
|
49
|
+
* which ends the group. The server answers groups in order and skips the rest
|
|
50
|
+
* of a group after an error, so one failed statement costs only its own group.
|
|
51
|
+
* In a failed group, `PG_DESCRIBED` comes once for each `prepare` the server
|
|
52
|
+
* parsed before the error: a program that counts them knows which statements
|
|
53
|
+
* exist (named ones last past the group) and which must be prepared again.
|
|
54
|
+
* Up to `maxInFlight` groups may wait for their answers at once; `canSend()`
|
|
55
|
+
* says whether another may start, and the builder calls refuse (and write
|
|
56
|
+
* nothing) when it may not. Nothing waits for a round trip that the protocol
|
|
57
|
+
* does not need: a program prepares a statement and executes it in the same
|
|
58
|
+
* flight.
|
|
59
|
+
*
|
|
60
|
+
* **Text results, no copies.** Every parameter and every result column is in
|
|
61
|
+
* text format. A row is read where it lies in the input buffer: `next()` frames
|
|
62
|
+
* each message once, validates its layout, and records each column's start and
|
|
63
|
+
* length in two reused arrays, so a row costs no allocation. `int`, `bool`,
|
|
64
|
+
* `isNull` and `PgText.timestamp` decode a column in place; `PgArrayWalker`
|
|
65
|
+
* walks a one-dimensional `text[]` in place. `columnText(c)` is the one that
|
|
66
|
+
* allocates, a string.
|
|
67
|
+
*
|
|
68
|
+
* **Authentication.** SCRAM-SHA-256 (RFC 5802, RFC 7677) or none (`trust`).
|
|
69
|
+
* The password arrives as a `Secret<u8[]>`; the connection keeps a copy in a
|
|
70
|
+
* holder that is wiped as soon as the proof is computed, or when the
|
|
71
|
+
* connection fails or is disposed. The server's signature is checked in
|
|
72
|
+
* constant time before `AuthenticationOk` is believed, and an
|
|
73
|
+
* `AuthenticationOk` that skips the end of an exchange SCRAM began is refused.
|
|
74
|
+
* Cleartext and MD5 passwords, and SASL mechanisms other than SCRAM-SHA-256,
|
|
75
|
+
* are refused: with no TLS here the first sends the password as it is, and the
|
|
76
|
+
* second is a hash any eavesdropper can replay. The password's bytes are used
|
|
77
|
+
* as they are, without SASLprep (RFC 4013): an ASCII password works, and a
|
|
78
|
+
* non-ASCII one works only when it is already in the form SASLprep would give
|
|
79
|
+
* (PostgreSQL itself falls back to the raw bytes when it cannot prepare one).
|
|
80
|
+
* `docs/security/postgres.md` is the record.
|
|
81
|
+
*
|
|
82
|
+
* **Caps.** `PgConfig` bounds what a server can make the client hold: the
|
|
83
|
+
* longest message (`maxMessage`), the widest row (`maxColumns`) and the SCRAM
|
|
84
|
+
* iteration count (`minIterations`, `maxIterations`); and what a program can
|
|
85
|
+
* queue unsent (`maxOutput`), past which a builder call answers false. A
|
|
86
|
+
* message longer than the cap, a length below 4, a layout that does not add
|
|
87
|
+
* up, a message the protocol does not allow at that point (COPY among them),
|
|
88
|
+
* an encoding other than UTF-8, and a `DateStyle` other than ISO or a
|
|
89
|
+
* `TimeZone` other than UTC (at startup or after a `SET`), each fail the
|
|
90
|
+
* connection with their own `failure` code. The constructor panics on a cap
|
|
91
|
+
* out of range, as on a missing user or a short nonce.
|
|
92
|
+
*
|
|
93
|
+
* **Pools.** `PgPool` holds up to `size` connections to one server and a FIFO
|
|
94
|
+
* queue of up to `queueSize` requests, still sans-IO: `wanted()` names a slot
|
|
95
|
+
* that needs a socket, `connect(slot, nonce)` starts its connection, and the
|
|
96
|
+
* program moves its bytes as above through `connection(slot)`. A request is a
|
|
97
|
+
* ticket from `request()`; `dispatch()` gives the oldest to the ready
|
|
98
|
+
* connection with the fewest groups in flight, the program builds its group and
|
|
99
|
+
* ends it with `send()`, and `next(slot)` reads that connection's events with
|
|
100
|
+
* `ticket` naming the request. Statements given to `register` are prepared on
|
|
101
|
+
* each connection ahead of its first request. A connection that fails reports
|
|
102
|
+
* `PG_LOST` for each request it took, then `PG_FAILED`; the program closes the
|
|
103
|
+
* socket and the slot is built again on its next use. `tests/postgres/carrier.ts`
|
|
104
|
+
* is a whole carrier: one `pollWait` loop holding the pool's sockets beside an
|
|
105
|
+
* HTTP listener.
|
|
106
|
+
*
|
|
107
|
+
* Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
|
|
108
|
+
* and RFC 5802 and RFC 7677; not ported from another implementation.
|
|
109
|
+
*/
|
|
110
|
+
import { copyBytes, getI32BE, getU16BE, setI32BE, setU16BE } from "nish/bytes"
|
|
111
|
+
import { base64StdPadDecode, base64StdPadEncode } from "nish/crypto/base64url"
|
|
112
|
+
import { timingSafeEqual } from "nish/crypto/ct"
|
|
113
|
+
import { hmacSha256, pbkdf2HmacSha256 } from "nish/crypto/hmac"
|
|
114
|
+
import { Sha256 } from "nish/crypto/sha256"
|
|
115
|
+
import { Secret, expose, exposeWith, secret, wipe } from "nish:secret"
|
|
116
|
+
|
|
117
|
+
/** `next()` needs more input before it can say anything. */
|
|
118
|
+
export const PG_NEED_MORE: i32 = 0
|
|
119
|
+
/** The connection is authenticated and ready for queries; answered once. */
|
|
120
|
+
export const PG_READY: i32 = 1
|
|
121
|
+
/** A row: `columns` columns, each at `colStart(c)` for `colLength(c)` bytes. */
|
|
122
|
+
export const PG_ROW: i32 = 2
|
|
123
|
+
/** An execution has finished: `tagIs`, `tagRows`. */
|
|
124
|
+
export const PG_COMPLETE: i32 = 3
|
|
125
|
+
/** A statement in the current group failed: `lastError`. */
|
|
126
|
+
export const PG_ERROR: i32 = 4
|
|
127
|
+
/** A group has been answered: `groupFailed`, `txStatus`. */
|
|
128
|
+
export const PG_GROUP_DONE: i32 = 5
|
|
129
|
+
/** A notice: `lastNotice`. */
|
|
130
|
+
export const PG_NOTICE: i32 = 6
|
|
131
|
+
/** A prepared statement has been described: `columns`, `columnType(c)`. */
|
|
132
|
+
export const PG_DESCRIBED: i32 = 7
|
|
133
|
+
/** Final: the connection is over, and `failure` says why. */
|
|
134
|
+
export const PG_FAILED: i32 = 8
|
|
135
|
+
|
|
136
|
+
/** `failure` while the connection is alive. */
|
|
137
|
+
export const PG_ERR_NONE: i32 = 0
|
|
138
|
+
/** A message length below 4, or a message whose layout does not add up. */
|
|
139
|
+
export const PG_ERR_FRAME: i32 = 1
|
|
140
|
+
/** A message longer than `maxMessage`, or a row wider than `maxColumns`. */
|
|
141
|
+
export const PG_ERR_TOO_BIG: i32 = 2
|
|
142
|
+
/** A message the protocol does not allow at this point. */
|
|
143
|
+
export const PG_ERR_UNEXPECTED: i32 = 3
|
|
144
|
+
/** An authentication method this client refuses (cleartext, MD5, GSS, SSPI, a SASL mechanism other than SCRAM-SHA-256). */
|
|
145
|
+
export const PG_ERR_AUTH_UNSUPPORTED: i32 = 4
|
|
146
|
+
/** A SCRAM exchange the server got wrong: a malformed message, a nonce that does not extend ours, an iteration count out of bounds. */
|
|
147
|
+
export const PG_ERR_SCRAM: i32 = 5
|
|
148
|
+
/** The server's SCRAM signature is not the one the password gives: it does not know the password. */
|
|
149
|
+
export const PG_ERR_SERVER_SIGNATURE: i32 = 6
|
|
150
|
+
/** The server sent a FATAL or PANIC error, or any error before the connection was ready: `lastError`. */
|
|
151
|
+
export const PG_ERR_SERVER: i32 = 7
|
|
152
|
+
/** The server's `client_encoding` is not UTF8. */
|
|
153
|
+
export const PG_ERR_ENCODING: i32 = 8
|
|
154
|
+
/** The program reported the end of the stream (`closed()`). */
|
|
155
|
+
export const PG_ERR_CLOSED: i32 = 9
|
|
156
|
+
/** The server's `DateStyle` is not ISO or its `TimeZone` is not UTC, the two the decoders read: at startup, or after a `SET`. */
|
|
157
|
+
export const PG_ERR_SETTING: i32 = 10
|
|
158
|
+
|
|
159
|
+
/** `PgArrayWalker.next()`: an element, at `start` for `length` bytes. */
|
|
160
|
+
export const PG_ARRAY_ELEMENT: i32 = 0
|
|
161
|
+
/** `PgArrayWalker.next()`: a NULL element. */
|
|
162
|
+
export const PG_ARRAY_NULL: i32 = -1
|
|
163
|
+
/** `PgArrayWalker.next()`: no more elements. */
|
|
164
|
+
export const PG_ARRAY_END: i32 = -2
|
|
165
|
+
/** `PgArrayWalker.next()`: the text is not a one-dimensional array literal. */
|
|
166
|
+
export const PG_ARRAY_BAD: i32 = -3
|
|
167
|
+
|
|
168
|
+
// The connection's states, in the order they come.
|
|
169
|
+
const PG_STATE_AUTH: i32 = 0
|
|
170
|
+
const PG_STATE_SCRAM_FIRST: i32 = 1
|
|
171
|
+
const PG_STATE_SCRAM_FINAL: i32 = 2
|
|
172
|
+
const PG_STATE_STARTING: i32 = 3
|
|
173
|
+
const PG_STATE_READY: i32 = 4
|
|
174
|
+
const PG_STATE_FAILED: i32 = 5
|
|
175
|
+
|
|
176
|
+
// The frontend's message types, and the bytes the builder writes.
|
|
177
|
+
const PG_FE_PASSWORD: i32 = 112 // 'p', every SASL response
|
|
178
|
+
const PG_FE_PARSE: i32 = 80 // 'P'
|
|
179
|
+
const PG_FE_DESCRIBE: i32 = 68 // 'D'
|
|
180
|
+
const PG_FE_BIND: i32 = 66 // 'B'
|
|
181
|
+
const PG_FE_EXECUTE: i32 = 69 // 'E'
|
|
182
|
+
const PG_FE_CLOSE: i32 = 67 // 'C'
|
|
183
|
+
const PG_FE_SYNC: i32 = 83 // 'S'
|
|
184
|
+
const PG_FE_TERMINATE: i32 = 88 // 'X'
|
|
185
|
+
const PG_TARGET_STATEMENT: i32 = 83 // 'S', what Describe and Close name
|
|
186
|
+
const PG_ZERO: i32 = 0
|
|
187
|
+
const PG_COMMA: i32 = 44
|
|
188
|
+
const PG_QUOTE: i32 = 34
|
|
189
|
+
const PG_BACKSLASH: i32 = 92
|
|
190
|
+
const PG_LBRACE: i32 = 123
|
|
191
|
+
const PG_RBRACE: i32 = 125
|
|
192
|
+
|
|
193
|
+
// The protocol version the startup message asks for: 3.0.
|
|
194
|
+
const PG_PROTOCOL: i32 = 196608
|
|
195
|
+
// The bytes of the client nonce, which base64 spells in 24 characters.
|
|
196
|
+
const PG_NONCE_BYTES: i32 = 18
|
|
197
|
+
// What a bind's end (Bind's result formats, then Execute: 2 + 10) and a Sync (5)
|
|
198
|
+
// take: kept free under `maxOutput` by every other builder call, so that
|
|
199
|
+
// `bindEnd()` and `sync()` are never refused for room.
|
|
200
|
+
const PG_TAIL: i32 = 17
|
|
201
|
+
// The largest `maxMessage` and `maxOutput`. The input buffer doubles up to one
|
|
202
|
+
// past them; the output buffer doubles only while twice its size fits in an i32.
|
|
203
|
+
const PG_MAX_CAP: i32 = 1073741823
|
|
204
|
+
// The largest `maxIterations`: the count is read digit by digit, times ten, in an i32.
|
|
205
|
+
const PG_MAX_ITERATIONS: i32 = 214748363
|
|
206
|
+
|
|
207
|
+
/** The caps a connection enforces, and the startup parameters it sends. */
|
|
208
|
+
export class PgConfig {
|
|
209
|
+
/** The role to connect as; required. */
|
|
210
|
+
user: string = ""
|
|
211
|
+
/** The database; empty means the server's default, the role's name. */
|
|
212
|
+
database: string = ""
|
|
213
|
+
/** `application_name`, shown in `pg_stat_activity`; empty sends none. */
|
|
214
|
+
applicationName: string = ""
|
|
215
|
+
/** The startup `options` parameter (`-c name=value ...`); empty sends none. */
|
|
216
|
+
options: string = ""
|
|
217
|
+
/** The longest backend message, in bytes, its type byte aside; past it, `PG_ERR_TOO_BIG`. */
|
|
218
|
+
maxMessage: i32 = 1048576
|
|
219
|
+
/** The most groups waiting for their answers at once. */
|
|
220
|
+
maxInFlight: i32 = 16
|
|
221
|
+
/** The input buffer's first size, in bytes; it grows to a message's length, up to `maxMessage` + 1. */
|
|
222
|
+
inputSize: i32 = 8192
|
|
223
|
+
/** The widest row; past it, `PG_ERR_TOO_BIG`. PostgreSQL's own limit is 1664. */
|
|
224
|
+
maxColumns: i32 = 1664
|
|
225
|
+
/** The fewest SCRAM iterations accepted; 4096 is PostgreSQL's default and RFC 7677's floor. */
|
|
226
|
+
minIterations: i32 = 4096
|
|
227
|
+
/** The most SCRAM iterations computed, so a server cannot make the client spin. */
|
|
228
|
+
maxIterations: i32 = 100000
|
|
229
|
+
/**
|
|
230
|
+
* The most bytes the builder calls may queue unsent; a call that would pass
|
|
231
|
+
* it answers false and writes nothing, so a program that never drains its
|
|
232
|
+
* output cannot grow the buffer without bound.
|
|
233
|
+
*/
|
|
234
|
+
maxOutput: i32 = 1048576
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A server's ErrorResponse or NoticeResponse: the fields a program acts on. */
|
|
238
|
+
export class PgError {
|
|
239
|
+
/** ERROR, FATAL or PANIC; WARNING, NOTICE, DEBUG, INFO or LOG for a notice. Never localised. */
|
|
240
|
+
readonly severity: string
|
|
241
|
+
/** The SQLSTATE, five characters: `23505` is a unique violation. */
|
|
242
|
+
readonly code: string
|
|
243
|
+
/** The primary message. */
|
|
244
|
+
readonly message: string
|
|
245
|
+
/** The detail, or "". */
|
|
246
|
+
readonly detail: string
|
|
247
|
+
/** The hint, or "". */
|
|
248
|
+
readonly hint: string
|
|
249
|
+
/** The constraint that was violated, or "". */
|
|
250
|
+
readonly constraint: string
|
|
251
|
+
/** The 1-based character position in the statement's text, or "". */
|
|
252
|
+
readonly position: string
|
|
253
|
+
|
|
254
|
+
constructor(
|
|
255
|
+
severity: string,
|
|
256
|
+
code: string,
|
|
257
|
+
message: string,
|
|
258
|
+
detail: string,
|
|
259
|
+
hint: string,
|
|
260
|
+
constraint: string,
|
|
261
|
+
position: string
|
|
262
|
+
) {
|
|
263
|
+
this.severity = severity
|
|
264
|
+
this.code = code
|
|
265
|
+
this.message = message
|
|
266
|
+
this.detail = detail
|
|
267
|
+
this.hint = hint
|
|
268
|
+
this.constraint = constraint
|
|
269
|
+
this.position = position
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A fresh copy of `bytes`; the password holder keeps one. */
|
|
274
|
+
const pgCopyBytes = (bytes: u8[]): u8[] => {
|
|
275
|
+
const copy: u8[] = new Array<u8>(toI32(bytes.length))
|
|
276
|
+
copy.set(bytes, 0)
|
|
277
|
+
return copy
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Whether every byte of `bytes` is zero. */
|
|
281
|
+
const pgAllZero = (bytes: u8[]): boolean => {
|
|
282
|
+
for (const b of bytes) {
|
|
283
|
+
if (toI32(b) !== 0) {
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The connection's copy of the password, wiped once the proof is computed.
|
|
292
|
+
* The holder outlives the wipe, so that `wiped()` reads the very bytes.
|
|
293
|
+
*/
|
|
294
|
+
class PgPassword {
|
|
295
|
+
readonly bytes: Secret<u8[]>
|
|
296
|
+
/** Whether `wipe()` has run; the bytes are no longer a password. */
|
|
297
|
+
gone: boolean = false
|
|
298
|
+
|
|
299
|
+
constructor(password: Secret<u8[]>) {
|
|
300
|
+
this.bytes = secret(expose(password, pgCopyBytes))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
wipe(): void {
|
|
304
|
+
wipe(this.bytes)
|
|
305
|
+
this.gone = true
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
wiped(): boolean {
|
|
309
|
+
return expose(this.bytes, pgAllZero)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Panics on a `PgConfig` that is a mistake in the program: no user, a cap out of range, a NUL in a startup parameter. */
|
|
314
|
+
const pgCheckConfig = (config: PgConfig): void => {
|
|
315
|
+
if (config.user.length === 0) {
|
|
316
|
+
panic("nish/net/postgres: PgConfig.user is required")
|
|
317
|
+
}
|
|
318
|
+
if (
|
|
319
|
+
config.maxMessage < 1024 ||
|
|
320
|
+
config.maxMessage > PG_MAX_CAP ||
|
|
321
|
+
config.maxOutput < 1024 ||
|
|
322
|
+
config.maxOutput > PG_MAX_CAP ||
|
|
323
|
+
config.maxInFlight < 1 ||
|
|
324
|
+
config.maxColumns < 1 ||
|
|
325
|
+
config.maxColumns > 65535 ||
|
|
326
|
+
config.minIterations < 1 ||
|
|
327
|
+
config.maxIterations < config.minIterations ||
|
|
328
|
+
config.maxIterations > PG_MAX_ITERATIONS
|
|
329
|
+
) {
|
|
330
|
+
panic("nish/net/postgres: a PgConfig cap out of range")
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
pgHasNul(config.user) ||
|
|
334
|
+
pgHasNul(config.database) ||
|
|
335
|
+
pgHasNul(config.applicationName) ||
|
|
336
|
+
pgHasNul(config.options)
|
|
337
|
+
) {
|
|
338
|
+
panic("nish/net/postgres: a startup parameter holds a NUL byte")
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The bytes of `text`. */
|
|
343
|
+
const pgBytesOf = (text: string): u8[] => {
|
|
344
|
+
const out: u8[] = []
|
|
345
|
+
const n: i32 = toI32(text.length)
|
|
346
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
347
|
+
out.push(toU8(text.charCodeAt(i)))
|
|
348
|
+
}
|
|
349
|
+
return out
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Whether `text` holds a NUL byte, which a protocol string cannot. */
|
|
353
|
+
const pgHasNul = (text: string): boolean => {
|
|
354
|
+
const n: i32 = toI32(text.length)
|
|
355
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
356
|
+
if (text.charCodeAt(i) === 0) {
|
|
357
|
+
return true
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return false
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The first `byte` in `b[from, end)`, or `end`. */
|
|
364
|
+
const pgFind = (b: u8[], from: i32, end: i32, byte: i32): i32 => {
|
|
365
|
+
if (from < 0 || end > toI32(b.length)) {
|
|
366
|
+
panic("nish/net/postgres: pgFind outside the buffer")
|
|
367
|
+
}
|
|
368
|
+
for (let i: i32 = from; i < end; i++) {
|
|
369
|
+
if (toI32(b[i]) === byte) {
|
|
370
|
+
return i
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return end
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Whether `b[start, start + length)` spells `text`. */
|
|
377
|
+
const pgBytesAre = (b: u8[], start: i32, length: i32, text: string): boolean => {
|
|
378
|
+
const n: i32 = toI32(text.length)
|
|
379
|
+
if (length !== n || start < 0 || start > toI32(b.length) - n) {
|
|
380
|
+
return false
|
|
381
|
+
}
|
|
382
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
383
|
+
if (toI32(b[start + i]) !== toI32(text.charCodeAt(i))) {
|
|
384
|
+
return false
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return true
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The salted password, PBKDF2-HMAC-SHA-256 over the password and the salt
|
|
392
|
+
* (RFC 5802 §3, `Hi`), 32 bytes, or `null` when it cannot be computed.
|
|
393
|
+
*
|
|
394
|
+
* The caller has bounded `iterations` by `maxIterations`, since the time is
|
|
395
|
+
* linear in it.
|
|
396
|
+
*/
|
|
397
|
+
const pgSaltedPassword = (password: Secret<u8[]>, salt: u8[], iterations: i32): Secret<u8[]> | null =>
|
|
398
|
+
pbkdf2HmacSha256(password, salt, iterations, 32)
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* SHA-256 of `data` through a hasher of its own, whose state, block and
|
|
402
|
+
* schedule are wiped before it returns: the one-shot `sha256` leaves the last
|
|
403
|
+
* block of its input in a hasher nothing wipes, and here the input is
|
|
404
|
+
* ClientKey.
|
|
405
|
+
*/
|
|
406
|
+
const pgSha256Wiped = (data: u8[]): u8[] => {
|
|
407
|
+
const h = new Sha256()
|
|
408
|
+
h.update(data, toI32(0), toI32(data.length))
|
|
409
|
+
const digest: u8[] = h.digest()
|
|
410
|
+
wipe(h.state)
|
|
411
|
+
wipe(h.block)
|
|
412
|
+
wipe(h.schedule)
|
|
413
|
+
h.fill = 0
|
|
414
|
+
h.total = 0
|
|
415
|
+
h.finished = true
|
|
416
|
+
return digest
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* RFC 5802 §3 from the salted password and the AuthMessage: the ClientProof
|
|
421
|
+
* (32 bytes) followed by the ServerSignature (32 bytes). Every key in between
|
|
422
|
+
* is wiped before it returns.
|
|
423
|
+
*/
|
|
424
|
+
const pgScramKeys = (salted: u8[], authMessage: u8[]): u8[] => {
|
|
425
|
+
const clientKey: u8[] = hmacSha256(salted, pgBytesOf("Client Key"))
|
|
426
|
+
const storedKey: u8[] = pgSha256Wiped(clientKey)
|
|
427
|
+
const clientSignature: u8[] = hmacSha256(storedKey, authMessage)
|
|
428
|
+
const serverKey: u8[] = hmacSha256(salted, pgBytesOf("Server Key"))
|
|
429
|
+
const serverSignature: u8[] = hmacSha256(serverKey, authMessage)
|
|
430
|
+
const out: u8[] = new Array<u8>(64)
|
|
431
|
+
if (clientKey.length < 32 || clientSignature.length < 32 || serverSignature.length < 32) {
|
|
432
|
+
panic("nish/net/postgres: an HMAC-SHA-256 shorter than 32 bytes")
|
|
433
|
+
}
|
|
434
|
+
for (let i: i32 = 0; i < 32; i++) {
|
|
435
|
+
out[i] = clientKey[i] ^ clientSignature[i]
|
|
436
|
+
out[32 + i] = serverSignature[i]
|
|
437
|
+
}
|
|
438
|
+
wipe(clientKey)
|
|
439
|
+
wipe(storedKey)
|
|
440
|
+
wipe(clientSignature)
|
|
441
|
+
wipe(serverKey)
|
|
442
|
+
return out
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Decoders for PostgreSQL's text format, in place; a bad input sets `failed`, which stays set. */
|
|
446
|
+
export class PgText {
|
|
447
|
+
/** Whether a decode since the last `reset()` was given something it could not read. */
|
|
448
|
+
failed: boolean = false
|
|
449
|
+
|
|
450
|
+
/** Clears `failed`. */
|
|
451
|
+
reset(): void {
|
|
452
|
+
this.failed = false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** An `int2`, `int4` or `int8` column: an optional `-` and 1 to 19 digits; 0 and `failed` otherwise, and on overflow. */
|
|
456
|
+
int(b: u8[], start: i32, length: i32): i64 {
|
|
457
|
+
const end: i32 = start + length
|
|
458
|
+
if (length <= 0 || start < 0 || end < start || end > toI32(b.length)) {
|
|
459
|
+
this.failed = true
|
|
460
|
+
return 0
|
|
461
|
+
}
|
|
462
|
+
const negative: boolean = toI32(b[start]) === 45
|
|
463
|
+
let i: i32 = start
|
|
464
|
+
if (negative) {
|
|
465
|
+
i++
|
|
466
|
+
}
|
|
467
|
+
if (i === end) {
|
|
468
|
+
this.failed = true
|
|
469
|
+
return 0
|
|
470
|
+
}
|
|
471
|
+
// Accumulated as a negative number, so that the most negative i64 fits.
|
|
472
|
+
const lastDigit: i64 = negative ? 8 : 7
|
|
473
|
+
// The most negative i64 divided by ten, which a literal cannot spell exactly.
|
|
474
|
+
const minTenth: i64 = toI64(-922337203) * 1000000000 - 685477580
|
|
475
|
+
let acc: i64 = 0
|
|
476
|
+
while (i < end) {
|
|
477
|
+
const d: i32 = toI32(b[i]) - 48
|
|
478
|
+
if (d < 0 || d > 9) {
|
|
479
|
+
this.failed = true
|
|
480
|
+
return 0
|
|
481
|
+
}
|
|
482
|
+
const digit: i64 = toI64(d)
|
|
483
|
+
if (acc < minTenth || (acc === minTenth && digit > lastDigit)) {
|
|
484
|
+
this.failed = true
|
|
485
|
+
return 0
|
|
486
|
+
}
|
|
487
|
+
acc = acc * 10 - digit
|
|
488
|
+
i++
|
|
489
|
+
}
|
|
490
|
+
return negative ? acc : -acc
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** A `bool` column: `t` or `f`; false and `failed` otherwise. */
|
|
494
|
+
bool(b: u8[], start: i32, length: i32): boolean {
|
|
495
|
+
if (length === 1 && start >= 0 && start < toI32(b.length)) {
|
|
496
|
+
const c: i32 = toI32(b[start])
|
|
497
|
+
if (c === 116) {
|
|
498
|
+
return true
|
|
499
|
+
}
|
|
500
|
+
if (c === 102) {
|
|
501
|
+
return false
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
this.failed = true
|
|
505
|
+
return false
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* A `timestamptz` column, as the connection's `DateStyle=ISO` and
|
|
510
|
+
* `TimeZone=UTC` spell it (`2024-01-02 03:04:05.123456+00`), or a
|
|
511
|
+
* `timestamp` column (the same without `+00`), written to `out[at, at + 24)`
|
|
512
|
+
* as `2024-01-02T03:04:05.123Z`, the fraction cut to milliseconds. Answers
|
|
513
|
+
* 24, or -1 and `failed` for anything else: a year outside 0001–9999, BC,
|
|
514
|
+
* `infinity`, an offset other than `+00`.
|
|
515
|
+
*/
|
|
516
|
+
timestamp(b: u8[], start: i32, length: i32, out: u8[], at: i32): i32 {
|
|
517
|
+
if (
|
|
518
|
+
start < 0 ||
|
|
519
|
+
length < 19 ||
|
|
520
|
+
start > toI32(b.length) - length ||
|
|
521
|
+
at < 0 ||
|
|
522
|
+
at + 24 > toI32(out.length)
|
|
523
|
+
) {
|
|
524
|
+
this.failed = true
|
|
525
|
+
return -1
|
|
526
|
+
}
|
|
527
|
+
// The fixed part: YYYY-MM-DD HH:MM:SS. Its five separators are where they
|
|
528
|
+
// belong and are not digits, so the other fourteen bytes are digits
|
|
529
|
+
// exactly when the part holds five non-digits.
|
|
530
|
+
let p: i32 = start + 19
|
|
531
|
+
const end: i32 = start + length
|
|
532
|
+
if (p < 0 || end > toI32(b.length)) {
|
|
533
|
+
this.failed = true
|
|
534
|
+
return -1
|
|
535
|
+
}
|
|
536
|
+
let others: i32 = 0
|
|
537
|
+
for (let i: i32 = start; i < p && i < toI32(b.length); i++) {
|
|
538
|
+
const c: i32 = toI32(b[i])
|
|
539
|
+
if (c < 48 || c > 57) {
|
|
540
|
+
others++
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (
|
|
544
|
+
others !== 5 ||
|
|
545
|
+
toI32(b[start + 4]) !== 45 ||
|
|
546
|
+
toI32(b[start + 7]) !== 45 ||
|
|
547
|
+
toI32(b[start + 10]) !== 32 ||
|
|
548
|
+
toI32(b[start + 13]) !== 58 ||
|
|
549
|
+
toI32(b[start + 16]) !== 58
|
|
550
|
+
) {
|
|
551
|
+
this.failed = true
|
|
552
|
+
return -1
|
|
553
|
+
}
|
|
554
|
+
let millis: i32 = 0
|
|
555
|
+
if (p < end && toI32(b[p]) === 46) {
|
|
556
|
+
p++
|
|
557
|
+
let digits: i32 = 0
|
|
558
|
+
while (p < end && toI32(b[p]) >= 48 && toI32(b[p]) <= 57) {
|
|
559
|
+
if (digits < 3) {
|
|
560
|
+
millis = millis * 10 + toI32(b[p]) - 48
|
|
561
|
+
}
|
|
562
|
+
digits++
|
|
563
|
+
p++
|
|
564
|
+
}
|
|
565
|
+
if (digits < 1 || digits > 6) {
|
|
566
|
+
this.failed = true
|
|
567
|
+
return -1
|
|
568
|
+
}
|
|
569
|
+
while (digits < 3) {
|
|
570
|
+
millis = millis * 10
|
|
571
|
+
digits++
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (p + 3 === end && toI32(b[p]) === 43 && toI32(b[p + 1]) === 48 && toI32(b[p + 2]) === 48) {
|
|
575
|
+
p = end
|
|
576
|
+
}
|
|
577
|
+
// The year's four bytes are digits, so their OR is '0' exactly when the year is 0000.
|
|
578
|
+
const year: i32 = toI32(b[start]) | toI32(b[start + 1]) | toI32(b[start + 2]) | toI32(b[start + 3])
|
|
579
|
+
if (p !== end || year === 48) {
|
|
580
|
+
this.failed = true
|
|
581
|
+
return -1
|
|
582
|
+
}
|
|
583
|
+
copyBytes(out, at, b, start, 19)
|
|
584
|
+
out[at + 10] = toU8(84)
|
|
585
|
+
out[at + 19] = toU8(46)
|
|
586
|
+
out[at + 20] = toU8(millis / 100 + 48)
|
|
587
|
+
out[at + 21] = toU8(((millis / 10) % 10) + 48)
|
|
588
|
+
out[at + 22] = toU8((millis % 10) + 48)
|
|
589
|
+
out[at + 23] = toU8(90)
|
|
590
|
+
return 24
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* A walk over a one-dimensional `text[]` column in place: `start` the
|
|
596
|
+
* column, then `next()` until `PG_ARRAY_END` or `PG_ARRAY_BAD`. Each element
|
|
597
|
+
* is the window `[start, start + length)` of the column's buffer; a quoted
|
|
598
|
+
* element with a backslash escape in it has `escaped` set, and `copy` writes
|
|
599
|
+
* its bytes unescaped. An unquoted `NULL`, in any case, is `PG_ARRAY_NULL`.
|
|
600
|
+
* Dimension decorations (`[1:2]={...}`) and nested arrays are `PG_ARRAY_BAD`.
|
|
601
|
+
*/
|
|
602
|
+
export class PgArrayWalker {
|
|
603
|
+
/** The current element's first byte in the buffer. */
|
|
604
|
+
start: i32 = 0
|
|
605
|
+
/** The current element's length, escapes included. */
|
|
606
|
+
length: i32 = 0
|
|
607
|
+
/** Whether the current element holds a backslash escape. */
|
|
608
|
+
escaped: boolean = false
|
|
609
|
+
private pos: i32 = 0
|
|
610
|
+
private end: i32 = 0
|
|
611
|
+
private first: boolean = true
|
|
612
|
+
private bad: boolean = false
|
|
613
|
+
private b: u8[]
|
|
614
|
+
|
|
615
|
+
constructor() {
|
|
616
|
+
this.b = []
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Starts a walk over `b[start, start + length)`. */
|
|
620
|
+
begin(b: u8[], start: i32, length: i32): void {
|
|
621
|
+
this.b = b
|
|
622
|
+
this.first = true
|
|
623
|
+
this.bad = !(start >= 0 && length >= 2 && start + length <= toI32(b.length))
|
|
624
|
+
if (!this.bad) {
|
|
625
|
+
this.bad = toI32(b[start]) !== 123 || toI32(b[start + length - 1]) !== 125
|
|
626
|
+
}
|
|
627
|
+
this.pos = start + 1
|
|
628
|
+
this.end = start + length - 1
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private refuse(): i32 {
|
|
632
|
+
this.bad = true
|
|
633
|
+
return PG_ARRAY_BAD
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** The next element: `PG_ARRAY_ELEMENT`, `PG_ARRAY_NULL`, `PG_ARRAY_END` or `PG_ARRAY_BAD`. */
|
|
637
|
+
next(): i32 {
|
|
638
|
+
if (this.bad) {
|
|
639
|
+
return PG_ARRAY_BAD
|
|
640
|
+
}
|
|
641
|
+
const b: u8[] = this.b
|
|
642
|
+
const end: i32 = this.end
|
|
643
|
+
if (this.pos < 0 || end > toI32(b.length)) {
|
|
644
|
+
return this.refuse()
|
|
645
|
+
}
|
|
646
|
+
if (this.first) {
|
|
647
|
+
this.first = false
|
|
648
|
+
if (this.pos === end) {
|
|
649
|
+
return PG_ARRAY_END
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
if (this.pos === end) {
|
|
653
|
+
return PG_ARRAY_END
|
|
654
|
+
}
|
|
655
|
+
if (toI32(b[this.pos]) !== 44) {
|
|
656
|
+
return this.refuse()
|
|
657
|
+
}
|
|
658
|
+
this.pos += 1
|
|
659
|
+
}
|
|
660
|
+
let p: i32 = this.pos
|
|
661
|
+
if (p < 0) {
|
|
662
|
+
return this.refuse()
|
|
663
|
+
}
|
|
664
|
+
this.escaped = false
|
|
665
|
+
if (p < end && toI32(b[p]) === 34) {
|
|
666
|
+
p++
|
|
667
|
+
this.start = p
|
|
668
|
+
while (p < end && toI32(b[p]) !== 34) {
|
|
669
|
+
if (toI32(b[p]) === 92) {
|
|
670
|
+
this.escaped = true
|
|
671
|
+
p++
|
|
672
|
+
}
|
|
673
|
+
p++
|
|
674
|
+
}
|
|
675
|
+
if (p >= end) {
|
|
676
|
+
return this.refuse()
|
|
677
|
+
}
|
|
678
|
+
this.length = p - this.start
|
|
679
|
+
this.pos = p + 1
|
|
680
|
+
if (this.pos !== end && toI32(b[this.pos]) !== 44) {
|
|
681
|
+
return this.refuse()
|
|
682
|
+
}
|
|
683
|
+
return PG_ARRAY_ELEMENT
|
|
684
|
+
}
|
|
685
|
+
this.start = p
|
|
686
|
+
while (p < end && toI32(b[p]) !== 44) {
|
|
687
|
+
const c: i32 = toI32(b[p])
|
|
688
|
+
if (c === 123 || c === 125 || c === 34 || c === 92 || c === 32) {
|
|
689
|
+
return this.refuse()
|
|
690
|
+
}
|
|
691
|
+
p++
|
|
692
|
+
}
|
|
693
|
+
this.length = p - this.start
|
|
694
|
+
this.pos = p
|
|
695
|
+
if (this.length === 0) {
|
|
696
|
+
return this.refuse()
|
|
697
|
+
}
|
|
698
|
+
if (this.length === 4 && p >= 4) {
|
|
699
|
+
const n: i32 = toI32(b[p - 4]) | 32
|
|
700
|
+
const u: i32 = toI32(b[p - 3]) | 32
|
|
701
|
+
const l1: i32 = toI32(b[p - 2]) | 32
|
|
702
|
+
const l2: i32 = toI32(b[p - 1]) | 32
|
|
703
|
+
if (n === 110 && u === 117 && l1 === 108 && l2 === 108) {
|
|
704
|
+
return PG_ARRAY_NULL
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return PG_ARRAY_ELEMENT
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Writes the current element, unescaped, to `out[at, ...)`; answers its length, or -1 when it does not fit. */
|
|
711
|
+
copy(out: u8[], at: i32): i32 {
|
|
712
|
+
const b: u8[] = this.b
|
|
713
|
+
let n: i32 = 0
|
|
714
|
+
let p: i32 = this.start
|
|
715
|
+
const end: i32 = this.start + this.length
|
|
716
|
+
if (p < 0 || end > toI32(b.length)) {
|
|
717
|
+
return -1
|
|
718
|
+
}
|
|
719
|
+
while (p < end) {
|
|
720
|
+
if (toI32(b[p]) === 92) {
|
|
721
|
+
p++
|
|
722
|
+
}
|
|
723
|
+
if (p >= end || at + n >= toI32(out.length) || at < 0) {
|
|
724
|
+
return -1
|
|
725
|
+
}
|
|
726
|
+
out[at + n] = b[p]
|
|
727
|
+
n++
|
|
728
|
+
p++
|
|
729
|
+
}
|
|
730
|
+
return n
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* One connection to a PostgreSQL server, sans IO: the program moves bytes
|
|
736
|
+
* between the socket and the two buffers, and `next()` says what they meant.
|
|
737
|
+
*/
|
|
738
|
+
export class PgConnection {
|
|
739
|
+
/** Why the connection failed, `PG_ERR_NONE` while it has not. */
|
|
740
|
+
failure: i32 = 0
|
|
741
|
+
/** The columns of the current row, or of the statement just described. */
|
|
742
|
+
columns: i32 = 0
|
|
743
|
+
/** The transaction status the last `PG_GROUP_DONE` reported: `I` idle, `T` in a transaction, `E` in a failed one (as a byte). */
|
|
744
|
+
txStatus: i32 = 0
|
|
745
|
+
/** The number of groups sent and not yet answered. */
|
|
746
|
+
inFlight: i32 = 0
|
|
747
|
+
/** The server's process id, from BackendKeyData. */
|
|
748
|
+
backendPid: i32 = 0
|
|
749
|
+
/** The number of notices received, stopping at 2^31 - 1. */
|
|
750
|
+
notices: i32 = 0
|
|
751
|
+
/** The server's `server_version` parameter. */
|
|
752
|
+
serverVersion: string = ""
|
|
753
|
+
/** The last ErrorResponse, kept until the next. */
|
|
754
|
+
lastError: PgError | null = null
|
|
755
|
+
/** The last NoticeResponse, kept until the next. */
|
|
756
|
+
lastNotice: PgError | null = null
|
|
757
|
+
/** The decoders `int`, `bool` and `isNull` use; `text.failed` says whether one was given a column it could not read. */
|
|
758
|
+
readonly text: PgText
|
|
759
|
+
|
|
760
|
+
private readonly config: PgConfig
|
|
761
|
+
private readonly password: PgPassword
|
|
762
|
+
private state: i32 = 0
|
|
763
|
+
private input: u8[]
|
|
764
|
+
private inStart: i32 = 0
|
|
765
|
+
private inEnd: i32 = 0
|
|
766
|
+
private out: u8[]
|
|
767
|
+
private outStart: i32 = 0
|
|
768
|
+
private outEnd: i32 = 0
|
|
769
|
+
private colStarts: i32[]
|
|
770
|
+
private colLengths: i32[]
|
|
771
|
+
private colTypes: i32[]
|
|
772
|
+
private tagStart: i32 = 0
|
|
773
|
+
private tagLength: i32 = 0
|
|
774
|
+
private errorInGroup: boolean = false
|
|
775
|
+
private groupOpen: boolean = false
|
|
776
|
+
/** Whether a statement in the group `PG_GROUP_DONE` just finished failed. */
|
|
777
|
+
groupFailed: boolean = false
|
|
778
|
+
private bindAt: i32 = -1
|
|
779
|
+
private paramCountAt: i32 = 0
|
|
780
|
+
private paramCount: i32 = 0
|
|
781
|
+
private clientNonce: string = ""
|
|
782
|
+
private serverSignature: u8[]
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* A connection that has queued its startup message. `password` is copied,
|
|
786
|
+
* so the caller may wipe its own; `nonce` must be at least 18 bytes from a
|
|
787
|
+
* cryptographic random source (`crypto.getRandomValues`), and is fixed only
|
|
788
|
+
* in a test.
|
|
789
|
+
*/
|
|
790
|
+
constructor(config: PgConfig, password: Secret<u8[]>, nonce: u8[]) {
|
|
791
|
+
if (toI32(nonce.length) < PG_NONCE_BYTES) {
|
|
792
|
+
panic("nish/net/postgres: the SCRAM nonce needs at least 18 random bytes")
|
|
793
|
+
}
|
|
794
|
+
pgCheckConfig(config)
|
|
795
|
+
this.config = config
|
|
796
|
+
this.text = new PgText()
|
|
797
|
+
this.password = new PgPassword(password)
|
|
798
|
+
const inputSize: i32 = config.inputSize < 64 ? 64 : config.inputSize
|
|
799
|
+
this.input = new Array<u8>(inputSize)
|
|
800
|
+
this.out = new Array<u8>(1024)
|
|
801
|
+
this.colStarts = new Array<i32>(16)
|
|
802
|
+
this.colLengths = new Array<i32>(16)
|
|
803
|
+
this.colTypes = new Array<i32>(16)
|
|
804
|
+
this.serverSignature = []
|
|
805
|
+
const head: u8[] = []
|
|
806
|
+
for (let i: i32 = 0; i < PG_NONCE_BYTES && i < toI32(nonce.length); i++) {
|
|
807
|
+
head.push(nonce[i])
|
|
808
|
+
}
|
|
809
|
+
this.clientNonce = base64StdPadEncode(head)
|
|
810
|
+
|
|
811
|
+
const at: i32 = this.outEnd
|
|
812
|
+
this.putI32(PG_ZERO)
|
|
813
|
+
this.putI32(PG_PROTOCOL)
|
|
814
|
+
this.putParam("user", config.user)
|
|
815
|
+
if (config.database.length > 0) {
|
|
816
|
+
this.putParam("database", config.database)
|
|
817
|
+
}
|
|
818
|
+
this.putParam("client_encoding", "UTF8")
|
|
819
|
+
this.putParam("DateStyle", "ISO")
|
|
820
|
+
this.putParam("TimeZone", "UTC")
|
|
821
|
+
if (config.applicationName.length > 0) {
|
|
822
|
+
this.putParam("application_name", config.applicationName)
|
|
823
|
+
}
|
|
824
|
+
if (config.options.length > 0) {
|
|
825
|
+
this.putParam("options", config.options)
|
|
826
|
+
}
|
|
827
|
+
this.putByte(PG_ZERO)
|
|
828
|
+
setI32BE(this.out, at, this.outEnd - at)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// --- Buffers -----------------------------------------------------------------
|
|
832
|
+
|
|
833
|
+
/** The input buffer: read from the socket into it at `inputEnd()`. */
|
|
834
|
+
inputBuffer(): u8[] {
|
|
835
|
+
return this.input
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** Where the next byte read goes. */
|
|
839
|
+
inputEnd(): i32 {
|
|
840
|
+
return this.inEnd
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** How many bytes may be read into the input buffer now. */
|
|
844
|
+
inputRoom(): i32 {
|
|
845
|
+
return toI32(this.input.length) - this.inEnd
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Records that `n` bytes were read into the input buffer at `inputEnd()`. */
|
|
849
|
+
received(n: i32): void {
|
|
850
|
+
if (n < 0 || n > this.inputRoom()) {
|
|
851
|
+
panic("nish/net/postgres: received more than inputRoom()")
|
|
852
|
+
}
|
|
853
|
+
this.inEnd += n
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** Copies what fits of `src[start, start + length)` into the input buffer; answers how much. */
|
|
857
|
+
receive(src: u8[], start: i32, length: i32): i32 {
|
|
858
|
+
if (start < 0 || length < 0 || length > toI32(src.length) - start) {
|
|
859
|
+
panic("nish/net/postgres: receive() outside its source")
|
|
860
|
+
}
|
|
861
|
+
const room: i32 = this.inputRoom()
|
|
862
|
+
const n: i32 = length < room ? length : room
|
|
863
|
+
copyBytes(this.input, this.inEnd, src, start, n)
|
|
864
|
+
this.inEnd += n
|
|
865
|
+
return n
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** The output buffer: write `[outputStart(), outputEnd())` to the socket. */
|
|
869
|
+
outputBuffer(): u8[] {
|
|
870
|
+
return this.out
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** The first byte not yet sent. */
|
|
874
|
+
outputStart(): i32 {
|
|
875
|
+
return this.outStart
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** One past the last byte that may be sent: a Bind still open is held back until `bindEnd()` has written its lengths. */
|
|
879
|
+
outputEnd(): i32 {
|
|
880
|
+
return this.bindAt >= 0 ? this.bindAt : this.outEnd
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Records that `n` bytes from `outputStart()` were written to the socket. */
|
|
884
|
+
sent(n: i32): void {
|
|
885
|
+
if (n < 0 || n > this.outputEnd() - this.outStart) {
|
|
886
|
+
panic("nish/net/postgres: sent more than was queued")
|
|
887
|
+
}
|
|
888
|
+
this.outStart += n
|
|
889
|
+
if (this.outStart === this.outEnd) {
|
|
890
|
+
this.outStart = 0
|
|
891
|
+
this.outEnd = 0
|
|
892
|
+
} else if (this.outStart >= toI32(this.out.length) / 2) {
|
|
893
|
+
this.compactOutput()
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Reports the end of the stream: unless it already has, the connection fails with `PG_ERR_CLOSED`. */
|
|
898
|
+
closed(): i32 {
|
|
899
|
+
if (this.state !== PG_STATE_FAILED) {
|
|
900
|
+
return this.fail(PG_ERR_CLOSED)
|
|
901
|
+
}
|
|
902
|
+
return PG_FAILED
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Whether every byte of the connection's copy of the password is zero: true
|
|
907
|
+
* once the proof is computed, the connection has failed or `dispose()` has
|
|
908
|
+
* run (and for an empty password). It reads the copy itself, so a test can
|
|
909
|
+
* see the wipe happen; a program has no use for it.
|
|
910
|
+
*/
|
|
911
|
+
passwordWiped(): boolean {
|
|
912
|
+
return this.password.wiped()
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** Wipes the password if the connection still holds it. A program that drops a connection before it is ready calls this. */
|
|
916
|
+
dispose(): void {
|
|
917
|
+
if (!this.password.gone) {
|
|
918
|
+
this.password.wipe()
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// --- Reading -----------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
/** The next event from the input. */
|
|
925
|
+
next(): i32 {
|
|
926
|
+
while (true) {
|
|
927
|
+
if (this.state === PG_STATE_FAILED) {
|
|
928
|
+
return PG_FAILED
|
|
929
|
+
}
|
|
930
|
+
const avail: i32 = this.inEnd - this.inStart
|
|
931
|
+
if (avail < 5) {
|
|
932
|
+
this.compact()
|
|
933
|
+
return PG_NEED_MORE
|
|
934
|
+
}
|
|
935
|
+
const at: i32 = this.inStart
|
|
936
|
+
const b: u8[] = this.input
|
|
937
|
+
// inStart and inEnd stay within the buffer, so this test never fails; it proves the indices.
|
|
938
|
+
if (at < 0 || at + 4 >= toI32(b.length)) {
|
|
939
|
+
return this.fail(PG_ERR_FRAME)
|
|
940
|
+
}
|
|
941
|
+
const type: i32 = toI32(b[at])
|
|
942
|
+
// getI32BE written out, as in readRow: this runs once per message.
|
|
943
|
+
const length: i32 =
|
|
944
|
+
(toI32(b[at + 1]) << 24) | (toI32(b[at + 2]) << 16) | (toI32(b[at + 3]) << 8) | toI32(b[at + 4])
|
|
945
|
+
if (length < 4) {
|
|
946
|
+
return this.fail(PG_ERR_FRAME)
|
|
947
|
+
}
|
|
948
|
+
if (length > this.config.maxMessage) {
|
|
949
|
+
return this.fail(PG_ERR_TOO_BIG)
|
|
950
|
+
}
|
|
951
|
+
if (avail - 1 < length) {
|
|
952
|
+
this.compact()
|
|
953
|
+
const size: i32 = toI32(this.input.length)
|
|
954
|
+
if (size < length + 1) {
|
|
955
|
+
// Doubled, up to one message of maxMessage, so that a run of growing messages copies each byte a bounded number of times.
|
|
956
|
+
const cap: i32 = this.config.maxMessage + 1
|
|
957
|
+
const doubled: i32 = size < cap / 2 ? size * 2 : cap
|
|
958
|
+
const grown: u8[] = new Array<u8>(doubled > length + 1 ? doubled : length + 1)
|
|
959
|
+
grown.set(this.input, 0)
|
|
960
|
+
this.input = grown
|
|
961
|
+
}
|
|
962
|
+
return PG_NEED_MORE
|
|
963
|
+
}
|
|
964
|
+
this.inStart = at + 1 + length
|
|
965
|
+
const event: i32 = this.dispatch(type, at + 5, at + 1 + length)
|
|
966
|
+
if (event !== PG_NEED_MORE) {
|
|
967
|
+
return event
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** Moves the unread input, at most one partial message, to the start of the buffer. */
|
|
973
|
+
private compact(): void {
|
|
974
|
+
const start: i32 = this.inStart
|
|
975
|
+
if (start === 0) {
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
const n: i32 = this.inEnd - start
|
|
979
|
+
copyBytes(this.input, 0, this.input, start, n)
|
|
980
|
+
this.inStart = 0
|
|
981
|
+
this.inEnd = n
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** Fails the connection: the password is wiped and every later `next()` answers `PG_FAILED`. */
|
|
985
|
+
private fail(code: i32): i32 {
|
|
986
|
+
this.state = PG_STATE_FAILED
|
|
987
|
+
this.failure = code
|
|
988
|
+
this.dispose()
|
|
989
|
+
wipe(this.serverSignature)
|
|
990
|
+
this.serverSignature = []
|
|
991
|
+
return PG_FAILED
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** One message, `[body, end)` its contents; answers its event, or `PG_NEED_MORE` when it has none. */
|
|
995
|
+
private dispatch(type: i32, body: i32, end: i32): i32 {
|
|
996
|
+
const state: i32 = this.state
|
|
997
|
+
if (type === 82) {
|
|
998
|
+
// 'R': an authentication request.
|
|
999
|
+
if (state >= PG_STATE_STARTING || end - body < 4) {
|
|
1000
|
+
return this.fail(end - body < 4 ? PG_ERR_FRAME : PG_ERR_UNEXPECTED)
|
|
1001
|
+
}
|
|
1002
|
+
return this.authenticate(getI32BE(this.input, body), body + 4, end)
|
|
1003
|
+
}
|
|
1004
|
+
if (type === 69 || type === 78) {
|
|
1005
|
+
// 'E' and 'N': an error or a notice.
|
|
1006
|
+
const error: PgError | null = this.readError(body, end)
|
|
1007
|
+
if (error === null) {
|
|
1008
|
+
return this.fail(PG_ERR_FRAME)
|
|
1009
|
+
}
|
|
1010
|
+
if (type === 78) {
|
|
1011
|
+
if (this.notices < 2147483647) {
|
|
1012
|
+
this.notices += 1
|
|
1013
|
+
}
|
|
1014
|
+
this.lastNotice = error
|
|
1015
|
+
return PG_NOTICE
|
|
1016
|
+
}
|
|
1017
|
+
this.lastError = error
|
|
1018
|
+
if (state < PG_STATE_READY || error.severity === "FATAL" || error.severity === "PANIC") {
|
|
1019
|
+
return this.fail(PG_ERR_SERVER)
|
|
1020
|
+
}
|
|
1021
|
+
if (this.inFlight === 0) {
|
|
1022
|
+
return this.fail(PG_ERR_UNEXPECTED)
|
|
1023
|
+
}
|
|
1024
|
+
this.errorInGroup = true
|
|
1025
|
+
return PG_ERROR
|
|
1026
|
+
}
|
|
1027
|
+
if (type === 83) {
|
|
1028
|
+
// 'S': ParameterStatus, name and value.
|
|
1029
|
+
const nameEnd: i32 = pgFind(this.input, body, end, 0)
|
|
1030
|
+
if (nameEnd === end || pgFind(this.input, nameEnd + 1, end, 0) !== end - 1) {
|
|
1031
|
+
return this.fail(PG_ERR_FRAME)
|
|
1032
|
+
}
|
|
1033
|
+
if (pgBytesAre(this.input, body, nameEnd - body, "client_encoding")) {
|
|
1034
|
+
if (!pgBytesAre(this.input, nameEnd + 1, end - 2 - nameEnd, "UTF8")) {
|
|
1035
|
+
return this.fail(PG_ERR_ENCODING)
|
|
1036
|
+
}
|
|
1037
|
+
} else if (pgBytesAre(this.input, body, nameEnd - body, "DateStyle")) {
|
|
1038
|
+
// "ISO, MDY": the output style comes first, the input order after the comma.
|
|
1039
|
+
const valueLength: i32 = end - 2 - nameEnd
|
|
1040
|
+
const iso: boolean = valueLength >= 3 && pgBytesAre(this.input, nameEnd + 1, 3, "ISO")
|
|
1041
|
+
if (!iso || (valueLength > 3 && toI32(this.input[nameEnd + 4]) !== PG_COMMA)) {
|
|
1042
|
+
return this.fail(PG_ERR_SETTING)
|
|
1043
|
+
}
|
|
1044
|
+
} else if (pgBytesAre(this.input, body, nameEnd - body, "TimeZone")) {
|
|
1045
|
+
if (!pgBytesAre(this.input, nameEnd + 1, end - 2 - nameEnd, "UTC")) {
|
|
1046
|
+
return this.fail(PG_ERR_SETTING)
|
|
1047
|
+
}
|
|
1048
|
+
} else if (pgBytesAre(this.input, body, nameEnd - body, "server_version")) {
|
|
1049
|
+
this.serverVersion = String.fromBytes(this.input, nameEnd + 1, end - 1)
|
|
1050
|
+
}
|
|
1051
|
+
return PG_NEED_MORE
|
|
1052
|
+
}
|
|
1053
|
+
if (type === 65) {
|
|
1054
|
+
// 'A': a notification, for a LISTEN this client never sends.
|
|
1055
|
+
return PG_NEED_MORE
|
|
1056
|
+
}
|
|
1057
|
+
if (state < PG_STATE_STARTING) {
|
|
1058
|
+
return this.fail(PG_ERR_UNEXPECTED)
|
|
1059
|
+
}
|
|
1060
|
+
if (type === 75) {
|
|
1061
|
+
// 'K': BackendKeyData, the process id and the cancel key.
|
|
1062
|
+
if (state !== PG_STATE_STARTING || end - body !== 8) {
|
|
1063
|
+
return this.fail(state !== PG_STATE_STARTING ? PG_ERR_UNEXPECTED : PG_ERR_FRAME)
|
|
1064
|
+
}
|
|
1065
|
+
this.backendPid = getI32BE(this.input, body)
|
|
1066
|
+
return PG_NEED_MORE
|
|
1067
|
+
}
|
|
1068
|
+
if (type === 90) {
|
|
1069
|
+
// 'Z': ReadyForQuery, and the transaction status.
|
|
1070
|
+
if (end - body !== 1) {
|
|
1071
|
+
return this.fail(PG_ERR_FRAME)
|
|
1072
|
+
}
|
|
1073
|
+
const status: i32 = toI32(this.input[body])
|
|
1074
|
+
if (status !== 73 && status !== 84 && status !== 69) {
|
|
1075
|
+
return this.fail(PG_ERR_FRAME)
|
|
1076
|
+
}
|
|
1077
|
+
this.txStatus = status
|
|
1078
|
+
if (state === PG_STATE_STARTING) {
|
|
1079
|
+
this.state = PG_STATE_READY
|
|
1080
|
+
return PG_READY
|
|
1081
|
+
}
|
|
1082
|
+
if (this.inFlight === 0 || (this.inFlight === 1 && this.groupOpen)) {
|
|
1083
|
+
return this.fail(PG_ERR_UNEXPECTED)
|
|
1084
|
+
}
|
|
1085
|
+
this.inFlight -= 1
|
|
1086
|
+
this.groupFailed = this.errorInGroup
|
|
1087
|
+
this.errorInGroup = false
|
|
1088
|
+
return PG_GROUP_DONE
|
|
1089
|
+
}
|
|
1090
|
+
if (state !== PG_STATE_READY || this.inFlight === 0) {
|
|
1091
|
+
return this.fail(PG_ERR_UNEXPECTED)
|
|
1092
|
+
}
|
|
1093
|
+
if (type === 68) {
|
|
1094
|
+
return this.readRow(body, end)
|
|
1095
|
+
}
|
|
1096
|
+
if (type === 67) {
|
|
1097
|
+
// 'C': CommandComplete, the tag.
|
|
1098
|
+
if (end - body < 1 || pgFind(this.input, body, end, 0) !== end - 1) {
|
|
1099
|
+
return this.fail(PG_ERR_FRAME)
|
|
1100
|
+
}
|
|
1101
|
+
this.tagStart = body
|
|
1102
|
+
this.tagLength = end - 1 - body
|
|
1103
|
+
return PG_COMPLETE
|
|
1104
|
+
}
|
|
1105
|
+
if (type === 73) {
|
|
1106
|
+
// 'I': EmptyQueryResponse, an empty statement's completion.
|
|
1107
|
+
if (end !== body) {
|
|
1108
|
+
return this.fail(PG_ERR_FRAME)
|
|
1109
|
+
}
|
|
1110
|
+
this.tagStart = body
|
|
1111
|
+
this.tagLength = 0
|
|
1112
|
+
return PG_COMPLETE
|
|
1113
|
+
}
|
|
1114
|
+
if (type === 49 || type === 50 || type === 51) {
|
|
1115
|
+
// '1', '2', '3': ParseComplete, BindComplete, CloseComplete.
|
|
1116
|
+
return end === body ? PG_NEED_MORE : this.fail(PG_ERR_FRAME)
|
|
1117
|
+
}
|
|
1118
|
+
if (type === 116) {
|
|
1119
|
+
// 't': ParameterDescription.
|
|
1120
|
+
if (end - body < 2 || end - body !== toI32(getU16BE(this.input, body)) * 4 + 2) {
|
|
1121
|
+
return this.fail(PG_ERR_FRAME)
|
|
1122
|
+
}
|
|
1123
|
+
return PG_NEED_MORE
|
|
1124
|
+
}
|
|
1125
|
+
if (type === 84) {
|
|
1126
|
+
return this.readDescription(body, end)
|
|
1127
|
+
}
|
|
1128
|
+
if (type === 110) {
|
|
1129
|
+
// 'n': NoData, a statement that returns no rows.
|
|
1130
|
+
if (end !== body) {
|
|
1131
|
+
return this.fail(PG_ERR_FRAME)
|
|
1132
|
+
}
|
|
1133
|
+
this.columns = 0
|
|
1134
|
+
return PG_DESCRIBED
|
|
1135
|
+
}
|
|
1136
|
+
return this.fail(PG_ERR_UNEXPECTED)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** Makes the column arrays hold at least `n`. */
|
|
1140
|
+
private reserveColumns(n: i32): void {
|
|
1141
|
+
if (n <= toI32(this.colStarts.length)) {
|
|
1142
|
+
return
|
|
1143
|
+
}
|
|
1144
|
+
const size: i32 = n < 2 * toI32(this.colStarts.length) ? 2 * toI32(this.colStarts.length) : n
|
|
1145
|
+
const starts: i32[] = new Array<i32>(size)
|
|
1146
|
+
const lengths: i32[] = new Array<i32>(size)
|
|
1147
|
+
const types: i32[] = new Array<i32>(size)
|
|
1148
|
+
starts.set(this.colStarts, 0)
|
|
1149
|
+
lengths.set(this.colLengths, 0)
|
|
1150
|
+
types.set(this.colTypes, 0)
|
|
1151
|
+
this.colStarts = starts
|
|
1152
|
+
this.colLengths = lengths
|
|
1153
|
+
this.colTypes = types
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** 'D': DataRow, each column's length (-1 for NULL) and bytes, which must fill the message exactly. */
|
|
1157
|
+
private readRow(body: i32, end: i32): i32 {
|
|
1158
|
+
if (end - body < 2) {
|
|
1159
|
+
return this.fail(PG_ERR_FRAME)
|
|
1160
|
+
}
|
|
1161
|
+
const b: u8[] = this.input
|
|
1162
|
+
const n: i32 = toI32(getU16BE(b, body))
|
|
1163
|
+
if (n > this.config.maxColumns) {
|
|
1164
|
+
return this.fail(PG_ERR_TOO_BIG)
|
|
1165
|
+
}
|
|
1166
|
+
this.reserveColumns(n)
|
|
1167
|
+
const starts: i32[] = this.colStarts
|
|
1168
|
+
const lengths: i32[] = this.colLengths
|
|
1169
|
+
let p: i32 = body + 2
|
|
1170
|
+
// reserveColumns made both arrays hold n; the length tests prove the indices.
|
|
1171
|
+
for (let c: i32 = 0; c < n && c < toI32(starts.length) && c < toI32(lengths.length); c++) {
|
|
1172
|
+
// The buffer tests never fail on a frame next() cut from it; they prove the indices.
|
|
1173
|
+
if (end - p < 4 || p < 0 || p + 3 >= toI32(b.length)) {
|
|
1174
|
+
return this.fail(PG_ERR_FRAME)
|
|
1175
|
+
}
|
|
1176
|
+
// getI32BE written out: a call per column was a fifth of the row's cost.
|
|
1177
|
+
const length: i32 =
|
|
1178
|
+
(toI32(b[p]) << 24) | (toI32(b[p + 1]) << 16) | (toI32(b[p + 2]) << 8) | toI32(b[p + 3])
|
|
1179
|
+
p += 4
|
|
1180
|
+
if (length < -1 || length > end - p) {
|
|
1181
|
+
return this.fail(PG_ERR_FRAME)
|
|
1182
|
+
}
|
|
1183
|
+
starts[c] = p
|
|
1184
|
+
lengths[c] = length
|
|
1185
|
+
if (length > 0) {
|
|
1186
|
+
p += length
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (p !== end) {
|
|
1190
|
+
return this.fail(PG_ERR_FRAME)
|
|
1191
|
+
}
|
|
1192
|
+
this.columns = n
|
|
1193
|
+
return PG_ROW
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** 'T': RowDescription; each field is a name and 18 bytes, the type's OID 6 bytes into them. */
|
|
1197
|
+
private readDescription(body: i32, end: i32): i32 {
|
|
1198
|
+
if (end - body < 2) {
|
|
1199
|
+
return this.fail(PG_ERR_FRAME)
|
|
1200
|
+
}
|
|
1201
|
+
const b: u8[] = this.input
|
|
1202
|
+
const n: i32 = toI32(getU16BE(b, body))
|
|
1203
|
+
if (n > this.config.maxColumns) {
|
|
1204
|
+
return this.fail(PG_ERR_TOO_BIG)
|
|
1205
|
+
}
|
|
1206
|
+
this.reserveColumns(n)
|
|
1207
|
+
let p: i32 = body + 2
|
|
1208
|
+
for (let c: i32 = 0; c < n; c++) {
|
|
1209
|
+
const nameEnd: i32 = pgFind(b, p, end, 0)
|
|
1210
|
+
if (end - nameEnd < 19) {
|
|
1211
|
+
return this.fail(PG_ERR_FRAME)
|
|
1212
|
+
}
|
|
1213
|
+
this.colTypes[c] = getI32BE(b, nameEnd + 7)
|
|
1214
|
+
p = nameEnd + 19
|
|
1215
|
+
}
|
|
1216
|
+
if (p !== end) {
|
|
1217
|
+
return this.fail(PG_ERR_FRAME)
|
|
1218
|
+
}
|
|
1219
|
+
this.columns = n
|
|
1220
|
+
return PG_DESCRIBED
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/** 'E' and 'N': the fields, each a type byte and a string, then a 0; `null` when they do not fill the message exactly. */
|
|
1224
|
+
private readError(body: i32, end: i32): PgError | null {
|
|
1225
|
+
const b: u8[] = this.input
|
|
1226
|
+
if (body < 0 || end > toI32(b.length)) {
|
|
1227
|
+
return null
|
|
1228
|
+
}
|
|
1229
|
+
let severity: string = ""
|
|
1230
|
+
let localised: string = ""
|
|
1231
|
+
let code: string = ""
|
|
1232
|
+
let message: string = ""
|
|
1233
|
+
let detail: string = ""
|
|
1234
|
+
let hint: string = ""
|
|
1235
|
+
let constraint: string = ""
|
|
1236
|
+
let position: string = ""
|
|
1237
|
+
let p: i32 = body
|
|
1238
|
+
while (true) {
|
|
1239
|
+
if (p < 0 || p >= end || p >= toI32(b.length)) {
|
|
1240
|
+
return null
|
|
1241
|
+
}
|
|
1242
|
+
const field: i32 = toI32(b[p])
|
|
1243
|
+
if (field === 0) {
|
|
1244
|
+
if (p !== end - 1) {
|
|
1245
|
+
return null
|
|
1246
|
+
}
|
|
1247
|
+
break
|
|
1248
|
+
}
|
|
1249
|
+
const valueEnd: i32 = pgFind(b, p + 1, end, 0)
|
|
1250
|
+
if (valueEnd === end) {
|
|
1251
|
+
return null
|
|
1252
|
+
}
|
|
1253
|
+
const value: string = String.fromBytes(b, p + 1, valueEnd)
|
|
1254
|
+
if (field === 86) {
|
|
1255
|
+
severity = value
|
|
1256
|
+
} else if (field === 83) {
|
|
1257
|
+
localised = value
|
|
1258
|
+
} else if (field === 67) {
|
|
1259
|
+
code = value
|
|
1260
|
+
} else if (field === 77) {
|
|
1261
|
+
message = value
|
|
1262
|
+
} else if (field === 68) {
|
|
1263
|
+
detail = value
|
|
1264
|
+
} else if (field === 72) {
|
|
1265
|
+
hint = value
|
|
1266
|
+
} else if (field === 110) {
|
|
1267
|
+
constraint = value
|
|
1268
|
+
} else if (field === 80) {
|
|
1269
|
+
position = value
|
|
1270
|
+
}
|
|
1271
|
+
p = valueEnd + 1
|
|
1272
|
+
}
|
|
1273
|
+
return new PgError(
|
|
1274
|
+
severity.length > 0 ? severity : localised,
|
|
1275
|
+
code,
|
|
1276
|
+
message,
|
|
1277
|
+
detail,
|
|
1278
|
+
hint,
|
|
1279
|
+
constraint,
|
|
1280
|
+
position
|
|
1281
|
+
)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// --- Authentication ----------------------------------------------------------
|
|
1285
|
+
|
|
1286
|
+
/** 'R': the authentication request `code`, its data at `[body, end)`. */
|
|
1287
|
+
private authenticate(code: i32, body: i32, end: i32): i32 {
|
|
1288
|
+
const state: i32 = this.state
|
|
1289
|
+
if (code === 0) {
|
|
1290
|
+
// AuthenticationOk: without a password, or after the server proved it knows it.
|
|
1291
|
+
if (
|
|
1292
|
+
state === PG_STATE_SCRAM_FIRST ||
|
|
1293
|
+
(state === PG_STATE_SCRAM_FINAL && this.serverSignature.length > 0)
|
|
1294
|
+
) {
|
|
1295
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1296
|
+
}
|
|
1297
|
+
if (end !== body) {
|
|
1298
|
+
return this.fail(PG_ERR_FRAME)
|
|
1299
|
+
}
|
|
1300
|
+
this.dispose()
|
|
1301
|
+
this.state = PG_STATE_STARTING
|
|
1302
|
+
return PG_NEED_MORE
|
|
1303
|
+
}
|
|
1304
|
+
if (code === 10 && state === PG_STATE_AUTH) {
|
|
1305
|
+
return this.scramStart(body, end)
|
|
1306
|
+
}
|
|
1307
|
+
if (code === 11 && state === PG_STATE_SCRAM_FIRST) {
|
|
1308
|
+
return this.scramContinue(body, end)
|
|
1309
|
+
}
|
|
1310
|
+
if (code === 12 && state === PG_STATE_SCRAM_FINAL) {
|
|
1311
|
+
return this.scramFinal(body, end)
|
|
1312
|
+
}
|
|
1313
|
+
if (state === PG_STATE_AUTH && code !== 10 && code !== 11 && code !== 12) {
|
|
1314
|
+
return this.fail(PG_ERR_AUTH_UNSUPPORTED)
|
|
1315
|
+
}
|
|
1316
|
+
return this.fail(PG_ERR_UNEXPECTED)
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** AuthenticationSASL: the mechanisms, each NUL-terminated, then a 0; SCRAM-SHA-256 must be among them. */
|
|
1320
|
+
private scramStart(body: i32, end: i32): i32 {
|
|
1321
|
+
const b: u8[] = this.input
|
|
1322
|
+
if (body < 0 || end > toI32(b.length)) {
|
|
1323
|
+
return this.fail(PG_ERR_FRAME)
|
|
1324
|
+
}
|
|
1325
|
+
let offered: boolean = false
|
|
1326
|
+
let p: i32 = body
|
|
1327
|
+
while (true) {
|
|
1328
|
+
if (p < 0 || p >= end || p >= toI32(b.length)) {
|
|
1329
|
+
return this.fail(PG_ERR_FRAME)
|
|
1330
|
+
}
|
|
1331
|
+
if (toI32(b[p]) === 0) {
|
|
1332
|
+
if (p !== end - 1) {
|
|
1333
|
+
return this.fail(PG_ERR_FRAME)
|
|
1334
|
+
}
|
|
1335
|
+
break
|
|
1336
|
+
}
|
|
1337
|
+
const nameEnd: i32 = pgFind(b, p, end, 0)
|
|
1338
|
+
if (nameEnd === end) {
|
|
1339
|
+
return this.fail(PG_ERR_FRAME)
|
|
1340
|
+
}
|
|
1341
|
+
if (pgBytesAre(b, p, nameEnd - p, "SCRAM-SHA-256")) {
|
|
1342
|
+
offered = true
|
|
1343
|
+
}
|
|
1344
|
+
p = nameEnd + 1
|
|
1345
|
+
}
|
|
1346
|
+
if (!offered) {
|
|
1347
|
+
return this.fail(PG_ERR_AUTH_UNSUPPORTED)
|
|
1348
|
+
}
|
|
1349
|
+
// SASLInitialResponse: the mechanism, then client-first-message, `n,,`
|
|
1350
|
+
// and the bare part. PostgreSQL takes the user from the startup message,
|
|
1351
|
+
// so the SCRAM user name is empty (§53.3.1).
|
|
1352
|
+
const first: string = `n,,${this.firstBare()}`
|
|
1353
|
+
const at: i32 = this.startMessage(PG_FE_PASSWORD)
|
|
1354
|
+
this.putCString("SCRAM-SHA-256")
|
|
1355
|
+
this.putI32(toI32(first.length))
|
|
1356
|
+
this.putString(first)
|
|
1357
|
+
this.endMessage(at)
|
|
1358
|
+
this.state = PG_STATE_SCRAM_FIRST
|
|
1359
|
+
return PG_NEED_MORE
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** client-first-message-bare. */
|
|
1363
|
+
private firstBare(): string {
|
|
1364
|
+
return `n=,r=${this.clientNonce}`
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* AuthenticationSASLContinue: server-first-message, exactly `r=`, `s=`,
|
|
1369
|
+
* `i=` in that order. Answers with client-final-message and its proof.
|
|
1370
|
+
*/
|
|
1371
|
+
private scramContinue(body: i32, end: i32): i32 {
|
|
1372
|
+
const b: u8[] = this.input
|
|
1373
|
+
if (body < 0 || end > toI32(b.length)) {
|
|
1374
|
+
return this.fail(PG_ERR_FRAME)
|
|
1375
|
+
}
|
|
1376
|
+
const nonceEnd: i32 = pgFind(b, body, end, 44)
|
|
1377
|
+
const saltEnd: i32 = pgFind(b, nonceEnd + 1 < end ? nonceEnd + 1 : end, end, 44)
|
|
1378
|
+
if (
|
|
1379
|
+
nonceEnd === end ||
|
|
1380
|
+
saltEnd === end ||
|
|
1381
|
+
!pgBytesAre(b, body, 2, "r=") ||
|
|
1382
|
+
!pgBytesAre(b, nonceEnd + 1, 2, "s=") ||
|
|
1383
|
+
end - saltEnd < 4 ||
|
|
1384
|
+
!pgBytesAre(b, saltEnd + 1, 2, "i=")
|
|
1385
|
+
) {
|
|
1386
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1387
|
+
}
|
|
1388
|
+
// The nonce extends ours with printable characters other than a comma.
|
|
1389
|
+
const ours: i32 = toI32(this.clientNonce.length)
|
|
1390
|
+
if (nonceEnd - body - 2 <= ours || !pgBytesAre(b, body + 2, ours, this.clientNonce)) {
|
|
1391
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1392
|
+
}
|
|
1393
|
+
const nonceStart: i32 = body + 2 + ours
|
|
1394
|
+
const digitsStart: i32 = saltEnd + 3
|
|
1395
|
+
if (nonceStart < 0 || nonceEnd > toI32(b.length)) {
|
|
1396
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1397
|
+
}
|
|
1398
|
+
for (let i: i32 = nonceStart; i < nonceEnd && i < toI32(b.length); i++) {
|
|
1399
|
+
const c: i32 = toI32(b[i])
|
|
1400
|
+
if (c < 33 || c > 126) {
|
|
1401
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (digitsStart < 0) {
|
|
1405
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1406
|
+
}
|
|
1407
|
+
let iterations: i32 = 0
|
|
1408
|
+
for (let i: i32 = digitsStart; i < end && i < toI32(b.length); i++) {
|
|
1409
|
+
const d: i32 = toI32(b[i]) - 48
|
|
1410
|
+
if (d < 0 || d > 9 || iterations > this.config.maxIterations) {
|
|
1411
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1412
|
+
}
|
|
1413
|
+
iterations = iterations * 10 + d
|
|
1414
|
+
}
|
|
1415
|
+
if (iterations < this.config.minIterations || iterations > this.config.maxIterations) {
|
|
1416
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1417
|
+
}
|
|
1418
|
+
const salt: u8[] | null = base64StdPadDecode(String.fromBytes(b, nonceEnd + 3, saltEnd))
|
|
1419
|
+
if (salt === null || salt.length === 0) {
|
|
1420
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1421
|
+
}
|
|
1422
|
+
const password: PgPassword = this.password
|
|
1423
|
+
if (password.gone) {
|
|
1424
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1425
|
+
}
|
|
1426
|
+
const salted: Secret<u8[]> | null = pgSaltedPassword(password.bytes, salt, iterations)
|
|
1427
|
+
if (salted === null) {
|
|
1428
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1429
|
+
}
|
|
1430
|
+
const withoutProof: string = `c=biws,r=${String.fromBytes(b, body + 2, nonceEnd)}`
|
|
1431
|
+
const authMessage: u8[] = pgBytesOf(
|
|
1432
|
+
`${this.firstBare()},${String.fromBytes(b, body, end)},${withoutProof}`
|
|
1433
|
+
)
|
|
1434
|
+
const keys: u8[] = exposeWith(salted, authMessage, pgScramKeys)
|
|
1435
|
+
wipe(salted)
|
|
1436
|
+
this.dispose()
|
|
1437
|
+
const proof: u8[] = new Array<u8>(32)
|
|
1438
|
+
const signature: u8[] = new Array<u8>(32)
|
|
1439
|
+
if (toI32(keys.length) < 64) {
|
|
1440
|
+
panic("nish/net/postgres: the SCRAM keys are not 64 bytes")
|
|
1441
|
+
}
|
|
1442
|
+
for (let i: i32 = 0; i < 32; i++) {
|
|
1443
|
+
proof[i] = keys[i]
|
|
1444
|
+
signature[i] = keys[32 + i]
|
|
1445
|
+
}
|
|
1446
|
+
wipe(keys)
|
|
1447
|
+
this.serverSignature = signature
|
|
1448
|
+
// The proof goes on the wire as it is, so its base64 string is no secret.
|
|
1449
|
+
const encoded: string = base64StdPadEncode(proof)
|
|
1450
|
+
wipe(proof)
|
|
1451
|
+
const at: i32 = this.startMessage(PG_FE_PASSWORD)
|
|
1452
|
+
this.putString(`${withoutProof},p=${encoded}`)
|
|
1453
|
+
this.endMessage(at)
|
|
1454
|
+
this.state = PG_STATE_SCRAM_FINAL
|
|
1455
|
+
return PG_NEED_MORE
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/** AuthenticationSASLFinal: exactly `v=` and the ServerSignature, compared in constant time. */
|
|
1459
|
+
private scramFinal(body: i32, end: i32): i32 {
|
|
1460
|
+
const b: u8[] = this.input
|
|
1461
|
+
if (end - body < 2 || !pgBytesAre(b, body, 2, "v=")) {
|
|
1462
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1463
|
+
}
|
|
1464
|
+
const signature: u8[] | null = base64StdPadDecode(String.fromBytes(b, body + 2, end))
|
|
1465
|
+
if (signature === null || signature.length !== 32) {
|
|
1466
|
+
return this.fail(PG_ERR_SCRAM)
|
|
1467
|
+
}
|
|
1468
|
+
if (!timingSafeEqual(signature, this.serverSignature)) {
|
|
1469
|
+
return this.fail(PG_ERR_SERVER_SIGNATURE)
|
|
1470
|
+
}
|
|
1471
|
+
wipe(this.serverSignature)
|
|
1472
|
+
this.serverSignature = []
|
|
1473
|
+
return PG_NEED_MORE
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// --- Writing -----------------------------------------------------------------
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Whether a builder call may queue `size` more bytes and still leave
|
|
1480
|
+
* `PG_TAIL` under `maxOutput`. When they would not fit after the end of the
|
|
1481
|
+
* buffer, the unsent bytes move to its start first: a builder call asks
|
|
1482
|
+
* before it takes any offset into the buffer, so nothing it holds moves.
|
|
1483
|
+
*/
|
|
1484
|
+
private room(size: i32): boolean {
|
|
1485
|
+
const queued: i32 = this.outEnd - this.outStart
|
|
1486
|
+
if (size < 0 || size > this.config.maxOutput - PG_TAIL - queued) {
|
|
1487
|
+
return false
|
|
1488
|
+
}
|
|
1489
|
+
if (this.outStart > 0 && this.outEnd + size > toI32(this.out.length)) {
|
|
1490
|
+
this.compactOutput()
|
|
1491
|
+
}
|
|
1492
|
+
return true
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Makes room for the `size` bytes of a tail message (`bindEnd`, `sync`,
|
|
1497
|
+
* `terminate`) the way `room` does for the rest, so that the buffer is not
|
|
1498
|
+
* grown past `maxOutput` while sent bytes still fill its start.
|
|
1499
|
+
*/
|
|
1500
|
+
private tailRoom(size: i32): void {
|
|
1501
|
+
if (this.outStart > 0 && this.outEnd + size > toI32(this.out.length)) {
|
|
1502
|
+
this.compactOutput()
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/** Moves the unsent output to the start of the buffer, the open bind's offsets with it. */
|
|
1507
|
+
private compactOutput(): void {
|
|
1508
|
+
const shift: i32 = this.outStart
|
|
1509
|
+
if (shift === 0) {
|
|
1510
|
+
return
|
|
1511
|
+
}
|
|
1512
|
+
copyBytes(this.out, 0, this.out, shift, this.outEnd - shift)
|
|
1513
|
+
this.outEnd -= shift
|
|
1514
|
+
this.outStart = 0
|
|
1515
|
+
if (this.bindAt >= 0) {
|
|
1516
|
+
this.bindAt -= shift
|
|
1517
|
+
this.paramCountAt -= shift
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/** Makes the output buffer at least `want` bytes long. */
|
|
1522
|
+
private reserve(want: i32): void {
|
|
1523
|
+
const size: i32 = toI32(this.out.length)
|
|
1524
|
+
if (want <= size) {
|
|
1525
|
+
return
|
|
1526
|
+
}
|
|
1527
|
+
const grown: u8[] = new Array<u8>(size <= PG_MAX_CAP && want < 2 * size ? 2 * size : want)
|
|
1528
|
+
grown.set(this.out, 0)
|
|
1529
|
+
this.out = grown
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
private putByte(v: i32): void {
|
|
1533
|
+
this.reserve(this.outEnd + 1)
|
|
1534
|
+
this.out[this.outEnd] = toU8(v)
|
|
1535
|
+
this.outEnd += 1
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
private putI32(v: i32): void {
|
|
1539
|
+
this.reserve(this.outEnd + 4)
|
|
1540
|
+
setI32BE(this.out, this.outEnd, v)
|
|
1541
|
+
this.outEnd += 4
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
private putI16(v: i32): void {
|
|
1545
|
+
this.reserve(this.outEnd + 2)
|
|
1546
|
+
setU16BE(this.out, this.outEnd, toU16(v))
|
|
1547
|
+
this.outEnd += 2
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
private putString(text: string): void {
|
|
1551
|
+
const n: i32 = toI32(text.length)
|
|
1552
|
+
this.reserve(this.outEnd + n)
|
|
1553
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
1554
|
+
this.out[this.outEnd + i] = toU8(text.charCodeAt(i))
|
|
1555
|
+
}
|
|
1556
|
+
this.outEnd += n
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private putCString(text: string): void {
|
|
1560
|
+
this.putString(text)
|
|
1561
|
+
this.putByte(PG_ZERO)
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
private putParam(name: string, value: string): void {
|
|
1565
|
+
this.putCString(name)
|
|
1566
|
+
this.putCString(value)
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Starts a message of `type`; answers where, for `endMessage`. */
|
|
1570
|
+
private startMessage(type: i32): i32 {
|
|
1571
|
+
const at: i32 = this.outEnd
|
|
1572
|
+
this.putByte(type)
|
|
1573
|
+
this.putI32(PG_ZERO)
|
|
1574
|
+
return at
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/** Writes the length of the message started at `at`. */
|
|
1578
|
+
private endMessage(at: i32): void {
|
|
1579
|
+
setI32BE(this.out, at + 1, this.outEnd - at - 1)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/** Whether another group may start, or the open one take another message. */
|
|
1583
|
+
canSend(): boolean {
|
|
1584
|
+
return this.state === PG_STATE_READY && (this.groupOpen || this.inFlight < this.config.maxInFlight)
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/** Opens a group if none is, for a message of `size` bytes; false, and no group opened, when none may be or there is no room. */
|
|
1588
|
+
private canOpen(size: i32): boolean {
|
|
1589
|
+
if (!this.canSend() || this.bindAt >= 0 || !this.room(size)) {
|
|
1590
|
+
return false
|
|
1591
|
+
}
|
|
1592
|
+
if (!this.groupOpen) {
|
|
1593
|
+
this.groupOpen = true
|
|
1594
|
+
this.inFlight += 1
|
|
1595
|
+
}
|
|
1596
|
+
return true
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Parse and Describe: prepares `sql` as the statement `name` ("" is the
|
|
1601
|
+
* unnamed one), its parameters' types given by `paramTypes` (OIDs, 0 to let
|
|
1602
|
+
* the server infer one, and parameters past the array are inferred too).
|
|
1603
|
+
* The server answers `PG_DESCRIBED`. False, and nothing written, when
|
|
1604
|
+
* `canSend()` is false, a bind is open, `name` or `sql` holds a NUL, or the
|
|
1605
|
+
* message would pass `maxOutput`.
|
|
1606
|
+
*/
|
|
1607
|
+
prepare(name: string, sql: string, paramTypes: i32[]): boolean {
|
|
1608
|
+
const nameLength: i32 = toI32(name.length)
|
|
1609
|
+
const types: i32 = toI32(paramTypes.length)
|
|
1610
|
+
if (pgHasNul(name) || pgHasNul(sql) || types > 65535 || nameLength > PG_MAX_CAP / 4) {
|
|
1611
|
+
return false
|
|
1612
|
+
}
|
|
1613
|
+
const sqlLength: i32 = toI32(sql.length)
|
|
1614
|
+
if (sqlLength > PG_MAX_CAP || !this.canOpen(toI32(16) + 2 * nameLength + sqlLength + 4 * types)) {
|
|
1615
|
+
return false
|
|
1616
|
+
}
|
|
1617
|
+
const at: i32 = this.startMessage(PG_FE_PARSE)
|
|
1618
|
+
this.putCString(name)
|
|
1619
|
+
this.putCString(sql)
|
|
1620
|
+
this.putI16(toI32(paramTypes.length))
|
|
1621
|
+
for (const oid of paramTypes) {
|
|
1622
|
+
this.putI32(oid)
|
|
1623
|
+
}
|
|
1624
|
+
this.endMessage(at)
|
|
1625
|
+
const describe: i32 = this.startMessage(PG_FE_DESCRIBE)
|
|
1626
|
+
this.putByte(PG_TARGET_STATEMENT)
|
|
1627
|
+
this.putCString(name)
|
|
1628
|
+
this.endMessage(describe)
|
|
1629
|
+
return true
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/** Close: forgets the prepared statement `name`. False as `prepare` is. */
|
|
1633
|
+
closeStatement(name: string): boolean {
|
|
1634
|
+
if (pgHasNul(name) || toI32(name.length) > PG_MAX_CAP || !this.canOpen(7 + toI32(name.length))) {
|
|
1635
|
+
return false
|
|
1636
|
+
}
|
|
1637
|
+
const at: i32 = this.startMessage(PG_FE_CLOSE)
|
|
1638
|
+
this.putByte(PG_TARGET_STATEMENT)
|
|
1639
|
+
this.putCString(name)
|
|
1640
|
+
this.endMessage(at)
|
|
1641
|
+
return true
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Starts a Bind of the prepared statement `name`; its parameters follow, in
|
|
1646
|
+
* order, then `bindEnd()`. False as `prepare` is.
|
|
1647
|
+
*/
|
|
1648
|
+
bindStart(name: string): boolean {
|
|
1649
|
+
if (pgHasNul(name) || toI32(name.length) > PG_MAX_CAP || !this.canOpen(11 + toI32(name.length))) {
|
|
1650
|
+
return false
|
|
1651
|
+
}
|
|
1652
|
+
this.bindAt = this.startMessage(PG_FE_BIND)
|
|
1653
|
+
this.putByte(PG_ZERO)
|
|
1654
|
+
this.putCString(name)
|
|
1655
|
+
this.putI16(PG_ZERO)
|
|
1656
|
+
this.paramCountAt = this.outEnd
|
|
1657
|
+
this.putI16(PG_ZERO)
|
|
1658
|
+
this.paramCount = 0
|
|
1659
|
+
return true
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Opens a parameter of `size` bytes, its length word aside, and answers
|
|
1664
|
+
* where its length goes; -1, and nothing written, when no bind is open, the
|
|
1665
|
+
* bind has 65535 parameters already, or there is no room under `maxOutput`.
|
|
1666
|
+
*/
|
|
1667
|
+
private startParam(size: i32): i32 {
|
|
1668
|
+
if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size)) {
|
|
1669
|
+
return -1
|
|
1670
|
+
}
|
|
1671
|
+
this.paramCount += 1
|
|
1672
|
+
const at: i32 = this.outEnd
|
|
1673
|
+
this.putI32(PG_ZERO)
|
|
1674
|
+
return at
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
private endParam(at: i32): void {
|
|
1678
|
+
setI32BE(this.out, at, this.outEnd - at - 4)
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// Every `param...` call answers false, and writes nothing, when no bind is
|
|
1682
|
+
// open, the bind has 65535 parameters already, or the parameter would pass
|
|
1683
|
+
// `maxOutput`; a program that checked `bindStart()` meets only the last.
|
|
1684
|
+
|
|
1685
|
+
/** A text parameter. */
|
|
1686
|
+
paramText(value: string): boolean {
|
|
1687
|
+
const at: i32 = this.startParam(toI32(value.length))
|
|
1688
|
+
if (at < 0) {
|
|
1689
|
+
return false
|
|
1690
|
+
}
|
|
1691
|
+
this.putString(value)
|
|
1692
|
+
this.endParam(at)
|
|
1693
|
+
return true
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/** A text parameter from `bytes[start, start + length)`, UTF-8; false too when the window is not inside `bytes`. */
|
|
1697
|
+
paramBytes(bytes: u8[], start: i32, length: i32): boolean {
|
|
1698
|
+
if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
|
|
1699
|
+
return false
|
|
1700
|
+
}
|
|
1701
|
+
const at: i32 = this.startParam(length)
|
|
1702
|
+
if (at < 0) {
|
|
1703
|
+
return false
|
|
1704
|
+
}
|
|
1705
|
+
this.reserve(this.outEnd + length)
|
|
1706
|
+
copyBytes(this.out, this.outEnd, bytes, start, length)
|
|
1707
|
+
this.outEnd += length
|
|
1708
|
+
this.endParam(at)
|
|
1709
|
+
return true
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
/** A NULL parameter. */
|
|
1713
|
+
paramNull(): boolean {
|
|
1714
|
+
if (this.startParam(toI32(0)) < 0) {
|
|
1715
|
+
return false
|
|
1716
|
+
}
|
|
1717
|
+
setI32BE(this.out, this.outEnd - 4, -1)
|
|
1718
|
+
return true
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/** An integer parameter, in decimal. */
|
|
1722
|
+
paramInt(value: i64): boolean {
|
|
1723
|
+
const at: i32 = this.startParam(toI32(20))
|
|
1724
|
+
if (at < 0) {
|
|
1725
|
+
return false
|
|
1726
|
+
}
|
|
1727
|
+
this.reserve(this.outEnd + 20)
|
|
1728
|
+
// The digits are written from the least significant, then reversed; the
|
|
1729
|
+
// value is made negative first so that the most negative i64 has its digits.
|
|
1730
|
+
let v: i64 = value < 0 ? value : -value
|
|
1731
|
+
if (value < 0) {
|
|
1732
|
+
this.out[this.outEnd] = toU8(45)
|
|
1733
|
+
this.outEnd += 1
|
|
1734
|
+
}
|
|
1735
|
+
const first: i32 = this.outEnd
|
|
1736
|
+
while (true) {
|
|
1737
|
+
this.out[this.outEnd] = toU8(48 - toI32(v % 10))
|
|
1738
|
+
this.outEnd += 1
|
|
1739
|
+
v = v / 10
|
|
1740
|
+
if (v === 0) {
|
|
1741
|
+
break
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
let lo: i32 = first
|
|
1745
|
+
let hi: i32 = this.outEnd - 1
|
|
1746
|
+
while (lo < hi) {
|
|
1747
|
+
const t: u8 = this.out[lo]
|
|
1748
|
+
this.out[lo] = this.out[hi]
|
|
1749
|
+
this.out[hi] = t
|
|
1750
|
+
lo++
|
|
1751
|
+
hi--
|
|
1752
|
+
}
|
|
1753
|
+
this.endParam(at)
|
|
1754
|
+
return true
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/** A `text[]` parameter: every element quoted, `"` and `\` escaped. */
|
|
1758
|
+
paramTextArray(items: string[]): boolean {
|
|
1759
|
+
// Its exact size first, so that room is asked for once and a refusal writes nothing.
|
|
1760
|
+
let size: i64 = 2
|
|
1761
|
+
for (const item of items) {
|
|
1762
|
+
size += toI64(item.length) + 3
|
|
1763
|
+
const n: i32 = toI32(item.length)
|
|
1764
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
1765
|
+
const c: i32 = toI32(item.charCodeAt(i))
|
|
1766
|
+
if (c === 34 || c === 92) {
|
|
1767
|
+
size += 1
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const at: i32 = this.startParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
|
|
1772
|
+
if (at < 0) {
|
|
1773
|
+
return false
|
|
1774
|
+
}
|
|
1775
|
+
this.putByte(PG_LBRACE)
|
|
1776
|
+
let first: boolean = true
|
|
1777
|
+
for (const item of items) {
|
|
1778
|
+
if (!first) {
|
|
1779
|
+
this.putByte(PG_COMMA)
|
|
1780
|
+
}
|
|
1781
|
+
first = false
|
|
1782
|
+
this.putByte(PG_QUOTE)
|
|
1783
|
+
const n: i32 = toI32(item.length)
|
|
1784
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
1785
|
+
const c: i32 = toI32(item.charCodeAt(i))
|
|
1786
|
+
if (c === 34 || c === 92) {
|
|
1787
|
+
this.putByte(PG_BACKSLASH)
|
|
1788
|
+
}
|
|
1789
|
+
this.putByte(c)
|
|
1790
|
+
}
|
|
1791
|
+
this.putByte(PG_QUOTE)
|
|
1792
|
+
}
|
|
1793
|
+
this.putByte(PG_RBRACE)
|
|
1794
|
+
this.endParam(at)
|
|
1795
|
+
return true
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/**
|
|
1799
|
+
* Ends the Bind (every result column in text) and queues its Execute, for
|
|
1800
|
+
* every row. False, and nothing written, when no bind is open; never for
|
|
1801
|
+
* room, which the calls before it kept.
|
|
1802
|
+
*/
|
|
1803
|
+
bindEnd(): boolean {
|
|
1804
|
+
if (this.bindAt < 0) {
|
|
1805
|
+
return false
|
|
1806
|
+
}
|
|
1807
|
+
this.tailRoom(PG_TAIL)
|
|
1808
|
+
const at: i32 = this.bindAt
|
|
1809
|
+
setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
|
|
1810
|
+
this.putI16(PG_ZERO)
|
|
1811
|
+
this.endMessage(at)
|
|
1812
|
+
this.bindAt = -1
|
|
1813
|
+
const execute: i32 = this.startMessage(PG_FE_EXECUTE)
|
|
1814
|
+
this.putByte(PG_ZERO)
|
|
1815
|
+
this.putI32(PG_ZERO)
|
|
1816
|
+
this.endMessage(execute)
|
|
1817
|
+
return true
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/** Sync: ends the open group, which the server answers with `PG_GROUP_DONE`. False when no group is open or a bind is; never for room. */
|
|
1821
|
+
sync(): boolean {
|
|
1822
|
+
if (!this.groupOpen || this.bindAt >= 0) {
|
|
1823
|
+
return false
|
|
1824
|
+
}
|
|
1825
|
+
this.groupOpen = false
|
|
1826
|
+
this.tailRoom(toI32(5))
|
|
1827
|
+
const at: i32 = this.startMessage(PG_FE_SYNC)
|
|
1828
|
+
this.endMessage(at)
|
|
1829
|
+
return true
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/** Terminate: tells the server the connection is ending; close the socket once it is sent. A bind still open is dropped unsent. */
|
|
1833
|
+
terminate(): void {
|
|
1834
|
+
if (this.bindAt >= 0) {
|
|
1835
|
+
this.outEnd = this.bindAt
|
|
1836
|
+
this.bindAt = -1
|
|
1837
|
+
}
|
|
1838
|
+
this.tailRoom(toI32(5))
|
|
1839
|
+
const at: i32 = this.startMessage(PG_FE_TERMINATE)
|
|
1840
|
+
this.endMessage(at)
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// --- Results -----------------------------------------------------------------
|
|
1844
|
+
|
|
1845
|
+
/** Column `c`'s first byte in `inputBuffer()`. */
|
|
1846
|
+
colStart(c: i32): i32 {
|
|
1847
|
+
return this.colStarts[c]
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/** Column `c`'s length, -1 for NULL. */
|
|
1851
|
+
colLength(c: i32): i32 {
|
|
1852
|
+
return this.colLengths[c]
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/** Whether column `c` is NULL. */
|
|
1856
|
+
isNull(c: i32): boolean {
|
|
1857
|
+
return this.colLengths[c] < 0
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/** Column `c` as a string; the one result read that allocates. "" for NULL. */
|
|
1861
|
+
columnText(c: i32): string {
|
|
1862
|
+
const length: i32 = this.colLengths[c]
|
|
1863
|
+
if (length <= 0) {
|
|
1864
|
+
return ""
|
|
1865
|
+
}
|
|
1866
|
+
const start: i32 = this.colStarts[c]
|
|
1867
|
+
return String.fromBytes(this.input, start, start + length)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/** Column `c` of the statement just described: its type's OID (23 is `int4`, 25 `text`). */
|
|
1871
|
+
columnType(c: i32): i32 {
|
|
1872
|
+
return this.colTypes[c]
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/** Column `c` as an integer; NULL or a bad value sets `text.failed`. */
|
|
1876
|
+
int(c: i32): i64 {
|
|
1877
|
+
return this.text.int(this.input, this.colStarts[c], this.colLengths[c])
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/** Column `c` as a boolean; NULL or a bad value sets `text.failed`. */
|
|
1881
|
+
bool(c: i32): boolean {
|
|
1882
|
+
return this.text.bool(this.input, this.colStarts[c], this.colLengths[c])
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/** Whether the tag of the execution just completed starts with the word `word` (`INSERT`, `SELECT`, ...). */
|
|
1886
|
+
tagIs(word: string): boolean {
|
|
1887
|
+
const n: i32 = toI32(word.length)
|
|
1888
|
+
if (n > this.tagLength || !pgBytesAre(this.input, this.tagStart, n, word)) {
|
|
1889
|
+
return false
|
|
1890
|
+
}
|
|
1891
|
+
return n === this.tagLength || toI32(this.input[this.tagStart + n]) === 32
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/** The row count the tag ends with (`INSERT 0 3` is 3, `SELECT 5` is 5), or -1 when it has none or it does not fit an i64. */
|
|
1895
|
+
tagRows(): i64 {
|
|
1896
|
+
const end: i32 = this.tagStart + this.tagLength
|
|
1897
|
+
let p: i32 = end
|
|
1898
|
+
while (p > this.tagStart && toI32(this.input[p - 1]) >= 48 && toI32(this.input[p - 1]) <= 57) {
|
|
1899
|
+
p--
|
|
1900
|
+
}
|
|
1901
|
+
if (p === end || p === this.tagStart || toI32(this.input[p - 1]) !== 32) {
|
|
1902
|
+
return -1
|
|
1903
|
+
}
|
|
1904
|
+
// Read without touching `text.failed`, which belongs to the program's own decoding.
|
|
1905
|
+
const failed: boolean = this.text.failed
|
|
1906
|
+
this.text.failed = false
|
|
1907
|
+
const rows: i64 = this.text.int(this.input, p, end - p)
|
|
1908
|
+
const overflowed: boolean = this.text.failed
|
|
1909
|
+
this.text.failed = failed
|
|
1910
|
+
return overflowed ? -1 : rows
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// --- The pool ------------------------------------------------------------------
|
|
1915
|
+
|
|
1916
|
+
/** `PgPool.next(slot)`: the request `ticket` names will get no answer, because its connection failed first. */
|
|
1917
|
+
export const PG_LOST: i32 = 9
|
|
1918
|
+
/** `PgPool.dispatch()` and `PgPool.wanted()`: no slot. */
|
|
1919
|
+
export const PG_POOL_NONE: i32 = -1
|
|
1920
|
+
/** `PgPool.request()`: the queue is full, and the request was not taken. */
|
|
1921
|
+
export const PG_POOL_FULL: i32 = -2
|
|
1922
|
+
|
|
1923
|
+
/** The most slots a pool may have. */
|
|
1924
|
+
const PG_POOL_MAX_SLOTS: i32 = 1024
|
|
1925
|
+
/** The longest queue a pool may have. */
|
|
1926
|
+
const PG_POOL_MAX_QUEUE: i32 = 1048576
|
|
1927
|
+
/** The most groups a pooled connection may have in flight, which sizes each slot's ring. */
|
|
1928
|
+
const PG_POOL_MAX_IN_FLIGHT: i32 = 1024
|
|
1929
|
+
/** The most statements a pool may register. */
|
|
1930
|
+
const PG_POOL_MAX_STATEMENTS: i32 = 256
|
|
1931
|
+
|
|
1932
|
+
/** No connection: one is built the next time a request needs it. */
|
|
1933
|
+
const PG_SLOT_IDLE: i32 = 0
|
|
1934
|
+
/** A request needs a connection here: the carrier is to make a socket. */
|
|
1935
|
+
const PG_SLOT_WANTED: i32 = 1
|
|
1936
|
+
/** The connection is made and authenticating. */
|
|
1937
|
+
const PG_SLOT_STARTING: i32 = 2
|
|
1938
|
+
const PG_SLOT_READY: i32 = 3
|
|
1939
|
+
/** The connection failed: `next` is reporting the requests it took with it. */
|
|
1940
|
+
const PG_SLOT_DRAINING: i32 = 4
|
|
1941
|
+
|
|
1942
|
+
/** One of a pool's connections, and the groups it has in flight. */
|
|
1943
|
+
class PgPoolSlot {
|
|
1944
|
+
state: i32 = 0
|
|
1945
|
+
conn: PgConnection | null = null
|
|
1946
|
+
/** How many registered statements this connection has queued, and how many the server has described. */
|
|
1947
|
+
queued: i32 = 0
|
|
1948
|
+
described: i32 = 0
|
|
1949
|
+
/** The groups in flight, oldest first, in a ring: each one's ticket, and how many of the pool's own prepares lead it. */
|
|
1950
|
+
readonly tickets: i32[]
|
|
1951
|
+
readonly leads: i32[]
|
|
1952
|
+
head: i32 = 0
|
|
1953
|
+
count: i32 = 0
|
|
1954
|
+
|
|
1955
|
+
constructor(ring: i32) {
|
|
1956
|
+
this.tickets = new Array<i32>(ring)
|
|
1957
|
+
this.leads = new Array<i32>(ring)
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
push(ticket: i32, leads: i32): void {
|
|
1961
|
+
const at: i32 = (this.head + this.count) % toI32(this.tickets.length)
|
|
1962
|
+
this.tickets[at] = ticket
|
|
1963
|
+
this.leads[at] = leads
|
|
1964
|
+
this.count += 1
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
pop(): void {
|
|
1968
|
+
this.head = (this.head + 1) % toI32(this.tickets.length)
|
|
1969
|
+
this.count -= 1
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* A pool of connections to one server, sans-IO like `PgConnection`: it makes
|
|
1975
|
+
* no socket and reads no clock, and says which slot needs a socket and when.
|
|
1976
|
+
* Its size is a cap, and no connection is made until a request needs one and
|
|
1977
|
+
* no ready connection has room.
|
|
1978
|
+
*
|
|
1979
|
+
* A request is a ticket from `request()`, queued in FIFO order. `dispatch()`
|
|
1980
|
+
* hands the queue's head to the ready connection with the fewest groups in
|
|
1981
|
+
* flight, never past `maxInFlight` on one; the program builds the request's
|
|
1982
|
+
* group on `connection(slot)` and ends it with `send()`, not `sync()`.
|
|
1983
|
+
* `next(slot)` then reads that connection's events, `ticket` naming the
|
|
1984
|
+
* request each is about. A connection that fails reports `PG_LOST` for each
|
|
1985
|
+
* request it took with it, then `PG_FAILED`, and is built again on its next
|
|
1986
|
+
* use. Statements given to `register` are prepared on each connection in the
|
|
1987
|
+
* same group as its first request, so a request may bind them at once.
|
|
1988
|
+
*/
|
|
1989
|
+
export class PgPool {
|
|
1990
|
+
/** The request the last `dispatch()`, or event from `next(slot)`, is about; -1 for none. */
|
|
1991
|
+
ticket: i32 = -1
|
|
1992
|
+
/** The number of slots, the most connections the pool makes. */
|
|
1993
|
+
readonly size: i32
|
|
1994
|
+
|
|
1995
|
+
private readonly config: PgConfig
|
|
1996
|
+
private readonly password: PgPassword
|
|
1997
|
+
private readonly slots: PgPoolSlot[]
|
|
1998
|
+
private readonly queue: i32[]
|
|
1999
|
+
private queueHead: i32 = 0
|
|
2000
|
+
private queueCount: i32 = 0
|
|
2001
|
+
private nextTicket: i32 = 0
|
|
2002
|
+
/** The slot whose request is being built, between `dispatch()` and `send()`. */
|
|
2003
|
+
private building: i32 = -1
|
|
2004
|
+
private started: boolean = false
|
|
2005
|
+
private readonly names: string[]
|
|
2006
|
+
private readonly sqls: string[]
|
|
2007
|
+
private readonly types: i32[][]
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* A pool of up to `size` connections (1 to 1024) and up to `queueSize`
|
|
2011
|
+
* waiting requests (1 to 2^20). `password` is copied and kept until
|
|
2012
|
+
* `dispose()`, since a connection is made again after a failure. Panics as
|
|
2013
|
+
* `PgConnection` does on `config`, and on a `maxInFlight` over 1024.
|
|
2014
|
+
*/
|
|
2015
|
+
constructor(config: PgConfig, password: Secret<u8[]>, size: i32, queueSize: i32) {
|
|
2016
|
+
pgCheckConfig(config)
|
|
2017
|
+
if (size < 1 || size > PG_POOL_MAX_SLOTS) {
|
|
2018
|
+
panic("nish/net/postgres: a PgPool size out of range")
|
|
2019
|
+
}
|
|
2020
|
+
if (queueSize < 1 || queueSize > PG_POOL_MAX_QUEUE) {
|
|
2021
|
+
panic("nish/net/postgres: a PgPool queueSize out of range")
|
|
2022
|
+
}
|
|
2023
|
+
if (config.maxInFlight > PG_POOL_MAX_IN_FLIGHT) {
|
|
2024
|
+
panic("nish/net/postgres: a PgPool connection's maxInFlight is over 1024")
|
|
2025
|
+
}
|
|
2026
|
+
this.config = config
|
|
2027
|
+
this.password = new PgPassword(password)
|
|
2028
|
+
this.size = size
|
|
2029
|
+
this.slots = []
|
|
2030
|
+
for (let i: i32 = 0; i < size; i++) {
|
|
2031
|
+
this.slots.push(new PgPoolSlot(config.maxInFlight))
|
|
2032
|
+
}
|
|
2033
|
+
this.queue = new Array<i32>(queueSize)
|
|
2034
|
+
this.names = []
|
|
2035
|
+
this.sqls = []
|
|
2036
|
+
this.types = []
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Registers a statement for every connection to prepare before its first
|
|
2041
|
+
* request. False when a connection has been made already, `name` or `sql`
|
|
2042
|
+
* holds a NUL, or 256 are registered.
|
|
2043
|
+
*/
|
|
2044
|
+
register(name: string, sql: string, paramTypes: i32[]): boolean {
|
|
2045
|
+
if (
|
|
2046
|
+
this.started ||
|
|
2047
|
+
pgHasNul(name) ||
|
|
2048
|
+
pgHasNul(sql) ||
|
|
2049
|
+
toI32(this.names.length) === PG_POOL_MAX_STATEMENTS
|
|
2050
|
+
) {
|
|
2051
|
+
return false
|
|
2052
|
+
}
|
|
2053
|
+
const types: i32[] = []
|
|
2054
|
+
for (const oid of paramTypes) {
|
|
2055
|
+
types.push(oid)
|
|
2056
|
+
}
|
|
2057
|
+
this.names.push(name)
|
|
2058
|
+
this.sqls.push(sql)
|
|
2059
|
+
this.types.push(types)
|
|
2060
|
+
return true
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/** Queues a request and answers its ticket, or `PG_POOL_FULL` when `queueSize` are waiting. */
|
|
2064
|
+
request(): i32 {
|
|
2065
|
+
const room: i32 = toI32(this.queue.length)
|
|
2066
|
+
if (this.queueCount === room) {
|
|
2067
|
+
return PG_POOL_FULL
|
|
2068
|
+
}
|
|
2069
|
+
const ticket: i32 = this.nextTicket
|
|
2070
|
+
this.nextTicket = ticket === 2147483647 ? 0 : ticket + 1
|
|
2071
|
+
this.queue[(this.queueHead + this.queueCount) % room] = ticket
|
|
2072
|
+
this.queueCount += 1
|
|
2073
|
+
return ticket
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/** The number of requests waiting for a connection. */
|
|
2077
|
+
waiting(): i32 {
|
|
2078
|
+
return this.queueCount
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* Hands the oldest waiting request to a ready connection with room, and
|
|
2083
|
+
* answers its slot, `ticket` set to the request: build its group on
|
|
2084
|
+
* `connection(slot)`, then `send()`. `PG_POOL_NONE` when nothing waits, a
|
|
2085
|
+
* request is still being built, or no ready connection has room; then a
|
|
2086
|
+
* connection may be wanted (`wanted()`): one more for every `maxInFlight`
|
|
2087
|
+
* waiting requests that the connections already starting cannot take. A
|
|
2088
|
+
* request handed out is the program's to end with `send()`: until then no
|
|
2089
|
+
* other is handed out.
|
|
2090
|
+
*/
|
|
2091
|
+
dispatch(): i32 {
|
|
2092
|
+
if (this.building >= 0 || this.queueCount === 0) {
|
|
2093
|
+
return PG_POOL_NONE
|
|
2094
|
+
}
|
|
2095
|
+
const cap: i32 = this.config.maxInFlight
|
|
2096
|
+
let best: i32 = -1
|
|
2097
|
+
let load: i32 = 0
|
|
2098
|
+
let starting: i32 = 0
|
|
2099
|
+
let idle: i32 = -1
|
|
2100
|
+
for (let s: i32 = 0; s < this.size; s++) {
|
|
2101
|
+
const slot: PgPoolSlot = this.slots[s]
|
|
2102
|
+
const conn: PgConnection | null = slot.conn
|
|
2103
|
+
if (slot.state === PG_SLOT_READY && conn !== null) {
|
|
2104
|
+
if (conn.inFlight < cap && (best < 0 || conn.inFlight < load)) {
|
|
2105
|
+
best = s
|
|
2106
|
+
load = conn.inFlight
|
|
2107
|
+
}
|
|
2108
|
+
} else if (slot.state === PG_SLOT_WANTED || slot.state === PG_SLOT_STARTING) {
|
|
2109
|
+
starting += 1
|
|
2110
|
+
} else if (slot.state === PG_SLOT_IDLE && idle < 0) {
|
|
2111
|
+
idle = s
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
if (best < 0) {
|
|
2115
|
+
if (idle >= 0 && toI64(this.queueCount) > toI64(starting) * toI64(cap)) {
|
|
2116
|
+
this.slots[idle].state = PG_SLOT_WANTED
|
|
2117
|
+
}
|
|
2118
|
+
return PG_POOL_NONE
|
|
2119
|
+
}
|
|
2120
|
+
const ticket: i32 = this.queue[this.queueHead]
|
|
2121
|
+
this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
|
|
2122
|
+
this.queueCount -= 1
|
|
2123
|
+
const slot: PgPoolSlot = this.slots[best]
|
|
2124
|
+
const conn: PgConnection = this.connection(best)
|
|
2125
|
+
let leads: i32 = 0
|
|
2126
|
+
while (slot.queued < toI32(this.names.length)) {
|
|
2127
|
+
const k: i32 = slot.queued
|
|
2128
|
+
if (!conn.prepare(this.names[k], this.sqls[k], this.types[k])) {
|
|
2129
|
+
break
|
|
2130
|
+
}
|
|
2131
|
+
slot.queued += 1
|
|
2132
|
+
leads += 1
|
|
2133
|
+
}
|
|
2134
|
+
slot.push(ticket, leads)
|
|
2135
|
+
this.building = best
|
|
2136
|
+
this.ticket = ticket
|
|
2137
|
+
return best
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* Ends the group of the request `dispatch()` handed out. False when there
|
|
2142
|
+
* is none; when a bind is still open, so finish it and call again; and
|
|
2143
|
+
* when no message of the group was written, so the request is dropped and
|
|
2144
|
+
* gets no events (`PG_LOST` if its connection has failed).
|
|
2145
|
+
*/
|
|
2146
|
+
send(): boolean {
|
|
2147
|
+
const s: i32 = this.building
|
|
2148
|
+
if (s < 0) {
|
|
2149
|
+
return false
|
|
2150
|
+
}
|
|
2151
|
+
const slot: PgPoolSlot = this.slots[s]
|
|
2152
|
+
const conn: PgConnection = this.connection(s)
|
|
2153
|
+
if (conn.sync()) {
|
|
2154
|
+
this.building = -1
|
|
2155
|
+
return true
|
|
2156
|
+
}
|
|
2157
|
+
if (conn.inFlight < slot.count) {
|
|
2158
|
+
// No message opened the group. A failed connection reports the request
|
|
2159
|
+
// lost with the rest; a live one would never answer it, so it is dropped.
|
|
2160
|
+
if (conn.failure === PG_ERR_NONE) {
|
|
2161
|
+
slot.count -= 1
|
|
2162
|
+
}
|
|
2163
|
+
this.building = -1
|
|
2164
|
+
}
|
|
2165
|
+
return false
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/** A slot that needs a socket, or `PG_POOL_NONE`: connect it, then call `connect(slot, nonce)`. */
|
|
2169
|
+
wanted(): i32 {
|
|
2170
|
+
for (let s: i32 = 0; s < this.size; s++) {
|
|
2171
|
+
if (this.slots[s].state === PG_SLOT_WANTED) {
|
|
2172
|
+
return s
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return PG_POOL_NONE
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* Makes the wanted `slot`'s connection, `nonce` as `PgConnection` takes it:
|
|
2180
|
+
* send its output once the socket is connected, and if the socket fails,
|
|
2181
|
+
* call `connection(slot).closed()`. Panics when `slot` is not wanted or the
|
|
2182
|
+
* pool is disposed.
|
|
2183
|
+
*/
|
|
2184
|
+
connect(slot: i32, nonce: u8[]): void {
|
|
2185
|
+
if (slot < 0 || slot >= this.size || this.slots[slot].state !== PG_SLOT_WANTED) {
|
|
2186
|
+
panic("nish/net/postgres: PgPool.connect on a slot that wants no connection")
|
|
2187
|
+
}
|
|
2188
|
+
if (this.password.gone) {
|
|
2189
|
+
panic("nish/net/postgres: PgPool.connect after dispose")
|
|
2190
|
+
}
|
|
2191
|
+
const s: PgPoolSlot = this.slots[slot]
|
|
2192
|
+
s.conn = new PgConnection(this.config, this.password.bytes, nonce)
|
|
2193
|
+
s.state = PG_SLOT_STARTING
|
|
2194
|
+
s.queued = 0
|
|
2195
|
+
s.described = 0
|
|
2196
|
+
s.head = 0
|
|
2197
|
+
s.count = 0
|
|
2198
|
+
this.started = true
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
/** The connection in `slot`, its buffers the carrier's to fill and drain; the last one made after it fails. Panics when none was made. */
|
|
2202
|
+
connection(slot: i32): PgConnection {
|
|
2203
|
+
const conn: PgConnection | null = slot < 0 || slot >= this.size ? null : this.slots[slot].conn
|
|
2204
|
+
if (conn === null) {
|
|
2205
|
+
panic("nish/net/postgres: PgPool.connection on a slot with no connection")
|
|
2206
|
+
}
|
|
2207
|
+
return conn
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* The next event from `slot`'s input, as `PgConnection.next()` answers it,
|
|
2212
|
+
* `ticket` naming the request it is about (-1 for `PG_READY`, `PG_FAILED`,
|
|
2213
|
+
* and a notice while no request is in flight). The `PG_DESCRIBED` of a registered
|
|
2214
|
+
* statement is the pool's and is not answered. When the connection fails,
|
|
2215
|
+
* `PG_LOST` comes for each request in flight on it, oldest first, then
|
|
2216
|
+
* `PG_FAILED`: close its socket. A connection that fails before it is ready,
|
|
2217
|
+
* while no other is, takes the oldest waiting request with it, so that a
|
|
2218
|
+
* server that cannot be reached empties the queue rather than holds it.
|
|
2219
|
+
* `PG_NEED_MORE` once `PG_FAILED` was answered, until `connect` builds the
|
|
2220
|
+
* slot again. Panics as `connection(slot)` does.
|
|
2221
|
+
*/
|
|
2222
|
+
next(slot: i32): i32 {
|
|
2223
|
+
const conn: PgConnection = this.connection(slot)
|
|
2224
|
+
const s: PgPoolSlot = this.slots[slot]
|
|
2225
|
+
if (s.state === PG_SLOT_IDLE || s.state === PG_SLOT_WANTED) {
|
|
2226
|
+
this.ticket = -1
|
|
2227
|
+
return PG_NEED_MORE
|
|
2228
|
+
}
|
|
2229
|
+
while (s.state === PG_SLOT_STARTING || s.state === PG_SLOT_READY) {
|
|
2230
|
+
this.ticket = s.count > 0 ? s.tickets[s.head] : -1
|
|
2231
|
+
const event: i32 = conn.next()
|
|
2232
|
+
if (event === PG_READY) {
|
|
2233
|
+
s.state = PG_SLOT_READY
|
|
2234
|
+
} else if (event === PG_DESCRIBED && s.count > 0 && s.leads[s.head] > 0) {
|
|
2235
|
+
s.leads[s.head] -= 1
|
|
2236
|
+
s.described += 1
|
|
2237
|
+
continue
|
|
2238
|
+
} else if (event === PG_ERROR && s.count > 0 && s.leads[s.head] > 0) {
|
|
2239
|
+
// The rest of the group's prepares were skipped: the next request prepares them again.
|
|
2240
|
+
s.leads[s.head] = 0
|
|
2241
|
+
s.queued = s.described
|
|
2242
|
+
} else if (event === PG_GROUP_DONE) {
|
|
2243
|
+
s.pop()
|
|
2244
|
+
} else if (event === PG_FAILED) {
|
|
2245
|
+
if (s.state === PG_SLOT_STARTING && this.queueCount > 0 && !this.anyReady()) {
|
|
2246
|
+
s.push(this.queue[this.queueHead], toI32(0))
|
|
2247
|
+
this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
|
|
2248
|
+
this.queueCount -= 1
|
|
2249
|
+
}
|
|
2250
|
+
if (this.building === slot) {
|
|
2251
|
+
this.building = -1
|
|
2252
|
+
}
|
|
2253
|
+
s.state = PG_SLOT_DRAINING
|
|
2254
|
+
break
|
|
2255
|
+
}
|
|
2256
|
+
return event
|
|
2257
|
+
}
|
|
2258
|
+
if (s.state === PG_SLOT_DRAINING && s.count > 0) {
|
|
2259
|
+
this.ticket = s.tickets[s.head]
|
|
2260
|
+
s.pop()
|
|
2261
|
+
return PG_LOST
|
|
2262
|
+
}
|
|
2263
|
+
s.state = PG_SLOT_IDLE
|
|
2264
|
+
this.ticket = -1
|
|
2265
|
+
return PG_FAILED
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
private anyReady(): boolean {
|
|
2269
|
+
for (const slot of this.slots) {
|
|
2270
|
+
if (slot.state === PG_SLOT_READY) {
|
|
2271
|
+
return true
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
return false
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/** Whether every byte of the pool's copy of the password is zero, as `PgConnection.passwordWiped()` reads its own. */
|
|
2278
|
+
passwordWiped(): boolean {
|
|
2279
|
+
return this.password.wiped()
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
/** Wipes the pool's password and every connection's; `connect` panics after it. */
|
|
2283
|
+
dispose(): void {
|
|
2284
|
+
if (!this.password.gone) {
|
|
2285
|
+
this.password.wipe()
|
|
2286
|
+
}
|
|
2287
|
+
for (const slot of this.slots) {
|
|
2288
|
+
const conn: PgConnection | null = slot.conn
|
|
2289
|
+
if (conn !== null) {
|
|
2290
|
+
conn.dispose()
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|