@amritk/nish-x86_64-linux 0.13.0 → 0.14.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/package.json +1 -1
- package/runtime/nish.d.ts +65 -0
- package/runtime/nish.h +31 -0
- package/runtime/nish.mjs +35 -0
- package/runtime/runtime-host.c +178 -0
- package/runtime/runtime-os.c +15 -0
- package/runtime/shim.mjs +122 -0
- package/scripts/build.sh +8 -7
- package/std/README.md +40 -0
- package/std/crypto/base64url.ts +145 -0
- package/std/crypto/ct.ts +64 -0
- package/std/crypto/hkdf.ts +118 -0
- package/std/crypto/hmac.ts +155 -0
- package/std/crypto/sha256.ts +444 -0
- package/std/crypto/sha512.ts +510 -0
- package/std/crypto/x25519.ts +494 -0
package/bin/nish
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/runtime/nish.d.ts
CHANGED
|
@@ -161,6 +161,18 @@ declare function toF64(x: number | boolean): f64;
|
|
|
161
161
|
declare function f64ToBits(x: f64): i64;
|
|
162
162
|
declare function bitsToF64(bits: i64): f64;
|
|
163
163
|
|
|
164
|
+
// ---- Constant time (WP34 N6) ------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* `(a & mask) | (b & ~mask)` with the mask hidden from the optimiser, so it is
|
|
168
|
+
* never turned into a branch: `a` for an all-ones mask, `b` for zero. Every
|
|
169
|
+
* operand is one type, `u32` or `u64`. Both are `number` here, so one generic
|
|
170
|
+
* declaration stands for the two.
|
|
171
|
+
*/
|
|
172
|
+
declare function ctSelect<T extends u32 | u64>(mask: T, a: T, b: T): T;
|
|
173
|
+
/** All-ones of the operands' type when `a === b`, zero otherwise, without a branch. */
|
|
174
|
+
declare function ctEq<T extends u32 | u64>(a: T, b: T): T;
|
|
175
|
+
|
|
164
176
|
// ---- Streams and files (globals: Nish has no package resolution) ---------
|
|
165
177
|
|
|
166
178
|
/** `s` to stdout with no trailing newline and no conversion. */
|
|
@@ -182,6 +194,12 @@ declare function panic(message: string): never;
|
|
|
182
194
|
declare function readFileSync(path: string): string;
|
|
183
195
|
/** The same read, answering `null` for every path the other exits over. */
|
|
184
196
|
declare function readFileSyncOrNull(path: string): string | null;
|
|
197
|
+
/**
|
|
198
|
+
* The file's bytes as they are on disk — no UTF-8 assumed, so a zero byte and
|
|
199
|
+
* a byte of 0x80 or above survive — or `null` for every path
|
|
200
|
+
* `readFileSyncOrNull` answers `null` for.
|
|
201
|
+
*/
|
|
202
|
+
declare function readFileBytesSync(path: string): u8[] | null;
|
|
185
203
|
declare function writeFileSync(path: string, data: string): void;
|
|
186
204
|
declare function appendFileSync(path: string, data: string): void;
|
|
187
205
|
/** One directory, not recursive; whether a directory is there afterwards. */
|
|
@@ -213,6 +231,34 @@ declare function getenv(name: string): string | null;
|
|
|
213
231
|
* origin is arbitrary, so only the difference between two reads is meaningful.
|
|
214
232
|
*/
|
|
215
233
|
declare function monotonicNanos(): i64;
|
|
234
|
+
/**
|
|
235
|
+
* The modification time of `path` in milliseconds since the epoch, with the
|
|
236
|
+
* sub-millisecond fraction the file system keeps (Node's `mtimeMs`), or NaN
|
|
237
|
+
* when it cannot be stat'd. Follows a symbolic link; a directory has one too.
|
|
238
|
+
*/
|
|
239
|
+
declare function statMtimeSync(path: string): f64;
|
|
240
|
+
/**
|
|
241
|
+
* A descriptor that becomes readable when SIGTERM or SIGINT arrives, whichever
|
|
242
|
+
* thread the signal lands on, made once (every call answers the same one), or -1.
|
|
243
|
+
*/
|
|
244
|
+
declare function signalFd(): i32;
|
|
245
|
+
/**
|
|
246
|
+
* Block until SIGTERM or SIGINT arrives and answer its number, 15 or 2; -1 for
|
|
247
|
+
* any `fd` that is not `signalFd()`'s. No reading under Node, which throws.
|
|
248
|
+
*/
|
|
249
|
+
declare function readSignal(fd: i32): i32;
|
|
250
|
+
|
|
251
|
+
// ---- `Date` and `crypto` (WP34 N3) -----------------------------------------------
|
|
252
|
+
//
|
|
253
|
+
// `lib.es2022` already declares `Date`, whose `now()` is Nish's one member of it,
|
|
254
|
+
// so nothing is added: `tsc` accepting `new Date()` is `tsc` not being Nish's
|
|
255
|
+
// checker. `crypto` is a Web API that `lib.es2022` leaves out, so it is
|
|
256
|
+
// declared with its one member, typed as Nish types it.
|
|
257
|
+
|
|
258
|
+
declare var crypto: {
|
|
259
|
+
/** Every byte of `bytes` from the system's CSPRNG; at most 65,536 bytes a call. */
|
|
260
|
+
getRandomValues(bytes: u8[]): void;
|
|
261
|
+
};
|
|
216
262
|
|
|
217
263
|
// ---- The builtin modules (`nish:`) ---------------------------------------------
|
|
218
264
|
//
|
|
@@ -229,6 +275,7 @@ declare function monotonicNanos(): i64;
|
|
|
229
275
|
declare module "nish:fs" {
|
|
230
276
|
export function readFileSync(path: string): string;
|
|
231
277
|
export function readFileSyncOrNull(path: string): string | null;
|
|
278
|
+
export function readFileBytesSync(path: string): u8[] | null;
|
|
232
279
|
export function writeFileSync(path: string, data: string): void;
|
|
233
280
|
export function appendFileSync(path: string, data: string): void;
|
|
234
281
|
/** `true` when the directory was created, `false` when it already existed. */
|
|
@@ -240,6 +287,8 @@ declare module "nish:fs" {
|
|
|
240
287
|
*/
|
|
241
288
|
export function readdirSync(path: string): string[] | null;
|
|
242
289
|
export function realpathSync(path: string): string | null;
|
|
290
|
+
/** Node's `mtimeMs` for the path, or NaN when it cannot be stat'd. */
|
|
291
|
+
export function statMtimeSync(path: string): f64;
|
|
243
292
|
}
|
|
244
293
|
|
|
245
294
|
declare module "nish:process" {
|
|
@@ -257,6 +306,10 @@ declare module "nish:process" {
|
|
|
257
306
|
* difference between two reads is meaningful.
|
|
258
307
|
*/
|
|
259
308
|
export function monotonicNanos(): i64;
|
|
309
|
+
/** A descriptor readable when SIGTERM or SIGINT arrives; the same one every call, or -1. */
|
|
310
|
+
export function signalFd(): i32;
|
|
311
|
+
/** Block until SIGTERM or SIGINT arrives: 15 or 2, or -1 for a descriptor that is not `signalFd()`'s. */
|
|
312
|
+
export function readSignal(fd: i32): i32;
|
|
260
313
|
/** The command line; `argv[0]` is the program path, as in C. Read-only. */
|
|
261
314
|
export const argv: readonly string[];
|
|
262
315
|
/** The operating system the program runs on: `"linux"`, `"darwin"`, or `"unknown"`. */
|
|
@@ -303,6 +356,18 @@ declare interface CPtr {
|
|
|
303
356
|
readonly __nishForeignPointer: unique symbol;
|
|
304
357
|
}
|
|
305
358
|
|
|
359
|
+
// ---- `set` on an array ----------------------------------------------------------
|
|
360
|
+
//
|
|
361
|
+
// `dst.set(src, offset)` copies all of `src` into `dst` from `offset` on, with
|
|
362
|
+
// `TypedArray.prototype.set`'s meaning (WP34 N2). A `u8[]` is an `Array` to
|
|
363
|
+
// `tsc`, and `lib.es5.d.ts` gives `Array` a `fill` of the same meaning but no
|
|
364
|
+
// `set`, so this is the one method added to it; `runtime/nish.mjs` installs it
|
|
365
|
+
// under Node. Nish admits it on an array of numbers only.
|
|
366
|
+
|
|
367
|
+
interface Array<T> {
|
|
368
|
+
set(source: readonly T[], offset?: number): void;
|
|
369
|
+
}
|
|
370
|
+
|
|
306
371
|
// ---- What this file cannot say ----------------------------------------------
|
|
307
372
|
//
|
|
308
373
|
// The typed-array aliases (`Int32Array`, `Float32Array`, `Float64Array`,
|
package/runtime/nish.h
CHANGED
|
@@ -243,6 +243,12 @@ bool nish_is_dir(const nish_str *path);
|
|
|
243
243
|
* under WASI: `fd_readdir` lists a preopened directory rather than a path, so
|
|
244
244
|
* porting this there is a different contract and not a translation. */
|
|
245
245
|
nish_array *nish_readdir(const nish_str *path);
|
|
246
|
+
/* `readFileBytesSync(path)` (WP34 N2): the file's bytes as a `u8[]` with
|
|
247
|
+
* `len == cap`, zero bytes and bytes of 0x80 and above kept as they are, or
|
|
248
|
+
* NULL for every path `nish_read_file_or_null` answers NULL for. The header
|
|
249
|
+
* and the bytes live in the arena, so copy what you keep before the next reset
|
|
250
|
+
* or release. */
|
|
251
|
+
nish_array *nish_read_file_bytes(const nish_str *path);
|
|
246
252
|
/* `spawnSync(argv)`: run element 0 of `argv` (searched on `PATH`) with `argv`
|
|
247
253
|
* as its argument vector, wait for it, and answer its exit status, or
|
|
248
254
|
* `128 + n` when signal `n` killed it. -1 when `argv` is empty, when the
|
|
@@ -306,6 +312,31 @@ const nish_str *nish_arch(void);
|
|
|
306
312
|
* region into one and measure zero. */
|
|
307
313
|
int64_t nish_monotonic_nanos(void);
|
|
308
314
|
|
|
315
|
+
/* ---- The host (WP34 N3), runtime/runtime-host.c --------------------------
|
|
316
|
+
* `Date.now()`: the wall clock, `CLOCK_REALTIME`, in whole milliseconds since
|
|
317
|
+
* the epoch, as JavaScript answers it. Unlike `nish_monotonic_nanos` it can go
|
|
318
|
+
* backwards when the clock is corrected; it is for a time a person or a
|
|
319
|
+
* certificate means, not for measuring an interval. */
|
|
320
|
+
double nish_date_now(void);
|
|
321
|
+
/* `crypto.getRandomValues(bytes)`: fills `bytes->len` bytes from the kernel's
|
|
322
|
+
* CSPRNG (`getrandom` on Linux, `getentropy` elsewhere). More than 65,536
|
|
323
|
+
* bytes, the Web API's limit for one call, and a failing entropy source both
|
|
324
|
+
* print a message and exit 1; nothing weaker is ever substituted. */
|
|
325
|
+
void nish_random_fill(nish_array *bytes);
|
|
326
|
+
/* `statMtimeSync(path)`: the modification time in milliseconds with its
|
|
327
|
+
* sub-millisecond fraction, computed as Node's `mtimeMs` is, or NaN when
|
|
328
|
+
* `stat` fails. Follows a symbolic link. */
|
|
329
|
+
double nish_stat_mtime(const nish_str *path);
|
|
330
|
+
/* `signalFd()`: a descriptor that becomes readable when SIGTERM or SIGINT
|
|
331
|
+
* arrives: the read end of a pipe that a `sigaction` handler writes each
|
|
332
|
+
* signal's number to, whichever thread the signal lands on. Nothing is
|
|
333
|
+
* blocked. Made once; every call answers the same descriptor, and -1 when it
|
|
334
|
+
* could not be made. `readSignal(fd)`: block until one arrives and
|
|
335
|
+
* answer its number (15 or 2), or -1 for any `fd` that is not that descriptor
|
|
336
|
+
* or a read that fails. */
|
|
337
|
+
int32_t nish_signal_fd(void);
|
|
338
|
+
int32_t nish_read_signal(int32_t fd);
|
|
339
|
+
|
|
309
340
|
/* String to number (WP7), ASCII whitespace only. mode 0 is `parseFloat`
|
|
310
341
|
* (longest JS decimal literal or `Infinity`, else NaN), mode 1 is `Number`
|
|
311
342
|
* (the whole string, trimmed; blank is 0; `0x` hex accepted, as in JS),
|
package/runtime/nish.mjs
CHANGED
|
@@ -84,6 +84,7 @@ provide("writeError", shim.writeError);
|
|
|
84
84
|
provide("panic", shim.panic);
|
|
85
85
|
provide("readFileSync", shim.readFileSync);
|
|
86
86
|
provide("readFileSyncOrNull", shim.readFileSyncOrNull);
|
|
87
|
+
provide("readFileBytesSync", shim.readFileBytesSync);
|
|
87
88
|
provide("writeFileSync", shim.writeFileSync);
|
|
88
89
|
provide("appendFileSync", shim.appendFileSync);
|
|
89
90
|
provide("mkdirSync", shim.mkdirSync);
|
|
@@ -93,6 +94,35 @@ provide("realpathSync", shim.realpathSync);
|
|
|
93
94
|
provide("spawnSync", shim.spawnSync);
|
|
94
95
|
provide("spawnSyncTo", shim.spawnSyncTo);
|
|
95
96
|
provide("getenv", shim.getenv);
|
|
97
|
+
provide("statMtimeSync", shim.statMtimeSync);
|
|
98
|
+
provide("signalFd", shim.signalFd);
|
|
99
|
+
provide("readSignal", shim.readSignal);
|
|
100
|
+
|
|
101
|
+
// `crypto.getRandomValues(bytes)` (WP34 N3). Node has the global, but it
|
|
102
|
+
// takes only a typed array and a `u8[]` is a plain `Array` here, so the one
|
|
103
|
+
// method is replaced on Node's own `crypto` object by one that fills a plain
|
|
104
|
+
// array too (and hands a typed array to the original).
|
|
105
|
+
Object.defineProperty(globalThis.crypto, "getRandomValues", {
|
|
106
|
+
value: shim.getRandomValues,
|
|
107
|
+
writable: true,
|
|
108
|
+
configurable: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// `dst.set(src, offset)` (WP34 N2). A `u8[]` is a plain `Array` here, which
|
|
112
|
+
// has `fill` with the typed array's meaning already but no `set`, so the one
|
|
113
|
+
// method is added to `Array.prototype` — non-enumerable, as a builtin method is,
|
|
114
|
+
// so no `for...in` sees it — unless something got there first. It copies the
|
|
115
|
+
// source before writing, as the native `memmove` does, and fails a range past
|
|
116
|
+
// the end with the native panic.
|
|
117
|
+
if (!("set" in Array.prototype)) {
|
|
118
|
+
Object.defineProperty(Array.prototype, "set", {
|
|
119
|
+
value: function set(source, offset) {
|
|
120
|
+
shim.arraySet(this, source, offset);
|
|
121
|
+
},
|
|
122
|
+
writable: true,
|
|
123
|
+
configurable: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
96
126
|
|
|
97
127
|
// The clock. `monotonicNanos()` answers an `i64`, so the value is a BigInt here
|
|
98
128
|
// as it is in the rewritten runner — and unusually for the i64 surface that is
|
|
@@ -117,6 +147,11 @@ provide("toU64", (x) => shim.convert(x, "f64", "u64"));
|
|
|
117
147
|
provide("f64ToBits", shim.f64ToBits);
|
|
118
148
|
provide("bitsToF64", shim.bitsToF64);
|
|
119
149
|
|
|
150
|
+
// Constant time (WP34 N6). Pure functions of their operands, so the answers
|
|
151
|
+
// agree with a native run; the timing does not, and is not claimed here.
|
|
152
|
+
provide("ctSelect", shim.ctSelect);
|
|
153
|
+
provide("ctEq", shim.ctEq);
|
|
154
|
+
|
|
120
155
|
// `Result`. Everything works but `orReturn`, which needs the caller's control
|
|
121
156
|
// flow and therefore the rewriter; see the header.
|
|
122
157
|
provide("Ok", shim.Ok);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* Nish runtime, the host half: the wall clock, entropy, a file's modification
|
|
2
|
+
* time and a descriptor that becomes readable when SIGTERM or SIGINT arrives
|
|
3
|
+
* (WP34 N3) — the four facts a program that runs for days asks of the machine
|
|
4
|
+
* it runs on.
|
|
5
|
+
*
|
|
6
|
+
* A translation unit of its own for the reason runtime-os.c is one: each file
|
|
7
|
+
* carries its own measured `.text*` ceiling in tests/run.js. runtime-os.c had
|
|
8
|
+
* 143 bytes of its ceiling left when these arrived, the four measure more than
|
|
9
|
+
* that, and a ceiling is not raised to make room (docs/MASTER_PLAN.md §2 has
|
|
10
|
+
* the numbers). Section GC still means a program that calls none of them pays
|
|
11
|
+
* for none of them, and scripts/build.sh pairs this file with runtime.c like
|
|
12
|
+
* the other two halves, so a link line still names one runtime.
|
|
13
|
+
*
|
|
14
|
+
* Every function here is platform code, and the two platforms differ in three
|
|
15
|
+
* places: `getrandom` on Linux and `getentropy` elsewhere, `st_mtim`
|
|
16
|
+
* against Darwin's `st_mtimespec`, and `pipe2` against `pipe` for the signal
|
|
17
|
+
* descriptor. A WASI build has none of this
|
|
18
|
+
* (the checker refuses all four under a wasm target), so there the file is
|
|
19
|
+
* empty.
|
|
20
|
+
*/
|
|
21
|
+
#if defined(__wasi__) || defined(__wasm__)
|
|
22
|
+
/* ISO C wants at least one declaration in a translation unit. */
|
|
23
|
+
typedef int nish_host_unused;
|
|
24
|
+
#else
|
|
25
|
+
#if defined(__linux__)
|
|
26
|
+
/* glibc declares `clock_gettime`, `sigaction` and `pipe2` under `-std=c11`
|
|
27
|
+
only with a feature macro, and `pipe2` only with this one. Darwin declares
|
|
28
|
+
everything by default and hides `getentropy` and the `st_mtimespec`
|
|
29
|
+
spelling behind a strict feature level, so it gets none. */
|
|
30
|
+
#define _GNU_SOURCE
|
|
31
|
+
#endif
|
|
32
|
+
#include <errno.h>
|
|
33
|
+
#include <signal.h>
|
|
34
|
+
#include <stdint.h>
|
|
35
|
+
#include <stdio.h>
|
|
36
|
+
#include <sys/stat.h>
|
|
37
|
+
#include <time.h>
|
|
38
|
+
#include <unistd.h>
|
|
39
|
+
#include <fcntl.h>
|
|
40
|
+
#include <sys/random.h>
|
|
41
|
+
|
|
42
|
+
#include "nish.h"
|
|
43
|
+
|
|
44
|
+
/* The spelling runtime.c and runtime-os.c use for their cold paths. */
|
|
45
|
+
#define NISH_COLD __attribute__((noreturn, cold, noinline))
|
|
46
|
+
|
|
47
|
+
/* `Date.now()`: `CLOCK_REALTIME` in whole milliseconds since the epoch, as
|
|
48
|
+
JavaScript answers it — the milliseconds are counted in integers and then
|
|
49
|
+
converted, so the answer is exact until the year 287,396 and a negative
|
|
50
|
+
time floors the way `Date.now` does rather than rounding towards zero. */
|
|
51
|
+
double nish_date_now(void) {
|
|
52
|
+
struct timespec ts;
|
|
53
|
+
clock_gettime(CLOCK_REALTIME, &ts);
|
|
54
|
+
return (double)((int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static NISH_COLD void nish_entropy_fail(uint64_t asked) {
|
|
58
|
+
if (asked > 65536) {
|
|
59
|
+
dprintf(2, "crypto.getRandomValues: %llu bytes asked for, and one call fills at most 65536\n",
|
|
60
|
+
(unsigned long long)asked);
|
|
61
|
+
} else {
|
|
62
|
+
dprintf(2, "crypto.getRandomValues: the system's entropy source failed\n");
|
|
63
|
+
}
|
|
64
|
+
_exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* `crypto.getRandomValues(bytes)`: every byte of `bytes` from the kernel's
|
|
68
|
+
CSPRNG, or a panic. There is no fallback to a weaker source, because a
|
|
69
|
+
program handed predictable bytes for a key or a nonce cannot tell. The Web
|
|
70
|
+
API's limit of 65,536 bytes a call is kept, so a program behaves the same
|
|
71
|
+
under Node. `getrandom` without flags blocks only until the pool is first
|
|
72
|
+
initialised at boot and can return short or be interrupted, so it loops;
|
|
73
|
+
`getentropy` fills at most 256 bytes a call and retries nothing itself. */
|
|
74
|
+
void nish_random_fill(nish_array *bytes) {
|
|
75
|
+
uint64_t n = bytes->len;
|
|
76
|
+
char *p = bytes->data;
|
|
77
|
+
if (n > 65536) nish_entropy_fail(n);
|
|
78
|
+
while (n > 0) {
|
|
79
|
+
#if defined(__linux__)
|
|
80
|
+
ssize_t got = getrandom(p, n, 0);
|
|
81
|
+
if (got < 0) {
|
|
82
|
+
if (errno == EINTR) continue;
|
|
83
|
+
nish_entropy_fail(0);
|
|
84
|
+
}
|
|
85
|
+
#else
|
|
86
|
+
size_t got = n < 256 ? n : 256;
|
|
87
|
+
if (getentropy(p, got) != 0) nish_entropy_fail(0);
|
|
88
|
+
#endif
|
|
89
|
+
p += got;
|
|
90
|
+
n -= (uint64_t)got;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* `statMtimeSync(path)`: the modification time in milliseconds, with the
|
|
95
|
+
sub-millisecond part the file system keeps, or NaN when `stat` fails. The
|
|
96
|
+
arithmetic is Node's `mtimeMs` operation for operation — seconds times 1e3
|
|
97
|
+
plus nanoseconds over 1e6, in doubles — so the two readings print the same
|
|
98
|
+
digits. `stat` follows a symbolic link, as `fs.statSync` does. */
|
|
99
|
+
double nish_stat_mtime(const nish_str *path) {
|
|
100
|
+
struct stat st;
|
|
101
|
+
if (stat(path->data, &st) != 0) return __builtin_nan("");
|
|
102
|
+
#if defined(__APPLE__)
|
|
103
|
+
struct timespec t = st.st_mtimespec;
|
|
104
|
+
#else
|
|
105
|
+
struct timespec t = st.st_mtim;
|
|
106
|
+
#endif
|
|
107
|
+
return (double)t.tv_sec * 1e3 + (double)t.tv_nsec / 1e6;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ---- Signals: `signalFd()` and `readSignal(fd)`
|
|
111
|
+
*
|
|
112
|
+
* A descriptor rather than a handler in the language, because a handler would
|
|
113
|
+
* be a function value the language does not have, and because a descriptor is
|
|
114
|
+
* what a loop waiting in `epoll` or `poll` can wait on beside its sockets.
|
|
115
|
+
*
|
|
116
|
+
* Underneath it is a C handler writing each signal's number, one byte, to a
|
|
117
|
+
* pipe. It is not a `signalfd` on Linux, deliberately: a `signalfd` only
|
|
118
|
+
* hears a signal that is blocked in **every** thread, and a mask reaches only
|
|
119
|
+
* the calling thread, so a `scope()` task or a parallel worker already
|
|
120
|
+
* running when `signalFd()` was called would take the signal at its default
|
|
121
|
+
* action and end the process. A handler runs in whichever thread the kernel
|
|
122
|
+
* picks, so no thread's mask matters, and nothing is blocked for a child
|
|
123
|
+
* `spawnSync` starts to inherit: `exec` resets a caught signal to its
|
|
124
|
+
* default. Installing the handler also overrides a disposition the process
|
|
125
|
+
* was started with, so a signal its parent ignored is heard, as
|
|
126
|
+
* `process.on('SIGTERM')` hears it under Node.
|
|
127
|
+
*
|
|
128
|
+
* The handler is the whole of what runs in signal context, and `write` is
|
|
129
|
+
* async-signal-safe. The write end is non-blocking, so a thousand unread
|
|
130
|
+
* signals lose the newest rather than wedge the handler, and `errno` is put
|
|
131
|
+
* back for the code the signal interrupted. The descriptor is made once and
|
|
132
|
+
* every later `signalFd()` answers the same one. */
|
|
133
|
+
static int nish_signal_read_end = -1;
|
|
134
|
+
static int nish_signal_write_end = -1;
|
|
135
|
+
|
|
136
|
+
static void nish_on_signal(int sig) {
|
|
137
|
+
int saved = errno;
|
|
138
|
+
unsigned char b = (unsigned char)sig;
|
|
139
|
+
(void)!write(nish_signal_write_end, &b, 1);
|
|
140
|
+
errno = saved;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
int32_t nish_signal_fd(void) {
|
|
144
|
+
if (nish_signal_read_end >= 0) return nish_signal_read_end;
|
|
145
|
+
int p[2];
|
|
146
|
+
#if defined(__linux__)
|
|
147
|
+
/* Both ends close-on-exec from the moment they exist, so a child a sibling
|
|
148
|
+
thread spawns meanwhile cannot inherit either. */
|
|
149
|
+
if (pipe2(p, O_CLOEXEC) != 0) return -1;
|
|
150
|
+
#else
|
|
151
|
+
/* Darwin has no `pipe2`, so there is a window between `pipe` and the two
|
|
152
|
+
`fcntl`s in which a child spawned by another thread inherits the ends. */
|
|
153
|
+
if (pipe(p) != 0) return -1;
|
|
154
|
+
fcntl(p[0], F_SETFD, FD_CLOEXEC);
|
|
155
|
+
fcntl(p[1], F_SETFD, FD_CLOEXEC);
|
|
156
|
+
#endif
|
|
157
|
+
fcntl(p[1], F_SETFL, O_NONBLOCK);
|
|
158
|
+
nish_signal_write_end = p[1];
|
|
159
|
+
struct sigaction sa;
|
|
160
|
+
sa.sa_handler = nish_on_signal;
|
|
161
|
+
sa.sa_flags = SA_RESTART;
|
|
162
|
+
sigemptyset(&sa.sa_mask);
|
|
163
|
+
sigaction(SIGINT, &sa, 0);
|
|
164
|
+
sigaction(SIGTERM, &sa, 0);
|
|
165
|
+
return nish_signal_read_end = p[0];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Block until a signal's byte arrives and answer it: 15 or 2, or -1 for any
|
|
169
|
+
`fd` that is not the signal descriptor and for a read that fails. */
|
|
170
|
+
int32_t nish_read_signal(int32_t fd) {
|
|
171
|
+
if (fd != nish_signal_read_end) return -1;
|
|
172
|
+
unsigned char b;
|
|
173
|
+
ssize_t n;
|
|
174
|
+
do n = read(fd, &b, 1);
|
|
175
|
+
while (n < 0 && errno == EINTR);
|
|
176
|
+
return n == 1 ? (int32_t)b : -1;
|
|
177
|
+
}
|
|
178
|
+
#endif
|
package/runtime/runtime-os.c
CHANGED
|
@@ -102,6 +102,21 @@ nish_str *nish_read_file_or_null(const nish_str *path) {
|
|
|
102
102
|
return s;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/* `readFileBytesSync(path)` (WP34 N2): the same read, handed to the language as
|
|
106
|
+
a `u8[]`. The bytes are not copied a second time: `nish_read_file_or_null`
|
|
107
|
+
has already put them in the arena behind their 8-byte length, 8-aligned, so
|
|
108
|
+
the header points `data` there with `len == cap`, and the NUL written after
|
|
109
|
+
them is simply never indexed. Nothing on the way assumes UTF-8, so a zero
|
|
110
|
+
byte and a byte of 0x80 or above come back as they are on disk. A `push`
|
|
111
|
+
onto the result grows it into a fresh block, as it would any full array. */
|
|
112
|
+
nish_array *nish_read_file_bytes(const nish_str *path) {
|
|
113
|
+
nish_str *s = nish_read_file_or_null(path);
|
|
114
|
+
if (!s) return 0;
|
|
115
|
+
nish_array *a = nish_alloc_struct(sizeof *a);
|
|
116
|
+
*a = (nish_array){ s->len, s->len, s->data };
|
|
117
|
+
return a;
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
nish_str *nish_read_file(const nish_str *path) {
|
|
106
121
|
nish_str *s = nish_read_file_or_null(path);
|
|
107
122
|
if (!s) nish_io_fail("read ", path);
|
package/runtime/shim.mjs
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* The rewrite rules that call these helpers are listed in docs/wp13-differential.md.
|
|
43
43
|
*/
|
|
44
44
|
import child_process from "node:child_process";
|
|
45
|
+
import { webcrypto } from "node:crypto";
|
|
45
46
|
import fs from "node:fs";
|
|
46
47
|
import os from "node:os";
|
|
47
48
|
|
|
@@ -91,6 +92,37 @@ export function bitsToF64(b) {
|
|
|
91
92
|
return BITS.getFloat64(0);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* `ctSelect` / `ctEq` (WP34 N6). A `u32` is a `number` here and a `u64` a
|
|
97
|
+
* BigInt, so the operands' kind picks the width, and a mix of the two — which
|
|
98
|
+
* the native checker refuses, and which a `u64` written as a bare literal is
|
|
99
|
+
* under an unrewritten run — throws the `TypeError` BigInt arithmetic throws
|
|
100
|
+
* rather than comparing a number with a BigInt and answering zero. JavaScript's
|
|
101
|
+
* `&` reads a `number` as a signed 32-bit integer, so each answer is put back in
|
|
102
|
+
* range with `>>> 0` or `asUintN(64, ...)`. These branch: only the native
|
|
103
|
+
* lowering promises constant time.
|
|
104
|
+
*/
|
|
105
|
+
function ctWide(name, first, second, third) {
|
|
106
|
+
const wide = typeof first === "bigint";
|
|
107
|
+
if ((typeof second === "bigint") !== wide || (typeof third === "bigint") !== wide) {
|
|
108
|
+
throw new TypeError(`${name}: cannot mix a u64 (BigInt) with a u32 (number)`);
|
|
109
|
+
}
|
|
110
|
+
return wide;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const U64_ONES = (1n << 64n) - 1n;
|
|
114
|
+
|
|
115
|
+
export function ctSelect(mask, a, b) {
|
|
116
|
+
if (ctWide("ctSelect", mask, a, b)) return wrapU64((a & mask) | (b & ~mask));
|
|
117
|
+
return ((a & mask) | (b & ~mask)) >>> 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function ctEq(a, b) {
|
|
121
|
+
// `ctEq` has two operands, so the second one stands in for the third.
|
|
122
|
+
if (ctWide("ctEq", a, b, b)) return wrapU64(a ^ b) === 0n ? U64_ONES : 0n;
|
|
123
|
+
return (a ^ b) === 0 ? 0xffffffff : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
94
126
|
/** Wrap a BigInt to the i64 range: every i64 `+ - * /` and unary minus goes through here. */
|
|
95
127
|
export function wrapI64(x) {
|
|
96
128
|
return BigInt.asIntN(64, x);
|
|
@@ -401,6 +433,28 @@ export function updIdx(a, i, f) {
|
|
|
401
433
|
return v;
|
|
402
434
|
}
|
|
403
435
|
|
|
436
|
+
/**
|
|
437
|
+
* `dst.set(src, offset)` (WP34 N2): `TypedArray.prototype.set`'s copy on the
|
|
438
|
+
* plain array a `u8[]` is here. The source is copied first, so a self-copy or
|
|
439
|
+
* an overlapping one reads what was there before, as `memmove` does natively;
|
|
440
|
+
* a range past the end fails with the native panic and its words, where a
|
|
441
|
+
* typed array would throw a `RangeError` for the same offsets.
|
|
442
|
+
*/
|
|
443
|
+
export function arraySet(dst, src, offset) {
|
|
444
|
+
// `ToIntegerOrInfinity`: NaN is 0, as `llvm.fptosi.sat` makes it natively.
|
|
445
|
+
// `Math.trunc` rather than `toIndex`, which converts a bigint: an `i64` or
|
|
446
|
+
// `u64` offset is a bigint here, and it throws the `TypeError` the typed
|
|
447
|
+
// array and `Array.prototype.fill` throw for one, instead of being rounded
|
|
448
|
+
// to the nearest double past 2^53 (docs/RUN_UNDER_NODE.md).
|
|
449
|
+
const at = offset === undefined ? 0 : Math.trunc(offset) || 0;
|
|
450
|
+
const end = at + src.length;
|
|
451
|
+
if (!(at >= 0 && end <= dst.length)) panicSlice(at, end, dst.length);
|
|
452
|
+
// Two plain arrays overlap only when they are one array, which is the one
|
|
453
|
+
// case that must read the source before writing it.
|
|
454
|
+
const from = src === dst ? src.slice() : src;
|
|
455
|
+
for (let i = 0; i < from.length; i++) dst[at + i] = from[i];
|
|
456
|
+
}
|
|
457
|
+
|
|
404
458
|
/** `new Array<T>(n)`: `n` zero-filled elements (`0`, `0n`, or `false`). */
|
|
405
459
|
export function newArray(n, zero) {
|
|
406
460
|
return new Array(toIndex(n)).fill(zero);
|
|
@@ -434,6 +488,15 @@ export function readFileSyncOrNull(path) {
|
|
|
434
488
|
}
|
|
435
489
|
}
|
|
436
490
|
|
|
491
|
+
/** `readFileBytesSync(path)` (WP34 N2): the bytes as a plain array of numbers, or null. */
|
|
492
|
+
export function readFileBytesSync(path) {
|
|
493
|
+
try {
|
|
494
|
+
return Array.from(fs.readFileSync(path));
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
437
500
|
export function writeFileSync(path, data) {
|
|
438
501
|
try {
|
|
439
502
|
fs.writeFileSync(path, data, "utf8");
|
|
@@ -618,6 +681,65 @@ export function monotonicNanos() {
|
|
|
618
681
|
return process.hrtime.bigint();
|
|
619
682
|
}
|
|
620
683
|
|
|
684
|
+
// ---- The host (WP34 N3) ------------------------------------------------------
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* `statMtimeSync(path)`: Node's `mtimeMs` for the path, or NaN when it cannot
|
|
688
|
+
* be stat'd, which is the native answer too. `mtimeMs` is the same arithmetic
|
|
689
|
+
* `runtime-host.c` does, so the two print the same digits, fraction and all.
|
|
690
|
+
*/
|
|
691
|
+
export function statMtimeSync(path) {
|
|
692
|
+
const st = fs.statSync(path, { throwIfNoEntry: false });
|
|
693
|
+
if (st === undefined) {
|
|
694
|
+
return Number.NaN;
|
|
695
|
+
}
|
|
696
|
+
return st.mtimeMs;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Node's own fill, taken before `runtime/nish.mjs` puts the one below in its
|
|
701
|
+
* place on the same object: `webcrypto` is the global `crypto`.
|
|
702
|
+
*/
|
|
703
|
+
const webRandom = webcrypto.getRandomValues.bind(webcrypto);
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* `crypto.getRandomValues(bytes)` for the plain array a `u8[]` is here. Node's
|
|
707
|
+
* own takes only a typed array, so the bytes are drawn into one and copied
|
|
708
|
+
* across. More than 65,536 fails with the native panic and its words, where
|
|
709
|
+
* Node would throw a `QuotaExceededError`: the exit status is 1 either way. A
|
|
710
|
+
* typed array goes straight through.
|
|
711
|
+
*/
|
|
712
|
+
export function getRandomValues(bytes) {
|
|
713
|
+
if (!Array.isArray(bytes)) {
|
|
714
|
+
return webRandom(bytes);
|
|
715
|
+
}
|
|
716
|
+
if (bytes.length > 65536) {
|
|
717
|
+
panic(`crypto.getRandomValues: ${bytes.length} bytes asked for, and one call fills at most 65536`);
|
|
718
|
+
}
|
|
719
|
+
const drawn = webRandom(new Uint8Array(bytes.length));
|
|
720
|
+
for (let i = 0; i < drawn.length; i++) {
|
|
721
|
+
bytes[i] = drawn[i];
|
|
722
|
+
}
|
|
723
|
+
return bytes;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* `signalFd()` and `readSignal(fd)` have no faithful reading under Node, and
|
|
728
|
+
* these say so rather than answer something else. Node delivers a signal to
|
|
729
|
+
* its event loop (`process.on("SIGTERM")`), and a blocking read keeps the loop
|
|
730
|
+
* from ever running, so no synchronous function here can learn that one
|
|
731
|
+
* arrived. docs/wp33-round-trip.md §3.5 has the row and the translation.
|
|
732
|
+
*/
|
|
733
|
+
export function signalFd() {
|
|
734
|
+
throw new Error(
|
|
735
|
+
"signalFd has no synchronous reading under Node: a signal reaches the event loop, which a blocking readSignal never returns to (docs/wp33-round-trip.md)"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function readSignal() {
|
|
740
|
+
return signalFd();
|
|
741
|
+
}
|
|
742
|
+
|
|
621
743
|
/** `process.argv`: index 0 is the program (the script here, the executable natively), then the arguments. */
|
|
622
744
|
export function argv() {
|
|
623
745
|
return process.argv.slice(1);
|
package/scripts/build.sh
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
#
|
|
4
4
|
# scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm]
|
|
5
5
|
#
|
|
6
|
-
# The C runtime is
|
|
6
|
+
# The C runtime is four translation units and is named as one: an input
|
|
7
7
|
# <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
|
|
8
8
|
# system calls (files, directories, subprocesses, the environment, the clock),
|
|
9
|
-
#
|
|
10
|
-
# threads
|
|
9
|
+
# <dir>/runtime-parallel.c, the half that divides a range of work across
|
|
10
|
+
# threads, and <dir>/runtime-host.c, the wall clock, entropy, file times and
|
|
11
|
+
# signals. Each of those files says why they are compiled and measured apart.
|
|
11
12
|
#
|
|
12
13
|
# Profiles:
|
|
13
14
|
# debug clang defaults: no optimisation, symbols kept. The "before" number.
|
|
@@ -83,9 +84,9 @@ done
|
|
|
83
84
|
[ ${#inputs[@]} -gt 0 ] || { echo "error: no input files" >&2; exit 2; }
|
|
84
85
|
[ -n "$out" ] || { echo "error: -o <out> is required" >&2; exit 2; }
|
|
85
86
|
|
|
86
|
-
# The runtime is
|
|
87
|
-
# <dir>/runtime.c gets <dir>/runtime-os.c
|
|
88
|
-
# beside it. They were one file until the operating-system half was split out
|
|
87
|
+
# The runtime is four translation units, and a caller names one: whoever passes
|
|
88
|
+
# <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c and
|
|
89
|
+
# <dir>/runtime-host.c compiled beside it. They were one file until the operating-system half was split out
|
|
89
90
|
# for its own size budget, and the parallel half followed for the same reason
|
|
90
91
|
# (each file's header comment says why), and a link line is where those splits
|
|
91
92
|
# would otherwise leak: `nish --link` builds its command line in
|
|
@@ -97,7 +98,7 @@ done
|
|
|
97
98
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
98
99
|
case "$i" in
|
|
99
100
|
*/runtime.c|runtime.c)
|
|
100
|
-
for half in runtime-os.c runtime-parallel.c; do
|
|
101
|
+
for half in runtime-os.c runtime-parallel.c runtime-host.c; do
|
|
101
102
|
side="${i%runtime.c}$half"
|
|
102
103
|
have=0
|
|
103
104
|
for j in "${inputs[@]}"; do
|
package/std/README.md
CHANGED
|
@@ -16,6 +16,46 @@ whatever program imports it, and subject to the same rules as `examples/` or
|
|
|
16
16
|
| [`map.ts`](./map.ts) | `reserve(m, n)` and `getOrInsert(m, k, v)` for the global `Map`. Their bodies are the meaning, and what runs under Node: `reserve` does nothing, and `getOrInsert` is a `get`, and a `set` of `v` when the key was missing. Natively the compiler lowers every call in place — `reserve` to the table's `reserveSlots`, which grows the buckets once so that `n` entries fit without a rebuild, and `getOrInsert` to one `probe` and a `valueAt` or an `insertAt` through its answer — so, like `collections.ts`, it writes no `.ll` of its own ([`docs/wp32-map.md`](../docs/wp32-map.md) §9.2, [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)) |
|
|
17
17
|
| [`threads.ts`](./threads.ts) | `parallelMapInto(src, dst, f)` and `parallelReduce(src, f, identity)`: a function over every element of an array, on as many threads as the length is worth. Its bodies are the sequential meaning, which is what runs under Node; the compiler recognises the two templates by module and name, lowers the one loop in each onto `nish_parallel_range`, holds the function to the rules that make that safe, and compiles an importing program with `--threads` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#data-parallelism-nishthreads)). `tests/link/par_*` are its programs |
|
|
18
18
|
|
|
19
|
+
## `nish/crypto` — the primitives under TLS 1.3
|
|
20
|
+
|
|
21
|
+
The first lanes of [WP34](../docs/wp34-hosting-cs.md) §5: K1's hashes, MACs and
|
|
22
|
+
key derivation, and K4's key exchange, in pure Nish (decision S1), each module
|
|
23
|
+
imported by its own specifier. Every one is written from its specification
|
|
24
|
+
rather than ported, and reproduces that specification's published vectors in
|
|
25
|
+
its `tests/link/crypto_*` programs. The performance gate compiles every module
|
|
26
|
+
with no diagnostics under both `--number-mode i32` and `f64`, and the hashes,
|
|
27
|
+
HMAC, HKDF and X25519 also run their vectors in `f64` (`crypto_*_f64`).
|
|
28
|
+
|
|
29
|
+
| Module | What it is | Reproduces |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK` | FIPS 180-4 §6.2 |
|
|
32
|
+
| [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK` | FIPS 180-4 §6.4, §6.5 |
|
|
33
|
+
| [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual` | RFC 2104, RFC 4231 §4 |
|
|
34
|
+
| [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen. TLS 1.3's HKDF-Expand-Label is not here; it belongs with TLS | RFC 5869 §2, Appendix A |
|
|
35
|
+
| [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public | — |
|
|
36
|
+
| [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null` | RFC 4648 §5, §10 |
|
|
37
|
+
| [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)` and `x25519Base(scalar)`, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy | RFC 7748 §5.2, §6.1 |
|
|
38
|
+
|
|
39
|
+
Three rules hold across the modules:
|
|
40
|
+
|
|
41
|
+
- **A digest ends the computation.** After `digest()` on a hasher or an HMAC, a
|
|
42
|
+
further `update` or `digest` panics rather than answering a hash over the
|
|
43
|
+
padding, and so does a window outside its buffer. `Sha256.copy()` on a
|
|
44
|
+
digested hasher panics too; `Sha512.copy()` and `Sha384.copy()` answer a copy
|
|
45
|
+
that is itself spent, so any `update` or `digest` on it panics. Either way,
|
|
46
|
+
copy *before* `digest` when the computation has to go on.
|
|
47
|
+
- **An all-zero X25519 result is returned, not refused.** It is what a
|
|
48
|
+
low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol; TLS
|
|
49
|
+
1.3 (WP34 T1) makes it. A key exchange outside TLS has to make it itself.
|
|
50
|
+
- **Constant time by construction, not yet by proof.** No module branches on,
|
|
51
|
+
or indexes by, a secret: comparisons OR the differences into one word and
|
|
52
|
+
test it once, the ladder swaps with a mask and always runs 255 steps, and
|
|
53
|
+
base64url maps characters by arithmetic on range masks rather than a table.
|
|
54
|
+
Every branch is on a length, a loop counter or a bit position. What checks
|
|
55
|
+
that the machine code kept that shape is WP34 N6 — the `ctSelect` / `ctEq`
|
|
56
|
+
builtins behind an optimisation barrier, and a disassembly check — and it is
|
|
57
|
+
not built yet, so this is the discipline and not a verified property.
|
|
58
|
+
|
|
19
59
|
## How a program imports it
|
|
20
60
|
|
|
21
61
|
By its package specifier:
|