@amritk/nish-x86_64-linux 0.25.0 → 0.26.0
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/bin/nish +0 -0
- package/docs/AI.md +22 -1
- package/package.json +1 -1
- package/runtime/core/host.d.ts +4 -0
- package/runtime/core/host.mjs +24 -0
- package/runtime/nish.d.ts +48 -0
- package/runtime/nish.h +26 -1
- package/runtime/nish.mjs +4 -0
- package/runtime/node.mjs +79 -0
- package/runtime/runtime-os.c +248 -18
- package/std/README.md +2 -2
- package/std/net/postgres.ts +259 -26
package/bin/nish
CHANGED
|
Binary file
|
package/docs/AI.md
CHANGED
|
@@ -2137,11 +2137,18 @@ a `\u` escape `nish/json` decodes) is one unit per byte, and counts its bytes.
|
|
|
2137
2137
|
it is; `i32`: the length, or a negative errno), `renameSync(from, to)` (`0` or
|
|
2138
2138
|
a negative errno; atomic on one filesystem, `-18` across two, so write the
|
|
2139
2139
|
temporary beside the target), `unlinkSync(path)` (`0` or a negative errno; a
|
|
2140
|
-
directory is refused), `
|
|
2140
|
+
directory is refused), `writeFileSyncMode(path, data, mode)` (the file left
|
|
2141
|
+
at exactly `mode` (`0..0o777`), set before the first byte, written in place; a symbolic link refused; the
|
|
2142
|
+
length or a negative errno), `createFileExclusiveSync(path, data, mode)` (the
|
|
2143
|
+
same, but `-17` when the name exists: a lock file), `mkdirSyncMode(path, mode)`
|
|
2144
|
+
(`0`, `-17` when it exists, the umask applies), `writeBytes(bytes, off, len)` (a `u8[]` range to stdout
|
|
2141
2145
|
as it is, the length or a negative errno; the range panics like
|
|
2142
2146
|
`netWrite`'s), `mkdirSync(path)` (one level, `boolean`),
|
|
2143
2147
|
`isDirectorySync(path)`, `readdirSync(path)` (`string[] | null`, sorted by
|
|
2144
2148
|
bytes, no `.`/`..`), `spawnSync(argv)`, `spawnSyncTo(argv, outPath, errPath)`,
|
|
2149
|
+
`spawnPiped(argv, stdin, stdout, stderr)` (stdin from a `u8[]`, both outputs
|
|
2150
|
+
appended to `u8[]`s, the status; `-2` past 16 MiB; the way to hand a child a
|
|
2151
|
+
secret, never argv or the environment),
|
|
2145
2152
|
`getenv(name)` (`string | null` — unset and empty are different answers),
|
|
2146
2153
|
`realpathSync(path)` (`string | null`; symbolic links resolved, absolute, and
|
|
2147
2154
|
`null` when it does not resolve),
|
|
@@ -2625,6 +2632,20 @@ the array open: end it and send the rest in the next statement. Every other
|
|
|
2625
2632
|
guide's [PostgreSQL page](guide/using-nish/postgres.md#array-parameters) its
|
|
2626
2633
|
walk-through.)
|
|
2627
2634
|
|
|
2635
|
+
**Read PostgreSQL ids, counts and times without parsing them** — there is no
|
|
2636
|
+
driver option for binary results: in `nish/net/postgres`, open the bind with
|
|
2637
|
+
`bindStartBinary(name)` instead of `bindStart(name)`, and every result column
|
|
2638
|
+
of that execution comes in binary. Read an `int2`/`int4`/`int8` with
|
|
2639
|
+
`binaryInt(c)`, a `bool` with `binaryBool(c)`, a `float8` with
|
|
2640
|
+
`binaryFloat(c)`, a `timestamptz` with `conn.binary.timestamp(conn.inputBuffer(),
|
|
2641
|
+
conn.colStart(c), conn.colLength(c), out, at)` (or its microseconds since 2000
|
|
2642
|
+
with `binaryInt(c)`, written out by `conn.binary.iso(micros, out, at)`), and a
|
|
2643
|
+
`text` column with `columnText(c)` as before. Cast anything else (`numeric`,
|
|
2644
|
+
`text[]`) to `text` in the SQL. (No block, as for `nish/text`;
|
|
2645
|
+
`tests/postgres/live`'s `binary` scenario runs it against a server, and the
|
|
2646
|
+
user guide's [PostgreSQL page](guide/using-nish/postgres.md#binary-results)
|
|
2647
|
+
its walk-through.)
|
|
2648
|
+
|
|
2628
2649
|
**Batch a server's keyed reads into one PostgreSQL query** — there is no
|
|
2629
2650
|
DataLoader and no `setTimeout` to collect a tick's keys: in `nish/net/postgres`,
|
|
2630
2651
|
a `PgBatcher` built from a `PgBatchConfig` (`name`, `select` reading the key as
|
package/package.json
CHANGED
package/runtime/core/host.d.ts
CHANGED
|
@@ -47,6 +47,9 @@ export declare function appendFileSync(path: string, data: string): void;
|
|
|
47
47
|
export declare function writeFileBytesSync(path: string, bytes: readonly u8[]): i32;
|
|
48
48
|
export declare function renameSync(from: string, to: string): i32;
|
|
49
49
|
export declare function unlinkSync(path: string): i32;
|
|
50
|
+
export declare function writeFileSyncMode(path: string, data: string, mode: i32): i32;
|
|
51
|
+
export declare function createFileExclusiveSync(path: string, data: string, mode: i32): i32;
|
|
52
|
+
export declare function mkdirSyncMode(path: string, mode: i32): i32;
|
|
50
53
|
export declare function mkdirSync(path: string): boolean;
|
|
51
54
|
export declare function isDirectorySync(path: string): boolean;
|
|
52
55
|
export declare function readdirSync(path: string): string[] | null;
|
|
@@ -56,6 +59,7 @@ export declare function arch(): string;
|
|
|
56
59
|
export declare function getenv(name: string): string | null;
|
|
57
60
|
export declare function spawnSync(argv: string[]): number;
|
|
58
61
|
export declare function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
|
|
62
|
+
export declare function spawnPiped(argv: string[], stdin: readonly u8[], stdout: u8[], stderr: u8[]): i32;
|
|
59
63
|
export declare function kill(pid: i32, sig: i32): i32;
|
|
60
64
|
export declare function availableParallelism(): i32;
|
|
61
65
|
export declare function totalMemory(): i64;
|
package/runtime/core/host.mjs
CHANGED
|
@@ -157,6 +157,18 @@ export function unlinkSync(path) {
|
|
|
157
157
|
return host.call("unlinkSync", [path]);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
export function writeFileSyncMode(path, data, mode) {
|
|
161
|
+
return host.call("writeFileSyncMode", [path, data, mode]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createFileExclusiveSync(path, data, mode) {
|
|
165
|
+
return host.call("createFileExclusiveSync", [path, data, mode]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function mkdirSyncMode(path, mode) {
|
|
169
|
+
return host.call("mkdirSyncMode", [path, mode]);
|
|
170
|
+
}
|
|
171
|
+
|
|
160
172
|
export function mkdirSync(path) {
|
|
161
173
|
return host.call("mkdirSync", [path]);
|
|
162
174
|
}
|
|
@@ -193,6 +205,18 @@ export function spawnSyncTo(argv, out, err) {
|
|
|
193
205
|
return host.call("spawnSyncTo", [argv, out, err]);
|
|
194
206
|
}
|
|
195
207
|
|
|
208
|
+
/**
|
|
209
|
+
* `spawnPiped(argv, stdin, stdout, stderr)`: the host runs the child and hands
|
|
210
|
+
* back its status and both streams' bytes, and they are appended here, as the
|
|
211
|
+
* runtime appends them to the caller's arrays.
|
|
212
|
+
*/
|
|
213
|
+
export function spawnPiped(argv, input, out, err) {
|
|
214
|
+
const [status, outBytes, errBytes] = host.call("spawnPiped", [argv, input]);
|
|
215
|
+
for (const b of outBytes) out.push(b);
|
|
216
|
+
for (const b of errBytes) err.push(b);
|
|
217
|
+
return status;
|
|
218
|
+
}
|
|
219
|
+
|
|
196
220
|
export function kill(pid, sig) {
|
|
197
221
|
return host.call("kill", [pid, sig]);
|
|
198
222
|
}
|
package/runtime/nish.d.ts
CHANGED
|
@@ -276,6 +276,33 @@ declare function writeFileBytesSync(path: string, bytes: readonly u8[]): i32;
|
|
|
276
276
|
declare function renameSync(from: string, to: string): i32;
|
|
277
277
|
/** The name `path` removed: 0, or a negative errno. A directory is refused, not removed. */
|
|
278
278
|
declare function unlinkSync(path: string): i32;
|
|
279
|
+
/**
|
|
280
|
+
* `writeFileSync` with an explicit `mode`: the file is exactly `mode`
|
|
281
|
+
* (`0o600` for a secret) before a byte of `data` is in it, whatever the umask
|
|
282
|
+
* or a file already there had, and a symbolic link at the path is refused
|
|
283
|
+
* (-40). The byte count, or a negative errno: -22 for a mode outside
|
|
284
|
+
* 0..0o777, the setuid, setgid and sticky bits included. The file is
|
|
285
|
+
* truncated and written in place, so a crash or a failed write leaves it
|
|
286
|
+
* empty or partial, and a file this process may write but does not own is
|
|
287
|
+
* emptied before the mode change answers -1 (`EPERM`). For a file that must
|
|
288
|
+
* be the old one or the new one, write a temporary name with this and move it
|
|
289
|
+
* with `renameSync`. Never an exit.
|
|
290
|
+
*/
|
|
291
|
+
declare function writeFileSyncMode(path: string, data: string, mode: i32): i32;
|
|
292
|
+
/**
|
|
293
|
+
* `data` in a new file of exactly `mode`, created only if nothing is at `path`:
|
|
294
|
+
* -17 when something is, a dangling symbolic link included. Of several
|
|
295
|
+
* processes racing for one path exactly one succeeds, so it takes a lock. The
|
|
296
|
+
* byte count, or a negative errno; a write that fails after the create
|
|
297
|
+
* removes the file, so the lock is not left taken. Never an exit.
|
|
298
|
+
*/
|
|
299
|
+
declare function createFileExclusiveSync(path: string, data: string, mode: i32): i32;
|
|
300
|
+
/**
|
|
301
|
+
* One directory made with `mode` less the umask (`0o700` is exact under any
|
|
302
|
+
* umask that leaves the owner's bits): 0, or a negative errno. A path already
|
|
303
|
+
* there is -17 and its mode is not changed; -22 for a mode outside 0..0o777.
|
|
304
|
+
*/
|
|
305
|
+
declare function mkdirSyncMode(path: string, mode: i32): i32;
|
|
279
306
|
/** One directory, not recursive; whether a directory is there afterwards. */
|
|
280
307
|
declare function mkdirSync(path: string): boolean;
|
|
281
308
|
/** Whether a directory is at `path` right now. One `stat`, and never an exit. */
|
|
@@ -299,6 +326,13 @@ declare function spawnSync(argv: string[]): number;
|
|
|
299
326
|
* either path is refused, as `writeFileSync` refuses one, and the answer is -1.
|
|
300
327
|
*/
|
|
301
328
|
declare function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
|
|
329
|
+
/**
|
|
330
|
+
* The same run with all three streams on pipes: `stdin` is written to the
|
|
331
|
+
* child and closed, and what it prints is appended to `stdout` and `stderr`.
|
|
332
|
+
* The status as `spawnSync` answers it, or -2 when a stream passed 16 MiB:
|
|
333
|
+
* the child is then killed and each array keeps at most the first 16 MiB.
|
|
334
|
+
*/
|
|
335
|
+
declare function spawnPiped(argv: string[], stdin: readonly u8[], stdout: u8[], stderr: u8[]): i32;
|
|
302
336
|
/**
|
|
303
337
|
* Start `argv[0]` through `PATH` without waiting: the child's pid, or a
|
|
304
338
|
* negative errno. The child is the program's until `waitPid` reaps it. No
|
|
@@ -492,6 +526,9 @@ declare var crypto: {
|
|
|
492
526
|
//
|
|
493
527
|
// `docs/LANGUAGE.md` -> Builtins -> Builtin modules is the normative list.
|
|
494
528
|
|
|
529
|
+
// Every negative errno a file call answers is the platform's own, untranslated:
|
|
530
|
+
// the numbers shown here are Linux's (-2 ENOENT, -17 EEXIST, -18 EXDEV, -22
|
|
531
|
+
// EINVAL, -40 ELOOP), and Darwin's differ (ELOOP is -62 there).
|
|
495
532
|
declare module "nish:fs" {
|
|
496
533
|
export function readFileSync(path: string): string;
|
|
497
534
|
export function readFileSyncOrNull(path: string): string | null;
|
|
@@ -506,6 +543,12 @@ declare module "nish:fs" {
|
|
|
506
543
|
export function renameSync(from: string, to: string): i32;
|
|
507
544
|
/** 0, or a negative errno: a directory is refused, not removed. */
|
|
508
545
|
export function unlinkSync(path: string): i32;
|
|
546
|
+
/** The byte count, or a negative errno; the file is exactly `mode` (0..0o777) before it holds a byte, a link refused, written in place. */
|
|
547
|
+
export function writeFileSyncMode(path: string, data: string, mode: i32): i32;
|
|
548
|
+
/** As `writeFileSyncMode`, but -17 when anything is at `path`: one racer wins; a failed write removes the file. */
|
|
549
|
+
export function createFileExclusiveSync(path: string, data: string, mode: i32): i32;
|
|
550
|
+
/** 0, or a negative errno: -17 for a path already there, whose mode is left alone. */
|
|
551
|
+
export function mkdirSyncMode(path: string, mode: i32): i32;
|
|
509
552
|
/** `true` when the directory was created, `false` when it already existed. */
|
|
510
553
|
export function mkdirSync(path: string): boolean;
|
|
511
554
|
export function isDirectorySync(path: string): boolean;
|
|
@@ -530,6 +573,11 @@ declare module "nish:process" {
|
|
|
530
573
|
* at either path is refused, and the answer is -1.
|
|
531
574
|
*/
|
|
532
575
|
export function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
|
|
576
|
+
/**
|
|
577
|
+
* The same run with `stdin` written to the child and its stdout and stderr
|
|
578
|
+
* appended to the two arrays: the status, or -2 past 16 MiB on a stream.
|
|
579
|
+
*/
|
|
580
|
+
export function spawnPiped(argv: string[], stdin: readonly u8[], stdout: u8[], stderr: u8[]): i32;
|
|
533
581
|
/** Start a child without waiting: its pid, or a negative errno. */
|
|
534
582
|
export function spawn(argv: string[]): i32;
|
|
535
583
|
/** Send 0, 2, 9 or 15 to `pid`: 0, or a negative errno; any other signal is -22. */
|
package/runtime/nish.h
CHANGED
|
@@ -403,10 +403,24 @@ nish_array *nish_read_file_bytes(const nish_str *path);
|
|
|
403
403
|
* errno — -EXDEV for a rename across file systems, which moves nothing, and
|
|
404
404
|
* -EISDIR or -EPERM for a directory handed to `nish_unlink`, which never calls
|
|
405
405
|
* rmdir. A path holding a NUL answers -EINVAL, not -ENOENT, so a program can
|
|
406
|
-
* tell a malformed path from a missing file.
|
|
406
|
+
* tell a malformed path from a missing file. The numbers are the platform's
|
|
407
|
+
* errno values, untranslated: -ELOOP is -40 on Linux and -62 on Darwin. */
|
|
407
408
|
int32_t nish_rename(const nish_str *from, const nish_str *to);
|
|
408
409
|
int32_t nish_unlink(const nish_str *path);
|
|
409
410
|
int32_t nish_write_file_bytes(const nish_str *path, const nish_array *bytes);
|
|
411
|
+
/* `writeFileSyncMode(path, data, mode)`, `createFileExclusiveSync(path, data,
|
|
412
|
+
* mode)` and `mkdirSyncMode(path, mode)`: a file written, or created only if
|
|
413
|
+
* nothing is at the path (`O_EXCL`), then `fchmod`ed to exactly `mode` before a
|
|
414
|
+
* byte is written; a directory made with `mode & ~umask`. A link at a file's
|
|
415
|
+
* path is refused. The file writers answer the byte count and `nish_mkdir_mode`
|
|
416
|
+
* 0, or a negative platform errno: -EEXIST for an exclusive create or a mkdir
|
|
417
|
+
* that finds the path taken (a directory's mode is then left alone), -EINVAL
|
|
418
|
+
* for a mode outside 0..0777 (the setuid, setgid and sticky bits included) or
|
|
419
|
+
* a path holding a NUL, -ENOSYS under WASI. A failed exclusive create leaves
|
|
420
|
+
* no file behind. */
|
|
421
|
+
int32_t nish_write_file_mode(const nish_str *path, const nish_str *data, int32_t mode);
|
|
422
|
+
int32_t nish_create_file_exclusive(const nish_str *path, const nish_str *data, int32_t mode);
|
|
423
|
+
int32_t nish_mkdir_mode(const nish_str *path, int32_t mode);
|
|
410
424
|
/* `writeBytes(bytes, off, len)` (nish:io), in runtime-os.c: the bytes
|
|
411
425
|
* `[off, off + len)` of a `u8[]` on stdout, a short write finished and `EINTR`
|
|
412
426
|
* retried. The caller has range-checked `off` and `len`. Answers `len`, or the
|
|
@@ -438,6 +452,17 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
|
|
|
438
452
|
* own error otherwise, and -ENOSYS under WASI. The child is reaped by
|
|
439
453
|
* `nish_wait_pid`. */
|
|
440
454
|
int32_t nish_spawn_child(const nish_array *argv);
|
|
455
|
+
/* `spawnPiped(argv, stdin, stdout, stderr)`: start element 0 of `argv` as
|
|
456
|
+
* `nish_spawn` does, with the bytes of `stdin` (a `u8[]`) on its standard input
|
|
457
|
+
* and its standard output and error appended to `stdout` and `stderr`, wait for
|
|
458
|
+
* it, and answer `nish_spawn`'s status. -1 is `nish_spawn`'s -1, a pipe or
|
|
459
|
+
* `poll` that fails included, and always under WASI. -2 is a stream that passed
|
|
460
|
+
* 16 MiB: the child is killed with SIGKILL and reaped, and that array holds the
|
|
461
|
+
* stream's first 16 MiB. The two output arrays grow as `push` grows them, so
|
|
462
|
+
* the compiler treats both as pushed. `stdin` is read in place and never
|
|
463
|
+
* copied; SIGPIPE is blocked on the calling thread for the call, a SIGPIPE the
|
|
464
|
+
* call raised is consumed, and the thread's mask is restored after it. */
|
|
465
|
+
int32_t nish_spawn_piped(const nish_array *argv, const nish_array *in, nish_array *out, nish_array *err);
|
|
441
466
|
/* `kill(pid, sig)`: send `sig` to `pid` and answer 0, or a negative errno. Only
|
|
442
467
|
* 0, 2 (SIGINT), 9 (SIGKILL) and 15 (SIGTERM) are sent; any other signal, and
|
|
443
468
|
* a pid of 0 or below, is -EINVAL. -ESRCH when no process has the pid, -EPERM
|
package/runtime/nish.mjs
CHANGED
|
@@ -94,12 +94,16 @@ provide("appendFileSync", shim.appendFileSync);
|
|
|
94
94
|
provide("writeFileBytesSync", shim.writeFileBytesSync);
|
|
95
95
|
provide("renameSync", shim.renameSync);
|
|
96
96
|
provide("unlinkSync", shim.unlinkSync);
|
|
97
|
+
provide("writeFileSyncMode", shim.writeFileSyncMode);
|
|
98
|
+
provide("createFileExclusiveSync", shim.createFileExclusiveSync);
|
|
99
|
+
provide("mkdirSyncMode", shim.mkdirSyncMode);
|
|
97
100
|
provide("mkdirSync", shim.mkdirSync);
|
|
98
101
|
provide("isDirectorySync", shim.isDirectorySync);
|
|
99
102
|
provide("readdirSync", shim.readdirSync);
|
|
100
103
|
provide("realpathSync", shim.realpathSync);
|
|
101
104
|
provide("spawnSync", shim.spawnSync);
|
|
102
105
|
provide("spawnSyncTo", shim.spawnSyncTo);
|
|
106
|
+
provide("spawnPiped", shim.spawnPiped);
|
|
103
107
|
// WP34 N12. `spawn` and `waitPid` throw, naming themselves: a child is reaped
|
|
104
108
|
// by Node's event loop. `kill`, `availableParallelism` and `totalMemory` answer.
|
|
105
109
|
provide("spawn", shim.spawn);
|
package/runtime/node.mjs
CHANGED
|
@@ -102,6 +102,46 @@ const unlinkSync = (path) => errnoOr(0, () => fs.unlinkSync(path));
|
|
|
102
102
|
const writeFileBytesSync = (path, bytes) =>
|
|
103
103
|
errnoOr(bytes.length, () => fs.writeFileSync(path, Uint8Array.from(bytes), { flag: WRITE_FLAGS, mode: 0o644 }));
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* `writeFileSyncMode` and `createFileExclusiveSync`'s shared body, as
|
|
107
|
+
* `runtime-os.c`'s: the file opened with `flag`, `fchmod`ed to exactly `mode`
|
|
108
|
+
* before a byte is written, then every byte of `data`, then the count, or a
|
|
109
|
+
* negative errno. A mode outside 0..0o777 is -22 before the file is touched.
|
|
110
|
+
* After an exclusive create (`"wx"`), any failure unlinks the file it made.
|
|
111
|
+
*/
|
|
112
|
+
function putFileMode(path, data, flag, mode) {
|
|
113
|
+
if (!(mode >= 0 && mode <= 0o777)) return -22;
|
|
114
|
+
let fd;
|
|
115
|
+
try {
|
|
116
|
+
fd = fs.openSync(path, flag, mode);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return negErrno(e);
|
|
119
|
+
}
|
|
120
|
+
let r;
|
|
121
|
+
try {
|
|
122
|
+
fs.fchmodSync(fd, mode);
|
|
123
|
+
const buf = Buffer.from(data, "utf8");
|
|
124
|
+
for (let done = 0; done < buf.length; ) done += fs.writeSync(fd, buf, done);
|
|
125
|
+
r = buf.length;
|
|
126
|
+
} catch (e) {
|
|
127
|
+
r = negErrno(e);
|
|
128
|
+
}
|
|
129
|
+
const closed = errnoOr(0, () => fs.closeSync(fd));
|
|
130
|
+
const answer = r >= 0 && closed < 0 ? closed : r;
|
|
131
|
+
if (answer < 0 && flag === "wx") errnoOr(0, () => fs.unlinkSync(path));
|
|
132
|
+
return answer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `writeFileSyncMode(path, data, mode)`: `WRITE_FLAGS`, so a link at the path is refused. */
|
|
136
|
+
const writeFileSyncMode = (path, data, mode) => putFileMode(path, data, WRITE_FLAGS, mode);
|
|
137
|
+
|
|
138
|
+
/** `createFileExclusiveSync(path, data, mode)`: `"wx"`, -17 when anything is at the path. */
|
|
139
|
+
const createFileExclusiveSync = (path, data, mode) => putFileMode(path, data, "wx", mode);
|
|
140
|
+
|
|
141
|
+
/** `mkdirSyncMode(path, mode)`: 0, or a negative errno; -17 for a path already there. */
|
|
142
|
+
const mkdirSyncMode = (path, mode) =>
|
|
143
|
+
mode >= 0 && mode <= 0o777 ? errnoOr(0, () => fs.mkdirSync(path, { mode })) : -22;
|
|
144
|
+
|
|
105
145
|
/**
|
|
106
146
|
* `writeBytes`'s write, after the core checked the range `[from, to)`: every
|
|
107
147
|
* byte to stdout, then the count, or a negative errno. `fs.writeSync` may
|
|
@@ -268,6 +308,41 @@ function spawnSyncTo(argv, out, err) {
|
|
|
268
308
|
return spawnImpl(argv, out, err);
|
|
269
309
|
}
|
|
270
310
|
|
|
311
|
+
/** The bytes `spawnPiped` keeps of one stream, as `runtime-os.c`'s `NISH_PIPED_CAP`. */
|
|
312
|
+
const PIPED_CAP = 16 * 1024 * 1024;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* `spawnPiped(argv, stdin)`'s run, after the core took it apart: the status
|
|
316
|
+
* and the two streams' bytes, which the core appends to the caller's arrays.
|
|
317
|
+
* The status is `spawnImpl`'s, and -2 when a stream passed `PIPED_CAP`: Node
|
|
318
|
+
* then kills the child with `SIGKILL`, as the runtime does, and reports
|
|
319
|
+
* `ENOBUFS`, and what it read is cut to the cap here as the runtime keeps it.
|
|
320
|
+
* A child that exits without reading its stdin is not an error on either side.
|
|
321
|
+
*/
|
|
322
|
+
function spawnPiped(argv, input) {
|
|
323
|
+
if (argv.length === 0) return [-1, [], []];
|
|
324
|
+
let r;
|
|
325
|
+
// Node throws, rather than reporting, for an empty command or a NUL in any
|
|
326
|
+
// argument, which the runtime answers -1 for without starting a child.
|
|
327
|
+
try {
|
|
328
|
+
r = child_process.spawnSync(argv[0], argv.slice(1), {
|
|
329
|
+
input: Uint8Array.from(input),
|
|
330
|
+
maxBuffer: PIPED_CAP,
|
|
331
|
+
killSignal: "SIGKILL",
|
|
332
|
+
});
|
|
333
|
+
} catch {
|
|
334
|
+
return [-1, [], []];
|
|
335
|
+
}
|
|
336
|
+
const bytes = (b) => (b === null || b === undefined ? [] : Array.from(b.subarray(0, PIPED_CAP)));
|
|
337
|
+
const out = bytes(r.stdout);
|
|
338
|
+
const err = bytes(r.stderr);
|
|
339
|
+
// `EPIPE` is the child closing its stdin unread, which Node reports with
|
|
340
|
+
// the status it still waited for; the runtime ignores it the same way.
|
|
341
|
+
if (r.error !== undefined && r.error.code !== "EPIPE") return [r.error.code === "ENOBUFS" ? -2 : -1, out, err];
|
|
342
|
+
if (r.signal !== null && r.signal !== undefined) return [128 + (os.constants.signals[r.signal] ?? 0), out, err];
|
|
343
|
+
return [r.status === null ? -1 : r.status, out, err];
|
|
344
|
+
}
|
|
345
|
+
|
|
271
346
|
/**
|
|
272
347
|
* `kill(pid, sig)`: 0, or a negative errno, as `runtime-os.c`'s `nish_kill`
|
|
273
348
|
* answers. Only 0, 2, 9 and 15 are sent and a pid of 0 or below is refused,
|
|
@@ -393,6 +468,9 @@ const CALLS = {
|
|
|
393
468
|
writeFileBytesSync,
|
|
394
469
|
renameSync,
|
|
395
470
|
unlinkSync,
|
|
471
|
+
writeFileSyncMode,
|
|
472
|
+
createFileExclusiveSync,
|
|
473
|
+
mkdirSyncMode,
|
|
396
474
|
writeBytes,
|
|
397
475
|
mkdirSync,
|
|
398
476
|
isDirectorySync,
|
|
@@ -403,6 +481,7 @@ const CALLS = {
|
|
|
403
481
|
getenv,
|
|
404
482
|
spawnSync,
|
|
405
483
|
spawnSyncTo,
|
|
484
|
+
spawnPiped,
|
|
406
485
|
kill,
|
|
407
486
|
availableParallelism,
|
|
408
487
|
totalMemory,
|
package/runtime/runtime-os.c
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
#include <unistd.h>
|
|
50
50
|
#ifndef __wasi__
|
|
51
51
|
/* WASI has no processes, so `spawn.h` and `wait.h` do not exist there. */
|
|
52
|
+
#include <poll.h>
|
|
52
53
|
#include <signal.h>
|
|
53
54
|
#include <spawn.h>
|
|
54
55
|
#include <sys/resource.h>
|
|
@@ -170,20 +171,9 @@ nish_str *nish_read_file(const nish_str *path) {
|
|
|
170
171
|
return s;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
/* `
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
its source, so a link planted at that name in a checkout would otherwise
|
|
177
|
-
truncate any file the user can write. A link among the directories on the
|
|
178
|
-
way is still followed.
|
|
179
|
-
|
|
180
|
-
`O_CLOEXEC` for the reason the read has it (RT-12), and `EINTR` is retried
|
|
181
|
-
as the read retries it (RT-13): a write cut short is finished, and one that
|
|
182
|
-
fails is `cannot write`, never a silently short file.
|
|
183
|
-
|
|
184
|
-
The loop is `nish_put_all`, which `writeFileBytesSync` and `writeBytes`
|
|
185
|
-
share: all `n` bytes of `p` to `fd`, answering 0 or the negative errno of
|
|
186
|
-
the write that failed (-EIO for one that wrote nothing). */
|
|
174
|
+
/* All `n` bytes of `p` to `fd`, answering 0 or the negative errno of the
|
|
175
|
+
write that failed (-EIO for one that wrote nothing). A write cut short is
|
|
176
|
+
finished and `EINTR` retried (RT-13). Every writer below shares it. */
|
|
187
177
|
static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
|
|
188
178
|
for (uint64_t done = 0; done < n;) {
|
|
189
179
|
ssize_t k = write(fd, p + done, n - done);
|
|
@@ -193,6 +183,17 @@ static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
|
|
|
193
183
|
}
|
|
194
184
|
return 0;
|
|
195
185
|
}
|
|
186
|
+
|
|
187
|
+
/* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
|
|
188
|
+
component of the path is refused, as an unwritable file is, rather than
|
|
189
|
+
followed to whatever file it names. `nish file.ts` writes `file.ll` beside
|
|
190
|
+
its source, so a link planted at that name in a checkout would otherwise
|
|
191
|
+
truncate any file the user can write. A link among the directories on the
|
|
192
|
+
way is still followed.
|
|
193
|
+
|
|
194
|
+
`O_CLOEXEC` for the reason the read has it (RT-12), and the bytes go out
|
|
195
|
+
through `nish_put_all`, so a write that fails is `cannot write`, never a
|
|
196
|
+
silently short file. */
|
|
196
197
|
static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
|
|
197
198
|
int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
|
|
198
199
|
if (fd < 0 || nish_put_all(fd, data->data, data->len) < 0) nish_io_fail("write ", path);
|
|
@@ -248,6 +249,73 @@ int32_t nish_write_file_bytes(const nish_str *path, const nish_array *bytes) {
|
|
|
248
249
|
return r < 0 ? r : (int32_t)bytes->len;
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
/* ---- Files and directories with an explicit mode (docs/security/runtime.md,
|
|
253
|
+
RT-24 to RT-26): `writeFileSyncMode`, `createFileExclusiveSync` and
|
|
254
|
+
`mkdirSyncMode`. Each answers a value and never exits, as the writers just
|
|
255
|
+
above do. A mode outside 0..0777 is -EINVAL before any system call: the
|
|
256
|
+
setuid, setgid and sticky bits are refused, not applied, so the mode asked
|
|
257
|
+
for is the mode the file has. WASI
|
|
258
|
+
has no file modes to apply, so there each answers -ENOSYS and creates
|
|
259
|
+
nothing rather than a file whose mode was not set. */
|
|
260
|
+
|
|
261
|
+
/* The two file writers' shared body: `open(p, O_WRONLY | O_CREAT |
|
|
262
|
+
O_NOFOLLOW | O_CLOEXEC | flags, mode)`, then `fchmod(fd, mode)` before a
|
|
263
|
+
byte is written, so the file is exactly `mode` whatever the umask and
|
|
264
|
+
whatever mode a file already there had: no reader that opens it while it is
|
|
265
|
+
written finds it wider (RT-24). Then the data through `nish_put_all` and a
|
|
266
|
+
checked `close`, as `nish_write_file_bytes` does. The open truncates
|
|
267
|
+
before the `fchmod`, so a file there that this process may write but does
|
|
268
|
+
not own is emptied and then answers -EPERM. When `flags` holds `O_EXCL`,
|
|
269
|
+
any failure after the create unlinks the file this call made, so a lock
|
|
270
|
+
whose write failed does not stay taken. */
|
|
271
|
+
static int32_t nish_put_file_mode(const nish_str *path, const nish_str *data, int flags, int32_t mode) {
|
|
272
|
+
const char *p = nish_cpath(path);
|
|
273
|
+
if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
|
|
274
|
+
if (data->len > NISH_LENGTH_MAX) return -EFBIG;
|
|
275
|
+
#ifdef __wasi__
|
|
276
|
+
(void)flags;
|
|
277
|
+
return -ENOSYS;
|
|
278
|
+
#else
|
|
279
|
+
int fd = open(p, O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, (mode_t)mode);
|
|
280
|
+
if (fd < 0) return -errno;
|
|
281
|
+
int32_t r = fchmod(fd, (mode_t)mode) == 0 ? 0 : -errno;
|
|
282
|
+
if (r == 0) r = nish_put_all(fd, data->data, data->len);
|
|
283
|
+
if (close(fd) != 0 && r == 0 && errno != EINTR) r = -errno;
|
|
284
|
+
if (r < 0 && (flags & O_EXCL)) unlink(p);
|
|
285
|
+
return r < 0 ? r : (int32_t)data->len;
|
|
286
|
+
#endif
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* `writeFileSyncMode(path, data, mode)`: `writeFileSync` with the mode given
|
|
290
|
+
and an answer instead of an exit. A link at the path is refused (-ELOOP on
|
|
291
|
+
Linux) and the file it names is untouched (RT-25). */
|
|
292
|
+
int32_t nish_write_file_mode(const nish_str *path, const nish_str *data, int32_t mode) {
|
|
293
|
+
return nish_put_file_mode(path, data, O_TRUNC, mode);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* `createFileExclusiveSync(path, data, mode)`: `O_EXCL`, so the path is
|
|
297
|
+
created by this call or the call answers -EEXIST, a link there included,
|
|
298
|
+
dangling or not. Of N processes racing for one path exactly one wins, which
|
|
299
|
+
is what a lock file needs; a winner whose write then fails removes the file
|
|
300
|
+
and answers the error. */
|
|
301
|
+
int32_t nish_create_file_exclusive(const nish_str *path, const nish_str *data, int32_t mode) {
|
|
302
|
+
return nish_put_file_mode(path, data, O_EXCL, mode);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* `mkdirSyncMode(path, mode)`: mkdir(2) with `mode`, one directory, not
|
|
306
|
+
recursive. A path already there is -EEXIST and its mode is left as it was.
|
|
307
|
+
The kernel applies the umask, so the directory is `mode & ~umask`: 0700 is
|
|
308
|
+
exact under any umask that leaves the owner's bits (RT-26). */
|
|
309
|
+
int32_t nish_mkdir_mode(const nish_str *path, int32_t mode) {
|
|
310
|
+
const char *p = nish_cpath(path);
|
|
311
|
+
if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
|
|
312
|
+
#ifdef __wasi__
|
|
313
|
+
return -ENOSYS;
|
|
314
|
+
#else
|
|
315
|
+
return mkdir(p, (mode_t)mode) == 0 ? 0 : -errno;
|
|
316
|
+
#endif
|
|
317
|
+
}
|
|
318
|
+
|
|
251
319
|
/* `writeBytes(bytes, off, len)` (nish:io): the bytes `[off, off + len)` of a
|
|
252
320
|
`u8[]` on stdout, through `nish_put_all`, so a short write is finished and `EINTR` retried. The
|
|
253
321
|
emitter has range-checked `off` and `len` against `bytes->len`
|
|
@@ -290,17 +358,19 @@ _Bool nish_mkdir(const nish_str *path) {
|
|
|
290
358
|
value rather than through a child that has already run.
|
|
291
359
|
|
|
292
360
|
`out` and `err` are paths for the child's stdout and stderr, or NULL to let
|
|
293
|
-
it inherit this process's
|
|
361
|
+
it inherit this process's; `dup`, when not NULL, is three descriptors the
|
|
362
|
+
child takes as its stdin, stdout and stderr instead (`spawnPiped`'s pipes). Every spawn builtin starts its child here, so the
|
|
294
363
|
argv vector and the redirects are written once; it is `static`, so a program
|
|
295
364
|
that spawns nothing loses it to `--gc-sections`. The answer is the child's
|
|
296
365
|
pid, or a negative errno: -EINVAL for an empty argv or an argument holding a
|
|
297
366
|
NUL, and `posix_spawnp`'s own error otherwise (-ENOENT for a command that is
|
|
298
367
|
not on PATH, on glibc). */
|
|
299
|
-
static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err) {
|
|
368
|
+
static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err, const int *dup) {
|
|
300
369
|
#ifdef __wasi__
|
|
301
370
|
(void)argv;
|
|
302
371
|
(void)out;
|
|
303
372
|
(void)err;
|
|
373
|
+
(void)dup;
|
|
304
374
|
return -ENOSYS;
|
|
305
375
|
#else
|
|
306
376
|
if (!argv->len) return -EINVAL; /* argv[0] would read past an empty array */
|
|
@@ -323,6 +393,13 @@ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, con
|
|
|
323
393
|
posix_spawn_file_actions_t fa;
|
|
324
394
|
int failed = posix_spawn_file_actions_init(&fa);
|
|
325
395
|
if (failed) return -failed;
|
|
396
|
+
/* A `dup2` action that could not be recorded would start the child on this
|
|
397
|
+
process's own stream in its place, so it fails the spawn. */
|
|
398
|
+
for (int k = 0; dup && k < 3 && !failed; k++) failed = posix_spawn_file_actions_adddup2(&fa, dup[k], k);
|
|
399
|
+
if (failed) {
|
|
400
|
+
posix_spawn_file_actions_destroy(&fa);
|
|
401
|
+
return -failed;
|
|
402
|
+
}
|
|
326
403
|
const nish_str *to[2] = { out, err };
|
|
327
404
|
for (int k = 0; k < 2; k++) {
|
|
328
405
|
/* The child opens the file, not this process: a redirect that this process
|
|
@@ -348,7 +425,7 @@ static int32_t nish_exit_code(int status) {
|
|
|
348
425
|
/* The two blocking builtins: start, then wait for that one child. A failed
|
|
349
426
|
start is -1, the answer they have always given. */
|
|
350
427
|
static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
|
|
351
|
-
int32_t pid = nish_spawn_start(argv, out, err);
|
|
428
|
+
int32_t pid = nish_spawn_start(argv, out, err, 0);
|
|
352
429
|
if (pid < 0) return -1;
|
|
353
430
|
#ifdef __wasi__
|
|
354
431
|
return -1;
|
|
@@ -379,7 +456,160 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
|
|
|
379
456
|
/* `spawn(argv)`: the child's pid, or a negative errno, without waiting. The
|
|
380
457
|
child is this process's until a `waitPid` reaps it; one never reaped stays
|
|
381
458
|
a zombie until the program exits, as it would in C. */
|
|
382
|
-
int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0); }
|
|
459
|
+
int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0, 0); }
|
|
460
|
+
|
|
461
|
+
/* `spawnPiped(argv, stdin, stdout, stderr)`: run the child as `nish_spawn`
|
|
462
|
+
does, with `stdin`'s bytes on its standard input and its standard output
|
|
463
|
+
and error appended to `stdout` and `stderr`, and answer its status. -1 is
|
|
464
|
+
`nish_spawn`'s failure (an empty argv, a NUL, a child that cannot start or
|
|
465
|
+
be waited for, a pipe or `poll` that fails), and -2 is a stream that passed
|
|
466
|
+
NISH_PIPED_CAP bytes: the child is killed with SIGKILL and reaped, and the
|
|
467
|
+
array holds the first NISH_PIPED_CAP bytes of that stream.
|
|
468
|
+
|
|
469
|
+
The three pipes are close-on-exec, so no other child this process starts
|
|
470
|
+
inherits an end, and each end is moved off descriptors 0 to 2, where a
|
|
471
|
+
`dup2` onto itself would leave close-on-exec set and the child without
|
|
472
|
+
that stream. Darwin has no `pipe2`, so there an end is close-on-exec only
|
|
473
|
+
once `F_DUPFD_CLOEXEC` has moved it: a child another thread starts in
|
|
474
|
+
that window inherits it (docs/security/runtime.md, RT-30).
|
|
475
|
+
|
|
476
|
+
One `poll` loop over non-blocking ends writes stdin and reads both outputs
|
|
477
|
+
as each is ready, so a child that fills its stdout before it reads all its
|
|
478
|
+
stdin, or the reverse, cannot deadlock against this process. A child that
|
|
479
|
+
exits, or closes its stdin, before reading everything ends the writing
|
|
480
|
+
with EPIPE rather than SIGPIPE: the signal is blocked on this thread once
|
|
481
|
+
the child has started, the one a write raised is taken off the pending
|
|
482
|
+
set, and the mask is put back before the answer (docs/security/runtime.md,
|
|
483
|
+
RT-29). Ignoring it instead would be process-wide, and every child
|
|
484
|
+
started meanwhile, this one included, would inherit SIGPIPE ignored and
|
|
485
|
+
never die of a reader that has gone.
|
|
486
|
+
|
|
487
|
+
The bytes are written from the caller's array as they stand, and nothing
|
|
488
|
+
copies them: the only copies are the kernel's pipe buffer and the child.
|
|
489
|
+
The length is read once, so `stdout` being the same array as `stdin`
|
|
490
|
+
appends to it without feeding the output back in, and the pointer at each
|
|
491
|
+
write, because that array moves when it grows and its stdin bytes stay
|
|
492
|
+
its first `n`. */
|
|
493
|
+
#define NISH_PIPED_CAP (16u << 20)
|
|
494
|
+
#ifndef __wasi__
|
|
495
|
+
static int nish_pipe(int *p) {
|
|
496
|
+
#ifdef __linux__
|
|
497
|
+
if (pipe2(p, O_CLOEXEC)) return -1;
|
|
498
|
+
#else
|
|
499
|
+
if (pipe(p)) return -1;
|
|
500
|
+
#endif
|
|
501
|
+
for (int k = 0; k < 2; k++) {
|
|
502
|
+
#ifdef __linux__
|
|
503
|
+
if (p[k] > 2) continue;
|
|
504
|
+
#endif
|
|
505
|
+
int moved = fcntl(p[k], F_DUPFD_CLOEXEC, 3);
|
|
506
|
+
close(p[k]);
|
|
507
|
+
p[k] = moved;
|
|
508
|
+
if (moved < 0) return -1;
|
|
509
|
+
}
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#endif
|
|
514
|
+
|
|
515
|
+
int32_t nish_spawn_piped(const nish_array *argv, const nish_array *in, nish_array *out, nish_array *err) {
|
|
516
|
+
#ifdef __wasi__
|
|
517
|
+
(void)argv;
|
|
518
|
+
(void)in;
|
|
519
|
+
(void)out;
|
|
520
|
+
(void)err;
|
|
521
|
+
return -1;
|
|
522
|
+
#else
|
|
523
|
+
uint64_t n = in->len, sent = 0, got[3] = { 0, 0, 0 };
|
|
524
|
+
int p[6] = { -1, -1, -1, -1, -1, -1 };
|
|
525
|
+
int32_t rc = -1;
|
|
526
|
+
if (nish_pipe(p) || nish_pipe(p + 2) || nish_pipe(p + 4)) goto done;
|
|
527
|
+
int child[3] = { p[0], p[3], p[5] };
|
|
528
|
+
int32_t pid = nish_spawn_start(argv, 0, 0, child);
|
|
529
|
+
/* The child holds its ends now; this process keeps only its own. */
|
|
530
|
+
for (int k = 0; k < 3; k++) {
|
|
531
|
+
close(child[k]);
|
|
532
|
+
p[k ? 2 * k + 1 : 0] = -1;
|
|
533
|
+
}
|
|
534
|
+
if (pid < 0) goto done;
|
|
535
|
+
sigset_t pipe_sig, mask, pending;
|
|
536
|
+
sigemptyset(&pipe_sig);
|
|
537
|
+
sigaddset(&pipe_sig, SIGPIPE);
|
|
538
|
+
pthread_sigmask(SIG_BLOCK, &pipe_sig, &mask);
|
|
539
|
+
sigpending(&pending);
|
|
540
|
+
int queued = sigismember(&pending, SIGPIPE);
|
|
541
|
+
struct pollfd f[3] = { { p[1], POLLOUT, 0 }, { p[2], POLLIN, 0 }, { p[4], POLLIN, 0 } };
|
|
542
|
+
p[1] = p[2] = p[4] = -1;
|
|
543
|
+
/* A blocking end could stall the loop on one stream while the child fills
|
|
544
|
+
another, so an end that stays blocking fails the run: the loop is skipped
|
|
545
|
+
and the child killed. */
|
|
546
|
+
rc = 0;
|
|
547
|
+
for (int k = 0; k < 3; k++)
|
|
548
|
+
if (fcntl(f[k].fd, F_SETFL, O_NONBLOCK) < 0) rc = -1;
|
|
549
|
+
if (!n) {
|
|
550
|
+
close(f[0].fd);
|
|
551
|
+
f[0].fd = -1;
|
|
552
|
+
}
|
|
553
|
+
nish_array *to[3] = { 0, out, err };
|
|
554
|
+
while (!rc && (f[0].fd >= 0 || f[1].fd >= 0 || f[2].fd >= 0)) {
|
|
555
|
+
if (poll(f, 3, -1) < 0) {
|
|
556
|
+
if (errno != EINTR) rc = -1;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (f[0].revents) {
|
|
560
|
+
ssize_t k = write(f[0].fd, in->data + sent, n - sent);
|
|
561
|
+
if (k > 0) sent += k;
|
|
562
|
+
if (sent == n || (k < 0 && errno != EINTR && errno != EAGAIN)) {
|
|
563
|
+
close(f[0].fd);
|
|
564
|
+
f[0].fd = -1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
for (int s = 1; s < 3 && !rc; s++) {
|
|
568
|
+
if (!f[s].revents) continue;
|
|
569
|
+
nish_array *a = to[s];
|
|
570
|
+
/* Grown as `push` grows it, so the array keeps the shape every other
|
|
571
|
+
array has, and an ASan build tracks the block the same way. */
|
|
572
|
+
if (a->len == a->cap) nish_array_grow(a, 1);
|
|
573
|
+
/* One byte past the cap at most, which is how a stream that ends at
|
|
574
|
+
exactly the cap is told from one that goes on. */
|
|
575
|
+
uint64_t room = a->cap - a->len, left = NISH_PIPED_CAP + 1 - got[s];
|
|
576
|
+
ssize_t k = read(f[s].fd, a->data + a->len, room < left ? room : left);
|
|
577
|
+
if (k > 0) {
|
|
578
|
+
a->len += k;
|
|
579
|
+
got[s] += k;
|
|
580
|
+
if (got[s] > NISH_PIPED_CAP) {
|
|
581
|
+
a->len--;
|
|
582
|
+
rc = -2;
|
|
583
|
+
}
|
|
584
|
+
} else if (!k || (errno != EINTR && errno != EAGAIN)) {
|
|
585
|
+
close(f[s].fd);
|
|
586
|
+
f[s].fd = -1;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
sigpending(&pending);
|
|
591
|
+
if (!queued && sigismember(&pending, SIGPIPE)) {
|
|
592
|
+
int sig;
|
|
593
|
+
sigwait(&pipe_sig, &sig);
|
|
594
|
+
}
|
|
595
|
+
pthread_sigmask(SIG_SETMASK, &mask, 0);
|
|
596
|
+
if (rc) kill(pid, SIGKILL);
|
|
597
|
+
for (int k = 0; k < 3; k++)
|
|
598
|
+
if (f[k].fd >= 0) close(f[k].fd);
|
|
599
|
+
int status;
|
|
600
|
+
while (waitpid(pid, &status, 0) < 0) {
|
|
601
|
+
if (errno != EINTR) {
|
|
602
|
+
rc = -1;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (!rc) rc = nish_exit_code(status);
|
|
607
|
+
done:
|
|
608
|
+
for (int k = 0; k < 6; k++)
|
|
609
|
+
if (p[k] >= 0) close(p[k]);
|
|
610
|
+
return rc;
|
|
611
|
+
#endif
|
|
612
|
+
}
|
|
383
613
|
|
|
384
614
|
/* `kill(pid, sig)`: 0, or a negative errno. Only the four signals a program
|
|
385
615
|
asks a child to stop or report with are sent -- 0 (is it there), SIGINT,
|
package/std/README.md
CHANGED
|
@@ -239,7 +239,7 @@ reads one, and it still takes its randomness as an argument.
|
|
|
239
239
|
| [`net/webtransport.ts`](./net/webtransport.ts) | `WebTransport`, the server side of WebTransport over HTTP/3 as draft-ietf-webtrans-http3-02 has it and Chrome and `wtransport` 0.7 negotiate it, a layer over one `Http3Connection` whose `Http3Config.webtransportSessions` turns it on (extended CONNECT, SETTINGS_H3_DATAGRAM and the draft's two WebTransport settings): `next()` answers `WT_SESSION` (the program `accept`s or `refuse`s it), `WT_DATAGRAM`, `WT_STREAM`, `WT_STREAM_DATA`, `WT_STREAM_END`, `WT_STREAM_RESET`, `WT_STREAM_STOPPED`, `WT_WRITABLE`, `WT_DRAIN` and `WT_CLOSED`, and passes a plain request's `H3_*` events through; `sendDatagram` (the quarter stream ID, then the payload), `openStream`, `write`, `resetStream`, `stopSending`, `close` (CLOSE_WEBTRANSPORT_SESSION) and `drain`. Streams that arrive before their session wait, bounded; every cap is fixed at start-up, and a warm session allocates nothing per datagram or stream (`docs/security/webtransport.md`) | RFC 9220, RFC 9297 and draft-02: a Nish client over QUIC — sessions, datagrams at the size limits, streams both ways, close and drain from either side, every refusal — and one exchange recorded from a `wtransport` 0.7 client replayed byte for byte (`tests/link/net_webtransport`) |
|
|
240
240
|
| [`net/http-compress.ts`](./net/http-compress.ts) | Content coding for HTTP responses: `httpAcceptEncoding(buf, start, end, offered)` reads an `Accept-Encoding` value (q-values, `*`, `identity`, `x-gzip`) into `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`, `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, allocating nothing; `HttpCompressor(level, minLength, maxLength)` gzips a body into its own buffer when it is worth it (-1 to send it as it is) and builds the response fields (`addFields`: `Content-Encoding: gzip` and, while `vary`, `Vary: Accept-Encoding`). On HTTP/1.1, `Http1Connection.acceptEncoding()` and `respondEncoded(status, names, values, body, off, len, enc, coding)` answer a request in one call with the compressed `Content-Length`, or nothing at all (`H1_AGAIN`, `H1_INVALID`). Opt-in; BREACH is the program's to avoid (`docs/security/compress.md`) | RFC 9110 §8.4 and §12.5.3: the negotiation table, and sans-IO responses gunzipped back (`tests/link/std_http_compress`) |
|
|
241
241
|
| [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
|
|
242
|
-
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first, to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
|
|
242
|
+
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first, to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
|
|
243
243
|
|
|
244
244
|
**How a carrier drives `TlsServer`.** The server speaks in handshake
|
|
245
245
|
*messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
|
|
@@ -496,7 +496,7 @@ have, and all three have since landed as builtins:
|
|
|
496
496
|
| Was missing | Now |
|
|
497
497
|
| --- | --- |
|
|
498
498
|
| a directory listing | `readdirSync(path): string[] \| null`, sorted by bytes, because there is no `sort` for a caller to reach for |
|
|
499
|
-
| a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting |
|
|
499
|
+
| a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting; `spawnPiped(argv, stdin, stdout, stderr)`, stdin from a `u8[]` and both outputs into `u8[]`s, for a secret the child reads on stdin |
|
|
500
500
|
| a clock | `monotonicNanos(): i64` |
|
|
501
501
|
|
|
502
502
|
So the driver exists, in the language, and it is
|
package/std/net/postgres.ts
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* // PG_READY: authenticated; prepare statements and send queries
|
|
18
18
|
* // conn.prepare("by_id", "select id, name from users where id = $1", [23]); conn.sync()
|
|
19
19
|
* // conn.bindStart("by_id"); conn.paramInt(7); conn.bindEnd(); conn.sync()
|
|
20
|
+
* // or conn.bindStartBinary("by_id") for binary result columns
|
|
20
21
|
* // PG_ROW: conn.int(0), conn.colStart(1) .. + conn.colLength(1) in conn.inputBuffer()
|
|
22
|
+
* // after bindStartBinary: conn.binaryInt(0), the text column as before
|
|
21
23
|
* // PG_COMPLETE: conn.tagRows(); PG_ERROR: conn.lastError.code
|
|
22
24
|
* // PG_GROUP_DONE: one sync()'s group has been answered
|
|
23
25
|
* event = conn.next();
|
|
@@ -57,18 +59,29 @@
|
|
|
57
59
|
* does not need: a program prepares a statement and executes it in the same
|
|
58
60
|
* flight.
|
|
59
61
|
*
|
|
60
|
-
* **Text results, no copies.** Every parameter
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* length in two reused arrays, so
|
|
64
|
-
* `isNull` and `PgText.timestamp`
|
|
65
|
-
*
|
|
66
|
-
* allocates, a string. An array
|
|
67
|
-
* `paramIntArray` writes an `int8[]`
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* unsent, with `bindCancel`.
|
|
62
|
+
* **Text results, no copies.** Every parameter is in text format, and so is
|
|
63
|
+
* every result column unless the bind asks otherwise. A row is read where it
|
|
64
|
+
* lies in the input buffer: `next()` frames each message once, validates its
|
|
65
|
+
* layout, and records each column's start and length in two reused arrays, so
|
|
66
|
+
* a row costs no allocation. `int`, `bool`, `isNull` and `PgText.timestamp`
|
|
67
|
+
* decode a column in place; `PgArrayWalker` walks a one-dimensional `text[]`
|
|
68
|
+
* in place. `columnText(c)` is the one that allocates, a string. An array
|
|
69
|
+
* parameter is a text array literal too: `paramIntArray` writes an `int8[]`
|
|
70
|
+
* from an `i64[]` window, and `arrayStart`, `arrayElement`, `arrayInt`,
|
|
71
|
+
* `arrayNull` and `arrayEnd` build an array one element at a time into the
|
|
72
|
+
* Bind, so a list needs no string built first. A program whose element was
|
|
73
|
+
* refused for room can drop the whole bind, unsent, with `bindCancel`.
|
|
74
|
+
*
|
|
75
|
+
* **Binary results.** A bind opened with `bindStartBinary` instead of
|
|
76
|
+
* `bindStart` asks for every result column in binary format: an `int8` is
|
|
77
|
+
* eight bytes, a `timestamptz` eight bytes of microseconds since 2000, a
|
|
78
|
+
* `bool` one byte, so there is nothing to parse and less on the wire.
|
|
79
|
+
* `binaryInt`, `binaryBool` and `binaryFloat` read them in place, and
|
|
80
|
+
* `PgBinary`'s `timestamp` and `iso` write a time as `PgText.timestamp` does.
|
|
81
|
+
* A `text` column is the same bytes in either format, so `columnText` and the
|
|
82
|
+
* column windows read it unchanged. The program knows which of its binds
|
|
83
|
+
* asked for which, and reads each execution's rows with the matching
|
|
84
|
+
* decoders.
|
|
72
85
|
*
|
|
73
86
|
* **Authentication.** SCRAM-SHA-256 (RFC 5802, RFC 7677) or none (`trust`).
|
|
74
87
|
* The password arrives as a `Secret<u8[]>`; the connection keeps a copy in a
|
|
@@ -100,8 +113,9 @@
|
|
|
100
113
|
* that needs a socket, `connect(slot, nonce)` starts its connection, and the
|
|
101
114
|
* program moves its bytes as above through `connection(slot)`. A request is a
|
|
102
115
|
* ticket from `request()`; `dispatch()` gives the oldest to the ready
|
|
103
|
-
* connection with the fewest groups in flight,
|
|
104
|
-
*
|
|
116
|
+
* connection with the fewest groups in flight (or, with `PgConfig.pack`, to the
|
|
117
|
+
* first ready one with room, so a burst fills few connections in fuller
|
|
118
|
+
* writes), the program builds its group and ends it with `send()`, and `next(slot)` reads that connection's events with
|
|
105
119
|
* `ticket` naming the request. Statements given to `register` are prepared on
|
|
106
120
|
* each connection ahead of its first request. A connection that fails reports
|
|
107
121
|
* `PG_LOST` for each request it took, then `PG_FAILED`; the program closes the
|
|
@@ -127,7 +141,7 @@
|
|
|
127
141
|
* Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
|
|
128
142
|
* and RFC 5802 and RFC 7677; not ported from another implementation.
|
|
129
143
|
*/
|
|
130
|
-
import { copyBytes, getI32BE, getU16BE, setI32BE, setU16BE } from "nish/bytes"
|
|
144
|
+
import { copyBytes, getF64BE, getI32BE, getI64BE, getU16BE, setI32BE, setU16BE } from "nish/bytes"
|
|
131
145
|
import { base64StdPadDecode, base64StdPadEncode } from "nish/crypto/base64url"
|
|
132
146
|
import { timingSafeEqual } from "nish/crypto/ct"
|
|
133
147
|
import { hmacSha256, pbkdf2HmacSha256 } from "nish/crypto/hmac"
|
|
@@ -218,6 +232,11 @@ const PG_NONCE_BYTES: i32 = 18
|
|
|
218
232
|
// take: kept free under `maxOutput` by every other builder call, so that
|
|
219
233
|
// `bindEnd()` and `sync()` are never refused for room.
|
|
220
234
|
const PG_TAIL: i32 = 17
|
|
235
|
+
// What a binary bind's end takes beyond `PG_TAIL`: its one result format code,
|
|
236
|
+
// kept free from `bindStartBinary` on.
|
|
237
|
+
const PG_BINARY_TAIL: i32 = 2
|
|
238
|
+
// The result format code for binary.
|
|
239
|
+
const PG_FORMAT_BINARY: i32 = 1
|
|
221
240
|
// The largest `maxMessage` and `maxOutput`. The input buffer doubles up to one
|
|
222
241
|
// past them; the output buffer doubles only while twice its size fits in an i32.
|
|
223
242
|
const PG_MAX_CAP: i32 = 1073741823
|
|
@@ -238,6 +257,15 @@ export class PgConfig {
|
|
|
238
257
|
maxMessage: i32 = 1048576
|
|
239
258
|
/** The most groups waiting for their answers at once. */
|
|
240
259
|
maxInFlight: i32 = 16
|
|
260
|
+
/**
|
|
261
|
+
* Read by `PgPool` only: whether `dispatch()` packs requests onto the
|
|
262
|
+
* first ready connection with room, filling it to `maxInFlight` before the
|
|
263
|
+
* next, rather than spreading them over every connection, fewest in flight
|
|
264
|
+
* first. Fewer connections in use send fuller writes, fewer packets, which
|
|
265
|
+
* pays on a lossy link to the server; a connection is still wanted only once
|
|
266
|
+
* every ready one is full.
|
|
267
|
+
*/
|
|
268
|
+
pack: boolean = false
|
|
241
269
|
/** The input buffer's first size, in bytes; it grows to a message's length, up to `maxMessage` + 1. */
|
|
242
270
|
inputSize: i32 = 8192
|
|
243
271
|
/** The widest row; past it, `PG_ERR_TOO_BIG`. PostgreSQL's own limit is 1664. */
|
|
@@ -677,6 +705,159 @@ export class PgText {
|
|
|
677
705
|
}
|
|
678
706
|
}
|
|
679
707
|
|
|
708
|
+
// The days from 0000-03-01, where the civil calendar's year starts for the
|
|
709
|
+
// conversion below, to 2000-01-01, where PostgreSQL's binary times count from.
|
|
710
|
+
const PG_EPOCH_DAYS: i64 = 730425
|
|
711
|
+
// The first and last days `PgBinary.iso` writes, counted from 2000-01-01:
|
|
712
|
+
// 0001-01-01 and 9999-12-31, the years four digits spell.
|
|
713
|
+
const PG_FIRST_DAY: i64 = -730119
|
|
714
|
+
const PG_LAST_DAY: i64 = 2921939
|
|
715
|
+
|
|
716
|
+
/** `value`'s last `width` decimal digits into `out[at, at + width)`. */
|
|
717
|
+
const pgDigits = (out: u8[], at: i32, value: i32, width: i32): void => {
|
|
718
|
+
let v: i32 = value
|
|
719
|
+
for (let i: i32 = width - 1; i >= 0; i--) {
|
|
720
|
+
out[at + i] = toU8((v % 10) + 48)
|
|
721
|
+
v = v / 10
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Decoders for PostgreSQL's binary format, in place, for the rows of a bind
|
|
727
|
+
* that `bindStartBinary` opened; a bad input sets `failed`, which stays set.
|
|
728
|
+
* A `text` or `varchar` column's bytes are the same in either format, so
|
|
729
|
+
* `columnText`, `colStart` and `colLength` read it as they read a text one.
|
|
730
|
+
*/
|
|
731
|
+
export class PgBinary {
|
|
732
|
+
/** Whether a decode since the last `reset()` was given something it could not read. */
|
|
733
|
+
failed: boolean = false
|
|
734
|
+
|
|
735
|
+
/** Clears `failed`. */
|
|
736
|
+
reset(): void {
|
|
737
|
+
this.failed = false
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Whether `[start, start + length)` is inside `b`; sets `failed` when not.
|
|
741
|
+
private inside(b: u8[], start: i32, length: i32): boolean {
|
|
742
|
+
if (start < 0 || length < 0 || start > toI32(b.length) - length) {
|
|
743
|
+
this.failed = true
|
|
744
|
+
return false
|
|
745
|
+
}
|
|
746
|
+
return true
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* An `int2`, `int4` or `int8` column, told apart by its length (2, 4 or 8
|
|
751
|
+
* bytes, big-endian two's complement); 0 and `failed` for any other length,
|
|
752
|
+
* NULL's -1 among them. A `timestamptz` or `timestamp` reads as an `int8`:
|
|
753
|
+
* microseconds since 2000-01-01 00:00:00 UTC, which `iso` writes out.
|
|
754
|
+
*/
|
|
755
|
+
int(b: u8[], start: i32, length: i32): i64 {
|
|
756
|
+
if ((length !== 2 && length !== 4 && length !== 8) || !this.inside(b, start, length)) {
|
|
757
|
+
this.failed = true
|
|
758
|
+
return 0
|
|
759
|
+
}
|
|
760
|
+
if (length === 8) {
|
|
761
|
+
return getI64BE(b, start)
|
|
762
|
+
}
|
|
763
|
+
if (length === 4) {
|
|
764
|
+
return toI64(getI32BE(b, start))
|
|
765
|
+
}
|
|
766
|
+
const v: i32 = toI32(getU16BE(b, start))
|
|
767
|
+
return toI64(v >= 32768 ? v - 65536 : v)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** A `bool` column: one byte, 1 or 0; false and `failed` otherwise. */
|
|
771
|
+
bool(b: u8[], start: i32, length: i32): boolean {
|
|
772
|
+
if (length === 1 && this.inside(b, start, length)) {
|
|
773
|
+
const c: i32 = toI32(b[start])
|
|
774
|
+
if (c === 1) {
|
|
775
|
+
return true
|
|
776
|
+
}
|
|
777
|
+
if (c === 0) {
|
|
778
|
+
return false
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
this.failed = true
|
|
782
|
+
return false
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** A `float8` column: eight bytes, IEEE 754 big-endian; 0 and `failed` otherwise. */
|
|
786
|
+
float(b: u8[], start: i32, length: i32): f64 {
|
|
787
|
+
if (length !== 8 || !this.inside(b, start, length)) {
|
|
788
|
+
this.failed = true
|
|
789
|
+
return 0
|
|
790
|
+
}
|
|
791
|
+
return getF64BE(b, start)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* A `timestamptz` or `timestamp` column, written to `out[at, at + 24)` as
|
|
796
|
+
* `iso` writes it. Answers 24, or -1 and `failed` for a column that is not
|
|
797
|
+
* eight bytes or a time `iso` refuses.
|
|
798
|
+
*/
|
|
799
|
+
timestamp(b: u8[], start: i32, length: i32, out: u8[], at: i32): i32 {
|
|
800
|
+
if (length !== 8 || !this.inside(b, start, length)) {
|
|
801
|
+
this.failed = true
|
|
802
|
+
return -1
|
|
803
|
+
}
|
|
804
|
+
return this.iso(getI64BE(b, start), out, at)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* `micros`, microseconds since 2000-01-01 00:00:00 UTC, written to
|
|
809
|
+
* `out[at, at + 24)` as `2024-01-02T03:04:05.123Z`: the microseconds cut,
|
|
810
|
+
* not rounded, to milliseconds, as `PgText.timestamp` cuts the same time.
|
|
811
|
+
* Answers 24, or -1 and `failed` with `out` untouched for a year outside
|
|
812
|
+
* 0001–9999 (`infinity` and `-infinity` among them) or no room in `out`.
|
|
813
|
+
*/
|
|
814
|
+
iso(micros: i64, out: u8[], at: i32): i32 {
|
|
815
|
+
if (at < 0 || at > toI32(out.length) - 24) {
|
|
816
|
+
this.failed = true
|
|
817
|
+
return -1
|
|
818
|
+
}
|
|
819
|
+
// Floored, so that a time before 2000 keeps its day and is cut toward the past.
|
|
820
|
+
const dayMicros: i64 = toI64(86400000) * 1000
|
|
821
|
+
let days: i64 = micros / dayMicros
|
|
822
|
+
let rest: i64 = micros % dayMicros
|
|
823
|
+
if (rest < 0) {
|
|
824
|
+
days -= 1
|
|
825
|
+
rest += dayMicros
|
|
826
|
+
}
|
|
827
|
+
if (days < PG_FIRST_DAY || days > PG_LAST_DAY) {
|
|
828
|
+
this.failed = true
|
|
829
|
+
return -1
|
|
830
|
+
}
|
|
831
|
+
// The civil date of a day count: eras of 400 years (146097 days), each
|
|
832
|
+
// year starting on 1 March so that a leap day is its last.
|
|
833
|
+
const z: i64 = days + PG_EPOCH_DAYS
|
|
834
|
+
const era: i64 = z / 146097
|
|
835
|
+
const doe: i64 = z - era * 146097
|
|
836
|
+
const yoe: i64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
|
|
837
|
+
const doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100)
|
|
838
|
+
const mp: i64 = (5 * doy + 2) / 153
|
|
839
|
+
const day: i64 = doy - (153 * mp + 2) / 5 + 1
|
|
840
|
+
const month: i64 = mp < 10 ? mp + 3 : mp - 9
|
|
841
|
+
const year: i64 = yoe + era * 400 + (month <= 2 ? 1 : 0)
|
|
842
|
+
const millis: i32 = toI32(rest / 1000)
|
|
843
|
+
pgDigits(out, at, toI32(year), 4)
|
|
844
|
+
out[at + 4] = toU8(45)
|
|
845
|
+
pgDigits(out, at + 5, toI32(month), 2)
|
|
846
|
+
out[at + 7] = toU8(45)
|
|
847
|
+
pgDigits(out, at + 8, toI32(day), 2)
|
|
848
|
+
out[at + 10] = toU8(84)
|
|
849
|
+
pgDigits(out, at + 11, millis / 3600000, 2)
|
|
850
|
+
out[at + 13] = toU8(58)
|
|
851
|
+
pgDigits(out, at + 14, (millis / 60000) % 60, 2)
|
|
852
|
+
out[at + 16] = toU8(58)
|
|
853
|
+
pgDigits(out, at + 17, (millis / 1000) % 60, 2)
|
|
854
|
+
out[at + 19] = toU8(46)
|
|
855
|
+
pgDigits(out, at + 20, millis % 1000, 3)
|
|
856
|
+
out[at + 23] = toU8(90)
|
|
857
|
+
return 24
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
680
861
|
/**
|
|
681
862
|
* A walk over a one-dimensional `text[]` column in place: `start` the
|
|
682
863
|
* column, then `next()` until `PG_ARRAY_END` or `PG_ARRAY_BAD`. Each element
|
|
@@ -842,10 +1023,11 @@ export class PgConnection {
|
|
|
842
1023
|
lastNotice: PgError | null = null
|
|
843
1024
|
/** The decoders `int`, `bool` and `isNull` use; `text.failed` says whether one was given a column it could not read. */
|
|
844
1025
|
readonly text: PgText
|
|
1026
|
+
/** The decoders `binaryInt`, `binaryBool` and `binaryFloat` use, for a bind `bindStartBinary` opened; `binary.failed` as `text.failed`. */
|
|
1027
|
+
readonly binary: PgBinary
|
|
845
1028
|
|
|
846
1029
|
private readonly config: PgConfig
|
|
847
1030
|
private readonly password: PgPassword
|
|
848
|
-
private state: i32 = 0
|
|
849
1031
|
private input: u8[]
|
|
850
1032
|
private inStart: i32 = 0
|
|
851
1033
|
private inEnd: i32 = 0
|
|
@@ -863,12 +1045,15 @@ export class PgConnection {
|
|
|
863
1045
|
groupFailed: boolean = false
|
|
864
1046
|
// Whether the open bind opened its group, which `bindCancel` closes again.
|
|
865
1047
|
private bindOpened: boolean = false
|
|
1048
|
+
// Whether the open bind asks for its result columns in binary.
|
|
1049
|
+
private bindBinary: boolean = false
|
|
866
1050
|
private bindAt: i32 = -1
|
|
867
1051
|
private paramCountAt: i32 = 0
|
|
868
1052
|
private paramCount: i32 = 0
|
|
869
1053
|
// The open array parameter's length word, -1 when none is open, and its elements so far.
|
|
870
1054
|
private arrayAt: i32 = -1
|
|
871
1055
|
private arrayCount: i32 = 0
|
|
1056
|
+
private state: i32 = 0
|
|
872
1057
|
private clientNonce: string = ""
|
|
873
1058
|
private serverSignature: u8[]
|
|
874
1059
|
|
|
@@ -885,6 +1070,7 @@ export class PgConnection {
|
|
|
885
1070
|
pgCheckConfig(config)
|
|
886
1071
|
this.config = config
|
|
887
1072
|
this.text = new PgText()
|
|
1073
|
+
this.binary = new PgBinary()
|
|
888
1074
|
this.password = new PgPassword(password)
|
|
889
1075
|
const inputSize: i32 = config.inputSize < 64 ? 64 : config.inputSize
|
|
890
1076
|
this.input = new Array<u8>(inputSize)
|
|
@@ -1574,7 +1760,8 @@ export class PgConnection {
|
|
|
1574
1760
|
*/
|
|
1575
1761
|
private room(size: i32): boolean {
|
|
1576
1762
|
const queued: i32 = this.outEnd - this.outStart
|
|
1577
|
-
|
|
1763
|
+
const tail: i32 = this.bindAt >= 0 && this.bindBinary ? PG_TAIL + PG_BINARY_TAIL : PG_TAIL
|
|
1764
|
+
if (size < 0 || size > this.config.maxOutput - tail - queued) {
|
|
1578
1765
|
return false
|
|
1579
1766
|
}
|
|
1580
1767
|
if (this.outStart > 0 && this.outEnd + size > toI32(this.out.length)) {
|
|
@@ -1740,11 +1927,31 @@ export class PgConnection {
|
|
|
1740
1927
|
* order, then `bindEnd()`. False as `prepare` is.
|
|
1741
1928
|
*/
|
|
1742
1929
|
bindStart(name: string): boolean {
|
|
1930
|
+
return this.openBind(name, false)
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* `bindStart`, asking for every result column of the execution in binary
|
|
1935
|
+
* format rather than text: fixed-size integers, booleans, floats and
|
|
1936
|
+
* times, read in place by `binaryInt`, `binaryBool`, `binaryFloat` and
|
|
1937
|
+
* `binary.timestamp`. A `text` column's bytes are the same in either
|
|
1938
|
+
* format, so `columnText` and the column windows read it as before; a
|
|
1939
|
+
* `text[]` or `numeric` column comes in PostgreSQL's binary layout, which
|
|
1940
|
+
* no decoder here reads. Its `bindEnd()` writes two bytes more, kept free
|
|
1941
|
+
* under `maxOutput` from here on. False as `prepare` is.
|
|
1942
|
+
*/
|
|
1943
|
+
bindStartBinary(name: string): boolean {
|
|
1944
|
+
return this.openBind(name, true)
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
private openBind(name: string, binary: boolean): boolean {
|
|
1743
1948
|
const opens: boolean = !this.groupOpen
|
|
1744
|
-
|
|
1949
|
+
const extra: i32 = binary ? PG_BINARY_TAIL : 0
|
|
1950
|
+
if (pgHasNul(name) || toI32(name.length) > PG_MAX_CAP || !this.canOpen(11 + extra + toI32(name.length))) {
|
|
1745
1951
|
return false
|
|
1746
1952
|
}
|
|
1747
1953
|
this.bindOpened = opens
|
|
1954
|
+
this.bindBinary = binary
|
|
1748
1955
|
this.bindAt = this.startMessage(PG_FE_BIND)
|
|
1749
1956
|
this.putByte(PG_ZERO)
|
|
1750
1957
|
this.putCString(name)
|
|
@@ -2058,9 +2265,10 @@ export class PgConnection {
|
|
|
2058
2265
|
}
|
|
2059
2266
|
|
|
2060
2267
|
/**
|
|
2061
|
-
* Ends the Bind (every result column in text
|
|
2062
|
-
*
|
|
2063
|
-
* room, which the calls
|
|
2268
|
+
* Ends the Bind (every result column in text, or in binary after
|
|
2269
|
+
* `bindStartBinary`) and queues its Execute, for every row. False, and
|
|
2270
|
+
* nothing written, when no bind is open; never for room, which the calls
|
|
2271
|
+
* before it kept.
|
|
2064
2272
|
*/
|
|
2065
2273
|
bindEnd(): boolean {
|
|
2066
2274
|
if (this.bindAt < 0) {
|
|
@@ -2069,10 +2277,17 @@ export class PgConnection {
|
|
|
2069
2277
|
if (this.arrayAt >= 0) {
|
|
2070
2278
|
panic("nish/net/postgres: bindEnd() while an array is open")
|
|
2071
2279
|
}
|
|
2072
|
-
this.
|
|
2280
|
+
const binary: boolean = this.bindBinary
|
|
2281
|
+
this.tailRoom(binary ? PG_TAIL + PG_BINARY_TAIL : PG_TAIL)
|
|
2073
2282
|
const at: i32 = this.bindAt
|
|
2074
2283
|
setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
|
|
2075
|
-
|
|
2284
|
+
if (binary) {
|
|
2285
|
+
// One format code, which the server applies to every column.
|
|
2286
|
+
this.putI16(toI32(1))
|
|
2287
|
+
this.putI16(PG_FORMAT_BINARY)
|
|
2288
|
+
} else {
|
|
2289
|
+
this.putI16(PG_ZERO)
|
|
2290
|
+
}
|
|
2076
2291
|
this.endMessage(at)
|
|
2077
2292
|
this.bindAt = -1
|
|
2078
2293
|
const execute: i32 = this.startMessage(PG_FE_EXECUTE)
|
|
@@ -2170,6 +2385,21 @@ export class PgConnection {
|
|
|
2170
2385
|
return this.text.bool(this.input, this.colStarts[c], this.colLengths[c])
|
|
2171
2386
|
}
|
|
2172
2387
|
|
|
2388
|
+
/** Binary column `c`, an `int2`, `int4`, `int8` or a time's microseconds, as an integer; NULL or another length sets `binary.failed`. */
|
|
2389
|
+
binaryInt(c: i32): i64 {
|
|
2390
|
+
return this.binary.int(this.input, this.colStarts[c], this.colLengths[c])
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/** Binary column `c` as a boolean; NULL or a bad value sets `binary.failed`. */
|
|
2394
|
+
binaryBool(c: i32): boolean {
|
|
2395
|
+
return this.binary.bool(this.input, this.colStarts[c], this.colLengths[c])
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
/** Binary column `c`, a `float8`, as an f64; NULL or another length sets `binary.failed`. */
|
|
2399
|
+
binaryFloat(c: i32): f64 {
|
|
2400
|
+
return this.binary.float(this.input, this.colStarts[c], this.colLengths[c])
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2173
2403
|
/** Whether the tag of the execution just completed starts with the word `word` (`INSERT`, `SELECT`, ...). */
|
|
2174
2404
|
tagIs(word: string): boolean {
|
|
2175
2405
|
const n: i32 = toI32(word.length)
|
|
@@ -2266,7 +2496,8 @@ class PgPoolSlot {
|
|
|
2266
2496
|
*
|
|
2267
2497
|
* A request is a ticket from `request()`, queued in FIFO order. `dispatch()`
|
|
2268
2498
|
* hands the queue's head to the ready connection with the fewest groups in
|
|
2269
|
-
* flight,
|
|
2499
|
+
* flight, or under `PgConfig.pack` the lowest-numbered ready one with room,
|
|
2500
|
+
* never past `maxInFlight` on one; the program builds the request's
|
|
2270
2501
|
* group on `connection(slot)` and ends it with `send()`, not `sync()`.
|
|
2271
2502
|
* `next(slot)` then reads that connection's events, `ticket` naming the
|
|
2272
2503
|
* request each is about. A connection that fails reports `PG_LOST` for each
|
|
@@ -2367,7 +2598,8 @@ export class PgPool {
|
|
|
2367
2598
|
}
|
|
2368
2599
|
|
|
2369
2600
|
/**
|
|
2370
|
-
* Hands the oldest waiting request to a ready connection with room
|
|
2601
|
+
* Hands the oldest waiting request to a ready connection with room (the
|
|
2602
|
+
* one with the fewest in flight, or under `PgConfig.pack` the first), and
|
|
2371
2603
|
* answers its slot, `ticket` set to the request: build its group on
|
|
2372
2604
|
* `connection(slot)`, then `send()`. `PG_POOL_NONE` when nothing waits, a
|
|
2373
2605
|
* request is still being built, or no ready connection has room; then a
|
|
@@ -2381,6 +2613,7 @@ export class PgPool {
|
|
|
2381
2613
|
return PG_POOL_NONE
|
|
2382
2614
|
}
|
|
2383
2615
|
const cap: i32 = this.config.maxInFlight
|
|
2616
|
+
const pack: boolean = this.config.pack
|
|
2384
2617
|
let best: i32 = -1
|
|
2385
2618
|
let load: i32 = 0
|
|
2386
2619
|
let starting: i32 = 0
|
|
@@ -2389,7 +2622,7 @@ export class PgPool {
|
|
|
2389
2622
|
const slot: PgPoolSlot = this.slots[s]
|
|
2390
2623
|
const conn: PgConnection | null = slot.conn
|
|
2391
2624
|
if (slot.state === PG_SLOT_READY && conn !== null) {
|
|
2392
|
-
if (conn.inFlight < cap && (best < 0 || conn.inFlight < load)) {
|
|
2625
|
+
if (conn.inFlight < cap && (best < 0 || (!pack && conn.inFlight < load))) {
|
|
2393
2626
|
best = s
|
|
2394
2627
|
load = conn.inFlight
|
|
2395
2628
|
}
|